/**
 * Muuri v0.9.5
 * https://muuri.dev/
 * Copyright (c) 2015-present, Haltu Oy
 * Released under the MIT license
 * https://github.com/haltu/muuri/blob/master/LICENSE.md
 * @license MIT
 *
 * Muuri Packer
 * Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
 * @license MIT
 *
 * Muuri Ticker / Muuri Emitter / Muuri Dragger
 * Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
 * @license MIT
 *
 * Muuri AutoScroller
 * Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
 * @license MIT
 */

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
            (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BPBMMuuri = factory());
}(this, (function () { 'use strict';

    var GRID_INSTANCES = {};
    var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null;

    var ACTION_SWAP = 'swap';
    var ACTION_MOVE = 'move';

    var EVENT_SYNCHRONIZE = 'synchronize';
    var EVENT_LAYOUT_START = 'layoutStart';
    var EVENT_LAYOUT_END = 'layoutEnd';
    var EVENT_LAYOUT_ABORT = 'layoutAbort';
    var EVENT_ADD = 'add';
    var EVENT_REMOVE = 'remove';
    var EVENT_SHOW_START = 'showStart';
    var EVENT_SHOW_END = 'showEnd';
    var EVENT_HIDE_START = 'hideStart';
    var EVENT_HIDE_END = 'hideEnd';
    var EVENT_FILTER = 'filter';
    var EVENT_SORT = 'sort';
    var EVENT_MOVE = 'move';
    var EVENT_SEND = 'send';
    var EVENT_BEFORE_SEND = 'beforeSend';
    var EVENT_RECEIVE = 'receive';
    var EVENT_BEFORE_RECEIVE = 'beforeReceive';
    var EVENT_DRAG_INIT = 'dragInit';
    var EVENT_DRAG_START = 'dragStart';
    var EVENT_DRAG_MOVE = 'dragMove';
    var EVENT_DRAG_SCROLL = 'dragScroll';
    var EVENT_DRAG_END = 'dragEnd';
    var EVENT_DRAG_RELEASE_START = 'dragReleaseStart';
    var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd';
    var EVENT_DESTROY = 'destroy';

    var HAS_TOUCH_EVENTS = 'ontouchstart' in window;
    var HAS_POINTER_EVENTS = !!window.PointerEvent;
    var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled;

    var MAX_SAFE_FLOAT32_INTEGER = 16777216;

    /**
     * Event emitter constructor.
     *
     * @class
     */
    function Emitter() {
        this._events = {};
        this._queue = [];
        this._counter = 0;
        this._clearOnEmit = false;
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Bind an event listener.
     *
     * @public
     * @param {String} event
     * @param {Function} listener
     * @returns {Emitter}
     */
    Emitter.prototype.on = function (event, listener) {
        if (!this._events || !event || !listener) return this;

        // Get listeners queue and create it if it does not exist.
        var listeners = this._events[event];
        if (!listeners) listeners = this._events[event] = [];

        // Add the listener to the queue.
        listeners.push(listener);

        return this;
    };

    /**
     * Unbind all event listeners that match the provided listener function.
     *
     * @public
     * @param {String} event
     * @param {Function} listener
     * @returns {Emitter}
     */
    Emitter.prototype.off = function (event, listener) {
        if (!this._events || !event || !listener) return this;

        // Get listeners and return immediately if none is found.
        var listeners = this._events[event];
        if (!listeners || !listeners.length) return this;

        // Remove all matching listeners.
        var index;
        while ((index = listeners.indexOf(listener)) !== -1) {
            listeners.splice(index, 1);
        }

        return this;
    };

    /**
     * Unbind all listeners of the provided event.
     *
     * @public
     * @param {String} event
     * @returns {Emitter}
     */
    Emitter.prototype.clear = function (event) {
        if (!this._events || !event) return this;

        var listeners = this._events[event];
        if (listeners) {
            listeners.length = 0;
            delete this._events[event];
        }

        return this;
    };

    /**
     * Emit all listeners in a specified event with the provided arguments.
     *
     * @public
     * @param {String} event
     * @param {...*} [args]
     * @returns {Emitter}
     */
    Emitter.prototype.emit = function (event) {
        if (!this._events || !event) {
            this._clearOnEmit = false;
            return this;
        }

        // Get event listeners and quit early if there's no listeners.
        var listeners = this._events[event];
        if (!listeners || !listeners.length) {
            this._clearOnEmit = false;
            return this;
        }

        var queue = this._queue;
        var startIndex = queue.length;
        var argsLength = arguments.length - 1;
        var args;

        // If we have more than 3 arguments let's put the arguments in an array and
        // apply it to the listeners.
        if (argsLength > 3) {
            args = [];
            args.push.apply(args, arguments);
            args.shift();
        }

        // Add the current listeners to the callback queue before we process them.
        // This is necessary to guarantee that all of the listeners are called in
        // correct order even if new event listeners are removed/added during
        // processing and/or events are emitted during processing.
        queue.push.apply(queue, listeners);

        // Reset the event's listeners if need be.
        if (this._clearOnEmit) {
            listeners.length = 0;
            this._clearOnEmit = false;
        }

        // Increment queue counter. This is needed for the scenarios where emit is
        // triggered while the queue is already processing. We need to keep track of
        // how many "queue processors" there are active so that we can safely reset
        // the queue in the end when the last queue processor is finished.
        ++this._counter;

        // Process the queue (the specific part of it for this emit).
        var i = startIndex;
        var endIndex = queue.length;
        for (; i < endIndex; i++) {
            // prettier-ignore
            argsLength === 0 ? queue[i]() :
                argsLength === 1 ? queue[i](arguments[1]) :
                    argsLength === 2 ? queue[i](arguments[1], arguments[2]) :
                        argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) :
                            queue[i].apply(null, args);

            // Stop processing if the emitter is destroyed.
            if (!this._events) return this;
        }

        // Decrement queue process counter.
        --this._counter;

        // Reset the queue if there are no more queue processes running.
        if (!this._counter) queue.length = 0;

        return this;
    };

    /**
     * Emit all listeners in a specified event with the provided arguments and
     * remove the event's listeners just before calling the them. This method allows
     * the emitter to serve as a queue where all listeners are called only once.
     *
     * @public
     * @param {String} event
     * @param {...*} [args]
     * @returns {Emitter}
     */
    Emitter.prototype.burst = function () {
        if (!this._events) return this;
        this._clearOnEmit = true;
        this.emit.apply(this, arguments);
        return this;
    };

    /**
     * Check how many listeners there are for a specific event.
     *
     * @public
     * @param {String} event
     * @returns {Boolean}
     */
    Emitter.prototype.countListeners = function (event) {
        if (!this._events) return 0;
        var listeners = this._events[event];
        return listeners ? listeners.length : 0;
    };

    /**
     * Destroy emitter instance. Basically just removes all bound listeners.
     *
     * @public
     * @returns {Emitter}
     */
    Emitter.prototype.destroy = function () {
        if (!this._events) return this;
        this._queue.length = this._counter = 0;
        this._events = null;
        return this;
    };

    var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : '';
    var waitDuration = 100;

    /**
     * If you happen to use Edge or IE on a touch capable device there is a
     * a specific case where pointercancel and pointerend events are never emitted,
     * even though one them should always be emitted when you release your finger
     * from the screen. The bug appears specifically when Muuri shifts the dragged
     * element's position in the DOM after pointerdown event, IE and Edge don't like
     * that behaviour and quite often forget to emit the pointerend/pointercancel
     * event. But, they do emit pointerout event so we utilize that here.
     * Specifically, if there has been no pointermove event within 100 milliseconds
     * since the last pointerout event we force cancel the drag operation. This hack
     * works surprisingly well 99% of the time. There is that 1% chance there still
     * that dragged items get stuck but it is what it is.
     *
     * @class
     * @param {Dragger} dragger
     */
    function EdgeHack(dragger) {
        if (!pointerout) return;

        this._dragger = dragger;
        this._timeout = null;
        this._outEvent = null;
        this._isActive = false;

        this._addBehaviour = this._addBehaviour.bind(this);
        this._removeBehaviour = this._removeBehaviour.bind(this);
        this._onTimeout = this._onTimeout.bind(this);
        this._resetData = this._resetData.bind(this);
        this._onStart = this._onStart.bind(this);
        this._onOut = this._onOut.bind(this);

        this._dragger.on('start', this._onStart);
    }

    /**
     * @private
     */
    EdgeHack.prototype._addBehaviour = function () {
        if (this._isActive) return;
        this._isActive = true;
        this._dragger.on('move', this._resetData);
        this._dragger.on('cancel', this._removeBehaviour);
        this._dragger.on('end', this._removeBehaviour);
        window.addEventListener(pointerout, this._onOut);
    };

    /**
     * @private
     */
    EdgeHack.prototype._removeBehaviour = function () {
        if (!this._isActive) return;
        this._dragger.off('move', this._resetData);
        this._dragger.off('cancel', this._removeBehaviour);
        this._dragger.off('end', this._removeBehaviour);
        window.removeEventListener(pointerout, this._onOut);
        this._resetData();
        this._isActive = false;
    };

    /**
     * @private
     */
    EdgeHack.prototype._resetData = function () {
        window.clearTimeout(this._timeout);
        this._timeout = null;
        this._outEvent = null;
    };

    /**
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    EdgeHack.prototype._onStart = function (e) {
        if (e.pointerType === 'mouse') return;
        this._addBehaviour();
    };

    /**
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    EdgeHack.prototype._onOut = function (e) {
        if (!this._dragger._getTrackedTouch(e)) return;
        this._resetData();
        this._outEvent = e;
        this._timeout = window.setTimeout(this._onTimeout, waitDuration);
    };

    /**
     * @private
     */
    EdgeHack.prototype._onTimeout = function () {
        var e = this._outEvent;
        this._resetData();
        if (this._dragger.isActive()) this._dragger._onCancel(e);
    };

    /**
     * @public
     */
    EdgeHack.prototype.destroy = function () {
        if (!pointerout) return;
        this._dragger.off('start', this._onStart);
        this._removeBehaviour();
    };

    // Playing it safe here, test all potential prefixes capitalized and lowercase.
    var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O'];
    var cache$2 = {};

    /**
     * Get prefixed CSS property name when given a non-prefixed CSS property name.
     * Returns null if the property is not supported at all.
     *
     * @param {CSSStyleDeclaration} style
     * @param {String} prop
     * @returns {String}
     */
    function getPrefixedPropName(style, prop) {
        var prefixedProp = cache$2[prop] || '';
        if (prefixedProp) return prefixedProp;

        var camelProp = prop[0].toUpperCase() + prop.slice(1);
        var i = 0;
        while (i < vendorPrefixes.length) {
            prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop;
            if (prefixedProp in style) {
                cache$2[prop] = prefixedProp;
                return prefixedProp;
            }
            ++i;
        }

        return '';
    }

    /**
     * Check if passive events are supported.
     * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
     *
     * @returns {Boolean}
     */
    function hasPassiveEvents() {
        var isPassiveEventsSupported = false;

        try {
            var passiveOpts = Object.defineProperty({}, 'passive', {
                get: function () {
                    isPassiveEventsSupported = true;
                },
            });
            window.addEventListener('testPassive', null, passiveOpts);
            window.removeEventListener('testPassive', null, passiveOpts);
        } catch (e) {}

        return isPassiveEventsSupported;
    }

    var ua = window.navigator.userAgent.toLowerCase();
    var isEdge = ua.indexOf('edge') > -1;
    var isIE = ua.indexOf('trident') > -1;
    var isFirefox = ua.indexOf('firefox') > -1;
    var isAndroid = ua.indexOf('android') > -1;

    var listenerOptions = hasPassiveEvents() ? { passive: true } : false;

    var taProp = 'touchAction';
    var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp);
    var taDefaultValue = 'auto';

    /**
     * Creates a new Dragger instance for an element.
     *
     * @public
     * @class
     * @param {HTMLElement} element
     * @param {Object} [cssProps]
     */
    function Dragger(element, cssProps) {
        this._element = element;
        this._emitter = new Emitter();
        this._isDestroyed = false;
        this._cssProps = {};
        this._touchAction = '';
        this._isActive = false;

        this._pointerId = null;
        this._startTime = 0;
        this._startX = 0;
        this._startY = 0;
        this._currentX = 0;
        this._currentY = 0;

        this._onStart = this._onStart.bind(this);
        this._onMove = this._onMove.bind(this);
        this._onCancel = this._onCancel.bind(this);
        this._onEnd = this._onEnd.bind(this);

        // Can't believe had to build a freaking class for a hack!
        this._edgeHack = null;
        if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) {
            this._edgeHack = new EdgeHack(this);
        }

        // Apply initial CSS props.
        this.setCssProps(cssProps);

        // If touch action was not provided with initial CSS props let's assume it's
        // auto.
        if (!this._touchAction) {
            this.setTouchAction(taDefaultValue);
        }

        // Prevent native link/image dragging for the item and it's children.
        element.addEventListener('dragstart', Dragger._preventDefault, false);

        // Listen to start event.
        element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
    }

    /**
     * Protected properties
     * ********************
     */

    Dragger._pointerEvents = {
        start: 'pointerdown',
        move: 'pointermove',
        cancel: 'pointercancel',
        end: 'pointerup',
    };

    Dragger._msPointerEvents = {
        start: 'MSPointerDown',
        move: 'MSPointerMove',
        cancel: 'MSPointerCancel',
        end: 'MSPointerUp',
    };

    Dragger._touchEvents = {
        start: 'touchstart',
        move: 'touchmove',
        cancel: 'touchcancel',
        end: 'touchend',
    };

    Dragger._mouseEvents = {
        start: 'mousedown',
        move: 'mousemove',
        cancel: '',
        end: 'mouseup',
    };

    Dragger._inputEvents = (function () {
        if (HAS_TOUCH_EVENTS) return Dragger._touchEvents;
        if (HAS_POINTER_EVENTS) return Dragger._pointerEvents;
        if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents;
        return Dragger._mouseEvents;
    })();

    Dragger._emitter = new Emitter();

    Dragger._emitterEvents = {
        start: 'start',
        move: 'move',
        end: 'end',
        cancel: 'cancel',
    };

    Dragger._activeInstances = [];

    /**
     * Protected static methods
     * ************************
     */

    Dragger._preventDefault = function (e) {
        if (e.preventDefault && e.cancelable !== false) e.preventDefault();
    };

    Dragger._activateInstance = function (instance) {
        var index = Dragger._activeInstances.indexOf(instance);
        if (index > -1) return;

        Dragger._activeInstances.push(instance);
        Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove);
        Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel);
        Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd);

        if (Dragger._activeInstances.length === 1) {
            Dragger._bindListeners();
        }
    };

    Dragger._deactivateInstance = function (instance) {
        var index = Dragger._activeInstances.indexOf(instance);
        if (index === -1) return;

        Dragger._activeInstances.splice(index, 1);
        Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove);
        Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel);
        Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd);

        if (!Dragger._activeInstances.length) {
            Dragger._unbindListeners();
        }
    };

    Dragger._bindListeners = function () {
        window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
        window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
        if (Dragger._inputEvents.cancel) {
            window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
        }
    };

    Dragger._unbindListeners = function () {
        window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
        window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
        if (Dragger._inputEvents.cancel) {
            window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
        }
    };

    Dragger._getEventPointerId = function (event) {
        // If we have pointer id available let's use it.
        if (typeof event.pointerId === 'number') {
            return event.pointerId;
        }

        // For touch events let's get the first changed touch's identifier.
        if (event.changedTouches) {
            return event.changedTouches[0] ? event.changedTouches[0].identifier : null;
        }

        // For mouse/other events let's provide a static id.
        return 1;
    };

    Dragger._getTouchById = function (event, id) {
        // If we have a pointer event return the whole event if there's a match, and
        // null otherwise.
        if (typeof event.pointerId === 'number') {
            return event.pointerId === id ? event : null;
        }

        // For touch events let's check if there's a changed touch object that matches
        // the pointerId in which case return the touch object.
        if (event.changedTouches) {
            for (var i = 0; i < event.changedTouches.length; i++) {
                if (event.changedTouches[i].identifier === id) {
                    return event.changedTouches[i];
                }
            }
            return null;
        }

        // For mouse/other events let's assume there's only one pointer and just
        // return the event.
        return event;
    };

    Dragger._onMove = function (e) {
        Dragger._emitter.emit(Dragger._emitterEvents.move, e);
    };

    Dragger._onCancel = function (e) {
        Dragger._emitter.emit(Dragger._emitterEvents.cancel, e);
    };

    Dragger._onEnd = function (e) {
        Dragger._emitter.emit(Dragger._emitterEvents.end, e);
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Reset current drag operation (if any).
     *
     * @private
     */
    Dragger.prototype._reset = function () {
        this._pointerId = null;
        this._startTime = 0;
        this._startX = 0;
        this._startY = 0;
        this._currentX = 0;
        this._currentY = 0;
        this._isActive = false;
        Dragger._deactivateInstance(this);
    };

    /**
     * Create a custom dragger event from a raw event.
     *
     * @private
     * @param {String} type
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     * @returns {Object}
     */
    Dragger.prototype._createEvent = function (type, e) {
        var touch = this._getTrackedTouch(e);
        return {
            // Hammer.js compatibility interface.
            type: type,
            srcEvent: e,
            distance: this.getDistance(),
            deltaX: this.getDeltaX(),
            deltaY: this.getDeltaY(),
            deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(),
            isFirst: type === Dragger._emitterEvents.start,
            isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel,
            pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'),
            // Partial Touch API interface.
            identifier: this._pointerId,
            screenX: touch.screenX,
            screenY: touch.screenY,
            clientX: touch.clientX,
            clientY: touch.clientY,
            pageX: touch.pageX,
            pageY: touch.pageY,
            target: touch.target,
        };
    };

    /**
     * Emit a raw event as dragger event internally.
     *
     * @private
     * @param {String} type
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    Dragger.prototype._emit = function (type, e) {
        this._emitter.emit(type, this._createEvent(type, e));
    };

    /**
     * If the provided event is a PointerEvent this method will return it if it has
     * the same pointerId as the instance. If the provided event is a TouchEvent
     * this method will try to look for a Touch instance in the changedTouches that
     * has an identifier matching this instance's pointerId. If the provided event
     * is a MouseEvent (or just any other event than PointerEvent or TouchEvent)
     * it will be returned immediately.
     *
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     * @returns {?(Touch|PointerEvent|MouseEvent)}
     */
    Dragger.prototype._getTrackedTouch = function (e) {
        if (this._pointerId === null) return null;
        return Dragger._getTouchById(e, this._pointerId);
    };

    /**
     * Handler for start event.
     *
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    Dragger.prototype._onStart = function (e) {
        if (this._isDestroyed) return;

        // If pointer id is already assigned let's return early.
        if (this._pointerId !== null) return;

        // Get (and set) pointer id.
        this._pointerId = Dragger._getEventPointerId(e);
        if (this._pointerId === null) return;

        // Setup initial data and emit start event.
        var touch = this._getTrackedTouch(e);
        this._startX = this._currentX = touch.clientX;
        this._startY = this._currentY = touch.clientY;
        this._startTime = Date.now();
        this._isActive = true;
        this._emit(Dragger._emitterEvents.start, e);

        // If the drag procedure was not reset within the start procedure let's
        // activate the instance (start listening to move/cancel/end events).
        if (this._isActive) {
            Dragger._activateInstance(this);
        }
    };

    /**
     * Handler for move event.
     *
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    Dragger.prototype._onMove = function (e) {
        var touch = this._getTrackedTouch(e);
        if (!touch) return;
        this._currentX = touch.clientX;
        this._currentY = touch.clientY;
        this._emit(Dragger._emitterEvents.move, e);
    };

    /**
     * Handler for cancel event.
     *
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    Dragger.prototype._onCancel = function (e) {
        if (!this._getTrackedTouch(e)) return;
        this._emit(Dragger._emitterEvents.cancel, e);
        this._reset();
    };

    /**
     * Handler for end event.
     *
     * @private
     * @param {(PointerEvent|TouchEvent|MouseEvent)} e
     */
    Dragger.prototype._onEnd = function (e) {
        if (!this._getTrackedTouch(e)) return;
        this._emit(Dragger._emitterEvents.end, e);
        this._reset();
    };

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Check if the element is being dragged at the moment.
     *
     * @public
     * @returns {Boolean}
     */
    Dragger.prototype.isActive = function () {
        return this._isActive;
    };

    /**
     * Set element's touch-action CSS property.
     *
     * @public
     * @param {String} value
     */
    Dragger.prototype.setTouchAction = function (value) {
        // Store unmodified touch action value (we trust user input here).
        this._touchAction = value;

        // Set touch-action style.
        if (taPropPrefixed) {
            this._cssProps[taPropPrefixed] = '';
            this._element.style[taPropPrefixed] = value;
        }

        // If we have an unsupported touch-action value let's add a special listener
        // that prevents default action on touch start event. A dirty hack, but best
        // we can do for now. The other options would be to somehow polyfill the
        // unsupported touch action behavior with custom heuristics which sounds like
        // a can of worms. We do a special exception here for Firefox Android which's
        // touch-action does not work properly if the dragged element is moved in the
        // the DOM tree on touchstart.
        if (HAS_TOUCH_EVENTS) {
            this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
            if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) {
                this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
            }
        }
    };

    /**
     * Update element's CSS properties. Accepts an object with camel cased style
     * props with value pairs as it's first argument.
     *
     * @public
     * @param {Object} [newProps]
     */
    Dragger.prototype.setCssProps = function (newProps) {
        if (!newProps) return;

        var currentProps = this._cssProps;
        var element = this._element;
        var prop;
        var prefixedProp;

        // Reset current props.
        for (prop in currentProps) {
            element.style[prop] = currentProps[prop];
            delete currentProps[prop];
        }

        // Set new props.
        for (prop in newProps) {
            // Make sure we have a value for the prop.
            if (!newProps[prop]) continue;

            // Special handling for touch-action.
            if (prop === taProp) {
                this.setTouchAction(newProps[prop]);
                continue;
            }

            // Get prefixed prop and skip if it does not exist.
            prefixedProp = getPrefixedPropName(element.style, prop);
            if (!prefixedProp) continue;

            // Store the prop and add the style.
            currentProps[prefixedProp] = '';
            element.style[prefixedProp] = newProps[prop];
        }
    };

    /**
     * How much the pointer has moved on x-axis from start position, in pixels.
     * Positive value indicates movement from left to right.
     *
     * @public
     * @returns {Number}
     */
    Dragger.prototype.getDeltaX = function () {
        return this._currentX - this._startX;
    };

    /**
     * How much the pointer has moved on y-axis from start position, in pixels.
     * Positive value indicates movement from top to bottom.
     *
     * @public
     * @returns {Number}
     */
    Dragger.prototype.getDeltaY = function () {
        return this._currentY - this._startY;
    };

    /**
     * How far (in pixels) has pointer moved from start position.
     *
     * @public
     * @returns {Number}
     */
    Dragger.prototype.getDistance = function () {
        var x = this.getDeltaX();
        var y = this.getDeltaY();
        return Math.sqrt(x * x + y * y);
    };

    /**
     * How long has pointer been dragged.
     *
     * @public
     * @returns {Number}
     */
    Dragger.prototype.getDeltaTime = function () {
        return this._startTime ? Date.now() - this._startTime : 0;
    };

    /**
     * Bind drag event listeners.
     *
     * @public
     * @param {String} eventName
     *   - 'start', 'move', 'cancel' or 'end'.
     * @param {Function} listener
     */
    Dragger.prototype.on = function (eventName, listener) {
        this._emitter.on(eventName, listener);
    };

    /**
     * Unbind drag event listeners.
     *
     * @public
     * @param {String} eventName
     *   - 'start', 'move', 'cancel' or 'end'.
     * @param {Function} listener
     */
    Dragger.prototype.off = function (eventName, listener) {
        this._emitter.off(eventName, listener);
    };

    /**
     * Destroy the instance and unbind all drag event listeners.
     *
     * @public
     */
    Dragger.prototype.destroy = function () {
        if (this._isDestroyed) return;

        var element = this._element;

        if (this._edgeHack) this._edgeHack.destroy();

        // Reset data and deactivate the instance.
        this._reset();

        // Destroy emitter.
        this._emitter.destroy();

        // Unbind event handlers.
        element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
        element.removeEventListener('dragstart', Dragger._preventDefault, false);
        element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);

        // Reset styles.
        for (var prop in this._cssProps) {
            element.style[prop] = this._cssProps[prop];
            delete this._cssProps[prop];
        }

        // Reset data.
        this._element = null;

        // Mark as destroyed.
        this._isDestroyed = true;
    };

    var dt = 1000 / 60;

    var raf = (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function (callback) {
            return this.setTimeout(function () {
                callback(Date.now());
            }, dt);
        }
    ).bind(window);

    /**
     * A ticker system for handling DOM reads and writes in an efficient way.
     *
     * @class
     */
    function Ticker(numLanes) {
        this._nextStep = null;
        this._lanes = [];
        this._stepQueue = [];
        this._stepCallbacks = {};
        this._step = this._step.bind(this);
        for (var i = 0; i < numLanes; i++) {
            this._lanes.push(new TickerLane());
        }
    }

    Ticker.prototype._step = function (time) {
        var lanes = this._lanes;
        var stepQueue = this._stepQueue;
        var stepCallbacks = this._stepCallbacks;
        var i, j, id, laneQueue, laneCallbacks, laneIndices;

        this._nextStep = null;

        for (i = 0; i < lanes.length; i++) {
            laneQueue = lanes[i].queue;
            laneCallbacks = lanes[i].callbacks;
            laneIndices = lanes[i].indices;
            for (j = 0; j < laneQueue.length; j++) {
                id = laneQueue[j];
                if (!id) continue;
                stepQueue.push(id);
                stepCallbacks[id] = laneCallbacks[id];
                delete laneCallbacks[id];
                delete laneIndices[id];
            }
            laneQueue.length = 0;
        }

        for (i = 0; i < stepQueue.length; i++) {
            id = stepQueue[i];
            if (stepCallbacks[id]) stepCallbacks[id](time);
            delete stepCallbacks[id];
        }

        stepQueue.length = 0;
    };

    Ticker.prototype.add = function (laneIndex, id, callback) {
        this._lanes[laneIndex].add(id, callback);
        if (!this._nextStep) this._nextStep = raf(this._step);
    };

    Ticker.prototype.remove = function (laneIndex, id) {
        this._lanes[laneIndex].remove(id);
    };

    /**
     * A lane for ticker.
     *
     * @class
     */
    function TickerLane() {
        this.queue = [];
        this.indices = {};
        this.callbacks = {};
    }

    TickerLane.prototype.add = function (id, callback) {
        var index = this.indices[id];
        if (index !== undefined) this.queue[index] = undefined;
        this.queue.push(id);
        this.callbacks[id] = callback;
        this.indices[id] = this.queue.length - 1;
    };

    TickerLane.prototype.remove = function (id) {
        var index = this.indices[id];
        if (index === undefined) return;
        this.queue[index] = undefined;
        delete this.callbacks[id];
        delete this.indices[id];
    };

    var LAYOUT_READ = 'layoutRead';
    var LAYOUT_WRITE = 'layoutWrite';
    var VISIBILITY_READ = 'visibilityRead';
    var VISIBILITY_WRITE = 'visibilityWrite';
    var DRAG_START_READ = 'dragStartRead';
    var DRAG_START_WRITE = 'dragStartWrite';
    var DRAG_MOVE_READ = 'dragMoveRead';
    var DRAG_MOVE_WRITE = 'dragMoveWrite';
    var DRAG_SCROLL_READ = 'dragScrollRead';
    var DRAG_SCROLL_WRITE = 'dragScrollWrite';
    var DRAG_SORT_READ = 'dragSortRead';
    var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead';
    var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite';
    var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite';
    var AUTO_SCROLL_READ = 'autoScrollRead';
    var AUTO_SCROLL_WRITE = 'autoScrollWrite';
    var DEBOUNCE_READ = 'debounceRead';

    var LANE_READ = 0;
    var LANE_READ_TAIL = 1;
    var LANE_WRITE = 2;

    var ticker = new Ticker(3);

    function addLayoutTick(itemId, read, write) {
        ticker.add(LANE_READ, LAYOUT_READ + itemId, read);
        ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write);
    }

    function cancelLayoutTick(itemId) {
        ticker.remove(LANE_READ, LAYOUT_READ + itemId);
        ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId);
    }

    function addVisibilityTick(itemId, read, write) {
        ticker.add(LANE_READ, VISIBILITY_READ + itemId, read);
        ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write);
    }

    function cancelVisibilityTick(itemId) {
        ticker.remove(LANE_READ, VISIBILITY_READ + itemId);
        ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId);
    }

    function addDragStartTick(itemId, read, write) {
        ticker.add(LANE_READ, DRAG_START_READ + itemId, read);
        ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write);
    }

    function cancelDragStartTick(itemId) {
        ticker.remove(LANE_READ, DRAG_START_READ + itemId);
        ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId);
    }

    function addDragMoveTick(itemId, read, write) {
        ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read);
        ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write);
    }

    function cancelDragMoveTick(itemId) {
        ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId);
        ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId);
    }

    function addDragScrollTick(itemId, read, write) {
        ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read);
        ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write);
    }

    function cancelDragScrollTick(itemId) {
        ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId);
        ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId);
    }

    function addDragSortTick(itemId, read) {
        ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read);
    }

    function cancelDragSortTick(itemId) {
        ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId);
    }

    function addPlaceholderLayoutTick(itemId, read, write) {
        ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read);
        ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write);
    }

    function cancelPlaceholderLayoutTick(itemId) {
        ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId);
        ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId);
    }

    function addPlaceholderResizeTick(itemId, write) {
        ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write);
    }

    function cancelPlaceholderResizeTick(itemId) {
        ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId);
    }

    function addAutoScrollTick(read, write) {
        ticker.add(LANE_READ, AUTO_SCROLL_READ, read);
        ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write);
    }

    function cancelAutoScrollTick() {
        ticker.remove(LANE_READ, AUTO_SCROLL_READ);
        ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE);
    }

    function addDebounceTick(debounceId, read) {
        ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read);
    }

    function cancelDebounceTick(debounceId) {
        ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId);
    }

    var AXIS_X = 1;
    var AXIS_Y = 2;
    var FORWARD = 4;
    var BACKWARD = 8;
    var LEFT = AXIS_X | BACKWARD;
    var RIGHT = AXIS_X | FORWARD;
    var UP = AXIS_Y | BACKWARD;
    var DOWN = AXIS_Y | FORWARD;

    var functionType = 'function';

    /**
     * Check if a value is a function.
     *
     * @param {*} val
     * @returns {Boolean}
     */
    function isFunction(val) {
        return typeof val === functionType;
    }

    var cache$1 = typeof WeakMap === 'function' ? new WeakMap() : null;

    /**
     * Returns the computed value of an element's style property as a string.
     *
     * @param {HTMLElement} element
     * @param {String} style
     * @returns {String}
     */
    function getStyle(element, style) {
        var styles = cache$1 && cache$1.get(element);

        if (!styles) {
            styles = window.getComputedStyle(element, null);
            if (cache$1) cache$1.set(element, styles);
        }

        return styles.getPropertyValue(style);
    }

    /**
     * Returns the computed value of an element's style property transformed into
     * a float value.
     *
     * @param {HTMLElement} el
     * @param {String} style
     * @returns {Number}
     */
    function getStyleAsFloat(el, style) {
        return parseFloat(getStyle(el, style)) || 0;
    }

    var DOC_ELEM = document.documentElement;
    var BODY = document.body;
    var THRESHOLD_DATA = { value: 0, offset: 0 };

    /**
     * @param {HTMLElement|Window} element
     * @returns {HTMLElement|Window}
     */
    function getScrollElement(element) {
        if (element === window || element === DOC_ELEM || element === BODY) {
            return window;
        } else {
            return element;
        }
    }

    /**
     * @param {HTMLElement|Window} element
     * @returns {Number}
     */
    function getScrollLeft(element) {
        return element === window ? element.pageXOffset : element.scrollLeft;
    }

    /**
     * @param {HTMLElement|Window} element
     * @returns {Number}
     */
    function getScrollTop(element) {
        return element === window ? element.pageYOffset : element.scrollTop;
    }

    /**
     * @param {HTMLElement|Window} element
     * @returns {Number}
     */
    function getScrollLeftMax(element) {
        if (element === window) {
            return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth;
        } else {
            return element.scrollWidth - element.clientWidth;
        }
    }

    /**
     * @param {HTMLElement|Window} element
     * @returns {Number}
     */
    function getScrollTopMax(element) {
        if (element === window) {
            return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight;
        } else {
            return element.scrollHeight - element.clientHeight;
        }
    }

    /**
     * Get window's or element's client rectangle data relative to the element's
     * content dimensions (includes inner size + padding, excludes scrollbars,
     * borders and margins).
     *
     * @param {HTMLElement|Window} element
     * @returns {Rectangle}
     */
    function getContentRect(element, result) {
        result = result || {};

        if (element === window) {
            result.width = DOC_ELEM.clientWidth;
            result.height = DOC_ELEM.clientHeight;
            result.left = 0;
            result.right = result.width;
            result.top = 0;
            result.bottom = result.height;
        } else {
            var bcr = element.getBoundingClientRect();
            var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width');
            var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width');
            result.width = element.clientWidth;
            result.height = element.clientHeight;
            result.left = bcr.left + borderLeft;
            result.right = result.left + result.width;
            result.top = bcr.top + borderTop;
            result.bottom = result.top + result.height;
        }

        return result;
    }

    /**
     * @param {Item} item
     * @returns {Object}
     */
    function getItemAutoScrollSettings(item) {
        return item._drag._getGrid()._settings.dragAutoScroll;
    }

    /**
     * @param {Item} item
     */
    function prepareItemScrollSync(item) {
        if (!item._drag) return;
        item._drag._prepareScroll();
    }

    /**
     * @param {Item} item
     */
    function applyItemScrollSync(item) {
        if (!item._drag || !item._isActive) return;
        var drag = item._drag;
        drag._scrollDiffX = drag._scrollDiffY = 0;
        item._setTranslate(drag._left, drag._top);
    }

    /**
     * Compute threshold value and edge offset.
     *
     * @param {Number} threshold
     * @param {Number} safeZone
     * @param {Number} itemSize
     * @param {Number} targetSize
     * @returns {Object}
     */
    function computeThreshold(threshold, safeZone, itemSize, targetSize) {
        THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold);
        THRESHOLD_DATA.offset =
            Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2;
        return THRESHOLD_DATA;
    }

    function ScrollRequest() {
        this.reset();
    }

    ScrollRequest.prototype.reset = function () {
        if (this.isActive) this.onStop();
        this.item = null;
        this.element = null;
        this.isActive = false;
        this.isEnding = false;
        this.direction = null;
        this.value = null;
        this.maxValue = 0;
        this.threshold = 0;
        this.distance = 0;
        this.speed = 0;
        this.duration = 0;
        this.action = null;
    };

    ScrollRequest.prototype.hasReachedEnd = function () {
        return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0;
    };

    ScrollRequest.prototype.computeCurrentScrollValue = function () {
        if (this.value === null) {
            return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element);
        }
        return Math.max(0, Math.min(this.value, this.maxValue));
    };

    ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) {
        var delta = this.speed * (deltaTime / 1000);
        var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta;
        return Math.max(0, Math.min(nextValue, this.maxValue));
    };

    ScrollRequest.prototype.computeSpeed = (function () {
        var data = {
            direction: null,
            threshold: 0,
            distance: 0,
            value: 0,
            maxValue: 0,
            deltaTime: 0,
            duration: 0,
            isEnding: false,
        };

        return function (deltaTime) {
            var item = this.item;
            var speed = getItemAutoScrollSettings(item).speed;

            if (isFunction(speed)) {
                data.direction = this.direction;
                data.threshold = this.threshold;
                data.distance = this.distance;
                data.value = this.value;
                data.maxValue = this.maxValue;
                data.duration = this.duration;
                data.speed = this.speed;
                data.deltaTime = deltaTime;
                data.isEnding = this.isEnding;
                return speed(item, this.element, data);
            } else {
                return speed;
            }
        };
    })();

    ScrollRequest.prototype.tick = function (deltaTime) {
        if (!this.isActive) {
            this.isActive = true;
            this.onStart();
        }
        this.value = this.computeCurrentScrollValue();
        this.speed = this.computeSpeed(deltaTime);
        this.value = this.computeNextScrollValue(deltaTime);
        this.duration += deltaTime;
        return this.value;
    };

    ScrollRequest.prototype.onStart = function () {
        var item = this.item;
        var onStart = getItemAutoScrollSettings(item).onStart;
        if (isFunction(onStart)) onStart(item, this.element, this.direction);
    };

    ScrollRequest.prototype.onStop = function () {
        var item = this.item;
        var onStop = getItemAutoScrollSettings(item).onStop;
        if (isFunction(onStop)) onStop(item, this.element, this.direction);
        // Manually nudge sort to happen. There's a good chance that the item is still
        // after the scroll stops which means that the next sort will be triggered
        // only after the item is moved or it's parent scrolled.
        if (item._drag) item._drag.sort();
    };

    function ScrollAction() {
        this.element = null;
        this.requestX = null;
        this.requestY = null;
        this.scrollLeft = 0;
        this.scrollTop = 0;
    }

    ScrollAction.prototype.reset = function () {
        if (this.requestX) this.requestX.action = null;
        if (this.requestY) this.requestY.action = null;
        this.element = null;
        this.requestX = null;
        this.requestY = null;
        this.scrollLeft = 0;
        this.scrollTop = 0;
    };

    ScrollAction.prototype.addRequest = function (request) {
        if (AXIS_X & request.direction) {
            this.removeRequest(this.requestX);
            this.requestX = request;
        } else {
            this.removeRequest(this.requestY);
            this.requestY = request;
        }
        request.action = this;
    };

    ScrollAction.prototype.removeRequest = function (request) {
        if (!request) return;
        if (this.requestX === request) {
            this.requestX = null;
            request.action = null;
        } else if (this.requestY === request) {
            this.requestY = null;
            request.action = null;
        }
    };

    ScrollAction.prototype.computeScrollValues = function () {
        this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element);
        this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element);
    };

    ScrollAction.prototype.scroll = function () {
        var element = this.element;
        if (!element) return;

        if (element.scrollTo) {
            element.scrollTo(this.scrollLeft, this.scrollTop);
        } else {
            element.scrollLeft = this.scrollLeft;
            element.scrollTop = this.scrollTop;
        }
    };

    function Pool(createItem, releaseItem) {
        this.pool = [];
        this.createItem = createItem;
        this.releaseItem = releaseItem;
    }

    Pool.prototype.pick = function () {
        return this.pool.pop() || this.createItem();
    };

    Pool.prototype.release = function (item) {
        this.releaseItem(item);
        if (this.pool.indexOf(item) !== -1) return;
        this.pool.push(item);
    };

    Pool.prototype.reset = function () {
        this.pool.length = 0;
    };

    /**
     * Check if two rectangles are overlapping.
     *
     * @param {Object} a
     * @param {Object} b
     * @returns {Number}
     */
    function isOverlapping(a, b) {
        return !(
            a.left + a.width <= b.left ||
            b.left + b.width <= a.left ||
            a.top + a.height <= b.top ||
            b.top + b.height <= a.top
        );
    }

    /**
     * Calculate intersection area between two rectangle.
     *
     * @param {Object} a
     * @param {Object} b
     * @returns {Number}
     */
    function getIntersectionArea(a, b) {
        if (!isOverlapping(a, b)) return 0;
        var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left);
        var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
        return width * height;
    }

    /**
     * Calculate how many percent the intersection area of two rectangles is from
     * the maximum potential intersection area between the rectangles.
     *
     * @param {Object} a
     * @param {Object} b
     * @returns {Number}
     */
    function getIntersectionScore(a, b) {
        var area = getIntersectionArea(a, b);
        if (!area) return 0;
        var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height);
        return (area / maxArea) * 100;
    }

    var RECT_1 = {
        width: 0,
        height: 0,
        left: 0,
        right: 0,
        top: 0,
        bottom: 0,
    };

    var RECT_2 = {
        width: 0,
        height: 0,
        left: 0,
        right: 0,
        top: 0,
        bottom: 0,
    };

    function AutoScroller() {
        this._isDestroyed = false;
        this._isTicking = false;
        this._tickTime = 0;
        this._tickDeltaTime = 0;
        this._items = [];
        this._actions = [];
        this._requests = {};
        this._requests[AXIS_X] = {};
        this._requests[AXIS_Y] = {};
        this._requestOverlapCheck = {};
        this._dragPositions = {};
        this._dragDirections = {};
        this._overlapCheckInterval = 150;

        this._requestPool = new Pool(
            function () {
                return new ScrollRequest();
            },
            function (request) {
                request.reset();
            }
        );

        this._actionPool = new Pool(
            function () {
                return new ScrollAction();
            },
            function (action) {
                action.reset();
            }
        );

        this._readTick = this._readTick.bind(this);
        this._writeTick = this._writeTick.bind(this);
    }

    AutoScroller.AXIS_X = AXIS_X;
    AutoScroller.AXIS_Y = AXIS_Y;
    AutoScroller.FORWARD = FORWARD;
    AutoScroller.BACKWARD = BACKWARD;
    AutoScroller.LEFT = LEFT;
    AutoScroller.RIGHT = RIGHT;
    AutoScroller.UP = UP;
    AutoScroller.DOWN = DOWN;

    AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) {
        return function (item, element, data) {
            var targetSpeed = 0;
            if (!data.isEnding) {
                if (data.threshold > 0) {
                    var factor = data.threshold - Math.max(0, data.distance);
                    targetSpeed = (maxSpeed / data.threshold) * factor;
                } else {
                    targetSpeed = maxSpeed;
                }
            }

            var currentSpeed = data.speed;
            var nextSpeed = targetSpeed;

            if (currentSpeed === targetSpeed) {
                return nextSpeed;
            }

            if (currentSpeed < targetSpeed) {
                nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000);
                return Math.min(targetSpeed, nextSpeed);
            } else {
                nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000);
                return Math.max(targetSpeed, nextSpeed);
            }
        };
    };

    AutoScroller.pointerHandle = function (pointerSize) {
        var rect = { left: 0, top: 0, width: 0, height: 0 };
        var size = pointerSize || 1;
        return function (item, x, y, w, h, pX, pY) {
            rect.left = pX - size * 0.5;
            rect.top = pY - size * 0.5;
            rect.width = size;
            rect.height = size;
            return rect;
        };
    };

    AutoScroller.prototype._readTick = function (time) {
        if (this._isDestroyed) return;
        if (time && this._tickTime) {
            this._tickDeltaTime = time - this._tickTime;
            this._tickTime = time;
            this._updateRequests();
            this._updateActions();
        } else {
            this._tickTime = time;
            this._tickDeltaTime = 0;
        }
    };

    AutoScroller.prototype._writeTick = function () {
        if (this._isDestroyed) return;
        this._applyActions();
        addAutoScrollTick(this._readTick, this._writeTick);
    };

    AutoScroller.prototype._startTicking = function () {
        this._isTicking = true;
        addAutoScrollTick(this._readTick, this._writeTick);
    };

    AutoScroller.prototype._stopTicking = function () {
        this._isTicking = false;
        this._tickTime = 0;
        this._tickDeltaTime = 0;
        cancelAutoScrollTick();
    };

    AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) {
        var itemDrag = item._drag;

        if (handle) {
            var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent;
            var data = handle(
                item,
                itemDrag._clientX,
                itemDrag._clientY,
                item._width,
                item._height,
                ev.clientX,
                ev.clientY
            );
            rect.left = data.left;
            rect.top = data.top;
            rect.width = data.width;
            rect.height = data.height;
        } else {
            rect.left = itemDrag._clientX;
            rect.top = itemDrag._clientY;
            rect.width = item._width;
            rect.height = item._height;
        }

        rect.right = rect.left + rect.width;
        rect.bottom = rect.top + rect.height;

        return rect;
    };

    AutoScroller.prototype._requestItemScroll = function (
        item,
        axis,
        element,
        direction,
        threshold,
        distance,
        maxValue
    ) {
        var reqMap = this._requests[axis];
        var request = reqMap[item._id];

        if (request) {
            if (request.element !== element || request.direction !== direction) {
                request.reset();
            }
        } else {
            request = this._requestPool.pick();
        }

        request.item = item;
        request.element = element;
        request.direction = direction;
        request.threshold = threshold;
        request.distance = distance;
        request.maxValue = maxValue;
        reqMap[item._id] = request;
    };

    AutoScroller.prototype._cancelItemScroll = function (item, axis) {
        var reqMap = this._requests[axis];
        var request = reqMap[item._id];
        if (!request) return;
        if (request.action) request.action.removeRequest(request);
        this._requestPool.release(request);
        delete reqMap[item._id];
    };

    AutoScroller.prototype._checkItemOverlap = function (item, checkX, checkY) {
        var settings = getItemAutoScrollSettings(item);
        var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets;
        var threshold = settings.threshold;
        var safeZone = settings.safeZone;

        if (!targets || !targets.length) {
            checkX && this._cancelItemScroll(item, AXIS_X);
            checkY && this._cancelItemScroll(item, AXIS_Y);
            return;
        }

        var dragDirections = this._dragDirections[item._id];
        var dragDirectionX = dragDirections[0];
        var dragDirectionY = dragDirections[1];

        if (!dragDirectionX && !dragDirectionY) {
            checkX && this._cancelItemScroll(item, AXIS_X);
            checkY && this._cancelItemScroll(item, AXIS_Y);
            return;
        }

        var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1);
        var testRect = RECT_2;

        var target = null;
        var testElement = null;
        var testAxisX = true;
        var testAxisY = true;
        var testScore = 0;
        var testPriority = 0;
        var testThreshold = null;
        var testDirection = null;
        var testDistance = 0;
        var testMaxScrollX = 0;
        var testMaxScrollY = 0;

        var xElement = null;
        var xPriority = -Infinity;
        var xThreshold = 0;
        var xScore = 0;
        var xDirection = null;
        var xDistance = 0;
        var xMaxScroll = 0;

        var yElement = null;
        var yPriority = -Infinity;
        var yThreshold = 0;
        var yScore = 0;
        var yDirection = null;
        var yDistance = 0;
        var yMaxScroll = 0;

        for (var i = 0; i < targets.length; i++) {
            target = targets[i];
            testAxisX = checkX && dragDirectionX && target.axis !== AXIS_Y;
            testAxisY = checkY && dragDirectionY && target.axis !== AXIS_X;
            testPriority = target.priority || 0;

            // Ignore this item if it's x-axis and y-axis priority is lower than
            // the currently matching item's.
            if ((!testAxisX || testPriority < xPriority) && (!testAxisY || testPriority < yPriority)) {
                continue;
            }

            testElement = getScrollElement(target.element || target);
            testMaxScrollX = testAxisX ? getScrollLeftMax(testElement) : -1;
            testMaxScrollY = testAxisY ? getScrollTopMax(testElement) : -1;

            // Ignore this item if there is no possibility to scroll.
            if (!testMaxScrollX && !testMaxScrollY) continue;

            testRect = getContentRect(testElement, testRect);
            testScore = getIntersectionScore(itemRect, testRect);

            // Ignore this item if it's not overlapping at all with the dragged item.
            if (testScore <= 0) continue;

            // Test x-axis.
            if (
                testAxisX &&
                testPriority >= xPriority &&
                testMaxScrollX > 0 &&
                (testPriority > xPriority || testScore > xScore)
            ) {
                testDirection = null;
                testThreshold = computeThreshold(
                    typeof target.threshold === 'number' ? target.threshold : threshold,
                    safeZone,
                    itemRect.width,
                    testRect.width
                );
                if (dragDirectionX === RIGHT) {
                    testDistance = testRect.right + testThreshold.offset - itemRect.right;
                    if (testDistance <= testThreshold.value && getScrollLeft(testElement) < testMaxScrollX) {
                        testDirection = RIGHT;
                    }
                } else if (dragDirectionX === LEFT) {
                    testDistance = itemRect.left - (testRect.left - testThreshold.offset);
                    if (testDistance <= testThreshold.value && getScrollLeft(testElement) > 0) {
                        testDirection = LEFT;
                    }
                }

                if (testDirection !== null) {
                    xElement = testElement;
                    xPriority = testPriority;
                    xThreshold = testThreshold.value;
                    xScore = testScore;
                    xDirection = testDirection;
                    xDistance = testDistance;
                    xMaxScroll = testMaxScrollX;
                }
            }

            // Test y-axis.
            if (
                testAxisY &&
                testPriority >= yPriority &&
                testMaxScrollY > 0 &&
                (testPriority > yPriority || testScore > yScore)
            ) {
                testDirection = null;
                testThreshold = computeThreshold(
                    typeof target.threshold === 'number' ? target.threshold : threshold,
                    safeZone,
                    itemRect.height,
                    testRect.height
                );
                if (dragDirectionY === DOWN) {
                    testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom;
                    if (testDistance <= testThreshold.value && getScrollTop(testElement) < testMaxScrollY) {
                        testDirection = DOWN;
                    }
                } else if (dragDirectionY === UP) {
                    testDistance = itemRect.top - (testRect.top - testThreshold.offset);
                    if (testDistance <= testThreshold.value && getScrollTop(testElement) > 0) {
                        testDirection = UP;
                    }
                }

                if (testDirection !== null) {
                    yElement = testElement;
                    yPriority = testPriority;
                    yThreshold = testThreshold.value;
                    yScore = testScore;
                    yDirection = testDirection;
                    yDistance = testDistance;
                    yMaxScroll = testMaxScrollY;
                }
            }
        }

        // Request or cancel x-axis scroll.
        if (checkX) {
            if (xElement) {
                this._requestItemScroll(
                    item,
                    AXIS_X,
                    xElement,
                    xDirection,
                    xThreshold,
                    xDistance,
                    xMaxScroll
                );
            } else {
                this._cancelItemScroll(item, AXIS_X);
            }
        }

        // Request or cancel y-axis scroll.
        if (checkY) {
            if (yElement) {
                this._requestItemScroll(
                    item,
                    AXIS_Y,
                    yElement,
                    yDirection,
                    yThreshold,
                    yDistance,
                    yMaxScroll
                );
            } else {
                this._cancelItemScroll(item, AXIS_Y);
            }
        }
    };

    AutoScroller.prototype._updateScrollRequest = function (scrollRequest) {
        var item = scrollRequest.item;
        var settings = getItemAutoScrollSettings(item);
        var targets = isFunction(settings.targets) ? settings.targets(item) : settings.targets;
        var targetCount = (targets && targets.length) || 0;
        var threshold = settings.threshold;
        var safeZone = settings.safeZone;
        var itemRect = this._getItemHandleRect(item, settings.handle, RECT_1);
        var testRect = RECT_2;
        var target = null;
        var testElement = null;
        var testIsAxisX = false;
        var testScore = null;
        var testThreshold = null;
        var testDistance = null;
        var testScroll = null;
        var testMaxScroll = null;
        var hasReachedEnd = null;

        for (var i = 0; i < targetCount; i++) {
            target = targets[i];

            // Make sure we have a matching element.
            testElement = getScrollElement(target.element || target);
            if (testElement !== scrollRequest.element) continue;

            // Make sure we have a matching axis.
            testIsAxisX = !!(AXIS_X & scrollRequest.direction);
            if (testIsAxisX) {
                if (target.axis === AXIS_Y) continue;
            } else {
                if (target.axis === AXIS_X) continue;
            }

            // Stop scrolling if there is no room to scroll anymore.
            testMaxScroll = testIsAxisX ? getScrollLeftMax(testElement) : getScrollTopMax(testElement);
            if (testMaxScroll <= 0) {
                break;
            }

            testRect = getContentRect(testElement, testRect);
            testScore = getIntersectionScore(itemRect, testRect);

            // Stop scrolling if dragged item is not overlapping with the scroll
            // element anymore.
            if (testScore <= 0) {
                break;
            }

            // Compute threshold and edge offset.
            testThreshold = computeThreshold(
                typeof target.threshold === 'number' ? target.threshold : threshold,
                safeZone,
                testIsAxisX ? itemRect.width : itemRect.height,
                testIsAxisX ? testRect.width : testRect.height
            );

            // Compute distance (based on current direction).
            if (scrollRequest.direction === LEFT) {
                testDistance = itemRect.left - (testRect.left - testThreshold.offset);
            } else if (scrollRequest.direction === RIGHT) {
                testDistance = testRect.right + testThreshold.offset - itemRect.right;
            } else if (scrollRequest.direction === UP) {
                testDistance = itemRect.top - (testRect.top - testThreshold.offset);
            } else {
                testDistance = testRect.bottom + testThreshold.offset - itemRect.bottom;
            }

            // Stop scrolling if threshold is not exceeded.
            if (testDistance > testThreshold.value) {
                break;
            }

            // Stop scrolling if we have reached the end of the scroll value.
            testScroll = testIsAxisX ? getScrollLeft(testElement) : getScrollTop(testElement);
            hasReachedEnd =
                FORWARD & scrollRequest.direction ? testScroll >= testMaxScroll : testScroll <= 0;
            if (hasReachedEnd) {
                break;
            }

            // Scrolling can continue, let's update the values.
            scrollRequest.maxValue = testMaxScroll;
            scrollRequest.threshold = testThreshold.value;
            scrollRequest.distance = testDistance;
            scrollRequest.isEnding = false;
            return true;
        }

        // Before we end the request, let's see if we need to stop the scrolling
        // smoothly or immediately.
        if (settings.smoothStop === true && scrollRequest.speed > 0) {
            if (hasReachedEnd === null) hasReachedEnd = scrollRequest.hasReachedEnd();
            scrollRequest.isEnding = hasReachedEnd ? false : true;
        } else {
            scrollRequest.isEnding = false;
        }

        return scrollRequest.isEnding;
    };

    AutoScroller.prototype._updateRequests = function () {
        var items = this._items;
        var requestsX = this._requests[AXIS_X];
        var requestsY = this._requests[AXIS_Y];
        var item, reqX, reqY, checkTime, needsCheck, checkX, checkY;

        for (var i = 0; i < items.length; i++) {
            item = items[i];
            checkTime = this._requestOverlapCheck[item._id];
            needsCheck = checkTime > 0 && this._tickTime - checkTime > this._overlapCheckInterval;

            checkX = true;
            reqX = requestsX[item._id];
            if (reqX && reqX.isActive) {
                checkX = !this._updateScrollRequest(reqX);
                if (checkX) {
                    needsCheck = true;
                    this._cancelItemScroll(item, AXIS_X);
                }
            }

            checkY = true;
            reqY = requestsY[item._id];
            if (reqY && reqY.isActive) {
                checkY = !this._updateScrollRequest(reqY);
                if (checkY) {
                    needsCheck = true;
                    this._cancelItemScroll(item, AXIS_Y);
                }
            }

            if (needsCheck) {
                this._requestOverlapCheck[item._id] = 0;
                this._checkItemOverlap(item, checkX, checkY);
            }
        }
    };

    AutoScroller.prototype._requestAction = function (request, axis) {
        var actions = this._actions;
        var isAxisX = axis === AXIS_X;
        var action = null;

        for (var i = 0; i < actions.length; i++) {
            action = actions[i];

            // If the action's request does not match the request's -> skip.
            if (request.element !== action.element) {
                action = null;
                continue;
            }

            // If the request and action share the same element, but the request slot
            // for the requested axis is already reserved let's ignore and cancel this
            // request.
            if (isAxisX ? action.requestX : action.requestY) {
                this._cancelItemScroll(request.item, axis);
                return;
            }

            // Seems like we have found our action, let's break the loop.
            break;
        }

        if (!action) action = this._actionPool.pick();
        action.element = request.element;
        action.addRequest(request);

        request.tick(this._tickDeltaTime);
        actions.push(action);
    };

    AutoScroller.prototype._updateActions = function () {
        var items = this._items;
        var requests = this._requests;
        var actions = this._actions;
        var itemId;
        var reqX;
        var reqY;
        var i;

        // Generate actions.
        for (i = 0; i < items.length; i++) {
            itemId = items[i]._id;
            reqX = requests[AXIS_X][itemId];
            reqY = requests[AXIS_Y][itemId];
            if (reqX) this._requestAction(reqX, AXIS_X);
            if (reqY) this._requestAction(reqY, AXIS_Y);
        }

        // Compute actions' scroll values.
        for (i = 0; i < actions.length; i++) {
            actions[i].computeScrollValues();
        }
    };

    AutoScroller.prototype._applyActions = function () {
        var actions = this._actions;
        var items = this._items;
        var i;

        // No actions -> no scrolling.
        if (!actions.length) return;

        // Scroll all the required elements.
        for (i = 0; i < actions.length; i++) {
            actions[i].scroll();
            this._actionPool.release(actions[i]);
        }

        // Reset actions.
        actions.length = 0;

        // Sync the item position immediately after all the auto-scrolling business is
        // finished. Without this procedure the items will jitter during auto-scroll
        // (in some cases at least) since the drag scroll handler is async (bound to
        // raf tick). Note that this procedure should not emit any dragScroll events,
        // because otherwise they would be emitted twice for the same event.
        for (i = 0; i < items.length; i++) prepareItemScrollSync(items[i]);
        for (i = 0; i < items.length; i++) applyItemScrollSync(items[i]);
    };

    AutoScroller.prototype._updateDragDirection = function (item) {
        var dragPositions = this._dragPositions[item._id];
        var dragDirections = this._dragDirections[item._id];
        var x1 = item._drag._left;
        var y1 = item._drag._top;
        if (dragPositions.length) {
            var x2 = dragPositions[0];
            var y2 = dragPositions[1];
            dragDirections[0] = x1 > x2 ? RIGHT : x1 < x2 ? LEFT : dragDirections[0] || 0;
            dragDirections[1] = y1 > y2 ? DOWN : y1 < y2 ? UP : dragDirections[1] || 0;
        }
        dragPositions[0] = x1;
        dragPositions[1] = y1;
    };

    AutoScroller.prototype.addItem = function (item) {
        if (this._isDestroyed) return;
        var index = this._items.indexOf(item);
        if (index === -1) {
            this._items.push(item);
            this._requestOverlapCheck[item._id] = this._tickTime;
            this._dragDirections[item._id] = [0, 0];
            this._dragPositions[item._id] = [];
            if (!this._isTicking) this._startTicking();
        }
    };

    AutoScroller.prototype.updateItem = function (item) {
        if (this._isDestroyed) return;

        // Make sure the item still exists in the auto-scroller.
        if (!this._dragDirections[item._id]) return;

        this._updateDragDirection(item);
        if (!this._requestOverlapCheck[item._id]) {
            this._requestOverlapCheck[item._id] = this._tickTime;
        }
    };

    AutoScroller.prototype.removeItem = function (item) {
        if (this._isDestroyed) return;

        var index = this._items.indexOf(item);
        if (index === -1) return;

        var itemId = item._id;

        var reqX = this._requests[AXIS_X][itemId];
        if (reqX) {
            this._cancelItemScroll(item, AXIS_X);
            delete this._requests[AXIS_X][itemId];
        }

        var reqY = this._requests[AXIS_Y][itemId];
        if (reqY) {
            this._cancelItemScroll(item, AXIS_Y);
            delete this._requests[AXIS_Y][itemId];
        }

        delete this._requestOverlapCheck[itemId];
        delete this._dragPositions[itemId];
        delete this._dragDirections[itemId];
        this._items.splice(index, 1);

        if (this._isTicking && !this._items.length) {
            this._stopTicking();
        }
    };

    AutoScroller.prototype.isItemScrollingX = function (item) {
        var reqX = this._requests[AXIS_X][item._id];
        return !!(reqX && reqX.isActive);
    };

    AutoScroller.prototype.isItemScrollingY = function (item) {
        var reqY = this._requests[AXIS_Y][item._id];
        return !!(reqY && reqY.isActive);
    };

    AutoScroller.prototype.isItemScrolling = function (item) {
        return this.isItemScrollingX(item) || this.isItemScrollingY(item);
    };

    AutoScroller.prototype.destroy = function () {
        if (this._isDestroyed) return;

        var items = this._items.slice(0);
        for (var i = 0; i < items.length; i++) {
            this.removeItem(items[i]);
        }

        this._actions.length = 0;
        this._requestPool.reset();
        this._actionPool.reset();

        this._isDestroyed = true;
    };

    var ElProto = window.Element.prototype;
    var matchesFn =
        ElProto.matches ||
        ElProto.matchesSelector ||
        ElProto.webkitMatchesSelector ||
        ElProto.mozMatchesSelector ||
        ElProto.msMatchesSelector ||
        ElProto.oMatchesSelector ||
        function () {
            return false;
        };

    /**
     * Check if element matches a CSS selector.
     *
     * @param {Element} el
     * @param {String} selector
     * @returns {Boolean}
     */
    function elementMatches(el, selector) {
        return matchesFn.call(el, selector);
    }

    /**
     * Add class to an element.
     *
     * @param {HTMLElement} element
     * @param {String} className
     */
    function addClass(element, className) {
        if (!className) return;

        if (element.classList) {
            element.classList.add(className);
        } else {
            if (!elementMatches(element, '.' + className)) {
                element.className += ' ' + className;
            }
        }
    }

    var tempArray = [];
    var numberType = 'number';

    /**
     * Insert an item or an array of items to array to a specified index. Mutates
     * the array. The index can be negative in which case the items will be added
     * to the end of the array.
     *
     * @param {Array} array
     * @param {*} items
     * @param {Number} [index=-1]
     */
    function arrayInsert(array, items, index) {
        var startIndex = typeof index === numberType ? index : -1;
        if (startIndex < 0) startIndex = array.length - startIndex + 1;

        array.splice.apply(array, tempArray.concat(startIndex, 0, items));
        tempArray.length = 0;
    }

    /**
     * Normalize array index. Basically this function makes sure that the provided
     * array index is within the bounds of the provided array and also transforms
     * negative index to the matching positive index. The third (optional) argument
     * allows you to define offset for array's length in case you are adding items
     * to the array or removing items from the array.
     *
     * @param {Array} array
     * @param {Number} index
     * @param {Number} [sizeOffset]
     */
    function normalizeArrayIndex(array, index, sizeOffset) {
        var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0));
        return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index;
    }

    /**
     * Move array item to another index.
     *
     * @param {Array} array
     * @param {Number} fromIndex
     *   - Index (positive or negative) of the item that will be moved.
     * @param {Number} toIndex
     *   - Index (positive or negative) where the item should be moved to.
     */
    function arrayMove(array, fromIndex, toIndex) {
        // Make sure the array has two or more items.
        if (array.length < 2) return;

        // Normalize the indices.
        var from = normalizeArrayIndex(array, fromIndex);
        var to = normalizeArrayIndex(array, toIndex);

        // Add target item to the new position.
        if (from !== to) {
            array.splice(to, 0, array.splice(from, 1)[0]);
        }
    }

    /**
     * Swap array items.
     *
     * @param {Array} array
     * @param {Number} index
     *   - Index (positive or negative) of the item that will be swapped.
     * @param {Number} withIndex
     *   - Index (positive or negative) of the other item that will be swapped.
     */
    function arraySwap(array, index, withIndex) {
        // Make sure the array has two or more items.
        if (array.length < 2) return;

        // Normalize the indices.
        var indexA = normalizeArrayIndex(array, index);
        var indexB = normalizeArrayIndex(array, withIndex);
        var temp;

        // Swap the items.
        if (indexA !== indexB) {
            temp = array[indexA];
            array[indexA] = array[indexB];
            array[indexB] = temp;
        }
    }

    var transformProp = getPrefixedPropName(document.documentElement.style, 'transform') || 'transform';

    var styleNameRegEx = /([A-Z])/g;
    var prefixRegex = /^(webkit-|moz-|ms-|o-)/;
    var msPrefixRegex = /^(-m-s-)/;

    /**
     * Transforms a camel case style property to kebab case style property. Handles
     * vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and
     * "webkitTransform" are both transformed into "-webkit-transform".
     *
     * @param {String} property
     * @returns {String}
     */
    function getStyleName(property) {
        // Initial slicing, turns "fooBarProp" into "foo-bar-prop".
        var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase();

        // Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we
        // need to add an extra '-' to the beginnig).
        styleName = styleName.replace(prefixRegex, '-$1');

        // Handle properties that start with "MS" prefix (we need to transform the
        // "-m-s-" into "-ms-").
        styleName = styleName.replace(msPrefixRegex, '-ms-');

        return styleName;
    }

    var transformStyle = getStyleName(transformProp);

    var transformNone$1 = 'none';
    var displayInline = 'inline';
    var displayNone = 'none';
    var displayStyle = 'display';

    /**
     * Returns true if element is transformed, false if not. In practice the
     * element's display value must be anything else than "none" or "inline" as
     * well as have a valid transform value applied in order to be counted as a
     * transformed element.
     *
     * Borrowed from Mezr (v0.6.1):
     * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661
     *
     * @param {HTMLElement} element
     * @returns {Boolean}
     */
    function isTransformed(element) {
        var transform = getStyle(element, transformStyle);
        if (!transform || transform === transformNone$1) return false;

        var display = getStyle(element, displayStyle);
        if (display === displayInline || display === displayNone) return false;

        return true;
    }

    /**
     * Returns an absolute positioned element's containing block, which is
     * considered to be the closest ancestor element that the target element's
     * positioning is relative to. Disclaimer: this only works as intended for
     * absolute positioned elements.
     *
     * @param {HTMLElement} element
     * @returns {(Document|Element)}
     */
    function getContainingBlock(element) {
        // As long as the containing block is an element, static and not
        // transformed, try to get the element's parent element and fallback to
        // document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339
        var doc = document;
        var res = element || doc;
        while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) {
            res = res.parentElement || doc;
        }
        return res;
    }

    var offsetA = {};
    var offsetB = {};
    var offsetDiff = {};

    /**
     * Returns the element's document offset, which in practice means the vertical
     * and horizontal distance between the element's northwest corner and the
     * document's northwest corner. Note that this function always returns the same
     * object so be sure to read the data from it instead using it as a reference.
     *
     * @param {(Document|Element|Window)} element
     * @param {Object} [offsetData]
     *   - Optional data object where the offset data will be inserted to. If not
     *     provided a new object will be created for the return data.
     * @returns {Object}
     */
    function getOffset(element, offsetData) {
        var offset = offsetData || {};
        var rect;

        // Set up return data.
        offset.left = 0;
        offset.top = 0;

        // Document's offsets are always 0.
        if (element === document) return offset;

        // Add viewport scroll left/top to the respective offsets.
        offset.left = window.pageXOffset || 0;
        offset.top = window.pageYOffset || 0;

        // Window's offsets are the viewport scroll left/top values.
        if (element.self === window.self) return offset;

        // Add element's client rects to the offsets.
        rect = element.getBoundingClientRect();
        offset.left += rect.left;
        offset.top += rect.top;

        // Exclude element's borders from the offset.
        offset.left += getStyleAsFloat(element, 'border-left-width');
        offset.top += getStyleAsFloat(element, 'border-top-width');

        return offset;
    }

    /**
     * Calculate the offset difference two elements.
     *
     * @param {HTMLElement} elemA
     * @param {HTMLElement} elemB
     * @param {Boolean} [compareContainingBlocks=false]
     *   - When this is set to true the containing blocks of the provided elements
     *     will be used for calculating the difference. Otherwise the provided
     *     elements will be compared directly.
     * @returns {Object}
     */
    function getOffsetDiff(elemA, elemB, compareContainingBlocks) {
        offsetDiff.left = 0;
        offsetDiff.top = 0;

        // If elements are same let's return early.
        if (elemA === elemB) return offsetDiff;

        // Compare containing blocks if necessary.
        if (compareContainingBlocks) {
            elemA = getContainingBlock(elemA);
            elemB = getContainingBlock(elemB);

            // If containing blocks are identical, let's return early.
            if (elemA === elemB) return offsetDiff;
        }

        // Finally, let's calculate the offset diff.
        getOffset(elemA, offsetA);
        getOffset(elemB, offsetB);
        offsetDiff.left = offsetB.left - offsetA.left;
        offsetDiff.top = offsetB.top - offsetA.top;

        return offsetDiff;
    }

    /**
     * Check if overflow style value is scrollable.
     *
     * @param {String} value
     * @returns {Boolean}
     */
    function isScrollableOverflow(value) {
        return value === 'auto' || value === 'scroll' || value === 'overlay';
    }

    /**
     * Check if an element is scrollable.
     *
     * @param {HTMLElement} element
     * @returns {Boolean}
     */
    function isScrollable(element) {
        return (
            isScrollableOverflow(getStyle(element, 'overflow')) ||
            isScrollableOverflow(getStyle(element, 'overflow-x')) ||
            isScrollableOverflow(getStyle(element, 'overflow-y'))
        );
    }

    /**
     * Collect element's ancestors that are potentially scrollable elements. The
     * provided element is also also included in the check, meaning that if it is
     * scrollable it is added to the result array.
     *
     * @param {HTMLElement} element
     * @param {Array} [result]
     * @returns {Array}
     */
    function getScrollableAncestors(element, result) {
        result = result || [];

        // Find scroll parents.
        while (element && element !== document) {
            // If element is inside ShadowDOM let's get it's host node from the real
            // DOM and continue looping.
            if (element.getRootNode && element instanceof DocumentFragment) {
                element = element.getRootNode().host;
                continue;
            }

            // If element is scrollable let's add it to the scrollable list.
            if (isScrollable(element)) {
                result.push(element);
            }

            element = element.parentNode;
        }

        // Always add window to the results.
        result.push(window);

        return result;
    }

    var translateValue = {};
    var transformNone = 'none';
    var rxMat3d = /^matrix3d/;
    var rxMatTx = /([^,]*,){4}/;
    var rxMat3dTx = /([^,]*,){12}/;
    var rxNextItem = /[^,]*,/;

    /**
     * Returns the element's computed translateX and translateY values as a floats.
     * The returned object is always the same object and updated every time this
     * function is called.
     *
     * @param {HTMLElement} element
     * @returns {Object}
     */
    function getTranslate(element) {
        translateValue.x = 0;
        translateValue.y = 0;

        var transform = getStyle(element, transformStyle);
        if (!transform || transform === transformNone) {
            return translateValue;
        }

        // Transform style can be in either matrix3d(...) or matrix(...).
        var isMat3d = rxMat3d.test(transform);
        var tX = transform.replace(isMat3d ? rxMat3dTx : rxMatTx, '');
        var tY = tX.replace(rxNextItem, '');

        translateValue.x = parseFloat(tX) || 0;
        translateValue.y = parseFloat(tY) || 0;

        return translateValue;
    }

    /**
     * Remove class from an element.
     *
     * @param {HTMLElement} element
     * @param {String} className
     */
    function removeClass(element, className) {
        if (!className) return;

        if (element.classList) {
            element.classList.remove(className);
        } else {
            if (elementMatches(element, '.' + className)) {
                element.className = (' ' + element.className + ' ')
                    .replace(' ' + className + ' ', ' ')
                    .trim();
            }
        }
    }

    var IS_IOS =
        /^(iPad|iPhone|iPod)/.test(window.navigator.platform) ||
        (/^Mac/.test(window.navigator.platform) && window.navigator.maxTouchPoints > 1);
    var START_PREDICATE_INACTIVE = 0;
    var START_PREDICATE_PENDING = 1;
    var START_PREDICATE_RESOLVED = 2;
    var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false;

    /**
     * Bind touch interaction to an item.
     *
     * @class
     * @param {Item} item
     */
    function ItemDrag(item) {
        var element = item._element;
        var grid = item.getGrid();
        var settings = grid._settings;

        this._item = item;
        this._gridId = grid._id;
        this._isDestroyed = false;
        this._isMigrating = false;

        // Start predicate data.
        this._startPredicate = isFunction(settings.dragStartPredicate)
            ? settings.dragStartPredicate
            : ItemDrag.defaultStartPredicate;
        this._startPredicateState = START_PREDICATE_INACTIVE;
        this._startPredicateResult = undefined;

        // Data for drag sort predicate heuristics.
        this._isSortNeeded = false;
        this._sortTimer = undefined;
        this._blockedSortIndex = null;
        this._sortX1 = 0;
        this._sortX2 = 0;
        this._sortY1 = 0;
        this._sortY2 = 0;

        // Setup item's initial drag data.
        this._reset();

        // Bind the methods that needs binding.
        this._preStartCheck = this._preStartCheck.bind(this);
        this._preEndCheck = this._preEndCheck.bind(this);
        this._onScroll = this._onScroll.bind(this);
        this._prepareStart = this._prepareStart.bind(this);
        this._applyStart = this._applyStart.bind(this);
        this._prepareMove = this._prepareMove.bind(this);
        this._applyMove = this._applyMove.bind(this);
        this._prepareScroll = this._prepareScroll.bind(this);
        this._applyScroll = this._applyScroll.bind(this);
        this._handleSort = this._handleSort.bind(this);
        this._handleSortDelayed = this._handleSortDelayed.bind(this);

        // Get drag handle element.
        this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element;

        // Init dragger.
        this._dragger = new Dragger(this._handle, settings.dragCssProps);
        this._dragger.on('start', this._preStartCheck);
        this._dragger.on('move', this._preStartCheck);
        this._dragger.on('cancel', this._preEndCheck);
        this._dragger.on('end', this._preEndCheck);
    }

    /**
     * Public properties
     * *****************
     */

    /**
     * @public
     * @static
     * @type {AutoScroller}
     */
    ItemDrag.autoScroller = new AutoScroller();

    /**
     * Public static methods
     * *********************
     */

    /**
     * Default drag start predicate handler that handles anchor elements
     * gracefully. The return value of this function defines if the drag is
     * started, rejected or pending. When true is returned the dragging is started
     * and when false is returned the dragging is rejected. If nothing is returned
     * the predicate will be called again on the next drag movement.
     *
     * @public
     * @static
     * @param {Item} item
     * @param {Object} event
     * @param {Object} [options]
     *   - An optional options object which can be used to pass the predicate
     *     it's options manually. By default the predicate retrieves the options
     *     from the grid's settings.
     * @returns {(Boolean|undefined)}
     */
    ItemDrag.defaultStartPredicate = function (item, event, options) {
        var drag = item._drag;

        // Make sure left button is pressed on mouse.
        if (event.isFirst && event.srcEvent.button) {
            return false;
        }

        // If the start event is trusted, non-cancelable and it's default action has
        // not been prevented it is in most cases a sign that the gesture would be
        // cancelled anyways right after it has started (e.g. starting drag while
        // the page is scrolling).
        if (
            !IS_IOS &&
            event.isFirst &&
            event.srcEvent.isTrusted === true &&
            event.srcEvent.defaultPrevented === false &&
            event.srcEvent.cancelable === false
        ) {
            return false;
        }

        // Final event logic. At this stage return value does not matter anymore,
        // the predicate is either resolved or it's not and there's nothing to do
        // about it. Here we just reset data and if the item element is a link
        // we follow it (if there has only been slight movement).
        if (event.isFinal) {
            drag._finishStartPredicate(event);
            return;
        }

        // Setup predicate data from options if not already set.
        var predicate = drag._startPredicateData;
        if (!predicate) {
            var config = options || drag._getGrid()._settings.dragStartPredicate || {};
            drag._startPredicateData = predicate = {
                distance: Math.max(config.distance, 0) || 0,
                delay: Math.max(config.delay, 0) || 0,
            };
        }

        // If delay is defined let's keep track of the latest event and initiate
        // delay if it has not been done yet.
        if (predicate.delay) {
            predicate.event = event;
            if (!predicate.delayTimer) {
                predicate.delayTimer = window.setTimeout(function () {
                    predicate.delay = 0;
                    if (drag._resolveStartPredicate(predicate.event)) {
                        drag._forceResolveStartPredicate(predicate.event);
                        drag._resetStartPredicate();
                    }
                }, predicate.delay);
            }
        }

        return drag._resolveStartPredicate(event);
    };

    /**
     * Default drag sort predicate.
     *
     * @public
     * @static
     * @param {Item} item
     * @param {Object} [options]
     * @param {Number} [options.threshold=50]
     * @param {String} [options.action='move']
     * @returns {?Object}
     *   - Returns `null` if no valid index was found. Otherwise returns drag sort
     *     command.
     */
    ItemDrag.defaultSortPredicate = (function () {
        var itemRect = {};
        var targetRect = {};
        var returnData = {};
        var gridsArray = [];
        var minThreshold = 1;
        var maxThreshold = 100;

        function getTargetGrid(item, rootGrid, threshold) {
            var target = null;
            var dragSort = rootGrid._settings.dragSort;
            var bestScore = -1;
            var gridScore;
            var grids;
            var grid;
            var container;
            var containerRect;
            var left;
            var top;
            var right;
            var bottom;
            var i;

            // Get potential target grids.
            if (dragSort === true) {
                gridsArray[0] = rootGrid;
                grids = gridsArray;
            } else if (isFunction(dragSort)) {
                grids = dragSort.call(rootGrid, item);
            }

            // Return immediately if there are no grids.
            if (!grids || !Array.isArray(grids) || !grids.length) {
                return target;
            }

            // Loop through the grids and get the best match.
            for (i = 0; i < grids.length; i++) {
                grid = grids[i];

                // Filter out all destroyed grids.
                if (grid._isDestroyed) continue;

                // Compute the grid's client rect an clamp the initial boundaries to
                // viewport dimensions.
                grid._updateBoundingRect();
                left = Math.max(0, grid._left);
                top = Math.max(0, grid._top);
                right = Math.min(window.innerWidth, grid._right);
                bottom = Math.min(window.innerHeight, grid._bottom);

                // The grid might be inside one or more elements that clip it's visibility
                // (e.g overflow scroll/hidden) so we want to find out the visible portion
                // of the grid in the viewport and use that in our calculations.
                container = grid._element.parentNode;
                while (
                    container &&
                    container !== document &&
                    container !== document.documentElement &&
                    container !== document.body
                    ) {
                    if (container.getRootNode && container instanceof DocumentFragment) {
                        container = container.getRootNode().host;
                        continue;
                    }

                    if (getStyle(container, 'overflow') !== 'visible') {
                        containerRect = container.getBoundingClientRect();
                        left = Math.max(left, containerRect.left);
                        top = Math.max(top, containerRect.top);
                        right = Math.min(right, containerRect.right);
                        bottom = Math.min(bottom, containerRect.bottom);
                    }

                    if (getStyle(container, 'position') === 'fixed') {
                        break;
                    }

                    container = container.parentNode;
                }

                // No need to go further if target rect does not have visible area.
                if (left >= right || top >= bottom) continue;

                // Check how much dragged element overlaps the container element.
                targetRect.left = left;
                targetRect.top = top;
                targetRect.width = right - left;
                targetRect.height = bottom - top;
                gridScore = getIntersectionScore(itemRect, targetRect);

                // Check if this grid is the best match so far.
                if (gridScore > threshold && gridScore > bestScore) {
                    bestScore = gridScore;
                    target = grid;
                }
            }

            // Always reset grids array.
            gridsArray.length = 0;

            return target;
        }

        return function (item, options) {
            var drag = item._drag;
            var rootGrid = drag._getGrid();

            // Get drag sort predicate settings.
            var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50;
            var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;
            var migrateAction =
                options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;

            // Sort threshold must be a positive number capped to a max value of 100. If
            // that's not the case this function will not work correctly. So let's clamp
            // the threshold just in case.
            sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold);

            // Populate item rect data.
            itemRect.width = item._width;
            itemRect.height = item._height;
            itemRect.left = drag._clientX;
            itemRect.top = drag._clientY;

            // Calculate the target grid.
            var grid = getTargetGrid(item, rootGrid, sortThreshold);

            // Return early if we found no grid container element that overlaps the
            // dragged item enough.
            if (!grid) return null;

            var isMigration = item.getGrid() !== grid;
            var gridOffsetLeft = 0;
            var gridOffsetTop = 0;
            var matchScore = 0;
            var matchIndex = -1;
            var hasValidTargets = false;
            var target;
            var score;
            var i;

            // If item is moved within it's originating grid adjust item's left and
            // top props. Otherwise if item is moved to/within another grid get the
            // container element's offset (from the element's content edge).
            if (grid === rootGrid) {
                itemRect.left = drag._gridX + item._marginLeft;
                itemRect.top = drag._gridY + item._marginTop;
            } else {
                grid._updateBorders(1, 0, 1, 0);
                gridOffsetLeft = grid._left + grid._borderLeft;
                gridOffsetTop = grid._top + grid._borderTop;
            }

            // Loop through the target grid items and try to find the best match.
            for (i = 0; i < grid._items.length; i++) {
                target = grid._items[i];

                // If the target item is not active or the target item is the dragged
                // item let's skip to the next item.
                if (!target._isActive || target === item) {
                    continue;
                }

                // Mark the grid as having valid target items.
                hasValidTargets = true;

                // Calculate the target's overlap score with the dragged item.
                targetRect.width = target._width;
                targetRect.height = target._height;
                targetRect.left = target._left + target._marginLeft + gridOffsetLeft;
                targetRect.top = target._top + target._marginTop + gridOffsetTop;
                score = getIntersectionScore(itemRect, targetRect);

                // Update best match index and score if the target's overlap score with
                // the dragged item is higher than the current best match score.
                if (score > matchScore) {
                    matchIndex = i;
                    matchScore = score;
                }
            }

            // If there is no valid match and the dragged item is being moved into
            // another grid we need to do some guess work here. If there simply are no
            // valid targets (which means that the dragged item will be the only active
            // item in the new grid) we can just add it as the first item. If we have
            // valid items in the new grid and the dragged item is overlapping one or
            // more of the items in the new grid let's make an exception with the
            // threshold and just pick the item which the dragged item is overlapping
            // most. However, if the dragged item is not overlapping any of the valid
            // items in the new grid let's position it as the last item in the grid.
            if (isMigration && matchScore < sortThreshold) {
                matchIndex = hasValidTargets ? matchIndex : 0;
                matchScore = sortThreshold;
            }

            // Check if the best match overlaps enough to justify a placement switch.
            if (matchScore >= sortThreshold) {
                returnData.grid = grid;
                returnData.index = matchIndex;
                returnData.action = isMigration ? migrateAction : sortAction;
                return returnData;
            }

            return null;
        };
    })();

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Abort dragging and reset drag data.
     *
     * @public
     */
    ItemDrag.prototype.stop = function () {
        if (!this._isActive) return;

        // If the item is being dropped into another grid, finish it up and return
        // immediately.
        if (this._isMigrating) {
            this._finishMigration();
            return;
        }

        var item = this._item;
        var itemId = item._id;

        // Stop auto-scroll.
        ItemDrag.autoScroller.removeItem(item);

        // Cancel queued ticks.
        cancelDragStartTick(itemId);
        cancelDragMoveTick(itemId);
        cancelDragScrollTick(itemId);

        // Cancel sort procedure.
        this._cancelSort();

        if (this._isStarted) {
            // Remove scroll listeners.
            this._unbindScrollListeners();

            var element = item._element;
            var grid = this._getGrid();
            var draggingClass = grid._settings.itemDraggingClass;

            // Append item element to the container if it's not it's child. Also make
            // sure the translate values are adjusted to account for the DOM shift.
            if (element.parentNode !== grid._element) {
                grid._element.appendChild(element);
                item._setTranslate(this._gridX, this._gridY);

                // We need to do forced reflow to make sure the dragging class is removed
                // gracefully.
                // eslint-disable-next-line
                if (draggingClass) element.clientWidth;
            }

            // Remove dragging class.
            removeClass(element, draggingClass);
        }

        // Reset drag data.
        this._reset();
    };

    /**
     * Manually trigger drag sort. This is only needed for special edge cases where
     * e.g. you have disabled sort and want to trigger a sort right after enabling
     * it (and don't want to wait for the next move/scroll event).
     *
     * @private
     * @param {Boolean} [force=false]
     */
    ItemDrag.prototype.sort = function (force) {
        var item = this._item;
        if (this._isActive && item._isActive && this._dragMoveEvent) {
            if (force === true) {
                this._handleSort();
            } else {
                addDragSortTick(item._id, this._handleSort);
            }
        }
    };

    /**
     * Destroy instance.
     *
     * @public
     */
    ItemDrag.prototype.destroy = function () {
        if (this._isDestroyed) return;
        this.stop();
        this._dragger.destroy();
        ItemDrag.autoScroller.removeItem(this._item);
        this._isDestroyed = true;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Get Grid instance.
     *
     * @private
     * @returns {?Grid}
     */
    ItemDrag.prototype._getGrid = function () {
        return GRID_INSTANCES[this._gridId] || null;
    };

    /**
     * Setup/reset drag data.
     *
     * @private
     */
    ItemDrag.prototype._reset = function () {
        this._isActive = false;
        this._isStarted = false;

        // The dragged item's container element.
        this._container = null;

        // The dragged item's containing block.
        this._containingBlock = null;

        // Drag/scroll event data.
        this._dragStartEvent = null;
        this._dragMoveEvent = null;
        this._dragPrevMoveEvent = null;
        this._scrollEvent = null;

        // All the elements which need to be listened for scroll events during
        // dragging.
        this._scrollers = [];

        // The current translateX/translateY position.
        this._left = 0;
        this._top = 0;

        // Dragged element's current position within the grid.
        this._gridX = 0;
        this._gridY = 0;

        // Dragged element's current offset from window's northwest corner. Does
        // not account for element's margins.
        this._clientX = 0;
        this._clientY = 0;

        // Keep track of the clientX/Y diff for scrolling.
        this._scrollDiffX = 0;
        this._scrollDiffY = 0;

        // Keep track of the clientX/Y diff for moving.
        this._moveDiffX = 0;
        this._moveDiffY = 0;

        // Offset difference between the dragged element's temporary drag
        // container and it's original container.
        this._containerDiffX = 0;
        this._containerDiffY = 0;
    };

    /**
     * Bind drag scroll handlers to all scrollable ancestor elements of the
     * dragged element and the drag container element.
     *
     * @private
     */
    ItemDrag.prototype._bindScrollListeners = function () {
        var gridContainer = this._getGrid()._element;
        var dragContainer = this._container;
        var scrollers = this._scrollers;
        var gridScrollers;
        var i;

        // Get dragged element's scrolling parents.
        scrollers.length = 0;
        getScrollableAncestors(this._item._element.parentNode, scrollers);

        // If drag container is defined and it's not the same element as grid
        // container then we need to add the grid container and it's scroll parents
        // to the elements which are going to be listener for scroll events.
        if (dragContainer !== gridContainer) {
            gridScrollers = [];
            getScrollableAncestors(gridContainer, gridScrollers);
            for (i = 0; i < gridScrollers.length; i++) {
                if (scrollers.indexOf(gridScrollers[i]) < 0) {
                    scrollers.push(gridScrollers[i]);
                }
            }
        }

        // Bind scroll listeners.
        for (i = 0; i < scrollers.length; i++) {
            scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS);
        }
    };

    /**
     * Unbind currently bound drag scroll handlers from all scrollable ancestor
     * elements of the dragged element and the drag container element.
     *
     * @private
     */
    ItemDrag.prototype._unbindScrollListeners = function () {
        var scrollers = this._scrollers;
        var i;

        for (i = 0; i < scrollers.length; i++) {
            scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS);
        }

        scrollers.length = 0;
    };

    /**
     * Unbind currently bound drag scroll handlers from all scrollable ancestor
     * elements of the dragged element and the drag container element.
     *
     * @private
     * @param {Object} event
     * @returns {Boolean}
     */
    ItemDrag.prototype._resolveStartPredicate = function (event) {
        var predicate = this._startPredicateData;
        if (event.distance < predicate.distance || predicate.delay) return;
        this._resetStartPredicate();
        return true;
    };

    /**
     * Forcefully resolve drag start predicate.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._forceResolveStartPredicate = function (event) {
        if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) {
            this._startPredicateState = START_PREDICATE_RESOLVED;
            this._onStart(event);
        }
    };

    /**
     * Finalize start predicate.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._finishStartPredicate = function (event) {
        var element = this._item._element;

        // Check if this is a click (very subjective heuristics).
        var isClick = Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200;

        // Reset predicate.
        this._resetStartPredicate();

        // If the gesture can be interpreted as click let's try to open the element's
        // href url (if it is an anchor element).
        if (isClick) openAnchorHref(element);
    };

    /**
     * Reset drag sort heuristics.
     *
     * @private
     * @param {Number} x
     * @param {Number} y
     */
    ItemDrag.prototype._resetHeuristics = function (x, y) {
        this._blockedSortIndex = null;
        this._sortX1 = this._sortX2 = x;
        this._sortY1 = this._sortY2 = y;
    };

    /**
     * Run heuristics and return true if overlap check can be performed, and false
     * if it can not.
     *
     * @private
     * @param {Number} x
     * @param {Number} y
     * @returns {Boolean}
     */
    ItemDrag.prototype._checkHeuristics = function (x, y) {
        var settings = this._getGrid()._settings.dragSortHeuristics;
        var minDist = settings.minDragDistance;

        // Skip heuristics if not needed.
        if (minDist <= 0) {
            this._blockedSortIndex = null;
            return true;
        }

        var diffX = x - this._sortX2;
        var diffY = y - this._sortY2;

        // If we can't do proper bounce back check make sure that the blocked index
        // is not set.
        var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0;
        if (!canCheckBounceBack) {
            this._blockedSortIndex = null;
        }

        if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) {
            // Reset blocked index if angle changed enough. This check requires a
            // minimum value of 3 for minDragDistance to function properly.
            if (canCheckBounceBack) {
                var angle = Math.atan2(diffX, diffY);
                var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1);
                var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle));
                if (Math.abs(deltaAngle) > settings.minBounceBackAngle) {
                    this._blockedSortIndex = null;
                }
            }

            // Update points.
            this._sortX1 = this._sortX2;
            this._sortY1 = this._sortY2;
            this._sortX2 = x;
            this._sortY2 = y;

            return true;
        }

        return false;
    };

    /**
     * Reset for default drag start predicate function.
     *
     * @private
     */
    ItemDrag.prototype._resetStartPredicate = function () {
        var predicate = this._startPredicateData;
        if (predicate) {
            if (predicate.delayTimer) {
                predicate.delayTimer = window.clearTimeout(predicate.delayTimer);
            }
            this._startPredicateData = null;
        }
    };

    /**
     * Handle the sorting procedure. Manage drag sort heuristics/interval and
     * check overlap when necessary.
     *
     * @private
     */
    ItemDrag.prototype._handleSort = function () {
        if (!this._isActive) return;

        var settings = this._getGrid()._settings;

        // No sorting when drag sort is disabled. Also, account for the scenario where
        // dragSort is temporarily disabled during drag procedure so we need to reset
        // sort timer heuristics state too.
        if (
            !settings.dragSort ||
            (!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item))
        ) {
            this._sortX1 = this._sortX2 = this._gridX;
            this._sortY1 = this._sortY2 = this._gridY;
            // We set this to true intentionally so that overlap check would be
            // triggered as soon as possible after sort becomes enabled again.
            this._isSortNeeded = true;
            if (this._sortTimer !== undefined) {
                this._sortTimer = window.clearTimeout(this._sortTimer);
            }
            return;
        }

        // If sorting is enabled we always need to run the heuristics check to keep
        // the tracked coordinates updated. We also allow an exception when the sort
        // timer is finished because the heuristics are intended to prevent overlap
        // checks based on the dragged element's immediate movement and a delayed
        // overlap check is valid if it comes through, because it was valid when it
        // was invoked.
        var shouldSort = this._checkHeuristics(this._gridX, this._gridY);
        if (!this._isSortNeeded && !shouldSort) return;

        var sortInterval = settings.dragSortHeuristics.sortInterval;
        if (sortInterval <= 0 || this._isSortNeeded) {
            this._isSortNeeded = false;
            if (this._sortTimer !== undefined) {
                this._sortTimer = window.clearTimeout(this._sortTimer);
            }
            this._checkOverlap();
        } else if (this._sortTimer === undefined) {
            this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval);
        }
    };

    /**
     * Delayed sort handler.
     *
     * @private
     */
    ItemDrag.prototype._handleSortDelayed = function () {
        this._isSortNeeded = true;
        this._sortTimer = undefined;
        addDragSortTick(this._item._id, this._handleSort);
    };

    /**
     * Cancel and reset sort procedure.
     *
     * @private
     */
    ItemDrag.prototype._cancelSort = function () {
        this._isSortNeeded = false;
        if (this._sortTimer !== undefined) {
            this._sortTimer = window.clearTimeout(this._sortTimer);
        }
        cancelDragSortTick(this._item._id);
    };

    /**
     * Handle the ending of the drag procedure for sorting.
     *
     * @private
     */
    ItemDrag.prototype._finishSort = function () {
        var isSortEnabled = this._getGrid()._settings.dragSort;
        var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined);
        this._cancelSort();
        if (needsFinalCheck) this._checkOverlap();
    };

    /**
     * Check (during drag) if an item is overlapping other items and based on
     * the configuration layout the items.
     *
     * @private
     */
    ItemDrag.prototype._checkOverlap = function () {
        if (!this._isActive) return;

        var item = this._item;
        var settings = this._getGrid()._settings;
        var result;
        var currentGrid;
        var currentIndex;
        var targetGrid;
        var targetIndex;
        var targetItem;
        var sortAction;
        var isMigration;

        // Get overlap check result.
        if (isFunction(settings.dragSortPredicate)) {
            result = settings.dragSortPredicate(item, this._dragMoveEvent);
        } else {
            result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate);
        }

        // Let's make sure the result object has a valid index before going further.
        if (!result || typeof result.index !== 'number') return;

        sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;
        currentGrid = item.getGrid();
        targetGrid = result.grid || currentGrid;
        isMigration = currentGrid !== targetGrid;
        currentIndex = currentGrid._items.indexOf(item);
        targetIndex = normalizeArrayIndex(
            targetGrid._items,
            result.index,
            isMigration && sortAction === ACTION_MOVE ? 1 : 0
        );

        // Prevent position bounce.
        if (!isMigration && targetIndex === this._blockedSortIndex) {
            return;
        }

        // If the item was moved within it's current grid.
        if (!isMigration) {
            // Make sure the target index is not the current index.
            if (currentIndex !== targetIndex) {
                this._blockedSortIndex = currentIndex;

                // Do the sort.
                (sortAction === ACTION_SWAP ? arraySwap : arrayMove)(
                    currentGrid._items,
                    currentIndex,
                    targetIndex
                );

                // Emit move event.
                if (currentGrid._hasListeners(EVENT_MOVE)) {
                    currentGrid._emit(EVENT_MOVE, {
                        item: item,
                        fromIndex: currentIndex,
                        toIndex: targetIndex,
                        action: sortAction,
                    });
                }

                // Layout the grid.
                currentGrid.layout();
            }
        }

        // If the item was moved to another grid.
        else {
            this._blockedSortIndex = null;

            // Let's fetch the target item when it's still in it's original index.
            targetItem = targetGrid._items[targetIndex];

            // Emit beforeSend event.
            if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) {
                currentGrid._emit(EVENT_BEFORE_SEND, {
                    item: item,
                    fromGrid: currentGrid,
                    fromIndex: currentIndex,
                    toGrid: targetGrid,
                    toIndex: targetIndex,
                });
            }

            // Emit beforeReceive event.
            if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) {
                targetGrid._emit(EVENT_BEFORE_RECEIVE, {
                    item: item,
                    fromGrid: currentGrid,
                    fromIndex: currentIndex,
                    toGrid: targetGrid,
                    toIndex: targetIndex,
                });
            }

            // Update item's grid id reference.
            item._gridId = targetGrid._id;

            // Update drag instance's migrating indicator.
            this._isMigrating = item._gridId !== this._gridId;

            // Move item instance from current grid to target grid.
            currentGrid._items.splice(currentIndex, 1);
            arrayInsert(targetGrid._items, item, targetIndex);

            // Reset sort data.
            item._sortData = null;

            // Emit send event.
            if (currentGrid._hasListeners(EVENT_SEND)) {
                currentGrid._emit(EVENT_SEND, {
                    item: item,
                    fromGrid: currentGrid,
                    fromIndex: currentIndex,
                    toGrid: targetGrid,
                    toIndex: targetIndex,
                });
            }

            // Emit receive event.
            if (targetGrid._hasListeners(EVENT_RECEIVE)) {
                targetGrid._emit(EVENT_RECEIVE, {
                    item: item,
                    fromGrid: currentGrid,
                    fromIndex: currentIndex,
                    toGrid: targetGrid,
                    toIndex: targetIndex,
                });
            }

            // If the sort action is "swap" let's respect it and send the target item
            // (if it exists) from the target grid to the originating grid. This process
            // is done on purpose after the dragged item placed within the target grid
            // so that we can keep this implementation as simple as possible utilizing
            // the existing API.
            if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) {
                // Sanity check to make sure that the target item is still part of the
                // target grid. It could have been manipulated in the event handlers.
                if (targetGrid._items.indexOf(targetItem) > -1) {
                    targetGrid.send(targetItem, currentGrid, currentIndex, {
                        appendTo: this._container || document.body,
                        layoutSender: false,
                        layoutReceiver: false,
                    });
                }
            }

            // Layout both grids.
            currentGrid.layout();
            targetGrid.layout();
        }
    };

    /**
     * If item is dragged into another grid, finish the migration process
     * gracefully.
     *
     * @private
     */
    ItemDrag.prototype._finishMigration = function () {
        var item = this._item;
        var release = item._dragRelease;
        var element = item._element;
        var isActive = item._isActive;
        var targetGrid = item.getGrid();
        var targetGridElement = targetGrid._element;
        var targetSettings = targetGrid._settings;
        var targetContainer = targetSettings.dragContainer || targetGridElement;
        var currentSettings = this._getGrid()._settings;
        var currentContainer = element.parentNode;
        var currentVisClass = isActive
            ? currentSettings.itemVisibleClass
            : currentSettings.itemHiddenClass;
        var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass;
        var translate;
        var offsetDiff;

        // Destroy current drag. Note that we need to set the migrating flag to
        // false first, because otherwise we create an infinite loop between this
        // and the drag.stop() method.
        this._isMigrating = false;
        this.destroy();

        // Update item class.
        if (currentSettings.itemClass !== targetSettings.itemClass) {
            removeClass(element, currentSettings.itemClass);
            addClass(element, targetSettings.itemClass);
        }

        // Update visibility class.
        if (currentVisClass !== nextVisClass) {
            removeClass(element, currentVisClass);
            addClass(element, nextVisClass);
        }

        // Move the item inside the target container if it's different than the
        // current container.
        if (targetContainer !== currentContainer) {
            targetContainer.appendChild(element);
            offsetDiff = getOffsetDiff(currentContainer, targetContainer, true);
            translate = getTranslate(element);
            translate.x -= offsetDiff.left;
            translate.y -= offsetDiff.top;
        }

        // Update item's cached dimensions.
        item._refreshDimensions();

        // Calculate the offset difference between target's drag container (if any)
        // and actual grid container element. We save it later for the release
        // process.
        offsetDiff = getOffsetDiff(targetContainer, targetGridElement, true);
        release._containerDiffX = offsetDiff.left;
        release._containerDiffY = offsetDiff.top;

        // Recreate item's drag handler.
        item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;

        // Adjust the position of the item element if it was moved from a container
        // to another.
        if (targetContainer !== currentContainer) {
            item._setTranslate(translate.x, translate.y);
        }

        // Update child element's styles to reflect the current visibility state.
        item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles);

        // Start the release.
        release.start();
    };

    /**
     * Drag pre-start handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._preStartCheck = function (event) {
        // Let's activate drag start predicate state.
        if (this._startPredicateState === START_PREDICATE_INACTIVE) {
            this._startPredicateState = START_PREDICATE_PENDING;
        }

        // If predicate is pending try to resolve it.
        if (this._startPredicateState === START_PREDICATE_PENDING) {
            this._startPredicateResult = this._startPredicate(this._item, event);
            if (this._startPredicateResult === true) {
                this._startPredicateState = START_PREDICATE_RESOLVED;
                this._onStart(event);
            } else if (this._startPredicateResult === false) {
                this._resetStartPredicate(event);
                this._dragger._reset();
                this._startPredicateState = START_PREDICATE_INACTIVE;
            }
        }

        // Otherwise if predicate is resolved and drag is active, move the item.
        else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) {
            this._onMove(event);
        }
    };

    /**
     * Drag pre-end handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._preEndCheck = function (event) {
        var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED;

        // Do final predicate check to allow user to unbind stuff for the current
        // drag procedure within the predicate callback. The return value of this
        // check will have no effect to the state of the predicate.
        this._startPredicate(this._item, event);

        this._startPredicateState = START_PREDICATE_INACTIVE;

        if (!isResolved || !this._isActive) return;

        if (this._isStarted) {
            this._onEnd(event);
        } else {
            this.stop();
        }
    };

    /**
     * Drag start handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._onStart = function (event) {
        var item = this._item;
        if (!item._isActive) return;

        this._isActive = true;
        this._dragStartEvent = event;
        ItemDrag.autoScroller.addItem(item);

        addDragStartTick(item._id, this._prepareStart, this._applyStart);
    };

    /**
     * Prepare item to be dragged.
     *
     * @private
     *  ItemDrag.prototype
     */
    ItemDrag.prototype._prepareStart = function () {
        if (!this._isActive) return;

        var item = this._item;
        if (!item._isActive) return;

        var element = item._element;
        var grid = this._getGrid();
        var settings = grid._settings;
        var gridContainer = grid._element;
        var dragContainer = settings.dragContainer || gridContainer;
        var containingBlock = getContainingBlock(dragContainer);
        var translate = getTranslate(element);
        var elementRect = element.getBoundingClientRect();
        var hasDragContainer = dragContainer !== gridContainer;

        this._container = dragContainer;
        this._containingBlock = containingBlock;
        this._clientX = elementRect.left;
        this._clientY = elementRect.top;
        this._left = this._gridX = translate.x;
        this._top = this._gridY = translate.y;
        this._scrollDiffX = this._scrollDiffY = 0;
        this._moveDiffX = this._moveDiffY = 0;

        this._resetHeuristics(this._gridX, this._gridY);

        // If a specific drag container is set and it is different from the
        // grid's container element we store the offset between containers.
        if (hasDragContainer) {
            var offsetDiff = getOffsetDiff(containingBlock, gridContainer);
            this._containerDiffX = offsetDiff.left;
            this._containerDiffY = offsetDiff.top;
        }
    };

    /**
     * Start drag for the item.
     *
     * @private
     */
    ItemDrag.prototype._applyStart = function () {
        if (!this._isActive) return;

        var item = this._item;
        if (!item._isActive) return;

        var grid = this._getGrid();
        var element = item._element;
        var release = item._dragRelease;
        var migrate = item._migrate;
        var hasDragContainer = this._container !== grid._element;

        if (item.isPositioning()) {
            item._layout.stop(true, this._left, this._top);
        }

        if (migrate._isActive) {
            this._left -= migrate._containerDiffX;
            this._top -= migrate._containerDiffY;
            this._gridX -= migrate._containerDiffX;
            this._gridY -= migrate._containerDiffY;
            migrate.stop(true, this._left, this._top);
        }

        if (item.isReleasing()) {
            release._reset();
        }

        if (grid._settings.dragPlaceholder.enabled) {
            item._dragPlaceholder.create();
        }

        this._isStarted = true;

        grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent);

        if (hasDragContainer) {
            // If the dragged element is a child of the drag container all we need to
            // do is setup the relative drag position data.
            if (element.parentNode === this._container) {
                this._gridX -= this._containerDiffX;
                this._gridY -= this._containerDiffY;
            }
                // Otherwise we need to append the element inside the correct container,
                // setup the actual drag position data and adjust the element's translate
            // values to account for the DOM position shift.
            else {
                this._left += this._containerDiffX;
                this._top += this._containerDiffY;
                this._container.appendChild(element);
                item._setTranslate(this._left, this._top);
            }
        }

        addClass(element, grid._settings.itemDraggingClass);
        this._bindScrollListeners();
        grid._emit(EVENT_DRAG_START, item, this._dragStartEvent);
    };

    /**
     * Drag move handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._onMove = function (event) {
        var item = this._item;

        if (!item._isActive) {
            this.stop();
            return;
        }

        this._dragMoveEvent = event;
        addDragMoveTick(item._id, this._prepareMove, this._applyMove);
        addDragSortTick(item._id, this._handleSort);
    };

    /**
     * Prepare dragged item for moving.
     *
     * @private
     */
    ItemDrag.prototype._prepareMove = function () {
        if (!this._isActive) return;

        var item = this._item;
        if (!item._isActive) return;

        var settings = this._getGrid()._settings;
        var axis = settings.dragAxis;
        var nextEvent = this._dragMoveEvent;
        var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent;

        // Update horizontal position data.
        if (axis !== 'y') {
            var moveDiffX = nextEvent.clientX - prevEvent.clientX;
            this._left = this._left - this._moveDiffX + moveDiffX;
            this._gridX = this._gridX - this._moveDiffX + moveDiffX;
            this._clientX = this._clientX - this._moveDiffX + moveDiffX;
            this._moveDiffX = moveDiffX;
        }

        // Update vertical position data.
        if (axis !== 'x') {
            var moveDiffY = nextEvent.clientY - prevEvent.clientY;
            this._top = this._top - this._moveDiffY + moveDiffY;
            this._gridY = this._gridY - this._moveDiffY + moveDiffY;
            this._clientY = this._clientY - this._moveDiffY + moveDiffY;
            this._moveDiffY = moveDiffY;
        }

        this._dragPrevMoveEvent = nextEvent;
    };

    /**
     * Apply movement to dragged item.
     *
     * @private
     */
    ItemDrag.prototype._applyMove = function () {
        if (!this._isActive) return;

        var item = this._item;
        if (!item._isActive) return;

        this._moveDiffX = this._moveDiffY = 0;
        item._setTranslate(this._left, this._top);
        this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent);
        ItemDrag.autoScroller.updateItem(item);
    };

    /**
     * Drag scroll handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._onScroll = function (event) {
        var item = this._item;

        if (!item._isActive) {
            this.stop();
            return;
        }

        this._scrollEvent = event;
        addDragScrollTick(item._id, this._prepareScroll, this._applyScroll);
        addDragSortTick(item._id, this._handleSort);
    };

    /**
     * Prepare dragged item for scrolling.
     *
     * @private
     */
    ItemDrag.prototype._prepareScroll = function () {
        if (!this._isActive) return;

        // If item is not active do nothing.
        var item = this._item;
        if (!item._isActive) return;

        var element = item._element;
        var grid = this._getGrid();
        var gridContainer = grid._element;
        var rect = element.getBoundingClientRect();

        // Update container diff.
        if (this._container !== gridContainer) {
            var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer);
            this._containerDiffX = offsetDiff.left;
            this._containerDiffY = offsetDiff.top;
        }

        // Update horizontal position data.
        var scrollDiffX = this._clientX - this._moveDiffX - rect.left;
        this._left = this._left - this._scrollDiffX + scrollDiffX;
        this._scrollDiffX = scrollDiffX;

        // Update vertical position data.
        var scrollDiffY = this._clientY - this._moveDiffY - rect.top;
        this._top = this._top - this._scrollDiffY + scrollDiffY;
        this._scrollDiffY = scrollDiffY;

        // Update grid position.
        this._gridX = this._left - this._containerDiffX;
        this._gridY = this._top - this._containerDiffY;
    };

    /**
     * Apply scroll to dragged item.
     *
     * @private
     */
    ItemDrag.prototype._applyScroll = function () {
        if (!this._isActive) return;

        var item = this._item;
        if (!item._isActive) return;

        this._scrollDiffX = this._scrollDiffY = 0;
        item._setTranslate(this._left, this._top);
        this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent);
    };

    /**
     * Drag end handler.
     *
     * @private
     * @param {Object} event
     */
    ItemDrag.prototype._onEnd = function (event) {
        var item = this._item;
        var element = item._element;
        var grid = this._getGrid();
        var settings = grid._settings;
        var release = item._dragRelease;

        // If item is not active, reset drag.
        if (!item._isActive) {
            this.stop();
            return;
        }

        // Cancel queued ticks.
        cancelDragStartTick(item._id);
        cancelDragMoveTick(item._id);
        cancelDragScrollTick(item._id);

        // Finish sort procedure (does final overlap check if needed).
        this._finishSort();

        // Remove scroll listeners.
        this._unbindScrollListeners();

        // Setup release data.
        release._containerDiffX = this._containerDiffX;
        release._containerDiffY = this._containerDiffY;

        // Reset drag data.
        this._reset();

        // Remove drag class name from element.
        removeClass(element, settings.itemDraggingClass);

        // Stop auto-scroll.
        ItemDrag.autoScroller.removeItem(item);

        // Emit dragEnd event.
        grid._emit(EVENT_DRAG_END, item, event);

        // Finish up the migration process or start the release process.
        this._isMigrating ? this._finishMigration() : release.start();
    };

    /**
     * Private helpers
     * ***************
     */

    /**
     * Check if an element is an anchor element and open the href url if possible.
     *
     * @param {HTMLElement} element
     */
    function openAnchorHref(element) {
        // Make sure the element is anchor element.
        if (element.tagName.toLowerCase() !== 'a') return;

        // Get href and make sure it exists.
        var href = element.getAttribute('href');
        if (!href) return;

        // Finally let's navigate to the link href.
        var target = element.getAttribute('target');
        if (target && target !== '_self') {
            window.open(href, target);
        } else {
            window.location.href = href;
        }
    }

    /**
     * Get current values of the provided styles definition object or array.
     *
     * @param {HTMLElement} element
     * @param {(Object|Array} styles
     * @return {Object}
     */
    function getCurrentStyles(element, styles) {
        var result = {};
        var prop, i;

        if (Array.isArray(styles)) {
            for (i = 0; i < styles.length; i++) {
                prop = styles[i];
                result[prop] = getStyle(element, getStyleName(prop));
            }
        } else {
            for (prop in styles) {
                result[prop] = getStyle(element, getStyleName(prop));
            }
        }

        return result;
    }

    var unprefixRegEx = /^(webkit|moz|ms|o|Webkit|Moz|MS|O)(?=[A-Z])/;
    var cache = {};

    /**
     * Remove any potential vendor prefixes from a property name.
     *
     * @param {String} prop
     * @returns {String}
     */
    function getUnprefixedPropName(prop) {
        var result = cache[prop];
        if (result) return result;

        result = prop.replace(unprefixRegEx, '');

        if (result !== prop) {
            result = result[0].toLowerCase() + result.slice(1);
        }

        cache[prop] = result;

        return result;
    }

    var nativeCode = '[native code]';

    /**
     * Check if a value (e.g. a method or constructor) is native code. Good for
     * detecting when a polyfill is used and when not.
     *
     * @param {*} feat
     * @returns {Boolean}
     */
    function isNative(feat) {
        var S = window.Symbol;
        return !!(
            feat &&
            isFunction(S) &&
            isFunction(S.toString) &&
            S(feat).toString().indexOf(nativeCode) > -1
        );
    }

    /**
     * Set inline styles to an element.
     *
     * @param {HTMLElement} element
     * @param {Object} styles
     */
    function setStyles(element, styles) {
        for (var prop in styles) {
            element.style[prop] = styles[prop];
        }
    }

    var HAS_WEB_ANIMATIONS = !!(Element && isFunction(Element.prototype.animate));
    var HAS_NATIVE_WEB_ANIMATIONS = !!(Element && isNative(Element.prototype.animate));

    /**
     * Item animation handler powered by Web Animations API.
     *
     * @class
     * @param {HTMLElement} element
     */
    function Animator(element) {
        this._element = element;
        this._animation = null;
        this._duration = 0;
        this._easing = '';
        this._callback = null;
        this._props = [];
        this._values = [];
        this._isDestroyed = false;
        this._onFinish = this._onFinish.bind(this);
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Start instance's animation. Automatically stops current animation if it is
     * running.
     *
     * @public
     * @param {Object} propsFrom
     * @param {Object} propsTo
     * @param {Object} [options]
     * @param {Number} [options.duration=300]
     * @param {String} [options.easing='ease']
     * @param {Function} [options.onFinish]
     */
    Animator.prototype.start = function (propsFrom, propsTo, options) {
        if (this._isDestroyed) return;

        var element = this._element;
        var opts = options || {};

        // If we don't have web animations available let's not animate.
        if (!HAS_WEB_ANIMATIONS) {
            setStyles(element, propsTo);
            this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;
            this._onFinish();
            return;
        }

        var animation = this._animation;
        var currentProps = this._props;
        var currentValues = this._values;
        var duration = opts.duration || 300;
        var easing = opts.easing || 'ease';
        var cancelAnimation = false;
        var propName, propCount, propIndex;

        // If we have an existing animation running, let's check if it needs to be
        // cancelled or if it can continue running.
        if (animation) {
            propCount = 0;

            // Cancel animation if duration or easing has changed.
            if (duration !== this._duration || easing !== this._easing) {
                cancelAnimation = true;
            }

            // Check if the requested animation target props and values match with the
            // current props and values.
            if (!cancelAnimation) {
                for (propName in propsTo) {
                    ++propCount;
                    propIndex = currentProps.indexOf(propName);
                    if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) {
                        cancelAnimation = true;
                        break;
                    }
                }

                // Check if the target props count matches current props count. This is
                // needed for the edge case scenario where target props contain the same
                // styles as current props, but the current props have some additional
                // props.
                if (propCount !== currentProps.length) {
                    cancelAnimation = true;
                }
            }
        }

        // Cancel animation (if required).
        if (cancelAnimation) animation.cancel();

        // Store animation callback.
        this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;

        // If we have a running animation that does not need to be cancelled, let's
        // call it a day here and let it run.
        if (animation && !cancelAnimation) return;

        // Store target props and values to instance.
        currentProps.length = currentValues.length = 0;
        for (propName in propsTo) {
            currentProps.push(propName);
            currentValues.push(propsTo[propName]);
        }

        // Start the animation. We need to provide unprefixed property names to the
        // Web Animations polyfill if it is being used. If we have native Web
        // Animations available we need to provide prefixed properties instead.
        this._duration = duration;
        this._easing = easing;
        this._animation = element.animate(
            [
                createFrame(propsFrom, HAS_NATIVE_WEB_ANIMATIONS),
                createFrame(propsTo, HAS_NATIVE_WEB_ANIMATIONS),
            ],
            {
                duration: duration,
                easing: easing,
            }
        );
        this._animation.onfinish = this._onFinish;

        // Set the end styles. This makes sure that the element stays at the end
        // values after animation is finished.
        setStyles(element, propsTo);
    };

    /**
     * Stop instance's current animation if running.
     *
     * @public
     */
    Animator.prototype.stop = function () {
        if (this._isDestroyed || !this._animation) return;
        this._animation.cancel();
        this._animation = this._callback = null;
        this._props.length = this._values.length = 0;
    };

    /**
     * Read the current values of the element's animated styles from the DOM.
     *
     * @public
     * @return {Object}
     */
    Animator.prototype.getCurrentStyles = function () {
        return getCurrentStyles(element, currentProps);
    };

    /**
     * Check if the item is being animated currently.
     *
     * @public
     * @return {Boolean}
     */
    Animator.prototype.isAnimating = function () {
        return !!this._animation;
    };

    /**
     * Destroy the instance and stop current animation if it is running.
     *
     * @public
     */
    Animator.prototype.destroy = function () {
        if (this._isDestroyed) return;
        this.stop();
        this._element = null;
        this._isDestroyed = true;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Animation end handler.
     *
     * @private
     */
    Animator.prototype._onFinish = function () {
        var callback = this._callback;
        this._animation = this._callback = null;
        this._props.length = this._values.length = 0;
        callback && callback();
    };

    /**
     * Private helpers
     * ***************
     */

    function createFrame(props, prefix) {
        var frame = {};
        for (var prop in props) {
            frame[prefix ? prop : getUnprefixedPropName(prop)] = props[prop];
        }
        return frame;
    }

    /**
     * Transform translateX and translateY value into CSS transform style
     * property's value.
     *
     * @param {Number} x
     * @param {Number} y
     * @returns {String}
     */
    function getTranslateString(x, y) {
        return 'translateX(' + x + 'px) translateY(' + y + 'px)';
    }

    /**
     * Drag placeholder.
     *
     * @class
     * @param {Item} item
     */
    function ItemDragPlaceholder(item) {
        this._item = item;
        this._animation = new Animator();
        this._element = null;
        this._className = '';
        this._didMigrate = false;
        this._resetAfterLayout = false;
        this._left = 0;
        this._top = 0;
        this._transX = 0;
        this._transY = 0;
        this._nextTransX = 0;
        this._nextTransY = 0;

        // Bind animation handlers.
        this._setupAnimation = this._setupAnimation.bind(this);
        this._startAnimation = this._startAnimation.bind(this);
        this._updateDimensions = this._updateDimensions.bind(this);

        // Bind event handlers.
        this._onLayoutStart = this._onLayoutStart.bind(this);
        this._onLayoutEnd = this._onLayoutEnd.bind(this);
        this._onReleaseEnd = this._onReleaseEnd.bind(this);
        this._onMigrate = this._onMigrate.bind(this);
        this._onHide = this._onHide.bind(this);
    }

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Update placeholder's dimensions to match the item's dimensions.
     *
     * @private
     */
    ItemDragPlaceholder.prototype._updateDimensions = function () {
        if (!this.isActive()) return;
        setStyles(this._element, {
            width: this._item._width + 'px',
            height: this._item._height + 'px',
        });
    };

    /**
     * Move placeholder to a new position.
     *
     * @private
     * @param {Item[]} items
     * @param {Boolean} isInstant
     */
    ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) {
        var item = this._item;

        // If the item is not part of the layout anymore reset placeholder.
        if (items.indexOf(item) === -1) {
            this.reset();
            return;
        }

        var nextLeft = item._left;
        var nextTop = item._top;
        var currentLeft = this._left;
        var currentTop = this._top;

        // Keep track of item layout position.
        this._left = nextLeft;
        this._top = nextTop;

        // If item's position did not change, and the item did not migrate and the
        // layout is not instant and we can safely skip layout.
        if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) {
            return;
        }

        // Slots data is calculated with item margins added to them so we need to add
        // item's left and top margin to the slot data to get the placeholder's
        // next position.
        var nextX = nextLeft + item._marginLeft;
        var nextY = nextTop + item._marginTop;

        // Just snap to new position without any animations if no animation is
        // required or if placeholder moves between grids.
        var grid = item.getGrid();
        var animEnabled = !isInstant && grid._settings.layoutDuration > 0;
        if (!animEnabled || this._didMigrate) {
            // Cancel potential (queued) layout tick.
            cancelPlaceholderLayoutTick(item._id);

            // Snap placeholder to correct position.
            this._element.style[transformProp] = getTranslateString(nextX, nextY);
            this._animation.stop();

            // Move placeholder inside correct container after migration.
            if (this._didMigrate) {
                grid.getElement().appendChild(this._element);
                this._didMigrate = false;
            }

            return;
        }

        // Start the placeholder's layout animation in the next tick. We do this to
        // avoid layout thrashing.
        this._nextTransX = nextX;
        this._nextTransY = nextY;
        addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation);
    };

    /**
     * Prepare placeholder for layout animation.
     *
     * @private
     */
    ItemDragPlaceholder.prototype._setupAnimation = function () {
        if (!this.isActive()) return;

        var translate = getTranslate(this._element);
        this._transX = translate.x;
        this._transY = translate.y;
    };

    /**
     * Start layout animation.
     *
     * @private
     */
    ItemDragPlaceholder.prototype._startAnimation = function () {
        if (!this.isActive()) return;

        var animation = this._animation;
        var currentX = this._transX;
        var currentY = this._transY;
        var nextX = this._nextTransX;
        var nextY = this._nextTransY;

        // If placeholder is already in correct position let's just stop animation
        // and be done with it.
        if (currentX === nextX && currentY === nextY) {
            if (animation.isAnimating()) {
                this._element.style[transformProp] = getTranslateString(nextX, nextY);
                animation.stop();
            }
            return;
        }

        // Otherwise let's start the animation.
        var settings = this._item.getGrid()._settings;
        var currentStyles = {};
        var targetStyles = {};
        currentStyles[transformProp] = getTranslateString(currentX, currentY);
        targetStyles[transformProp] = getTranslateString(nextX, nextY);
        animation.start(currentStyles, targetStyles, {
            duration: settings.layoutDuration,
            easing: settings.layoutEasing,
            onFinish: this._onLayoutEnd,
        });
    };

    /**
     * Layout end handler.
     *
     * @private
     */
    ItemDragPlaceholder.prototype._onLayoutEnd = function () {
        if (this._resetAfterLayout) {
            this.reset();
        }
    };

    /**
     * Drag end handler. This handler is called when dragReleaseEnd event is
     * emitted and receives the event data as it's argument.
     *
     * @private
     * @param {Item} item
     */
    ItemDragPlaceholder.prototype._onReleaseEnd = function (item) {
        if (item._id === this._item._id) {
            // If the placeholder is not animating anymore we can safely reset it.
            if (!this._animation.isAnimating()) {
                this.reset();
                return;
            }

            // If the placeholder item is still animating here, let's wait for it to
            // finish it's animation.
            this._resetAfterLayout = true;
        }
    };

    /**
     * Migration start handler. This handler is called when beforeSend event is
     * emitted and receives the event data as it's argument.
     *
     * @private
     * @param {Object} data
     * @param {Item} data.item
     * @param {Grid} data.fromGrid
     * @param {Number} data.fromIndex
     * @param {Grid} data.toGrid
     * @param {Number} data.toIndex
     */
    ItemDragPlaceholder.prototype._onMigrate = function (data) {
        // Make sure we have a matching item.
        if (data.item !== this._item) return;

        var grid = this._item.getGrid();
        var nextGrid = data.toGrid;

        // Unbind listeners from current grid.
        grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
        grid.off(EVENT_LAYOUT_START, this._onLayoutStart);
        grid.off(EVENT_BEFORE_SEND, this._onMigrate);
        grid.off(EVENT_HIDE_START, this._onHide);

        // Bind listeners to the next grid.
        nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
        nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart);
        nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate);
        nextGrid.on(EVENT_HIDE_START, this._onHide);

        // Mark the item as migrated.
        this._didMigrate = true;
    };

    /**
     * Reset placeholder if the associated item is hidden.
     *
     * @private
     * @param {Item[]} items
     */
    ItemDragPlaceholder.prototype._onHide = function (items) {
        if (items.indexOf(this._item) > -1) this.reset();
    };

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Create placeholder. Note that this method only writes to DOM and does not
     * read anything from DOM so it should not cause any additional layout
     * thrashing when it's called at the end of the drag start procedure.
     *
     * @public
     */
    ItemDragPlaceholder.prototype.create = function () {
        // If we already have placeholder set up we can skip the initiation logic.
        if (this.isActive()) {
            this._resetAfterLayout = false;
            return;
        }

        var item = this._item;
        var grid = item.getGrid();
        var settings = grid._settings;
        var animation = this._animation;

        // Keep track of layout position.
        this._left = item._left;
        this._top = item._top;

        // Create placeholder element.
        var element;
        if (isFunction(settings.dragPlaceholder.createElement)) {
            element = settings.dragPlaceholder.createElement(item);
        } else {
            element = document.createElement('div');
        }
        this._element = element;

        // Update element to animation instance.
        animation._element = element;

        // Add placeholder class to the placeholder element.
        this._className = settings.itemPlaceholderClass || '';
        if (this._className) {
            addClass(element, this._className);
        }

        // Set initial styles.
        setStyles(element, {
            position: 'absolute',
            left: '0px',
            top: '0px',
            width: item._width + 'px',
            height: item._height + 'px',
        });

        // Set initial position.
        element.style[transformProp] = getTranslateString(
            item._left + item._marginLeft,
            item._top + item._marginTop
        );

        // Bind event listeners.
        grid.on(EVENT_LAYOUT_START, this._onLayoutStart);
        grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
        grid.on(EVENT_BEFORE_SEND, this._onMigrate);
        grid.on(EVENT_HIDE_START, this._onHide);

        // onCreate hook.
        if (isFunction(settings.dragPlaceholder.onCreate)) {
            settings.dragPlaceholder.onCreate(item, element);
        }

        // Insert the placeholder element to the grid.
        grid.getElement().appendChild(element);
    };

    /**
     * Reset placeholder data.
     *
     * @public
     */
    ItemDragPlaceholder.prototype.reset = function () {
        if (!this.isActive()) return;

        var element = this._element;
        var item = this._item;
        var grid = item.getGrid();
        var settings = grid._settings;
        var animation = this._animation;

        // Reset flag.
        this._resetAfterLayout = false;

        // Cancel potential (queued) layout tick.
        cancelPlaceholderLayoutTick(item._id);
        cancelPlaceholderResizeTick(item._id);

        // Reset animation instance.
        animation.stop();
        animation._element = null;

        // Unbind event listeners.
        grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd);
        grid.off(EVENT_LAYOUT_START, this._onLayoutStart);
        grid.off(EVENT_BEFORE_SEND, this._onMigrate);
        grid.off(EVENT_HIDE_START, this._onHide);

        // Remove placeholder class from the placeholder element.
        if (this._className) {
            removeClass(element, this._className);
            this._className = '';
        }

        // Remove element.
        element.parentNode.removeChild(element);
        this._element = null;

        // onRemove hook. Note that here we use the current grid's onRemove callback
        // so if the item has migrated during drag the onRemove method will not be
        // the originating grid's method.
        if (isFunction(settings.dragPlaceholder.onRemove)) {
            settings.dragPlaceholder.onRemove(item, element);
        }
    };

    /**
     * Check if placeholder is currently active (visible).
     *
     * @public
     * @returns {Boolean}
     */
    ItemDragPlaceholder.prototype.isActive = function () {
        return !!this._element;
    };

    /**
     * Get placeholder element.
     *
     * @public
     * @returns {?HTMLElement}
     */
    ItemDragPlaceholder.prototype.getElement = function () {
        return this._element;
    };

    /**
     * Update placeholder's dimensions to match the item's dimensions. Note that
     * the updating is done asynchronously in the next tick to avoid layout
     * thrashing.
     *
     * @public
     */
    ItemDragPlaceholder.prototype.updateDimensions = function () {
        if (!this.isActive()) return;
        addPlaceholderResizeTick(this._item._id, this._updateDimensions);
    };

    /**
     * Destroy placeholder instance.
     *
     * @public
     */
    ItemDragPlaceholder.prototype.destroy = function () {
        this.reset();
        this._animation.destroy();
        this._item = this._animation = null;
    };

    /**
     * The release process handler constructor. Although this might seem as proper
     * fit for the drag process this needs to be separated into it's own logic
     * because there might be a scenario where drag is disabled, but the release
     * process still needs to be implemented (dragging from a grid to another).
     *
     * @class
     * @param {Item} item
     */
    function ItemDragRelease(item) {
        this._item = item;
        this._isActive = false;
        this._isDestroyed = false;
        this._isPositioningStarted = false;
        this._containerDiffX = 0;
        this._containerDiffY = 0;
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Start the release process of an item.
     *
     * @public
     */
    ItemDragRelease.prototype.start = function () {
        if (this._isDestroyed || this._isActive) return;

        var item = this._item;
        var grid = item.getGrid();
        var settings = grid._settings;

        this._isActive = true;
        addClass(item._element, settings.itemReleasingClass);
        if (!settings.dragRelease.useDragContainer) {
            this._placeToGrid();
        }
        grid._emit(EVENT_DRAG_RELEASE_START, item);

        // Let's start layout manually _only_ if there is no unfinished layout in
        // about to finish.
        if (!grid._nextLayoutData) item._layout.start(false);
    };

    /**
     * End the release process of an item. This method can be used to abort an
     * ongoing release process (animation) or finish the release process.
     *
     * @public
     * @param {Boolean} [abort=false]
     *  - Should the release be aborted? When true, the release end event won't be
     *    emitted. Set to true only when you need to abort the release process
     *    while the item is animating to it's position.
     * @param {Number} [left]
     *  - The element's current translateX value (optional).
     * @param {Number} [top]
     *  - The element's current translateY value (optional).
     */
    ItemDragRelease.prototype.stop = function (abort, left, top) {
        if (this._isDestroyed || !this._isActive) return;

        var item = this._item;
        var grid = item.getGrid();

        if (!abort && (left === undefined || top === undefined)) {
            left = item._left;
            top = item._top;
        }

        var didReparent = this._placeToGrid(left, top);
        this._reset(didReparent);

        if (!abort) grid._emit(EVENT_DRAG_RELEASE_END, item);
    };

    ItemDragRelease.prototype.isJustReleased = function () {
        return this._isActive && this._isPositioningStarted === false;
    };

    /**
     * Destroy instance.
     *
     * @public
     */
    ItemDragRelease.prototype.destroy = function () {
        if (this._isDestroyed) return;
        this.stop(true);
        this._item = null;
        this._isDestroyed = true;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Move the element back to the grid container element if it does not exist
     * there already.
     *
     * @private
     * @param {Number} [left]
     *  - The element's current translateX value (optional).
     * @param {Number} [top]
     *  - The element's current translateY value (optional).
     * @returns {Boolean}
     *   - Returns `true` if the element was reparented.
     */
    ItemDragRelease.prototype._placeToGrid = function (left, top) {
        if (this._isDestroyed) return;

        var item = this._item;
        var element = item._element;
        var container = item.getGrid()._element;
        var didReparent = false;

        if (element.parentNode !== container) {
            if (left === undefined || top === undefined) {
                var translate = getTranslate(element);
                left = translate.x - this._containerDiffX;
                top = translate.y - this._containerDiffY;
            }

            container.appendChild(element);
            item._setTranslate(left, top);
            didReparent = true;
        }

        this._containerDiffX = 0;
        this._containerDiffY = 0;

        return didReparent;
    };

    /**
     * Reset data and remove releasing class.
     *
     * @private
     * @param {Boolean} [needsReflow]
     */
    ItemDragRelease.prototype._reset = function (needsReflow) {
        if (this._isDestroyed) return;

        var item = this._item;
        var releasingClass = item.getGrid()._settings.itemReleasingClass;

        this._isActive = false;
        this._isPositioningStarted = false;
        this._containerDiffX = 0;
        this._containerDiffY = 0;

        // If the element was just reparented we need to do a forced reflow to remove
        // the class gracefully.
        if (releasingClass) {
            // eslint-disable-next-line
            if (needsReflow) item._element.clientWidth;
            removeClass(item._element, releasingClass);
        }
    };

    var MIN_ANIMATION_DISTANCE = 2;

    /**
     * Layout manager for Item instance, handles the positioning of an item.
     *
     * @class
     * @param {Item} item
     */
    function ItemLayout(item) {
        var element = item._element;
        var elementStyle = element.style;

        this._item = item;
        this._isActive = false;
        this._isDestroyed = false;
        this._isInterrupted = false;
        this._currentStyles = {};
        this._targetStyles = {};
        this._nextLeft = 0;
        this._nextTop = 0;
        this._offsetLeft = 0;
        this._offsetTop = 0;
        this._skipNextAnimation = false;
        this._animOptions = {
            onFinish: this._finish.bind(this),
            duration: 0,
            easing: 0,
        };

        // Set element's initial position styles.
        elementStyle.left = '0px';
        elementStyle.top = '0px';
        item._setTranslate(0, 0);

        this._animation = new Animator(element);
        this._queue = 'layout-' + item._id;

        // Bind animation handlers and finish method.
        this._setupAnimation = this._setupAnimation.bind(this);
        this._startAnimation = this._startAnimation.bind(this);
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Start item layout based on it's current data.
     *
     * @public
     * @param {Boolean} instant
     * @param {Function} [onFinish]
     */
    ItemLayout.prototype.start = function (instant, onFinish) {
        if (this._isDestroyed) return;

        var item = this._item;
        var release = item._dragRelease;
        var gridSettings = item.getGrid()._settings;
        var isPositioning = this._isActive;
        var isJustReleased = release.isJustReleased();
        var animDuration = isJustReleased
            ? gridSettings.dragRelease.duration
            : gridSettings.layoutDuration;
        var animEasing = isJustReleased ? gridSettings.dragRelease.easing : gridSettings.layoutEasing;
        var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0;

        // If the item is currently positioning cancel potential queued layout tick
        // and process current layout callback queue with interrupted flag on.
        if (isPositioning) {
            cancelLayoutTick(item._id);
            item._emitter.burst(this._queue, true, item);
        }

        // Mark release positioning as started.
        if (isJustReleased) release._isPositioningStarted = true;

        // Push the callback to the callback queue.
        if (isFunction(onFinish)) {
            item._emitter.on(this._queue, onFinish);
        }

        // Reset animation skipping flag.
        this._skipNextAnimation = false;

        // If no animations are needed, easy peasy!
        if (!animEnabled) {
            this._updateOffsets();
            item._setTranslate(this._nextLeft, this._nextTop);
            this._animation.stop();
            this._finish();
            return;
        }

        // Let's make sure an ongoing animation's callback is cancelled before going
        // further. Without this there's a chance that the animation will finish
        // before the next tick and mess up our logic.
        if (this._animation.isAnimating()) {
            this._animation._animation.onfinish = null;
        }

        // Kick off animation to be started in the next tick.
        this._isActive = true;
        this._animOptions.easing = animEasing;
        this._animOptions.duration = animDuration;
        this._isInterrupted = isPositioning;
        addLayoutTick(item._id, this._setupAnimation, this._startAnimation);
    };

    /**
     * Stop item's position animation if it is currently animating.
     *
     * @public
     * @param {Boolean} processCallbackQueue
     * @param {Number} [left]
     * @param {Number} [top]
     */
    ItemLayout.prototype.stop = function (processCallbackQueue, left, top) {
        if (this._isDestroyed || !this._isActive) return;

        var item = this._item;

        // Cancel animation init.
        cancelLayoutTick(item._id);

        // Stop animation.
        if (this._animation.isAnimating()) {
            if (left === undefined || top === undefined) {
                var translate = getTranslate(item._element);
                left = translate.x;
                top = translate.y;
            }
            item._setTranslate(left, top);
            this._animation.stop();
        }

        // Remove positioning class.
        removeClass(item._element, item.getGrid()._settings.itemPositioningClass);

        // Reset active state.
        this._isActive = false;

        // Process callback queue if needed.
        if (processCallbackQueue) {
            item._emitter.burst(this._queue, true, item);
        }
    };

    /**
     * Destroy the instance and stop current animation if it is running.
     *
     * @public
     */
    ItemLayout.prototype.destroy = function () {
        if (this._isDestroyed) return;

        var elementStyle = this._item._element.style;

        this.stop(true, 0, 0);
        this._item._emitter.clear(this._queue);
        this._animation.destroy();

        elementStyle[transformProp] = '';
        elementStyle.left = '';
        elementStyle.top = '';

        this._item = null;
        this._currentStyles = null;
        this._targetStyles = null;
        this._animOptions = null;
        this._isDestroyed = true;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Calculate and update item's current layout offset data.
     *
     * @private
     */
    ItemLayout.prototype._updateOffsets = function () {
        if (this._isDestroyed) return;

        var item = this._item;
        var migrate = item._migrate;
        var release = item._dragRelease;

        this._offsetLeft = release._isActive
            ? release._containerDiffX
            : migrate._isActive
                ? migrate._containerDiffX
                : 0;

        this._offsetTop = release._isActive
            ? release._containerDiffY
            : migrate._isActive
                ? migrate._containerDiffY
                : 0;

        this._nextLeft = this._item._left + this._offsetLeft;
        this._nextTop = this._item._top + this._offsetTop;
    };

    /**
     * Finish item layout procedure.
     *
     * @private
     */
    ItemLayout.prototype._finish = function () {
        if (this._isDestroyed) return;

        var item = this._item;
        var migrate = item._migrate;
        var release = item._dragRelease;

        // Update internal translate values.
        item._tX = this._nextLeft;
        item._tY = this._nextTop;

        // Mark the item as inactive and remove positioning classes.
        if (this._isActive) {
            this._isActive = false;
            removeClass(item._element, item.getGrid()._settings.itemPositioningClass);
        }

        // Finish up release and migration.
        if (release._isActive) release.stop();
        if (migrate._isActive) migrate.stop();

        // Process the callback queue.
        item._emitter.burst(this._queue, false, item);
    };

    /**
     * Prepare item for layout animation.
     *
     * @private
     */
    ItemLayout.prototype._setupAnimation = function () {
        var item = this._item;
        if (item._tX === undefined || item._tY === undefined) {
            var translate = getTranslate(item._element);
            item._tX = translate.x;
            item._tY = translate.y;
        }
    };

    /**
     * Start layout animation.
     *
     * @private
     */
    ItemLayout.prototype._startAnimation = function () {
        var item = this._item;
        var settings = item.getGrid()._settings;
        var isInstant = this._animOptions.duration <= 0;

        // Let's update the offset data and target styles.
        this._updateOffsets();

        var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft));
        var yDiff = Math.abs(item._top - (item._tY - this._offsetTop));

        // If there is no need for animation or if the item is already in correct
        // position (or near it) let's finish the process early.
        if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) {
            if (xDiff || yDiff || this._isInterrupted) {
                item._setTranslate(this._nextLeft, this._nextTop);
            }
            this._animation.stop();
            this._finish();
            return;
        }

        // Set item's positioning class if needed.
        if (!this._isInterrupted) {
            addClass(item._element, settings.itemPositioningClass);
        }

        // Get current/next styles for animation.
        this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY);
        this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop);

        // Set internal translation values to undefined for the duration of the
        // animation since they will be changing on each animation frame for the
        // duration of the animation and tracking them would mean reading the DOM on
        // each frame, which is pretty darn expensive.
        item._tX = item._tY = undefined;

        // Start animation.
        this._animation.start(this._currentStyles, this._targetStyles, this._animOptions);
    };

    /**
     * The migrate process handler constructor.
     *
     * @class
     * @param {Item} item
     */
    function ItemMigrate(item) {
        // Private props.
        this._item = item;
        this._isActive = false;
        this._isDestroyed = false;
        this._container = false;
        this._containerDiffX = 0;
        this._containerDiffY = 0;
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Start the migrate process of an item.
     *
     * @public
     * @param {Grid} targetGrid
     * @param {(HTMLElement|Number|Item)} position
     * @param {HTMLElement} [container]
     */
    ItemMigrate.prototype.start = function (targetGrid, position, container) {
        if (this._isDestroyed) return;

        var item = this._item;
        var element = item._element;
        var isActive = item.isActive();
        var isVisible = item.isVisible();
        var grid = item.getGrid();
        var settings = grid._settings;
        var targetSettings = targetGrid._settings;
        var targetElement = targetGrid._element;
        var targetItems = targetGrid._items;
        var currentIndex = grid._items.indexOf(item);
        var targetContainer = container || document.body;
        var targetIndex;
        var targetItem;
        var currentContainer;
        var offsetDiff;
        var containerDiff;
        var translate;
        var translateX;
        var translateY;
        var currentVisClass;
        var nextVisClass;

        // Get target index.
        if (typeof position === 'number') {
            targetIndex = normalizeArrayIndex(targetItems, position, 1);
        } else {
            targetItem = targetGrid.getItem(position);
            if (!targetItem) return;
            targetIndex = targetItems.indexOf(targetItem);
        }

        // Get current translateX and translateY values if needed.
        if (item.isPositioning() || this._isActive || item.isReleasing()) {
            translate = getTranslate(element);
            translateX = translate.x;
            translateY = translate.y;
        }

        // Abort current positioning.
        if (item.isPositioning()) {
            item._layout.stop(true, translateX, translateY);
        }

        // Abort current migration.
        if (this._isActive) {
            translateX -= this._containerDiffX;
            translateY -= this._containerDiffY;
            this.stop(true, translateX, translateY);
        }

        // Abort current release.
        if (item.isReleasing()) {
            translateX -= item._dragRelease._containerDiffX;
            translateY -= item._dragRelease._containerDiffY;
            item._dragRelease.stop(true, translateX, translateY);
        }

        // Stop current visibility animation.
        item._visibility.stop(true);

        // Destroy current drag.
        if (item._drag) item._drag.destroy();

        // Emit beforeSend event.
        if (grid._hasListeners(EVENT_BEFORE_SEND)) {
            grid._emit(EVENT_BEFORE_SEND, {
                item: item,
                fromGrid: grid,
                fromIndex: currentIndex,
                toGrid: targetGrid,
                toIndex: targetIndex,
            });
        }

        // Emit beforeReceive event.
        if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) {
            targetGrid._emit(EVENT_BEFORE_RECEIVE, {
                item: item,
                fromGrid: grid,
                fromIndex: currentIndex,
                toGrid: targetGrid,
                toIndex: targetIndex,
            });
        }

        // Update item class.
        if (settings.itemClass !== targetSettings.itemClass) {
            removeClass(element, settings.itemClass);
            addClass(element, targetSettings.itemClass);
        }

        // Update visibility class.
        currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass;
        nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass;
        if (currentVisClass !== nextVisClass) {
            removeClass(element, currentVisClass);
            addClass(element, nextVisClass);
        }

        // Move item instance from current grid to target grid.
        grid._items.splice(currentIndex, 1);
        arrayInsert(targetItems, item, targetIndex);

        // Update item's grid id reference.
        item._gridId = targetGrid._id;

        // If item is active we need to move the item inside the target container for
        // the duration of the (potential) animation if it's different than the
        // current container.
        if (isActive) {
            currentContainer = element.parentNode;
            if (targetContainer !== currentContainer) {
                targetContainer.appendChild(element);
                offsetDiff = getOffsetDiff(targetContainer, currentContainer, true);
                if (!translate) {
                    translate = getTranslate(element);
                    translateX = translate.x;
                    translateY = translate.y;
                }
                item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top);
            }
        }
        // If item is not active let's just append it to the target grid's element.
        else {
            targetElement.appendChild(element);
        }

        // Update child element's styles to reflect the current visibility state.
        item._visibility.setStyles(
            isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles
        );

        // Get offset diff for the migration data, if the item is active.
        if (isActive) {
            containerDiff = getOffsetDiff(targetContainer, targetElement, true);
        }

        // Update item's cached dimensions.
        item._refreshDimensions();

        // Reset item's sort data.
        item._sortData = null;

        // Create new drag handler.
        item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;

        // Setup migration data.
        if (isActive) {
            this._isActive = true;
            this._container = targetContainer;
            this._containerDiffX = containerDiff.left;
            this._containerDiffY = containerDiff.top;
        } else {
            this._isActive = false;
            this._container = null;
            this._containerDiffX = 0;
            this._containerDiffY = 0;
        }

        // Emit send event.
        if (grid._hasListeners(EVENT_SEND)) {
            grid._emit(EVENT_SEND, {
                item: item,
                fromGrid: grid,
                fromIndex: currentIndex,
                toGrid: targetGrid,
                toIndex: targetIndex,
            });
        }

        // Emit receive event.
        if (targetGrid._hasListeners(EVENT_RECEIVE)) {
            targetGrid._emit(EVENT_RECEIVE, {
                item: item,
                fromGrid: grid,
                fromIndex: currentIndex,
                toGrid: targetGrid,
                toIndex: targetIndex,
            });
        }
    };

    /**
     * End the migrate process of an item. This method can be used to abort an
     * ongoing migrate process (animation) or finish the migrate process.
     *
     * @public
     * @param {Boolean} [abort=false]
     *  - Should the migration be aborted?
     * @param {Number} [left]
     *  - The element's current translateX value (optional).
     * @param {Number} [top]
     *  - The element's current translateY value (optional).
     */
    ItemMigrate.prototype.stop = function (abort, left, top) {
        if (this._isDestroyed || !this._isActive) return;

        var item = this._item;
        var element = item._element;
        var grid = item.getGrid();
        var gridElement = grid._element;
        var translate;

        if (this._container !== gridElement) {
            if (left === undefined || top === undefined) {
                if (abort) {
                    translate = getTranslate(element);
                    left = translate.x - this._containerDiffX;
                    top = translate.y - this._containerDiffY;
                } else {
                    left = item._left;
                    top = item._top;
                }
            }

            gridElement.appendChild(element);
            item._setTranslate(left, top);
        }

        this._isActive = false;
        this._container = null;
        this._containerDiffX = 0;
        this._containerDiffY = 0;
    };

    /**
     * Destroy instance.
     *
     * @public
     */
    ItemMigrate.prototype.destroy = function () {
        if (this._isDestroyed) return;
        this.stop(true);
        this._item = null;
        this._isDestroyed = true;
    };

    /**
     * Visibility manager for Item instance, handles visibility of an item.
     *
     * @class
     * @param {Item} item
     */
    function ItemVisibility(item) {
        var isActive = item._isActive;
        var element = item._element;
        var childElement = element.children[0];
        var settings = item.getGrid()._settings;

        if (!childElement) {
            throw new Error('No valid child element found within item element.');
        }

        this._item = item;
        this._isDestroyed = false;
        this._isHidden = !isActive;
        this._isHiding = false;
        this._isShowing = false;
        this._childElement = childElement;
        this._currentStyleProps = [];
        this._animation = new Animator(childElement);
        this._queue = 'visibility-' + item._id;
        this._finishShow = this._finishShow.bind(this);
        this._finishHide = this._finishHide.bind(this);

        element.style.display = isActive ? '' : 'none';
        addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass);
        this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles);
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Show item.
     *
     * @public
     * @param {Boolean} instant
     * @param {Function} [onFinish]
     */
    ItemVisibility.prototype.show = function (instant, onFinish) {
        if (this._isDestroyed) return;

        var item = this._item;
        var element = item._element;
        var callback = isFunction(onFinish) ? onFinish : null;
        var grid = item.getGrid();
        var settings = grid._settings;

        // If item is visible call the callback and be done with it.
        if (!this._isShowing && !this._isHidden) {
            callback && callback(false, item);
            return;
        }

        // If item is showing and does not need to be shown instantly, let's just
        // push callback to the callback queue and be done with it.
        if (this._isShowing && !instant) {
            callback && item._emitter.on(this._queue, callback);
            return;
        }

        // If the item is hiding or hidden process the current visibility callback
        // queue with the interrupted flag active, update classes and set display
        // to block if necessary.
        if (!this._isShowing) {
            item._emitter.burst(this._queue, true, item);
            removeClass(element, settings.itemHiddenClass);
            addClass(element, settings.itemVisibleClass);
            if (!this._isHiding) element.style.display = '';
        }

        // Push callback to the callback queue.
        callback && item._emitter.on(this._queue, callback);

        // Update visibility states.
        this._isShowing = true;
        this._isHiding = this._isHidden = false;

        // Finally let's start show animation.
        this._startAnimation(true, instant, this._finishShow);
    };

    /**
     * Hide item.
     *
     * @public
     * @param {Boolean} instant
     * @param {Function} [onFinish]
     */
    ItemVisibility.prototype.hide = function (instant, onFinish) {
        if (this._isDestroyed) return;

        var item = this._item;
        var element = item._element;
        var callback = isFunction(onFinish) ? onFinish : null;
        var grid = item.getGrid();
        var settings = grid._settings;

        // If item is already hidden call the callback and be done with it.
        if (!this._isHiding && this._isHidden) {
            callback && callback(false, item);
            return;
        }

        // If item is hiding and does not need to be hidden instantly, let's just
        // push callback to the callback queue and be done with it.
        if (this._isHiding && !instant) {
            callback && item._emitter.on(this._queue, callback);
            return;
        }

        // If the item is showing or visible process the current visibility callback
        // queue with the interrupted flag active, update classes and set display
        // to block if necessary.
        if (!this._isHiding) {
            item._emitter.burst(this._queue, true, item);
            addClass(element, settings.itemHiddenClass);
            removeClass(element, settings.itemVisibleClass);
        }

        // Push callback to the callback queue.
        callback && item._emitter.on(this._queue, callback);

        // Update visibility states.
        this._isHidden = this._isHiding = true;
        this._isShowing = false;

        // Finally let's start hide animation.
        this._startAnimation(false, instant, this._finishHide);
    };

    /**
     * Stop current hiding/showing process.
     *
     * @public
     * @param {Boolean} processCallbackQueue
     */
    ItemVisibility.prototype.stop = function (processCallbackQueue) {
        if (this._isDestroyed) return;
        if (!this._isHiding && !this._isShowing) return;

        var item = this._item;

        cancelVisibilityTick(item._id);
        this._animation.stop();
        if (processCallbackQueue) {
            item._emitter.burst(this._queue, true, item);
        }
    };

    /**
     * Reset all existing visibility styles and apply new visibility styles to the
     * visibility element. This method should be used to set styles when there is a
     * chance that the current style properties differ from the new ones (basically
     * on init and on migrations).
     *
     * @public
     * @param {Object} styles
     */
    ItemVisibility.prototype.setStyles = function (styles) {
        var childElement = this._childElement;
        var currentStyleProps = this._currentStyleProps;
        this._removeCurrentStyles();
        for (var prop in styles) {
            currentStyleProps.push(prop);
            childElement.style[prop] = styles[prop];
        }
    };

    /**
     * Destroy the instance and stop current animation if it is running.
     *
     * @public
     */
    ItemVisibility.prototype.destroy = function () {
        if (this._isDestroyed) return;

        var item = this._item;
        var element = item._element;
        var grid = item.getGrid();
        var settings = grid._settings;

        this.stop(true);
        item._emitter.clear(this._queue);
        this._animation.destroy();
        this._removeCurrentStyles();
        removeClass(element, settings.itemVisibleClass);
        removeClass(element, settings.itemHiddenClass);
        element.style.display = '';

        // Reset state.
        this._isHiding = this._isShowing = false;
        this._isDestroyed = this._isHidden = true;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Start visibility animation.
     *
     * @private
     * @param {Boolean} toVisible
     * @param {Boolean} [instant]
     * @param {Function} [onFinish]
     */
    ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) {
        if (this._isDestroyed) return;

        var item = this._item;
        var animation = this._animation;
        var childElement = this._childElement;
        var settings = item.getGrid()._settings;
        var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles;
        var duration = toVisible ? settings.showDuration : settings.hideDuration;
        var easing = toVisible ? settings.showEasing : settings.hideEasing;
        var isInstant = instant || duration <= 0;
        var currentStyles;

        // No target styles? Let's quit early.
        if (!targetStyles) {
            onFinish && onFinish();
            return;
        }

        // Cancel queued visibility tick.
        cancelVisibilityTick(item._id);

        // If we need to apply the styles instantly without animation.
        if (isInstant) {
            setStyles(childElement, targetStyles);
            animation.stop();
            onFinish && onFinish();
            return;
        }

        // Let's make sure an ongoing animation's callback is cancelled before going
        // further. Without this there's a chance that the animation will finish
        // before the next tick and mess up our logic.
        if (animation.isAnimating()) {
            animation._animation.onfinish = null;
        }

        // Start the animation in the next tick (to avoid layout thrashing).
        addVisibilityTick(
            item._id,
            function () {
                currentStyles = getCurrentStyles(childElement, targetStyles);
            },
            function () {
                animation.start(currentStyles, targetStyles, {
                    duration: duration,
                    easing: easing,
                    onFinish: onFinish,
                });
            }
        );
    };

    /**
     * Finish show procedure.
     *
     * @private
     */
    ItemVisibility.prototype._finishShow = function () {
        if (this._isHidden) return;
        this._isShowing = false;
        this._item._emitter.burst(this._queue, false, this._item);
    };

    /**
     * Finish hide procedure.
     *
     * @private
     */
    ItemVisibility.prototype._finishHide = function () {
        if (!this._isHidden) return;
        var item = this._item;
        this._isHiding = false;
        item._layout.stop(true, 0, 0);
        item._element.style.display = 'none';
        item._emitter.burst(this._queue, false, item);
    };

    /**
     * Remove currently applied visibility related inline style properties.
     *
     * @private
     */
    ItemVisibility.prototype._removeCurrentStyles = function () {
        var childElement = this._childElement;
        var currentStyleProps = this._currentStyleProps;

        for (var i = 0; i < currentStyleProps.length; i++) {
            childElement.style[currentStyleProps[i]] = '';
        }

        currentStyleProps.length = 0;
    };

    var id = 0;

    /**
     * Returns a unique numeric id (increments a base value on every call).
     * @returns {Number}
     */
    function createUid() {
        return ++id;
    }

    /**
     * Creates a new Item instance for a Grid instance.
     *
     * @class
     * @param {Grid} grid
     * @param {HTMLElement} element
     * @param {Boolean} [isActive]
     */
    function Item(grid, element, isActive) {
        var settings = grid._settings;

        // Store item/element pair to a map (for faster item querying by element).
        if (ITEM_ELEMENT_MAP) {
            if (ITEM_ELEMENT_MAP.has(element)) {
                throw new Error('You can only create one Muuri Item per element!');
            } else {
                ITEM_ELEMENT_MAP.set(element, this);
            }
        }

        this._id = createUid();
        this._gridId = grid._id;
        this._element = element;
        this._isDestroyed = false;
        this._left = 0;
        this._top = 0;
        this._width = 0;
        this._height = 0;
        this._marginLeft = 0;
        this._marginRight = 0;
        this._marginTop = 0;
        this._marginBottom = 0;
        this._tX = undefined;
        this._tY = undefined;
        this._sortData = null;
        this._emitter = new Emitter();

        // If the provided item element is not a direct child of the grid container
        // element, append it to the grid container. Note, we are indeed reading the
        // DOM here but it's a property that does not cause reflowing.
        if (element.parentNode !== grid._element) {
            grid._element.appendChild(element);
        }

        // Set item class.
        addClass(element, settings.itemClass);

        // If isActive is not defined, let's try to auto-detect it. Note, we are
        // indeed reading the DOM here but it's a property that does not cause
        // reflowing.
        if (typeof isActive !== 'boolean') {
            isActive = getStyle(element, 'display') !== 'none';
        }

        // Set up active state (defines if the item is considered part of the layout
        // or not).
        this._isActive = isActive;

        // Setup visibility handler.
        this._visibility = new ItemVisibility(this);

        // Set up layout handler.
        this._layout = new ItemLayout(this);

        // Set up migration handler data.
        this._migrate = new ItemMigrate(this);

        // Set up drag handler.
        this._drag = settings.dragEnabled ? new ItemDrag(this) : null;

        // Set up release handler. Note that although this is fully linked to dragging
        // this still needs to be always instantiated to handle migration scenarios
        // correctly.
        this._dragRelease = new ItemDragRelease(this);

        // Set up drag placeholder handler. Note that although this is fully linked to
        // dragging this still needs to be always instantiated to handle migration
        // scenarios correctly.
        this._dragPlaceholder = new ItemDragPlaceholder(this);

        // Note! You must call the following methods before you start using the
        // instance. They are deliberately not called in the end as it would cause
        // potentially a massive amount of reflows if multiple items were instantiated
        // in a loop.
        // this._refreshDimensions();
        // this._refreshSortData();
    }

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Get the instance grid reference.
     *
     * @public
     * @returns {Grid}
     */
    Item.prototype.getGrid = function () {
        return GRID_INSTANCES[this._gridId];
    };

    /**
     * Get the instance element.
     *
     * @public
     * @returns {HTMLElement}
     */
    Item.prototype.getElement = function () {
        return this._element;
    };

    /**
     * Get instance element's cached width.
     *
     * @public
     * @returns {Number}
     */
    Item.prototype.getWidth = function () {
        return this._width;
    };

    /**
     * Get instance element's cached height.
     *
     * @public
     * @returns {Number}
     */
    Item.prototype.getHeight = function () {
        return this._height;
    };

    /**
     * Get instance element's cached margins.
     *
     * @public
     * @returns {Object}
     *   - The returned object contains left, right, top and bottom properties
     *     which indicate the item element's cached margins.
     */
    Item.prototype.getMargin = function () {
        return {
            left: this._marginLeft,
            right: this._marginRight,
            top: this._marginTop,
            bottom: this._marginBottom,
        };
    };

    /**
     * Get instance element's cached position.
     *
     * @public
     * @returns {Object}
     *   - The returned object contains left and top properties which indicate the
     *     item element's cached position in the grid.
     */
    Item.prototype.getPosition = function () {
        return {
            left: this._left,
            top: this._top,
        };
    };

    /**
     * Is the item active?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isActive = function () {
        return this._isActive;
    };

    /**
     * Is the item visible?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isVisible = function () {
        return !!this._visibility && !this._visibility._isHidden;
    };

    /**
     * Is the item being animated to visible?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isShowing = function () {
        return !!(this._visibility && this._visibility._isShowing);
    };

    /**
     * Is the item being animated to hidden?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isHiding = function () {
        return !!(this._visibility && this._visibility._isHiding);
    };

    /**
     * Is the item positioning?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isPositioning = function () {
        return !!(this._layout && this._layout._isActive);
    };

    /**
     * Is the item being dragged (or queued for dragging)?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isDragging = function () {
        return !!(this._drag && this._drag._isActive);
    };

    /**
     * Is the item being released?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isReleasing = function () {
        return !!(this._dragRelease && this._dragRelease._isActive);
    };

    /**
     * Is the item destroyed?
     *
     * @public
     * @returns {Boolean}
     */
    Item.prototype.isDestroyed = function () {
        return this._isDestroyed;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Recalculate item's dimensions.
     *
     * @private
     * @param {Boolean} [force=false]
     */
    Item.prototype._refreshDimensions = function (force) {
        if (this._isDestroyed) return;
        if (force !== true && this._visibility._isHidden) return;

        var element = this._element;
        var dragPlaceholder = this._dragPlaceholder;
        var rect = element.getBoundingClientRect();

        // Calculate width and height.
        this._width = rect.width;
        this._height = rect.height;

        // Calculate margins (ignore negative margins).
        this._marginLeft = Math.max(0, getStyleAsFloat(element, 'margin-left'));
        this._marginRight = Math.max(0, getStyleAsFloat(element, 'margin-right'));
        this._marginTop = Math.max(0, getStyleAsFloat(element, 'margin-top'));
        this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom'));

        // Keep drag placeholder's dimensions synced with the item's.
        if (dragPlaceholder) dragPlaceholder.updateDimensions();
    };

    /**
     * Fetch and store item's sort data.
     *
     * @private
     */
    Item.prototype._refreshSortData = function () {
        if (this._isDestroyed) return;

        var data = (this._sortData = {});
        var getters = this.getGrid()._settings.sortData;
        var prop;

        for (prop in getters) {
            data[prop] = getters[prop](this, this._element);
        }
    };

    /**
     * Add item to layout.
     *
     * @private
     */
    Item.prototype._addToLayout = function (left, top) {
        if (this._isActive === true) return;
        this._isActive = true;
        this._left = left || 0;
        this._top = top || 0;
    };

    /**
     * Remove item from layout.
     *
     * @private
     */
    Item.prototype._removeFromLayout = function () {
        if (this._isActive === false) return;
        this._isActive = false;
        this._left = 0;
        this._top = 0;
    };

    /**
     * Check if the layout procedure can be skipped for the item.
     *
     * @private
     * @param {Number} left
     * @param {Number} top
     * @returns {Boolean}
     */
    Item.prototype._canSkipLayout = function (left, top) {
        return (
            this._left === left &&
            this._top === top &&
            !this._migrate._isActive &&
            !this._layout._skipNextAnimation &&
            !this._dragRelease.isJustReleased()
        );
    };

    /**
     * Set the provided left and top arguments as the item element's translate
     * values in the DOM. This method keeps track of the currently applied
     * translate values and skips the update operation if the provided values are
     * identical to the currently applied values. Returns `false` if there was no
     * need for update and `true` if the translate value was updated.
     *
     * @private
     * @param {Number} left
     * @param {Number} top
     * @returns {Boolean}
     */
    Item.prototype._setTranslate = function (left, top) {
        if (this._tX === left && this._tY === top) return false;
        this._tX = left;
        this._tY = top;
        this._element.style[transformProp] = getTranslateString(left, top);
        return true;
    };

    /**
     * Destroy item instance.
     *
     * @private
     * @param {Boolean} [removeElement=false]
     */
    Item.prototype._destroy = function (removeElement) {
        if (this._isDestroyed) return;

        var element = this._element;
        var grid = this.getGrid();
        var settings = grid._settings;

        // Destroy handlers.
        this._dragPlaceholder.destroy();
        this._dragRelease.destroy();
        this._migrate.destroy();
        this._layout.destroy();
        this._visibility.destroy();
        if (this._drag) this._drag.destroy();

        // Destroy emitter.
        this._emitter.destroy();

        // Remove item class.
        removeClass(element, settings.itemClass);

        // Remove element from DOM.
        if (removeElement) element.parentNode.removeChild(element);

        // Remove item/element pair from map.
        if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element);

        // Reset state.
        this._isActive = false;
        this._isDestroyed = true;
    };

    function createPackerProcessor(isWorker) {
        var FILL_GAPS = 1;
        var HORIZONTAL = 2;
        var ALIGN_RIGHT = 4;
        var ALIGN_BOTTOM = 8;
        var ROUNDING = 16;

        var EPS = 0.001;
        var MIN_SLOT_SIZE = 0.5;

        // Rounds number first to three decimal precision and then floors the result
        // to two decimal precision.
        // Math.floor(Math.round(number * 1000) / 10) / 100
        function roundNumber(number) {
            return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100;
        }

        /**
         * @class
         */
        function PackerProcessor() {
            this.currentRects = [];
            this.nextRects = [];
            this.rectTarget = {};
            this.rectStore = [];
            this.slotSizes = [];
            this.rectId = 0;
            this.slotIndex = -1;
            this.slotData = { left: 0, top: 0, width: 0, height: 0 };
            this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this);
            this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this);
        }

        /**
         * Takes a layout object as an argument and computes positions (slots) for the
         * layout items. Also computes the final width and height of the layout. The
         * provided layout object's slots array is mutated as well as the width and
         * height properties.
         *
         * @param {Object} layout
         * @param {Number} layout.width
         *   - The start (current) width of the layout in pixels.
         * @param {Number} layout.height
         *   - The start (current) height of the layout in pixels.
         * @param {(Item[]|Number[])} layout.items
         *   - List of Muuri.Item instances or a list of item dimensions
         *     (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]).
         * @param {(Array|Float32Array)} layout.slots
         *   - An Array/Float32Array instance which's length should equal to
         *     the amount of items times two. The position (width and height) of each
         *     item will be written into this array.
         * @param {Number} settings
         *   - The layout's settings as bitmasks.
         * @returns {Object}
         */
        PackerProcessor.prototype.computeLayout = function (layout, settings) {
            var items = layout.items;
            var slots = layout.slots;
            var fillGaps = !!(settings & FILL_GAPS);
            var horizontal = !!(settings & HORIZONTAL);
            var alignRight = !!(settings & ALIGN_RIGHT);
            var alignBottom = !!(settings & ALIGN_BOTTOM);
            var rounding = !!(settings & ROUNDING);
            var isPreProcessed = typeof items[0] === 'number';
            var i, bump, item, slotWidth, slotHeight, slot;

            // No need to go further if items do not exist.
            if (!items.length) return layout;

            // Compute slots for the items.
            bump = isPreProcessed ? 2 : 1;
            for (i = 0; i < items.length; i += bump) {
                // If items are pre-processed it means that items array contains only
                // the raw dimensions of the items. Otherwise we assume it is an array
                // of normal Muuri items.
                if (isPreProcessed) {
                    slotWidth = items[i];
                    slotHeight = items[i + 1];
                } else {
                    item = items[i];
                    slotWidth = item._width + item._marginLeft + item._marginRight;
                    slotHeight = item._height + item._marginTop + item._marginBottom;
                }

                // If rounding is enabled let's round the item's width and height to
                // make the layout algorithm a bit more stable. This has a performance
                // cost so don't use this if not necessary.
                if (rounding) {
                    slotWidth = roundNumber(slotWidth);
                    slotHeight = roundNumber(slotHeight);
                }

                // Get slot data.
                slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal);

                // Update layout width/height.
                if (horizontal) {
                    if (slot.left + slot.width > layout.width) {
                        layout.width = slot.left + slot.width;
                    }
                } else {
                    if (slot.top + slot.height > layout.height) {
                        layout.height = slot.top + slot.height;
                    }
                }

                // Add item slot data to layout slots.
                slots[++this.slotIndex] = slot.left;
                slots[++this.slotIndex] = slot.top;

                // Store the size too (for later usage) if needed.
                if (alignRight || alignBottom) {
                    this.slotSizes.push(slot.width, slot.height);
                }
            }

            // If the alignment is set to right we need to adjust the results.
            if (alignRight) {
                for (i = 0; i < slots.length; i += 2) {
                    slots[i] = layout.width - (slots[i] + this.slotSizes[i]);
                }
            }

            // If the alignment is set to bottom we need to adjust the results.
            if (alignBottom) {
                for (i = 1; i < slots.length; i += 2) {
                    slots[i] = layout.height - (slots[i] + this.slotSizes[i]);
                }
            }

            // Reset stuff.
            this.slotSizes.length = 0;
            this.currentRects.length = 0;
            this.nextRects.length = 0;
            this.rectStore.length = 0;
            this.rectId = 0;
            this.slotIndex = -1;

            return layout;
        };

        /**
         * Calculate next slot in the layout. Returns a slot object with position and
         * dimensions data. The returned object is reused between calls.
         *
         * @param {Object} layout
         * @param {Number} slotWidth
         * @param {Number} slotHeight
         * @param {Boolean} fillGaps
         * @param {Boolean} horizontal
         * @returns {Object}
         */
        PackerProcessor.prototype.computeNextSlot = function (
            layout,
            slotWidth,
            slotHeight,
            fillGaps,
            horizontal
        ) {
            var slot = this.slotData;
            var currentRects = this.currentRects;
            var nextRects = this.nextRects;
            var ignoreCurrentRects = false;
            var rect;
            var rectId;
            var shards;
            var i;
            var j;

            // Reset new slots.
            nextRects.length = 0;

            // Set item slot initial data.
            slot.left = null;
            slot.top = null;
            slot.width = slotWidth;
            slot.height = slotHeight;

            // Try to find position for the slot from the existing free spaces in the
            // layout.
            for (i = 0; i < currentRects.length; i++) {
                rectId = currentRects[i];
                if (!rectId) continue;
                rect = this.getRect(rectId);
                if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) {
                    slot.left = rect.left;
                    slot.top = rect.top;
                    break;
                }
            }

            // If no position was found for the slot let's position the slot to
            // the bottom left (in vertical mode) or top right (in horizontal mode) of
            // the layout.
            if (slot.left === null) {
                if (horizontal) {
                    slot.left = layout.width;
                    slot.top = 0;
                } else {
                    slot.left = 0;
                    slot.top = layout.height;
                }

                // If gaps don't need filling let's throw away all the current free spaces
                // (currentRects).
                if (!fillGaps) {
                    ignoreCurrentRects = true;
                }
            }

            // In vertical mode, if the slot's bottom overlaps the layout's bottom.
            if (!horizontal && slot.top + slot.height > layout.height + EPS) {
                // If slot is not aligned to the left edge, create a new free space to the
                // left of the slot.
                if (slot.left > MIN_SLOT_SIZE) {
                    nextRects.push(this.addRect(0, layout.height, slot.left, Infinity));
                }

                // If slot is not aligned to the right edge, create a new free space to
                // the right of the slot.
                if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) {
                    nextRects.push(
                        this.addRect(
                            slot.left + slot.width,
                            layout.height,
                            layout.width - slot.left - slot.width,
                            Infinity
                        )
                    );
                }

                // Update layout height.
                layout.height = slot.top + slot.height;
            }

            // In horizontal mode, if the slot's right overlaps the layout's right edge.
            if (horizontal && slot.left + slot.width > layout.width + EPS) {
                // If slot is not aligned to the top, create a new free space above the
                // slot.
                if (slot.top > MIN_SLOT_SIZE) {
                    nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top));
                }

                // If slot is not aligned to the bottom, create a new free space below
                // the slot.
                if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) {
                    nextRects.push(
                        this.addRect(
                            layout.width,
                            slot.top + slot.height,
                            Infinity,
                            layout.height - slot.top - slot.height
                        )
                    );
                }

                // Update layout width.
                layout.width = slot.left + slot.width;
            }

            // Clean up the current free spaces making sure none of them overlap with
            // the slot. Split all overlapping free spaces into smaller shards that do
            // not overlap with the slot.
            if (!ignoreCurrentRects) {
                if (fillGaps) i = 0;
                for (; i < currentRects.length; i++) {
                    rectId = currentRects[i];
                    if (!rectId) continue;
                    rect = this.getRect(rectId);
                    shards = this.splitRect(rect, slot);
                    for (j = 0; j < shards.length; j++) {
                        rectId = shards[j];
                        rect = this.getRect(rectId);
                        // Make sure that the free space is within the boundaries of the
                        // layout. This routine is critical to the algorithm as it makes sure
                        // that there are no leftover spaces with infinite height/width.
                        // It's also essential that we don't compare values absolutely to each
                        // other but leave a little headroom (EPSILON) to get rid of false
                        // positives.
                        if (
                            horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS
                        ) {
                            nextRects.push(rectId);
                        }
                    }
                }
            }

            // Sanitize and sort all the new free spaces that will be used in the next
            // iteration. This procedure is critical to make the bin-packing algorithm
            // work. The free spaces have to be in correct order in the beginning of the
            // next iteration.
            if (nextRects.length > 1) {
                this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft);
            }

            // Finally we need to make sure that `this.currentRects` points to
            // `nextRects` array as that is used in the next iteration's beginning when
            // we try to find a space for the next slot.
            this.currentRects = nextRects;
            this.nextRects = currentRects;

            return slot;
        };

        /**
         * Add a new rectangle to the rectangle store. Returns the id of the new
         * rectangle.
         *
         * @param {Number} left
         * @param {Number} top
         * @param {Number} width
         * @param {Number} height
         * @returns {Number}
         */
        PackerProcessor.prototype.addRect = function (left, top, width, height) {
            var rectId = ++this.rectId;
            this.rectStore[rectId] = left || 0;
            this.rectStore[++this.rectId] = top || 0;
            this.rectStore[++this.rectId] = width || 0;
            this.rectStore[++this.rectId] = height || 0;
            return rectId;
        };

        /**
         * Get rectangle data from the rectangle store by id. Optionally you can
         * provide a target object where the rectangle data will be written in. By
         * default an internal object is reused as a target object.
         *
         * @param {Number} id
         * @param {Object} [target]
         * @returns {Object}
         */
        PackerProcessor.prototype.getRect = function (id, target) {
            if (!target) target = this.rectTarget;
            target.left = this.rectStore[id] || 0;
            target.top = this.rectStore[++id] || 0;
            target.width = this.rectStore[++id] || 0;
            target.height = this.rectStore[++id] || 0;
            return target;
        };

        /**
         * Punch a hole into a rectangle and return the shards (1-4).
         *
         * @param {Object} rect
         * @param {Object} hole
         * @returns {Number[]}
         */
        PackerProcessor.prototype.splitRect = (function () {
            var shards = [];
            var width = 0;
            var height = 0;
            return function (rect, hole) {
                // Reset old shards.
                shards.length = 0;

                // If the slot does not overlap with the hole add slot to the return data
                // as is. Note that in this case we are eager to keep the slot as is if
                // possible so we use the EPSILON in favour of that logic.
                if (
                    rect.left + rect.width <= hole.left + EPS ||
                    hole.left + hole.width <= rect.left + EPS ||
                    rect.top + rect.height <= hole.top + EPS ||
                    hole.top + hole.height <= rect.top + EPS
                ) {
                    shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height));
                    return shards;
                }

                // Left split.
                width = hole.left - rect.left;
                if (width >= MIN_SLOT_SIZE) {
                    shards.push(this.addRect(rect.left, rect.top, width, rect.height));
                }

                // Right split.
                width = rect.left + rect.width - (hole.left + hole.width);
                if (width >= MIN_SLOT_SIZE) {
                    shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height));
                }

                // Top split.
                height = hole.top - rect.top;
                if (height >= MIN_SLOT_SIZE) {
                    shards.push(this.addRect(rect.left, rect.top, rect.width, height));
                }

                // Bottom split.
                height = rect.top + rect.height - (hole.top + hole.height);
                if (height >= MIN_SLOT_SIZE) {
                    shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height));
                }

                return shards;
            };
        })();

        /**
         * Check if a rectangle is fully within another rectangle.
         *
         * @param {Object} a
         * @param {Object} b
         * @returns {Boolean}
         */
        PackerProcessor.prototype.isRectAWithinRectB = function (a, b) {
            return (
                a.left + EPS >= b.left &&
                a.top + EPS >= b.top &&
                a.left + a.width - EPS <= b.left + b.width &&
                a.top + a.height - EPS <= b.top + b.height
            );
        };

        /**
         * Loops through an array of rectangle ids and resets all that are fully
         * within another rectangle in the array. Resetting in this case means that
         * the rectangle id value is replaced with zero.
         *
         * @param {Number[]} rectIds
         * @returns {Number[]}
         */
        PackerProcessor.prototype.purgeRects = (function () {
            var rectA = {};
            var rectB = {};
            return function (rectIds) {
                var i = rectIds.length;
                var j;

                while (i--) {
                    j = rectIds.length;
                    if (!rectIds[i]) continue;
                    this.getRect(rectIds[i], rectA);
                    while (j--) {
                        if (!rectIds[j] || i === j) continue;
                        this.getRect(rectIds[j], rectB);
                        if (this.isRectAWithinRectB(rectA, rectB)) {
                            rectIds[i] = 0;
                            break;
                        }
                    }
                }

                return rectIds;
            };
        })();

        /**
         * Sort rectangles with top-left gravity.
         *
         * @param {Number} aId
         * @param {Number} bId
         * @returns {Number}
         */
        PackerProcessor.prototype.sortRectsTopLeft = (function () {
            var rectA = {};
            var rectB = {};
            return function (aId, bId) {
                this.getRect(aId, rectA);
                this.getRect(bId, rectB);

                return rectA.top < rectB.top && rectA.top + EPS < rectB.top
                    ? -1
                    : rectA.top > rectB.top && rectA.top - EPS > rectB.top
                        ? 1
                        : rectA.left < rectB.left && rectA.left + EPS < rectB.left
                            ? -1
                            : rectA.left > rectB.left && rectA.left - EPS > rectB.left
                                ? 1
                                : 0;
            };
        })();

        /**
         * Sort rectangles with left-top gravity.
         *
         * @param {Number} aId
         * @param {Number} bId
         * @returns {Number}
         */
        PackerProcessor.prototype.sortRectsLeftTop = (function () {
            var rectA = {};
            var rectB = {};
            return function (aId, bId) {
                this.getRect(aId, rectA);
                this.getRect(bId, rectB);
                return rectA.left < rectB.left && rectA.left + EPS < rectB.left
                    ? -1
                    : rectA.left > rectB.left && rectA.left - EPS < rectB.left
                        ? 1
                        : rectA.top < rectB.top && rectA.top + EPS < rectB.top
                            ? -1
                            : rectA.top > rectB.top && rectA.top - EPS > rectB.top
                                ? 1
                                : 0;
            };
        })();

        if (isWorker) {
            var PACKET_INDEX_WIDTH = 1;
            var PACKET_INDEX_HEIGHT = 2;
            var PACKET_INDEX_OPTIONS = 3;
            var PACKET_HEADER_SLOTS = 4;
            var processor = new PackerProcessor();

            self.onmessage = function (msg) {
                var data = new Float32Array(msg.data);
                var items = data.subarray(PACKET_HEADER_SLOTS, data.length);
                var slots = new Float32Array(items.length);
                var settings = data[PACKET_INDEX_OPTIONS];
                var layout = {
                    items: items,
                    slots: slots,
                    width: data[PACKET_INDEX_WIDTH],
                    height: data[PACKET_INDEX_HEIGHT],
                };

                // Compute the layout (width / height / slots).
                processor.computeLayout(layout, settings);

                // Copy layout data to the return data.
                data[PACKET_INDEX_WIDTH] = layout.width;
                data[PACKET_INDEX_HEIGHT] = layout.height;
                data.set(layout.slots, PACKET_HEADER_SLOTS);

                // Send layout back to the main thread.
                postMessage(data.buffer, [data.buffer]);
            };
        }

        return PackerProcessor;
    }

    var PackerProcessor = createPackerProcessor();

    //
    // WORKER UTILS
    //

    var blobUrl = null;
    var activeWorkers = [];

    function createWorkerProcessors(amount, onmessage) {
        var workers = [];

        if (amount > 0) {
            if (!blobUrl) {
                blobUrl = URL.createObjectURL(
                    new Blob(['(' + createPackerProcessor.toString() + ')(true)'], {
                        type: 'application/javascript',
                    })
                );
            }

            for (var i = 0, worker; i < amount; i++) {
                worker = new Worker(blobUrl);
                if (onmessage) worker.onmessage = onmessage;
                workers.push(worker);
                activeWorkers.push(worker);
            }
        }

        return workers;
    }

    function destroyWorkerProcessors(workers) {
        var worker;
        var index;

        for (var i = 0; i < workers.length; i++) {
            worker = workers[i];
            worker.onmessage = null;
            worker.onerror = null;
            worker.onmessageerror = null;
            worker.terminate();

            index = activeWorkers.indexOf(worker);
            if (index > -1) activeWorkers.splice(index, 1);
        }

        if (blobUrl && !activeWorkers.length) {
            URL.revokeObjectURL(blobUrl);
            blobUrl = null;
        }
    }

    function isWorkerProcessorsSupported() {
        return !!(window.Worker && window.URL && window.Blob);
    }

    var FILL_GAPS = 1;
    var HORIZONTAL = 2;
    var ALIGN_RIGHT = 4;
    var ALIGN_BOTTOM = 8;
    var ROUNDING = 16;
    var PACKET_INDEX_ID = 0;
    var PACKET_INDEX_WIDTH = 1;
    var PACKET_INDEX_HEIGHT = 2;
    var PACKET_INDEX_OPTIONS = 3;
    var PACKET_HEADER_SLOTS = 4;

    /**
     * @class
     * @param {Number} [numWorkers=0]
     * @param {Object} [options]
     * @param {Boolean} [options.fillGaps=false]
     * @param {Boolean} [options.horizontal=false]
     * @param {Boolean} [options.alignRight=false]
     * @param {Boolean} [options.alignBottom=false]
     * @param {Boolean} [options.rounding=false]
     */
    function Packer(numWorkers, options) {
        this._options = 0;
        this._processor = null;
        this._layoutQueue = [];
        this._layouts = {};
        this._layoutCallbacks = {};
        this._layoutWorkers = {};
        this._layoutWorkerData = {};
        this._workers = [];
        this._onWorkerMessage = this._onWorkerMessage.bind(this);

        // Set initial options.
        this.setOptions(options);

        // Init the worker(s) or the processor if workers can't be used.
        numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0;
        if (numWorkers && isWorkerProcessorsSupported()) {
            try {
                this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage);
            } catch (e) {
                this._processor = new PackerProcessor();
            }
        } else {
            this._processor = new PackerProcessor();
        }
    }

    Packer.prototype._sendToWorker = function () {
        if (!this._layoutQueue.length || !this._workers.length) return;

        var layoutId = this._layoutQueue.shift();
        var worker = this._workers.pop();
        var data = this._layoutWorkerData[layoutId];

        delete this._layoutWorkerData[layoutId];
        this._layoutWorkers[layoutId] = worker;
        worker.postMessage(data.buffer, [data.buffer]);
    };

    Packer.prototype._onWorkerMessage = function (msg) {
        var data = new Float32Array(msg.data);
        var layoutId = data[PACKET_INDEX_ID];
        var layout = this._layouts[layoutId];
        var callback = this._layoutCallbacks[layoutId];
        var worker = this._layoutWorkers[layoutId];

        if (layout) delete this._layouts[layoutId];
        if (callback) delete this._layoutCallbacks[layoutId];
        if (worker) delete this._layoutWorkers[layoutId];

        if (layout && callback) {
            layout.width = data[PACKET_INDEX_WIDTH];
            layout.height = data[PACKET_INDEX_HEIGHT];
            layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length);
            this._finalizeLayout(layout);
            callback(layout);
        }

        if (worker) {
            this._workers.push(worker);
            this._sendToWorker();
        }
    };

    Packer.prototype._finalizeLayout = function (layout) {
        var grid = layout._grid;
        var isHorizontal = layout._settings & HORIZONTAL;
        var isBorderBox = grid._boxSizing === 'border-box';

        delete layout._grid;
        delete layout._settings;

        layout.styles = {};

        if (isHorizontal) {
            layout.styles.width =
                (isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px';
        } else {
            layout.styles.height =
                (isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px';
        }

        return layout;
    };

    /**
     * @public
     * @param {Object} [options]
     * @param {Boolean} [options.fillGaps]
     * @param {Boolean} [options.horizontal]
     * @param {Boolean} [options.alignRight]
     * @param {Boolean} [options.alignBottom]
     * @param {Boolean} [options.rounding]
     */
    Packer.prototype.setOptions = function (options) {
        if (!options) return;

        var fillGaps;
        if (typeof options.fillGaps === 'boolean') {
            fillGaps = options.fillGaps ? FILL_GAPS : 0;
        } else {
            fillGaps = this._options & FILL_GAPS;
        }

        var horizontal;
        if (typeof options.horizontal === 'boolean') {
            horizontal = options.horizontal ? HORIZONTAL : 0;
        } else {
            horizontal = this._options & HORIZONTAL;
        }

        var alignRight;
        if (typeof options.alignRight === 'boolean') {
            alignRight = options.alignRight ? ALIGN_RIGHT : 0;
        } else {
            alignRight = this._options & ALIGN_RIGHT;
        }

        var alignBottom;
        if (typeof options.alignBottom === 'boolean') {
            alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0;
        } else {
            alignBottom = this._options & ALIGN_BOTTOM;
        }

        var rounding;
        if (typeof options.rounding === 'boolean') {
            rounding = options.rounding ? ROUNDING : 0;
        } else {
            rounding = this._options & ROUNDING;
        }

        this._options = fillGaps | horizontal | alignRight | alignBottom | rounding;
    };

    /**
     * @public
     * @param {Grid} grid
     * @param {Number} layoutId
     * @param {Item[]} items
     * @param {Number} width
     * @param {Number} height
     * @param {Function} callback
     * @returns {?Function}
     */
    Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) {
        if (this._layouts[layoutId]) {
            throw new Error('A layout with the provided id is currently being processed.');
        }

        var horizontal = this._options & HORIZONTAL;
        var layout = {
            id: layoutId,
            items: items,
            slots: null,
            width: horizontal ? 0 : width,
            height: !horizontal ? 0 : height,
            // Temporary data, which will be removed before sending the layout data
            // outside of Packer's context.
            _grid: grid,
            _settings: this._options,
        };

        // If there are no items let's call the callback immediately.
        if (!items.length) {
            layout.slots = [];
            this._finalizeLayout(layout);
            callback(layout);
            return;
        }

        // Create layout synchronously if needed.
        if (this._processor) {
            layout.slots = window.Float32Array
                ? new Float32Array(items.length * 2)
                : new Array(items.length * 2);
            this._processor.computeLayout(layout, layout._settings);
            this._finalizeLayout(layout);
            callback(layout);
            return;
        }

        // Worker data.
        var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2);

        // Worker data header.
        data[PACKET_INDEX_ID] = layoutId;
        data[PACKET_INDEX_WIDTH] = layout.width;
        data[PACKET_INDEX_HEIGHT] = layout.height;
        data[PACKET_INDEX_OPTIONS] = layout._settings;

        // Worker data items.
        var i, j, item;
        for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) {
            item = items[i];
            data[++j] = item._width + item._marginLeft + item._marginRight;
            data[++j] = item._height + item._marginTop + item._marginBottom;
        }

        this._layoutQueue.push(layoutId);
        this._layouts[layoutId] = layout;
        this._layoutCallbacks[layoutId] = callback;
        this._layoutWorkerData[layoutId] = data;

        this._sendToWorker();

        return this.cancelLayout.bind(this, layoutId);
    };

    /**
     * @public
     * @param {Number} layoutId
     */
    Packer.prototype.cancelLayout = function (layoutId) {
        var layout = this._layouts[layoutId];
        if (!layout) return;

        delete this._layouts[layoutId];
        delete this._layoutCallbacks[layoutId];

        if (this._layoutWorkerData[layoutId]) {
            delete this._layoutWorkerData[layoutId];
            var queueIndex = this._layoutQueue.indexOf(layoutId);
            if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1);
        }
    };

    /**
     * @public
     */
    Packer.prototype.destroy = function () {
        // Move all currently used workers back in the workers array.
        for (var key in this._layoutWorkers) {
            this._workers.push(this._layoutWorkers[key]);
        }

        // Destroy all instance's workers.
        destroyWorkerProcessors(this._workers);

        // Reset data.
        this._workers.length = 0;
        this._layoutQueue.length = 0;
        this._layouts = {};
        this._layoutCallbacks = {};
        this._layoutWorkers = {};
        this._layoutWorkerData = {};
    };

    var debounceId = 0;

    /**
     * Returns a function, that, as long as it continues to be invoked, will not
     * be triggered. The function will be called after it stops being called for
     * N milliseconds. The returned function accepts one argument which, when
     * being `true`, cancels the debounce function immediately. When the debounce
     * function is canceled it cannot be invoked again.
     *
     * @param {Function} fn
     * @param {Number} durationMs
     * @returns {Function}
     */
    function debounce(fn, durationMs) {
        var id = ++debounceId;
        var timer = 0;
        var lastTime = 0;
        var isCanceled = false;
        var tick = function (time) {
            if (isCanceled) return;

            if (lastTime) timer -= time - lastTime;
            lastTime = time;

            if (timer > 0) {
                addDebounceTick(id, tick);
            } else {
                timer = lastTime = 0;
                fn();
            }
        };

        return function (cancel) {
            if (isCanceled) return;

            if (durationMs <= 0) {
                if (cancel !== true) fn();
                return;
            }

            if (cancel === true) {
                isCanceled = true;
                timer = lastTime = 0;
                tick = undefined;
                cancelDebounceTick(id);
                return;
            }

            if (timer <= 0) {
                timer = durationMs;
                tick(0);
            } else {
                timer = durationMs;
            }
        };
    }

    var htmlCollectionType = '[object HTMLCollection]';
    var nodeListType = '[object NodeList]';

    /**
     * Check if a value is a node list or a html collection.
     *
     * @param {*} val
     * @returns {Boolean}
     */
    function isNodeList(val) {
        var type = Object.prototype.toString.call(val);
        return type === htmlCollectionType || type === nodeListType;
    }

    var objectType = 'object';
    var objectToStringType = '[object Object]';
    var toString = Object.prototype.toString;

    /**
     * Check if a value is a plain object.
     *
     * @param {*} val
     * @returns {Boolean}
     */
    function isPlainObject(val) {
        return typeof val === objectType && toString.call(val) === objectToStringType;
    }

    function noop() {}

    /**
     * Converts a value to an array or clones an array.
     *
     * @param {*} val
     * @returns {Array}
     */
    function toArray(val) {
        return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val);
    }

    var NUMBER_TYPE = 'number';
    var STRING_TYPE = 'string';
    var INSTANT_LAYOUT = 'instant';
    var layoutId = 0;

    /**
     * Creates a new Grid instance.
     *
     * @class
     * @param {(HTMLElement|String)} element
     * @param {Object} [options]
     * @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"]
     * @param {Number} [options.showDuration=300]
     * @param {String} [options.showEasing="ease"]
     * @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}]
     * @param {Number} [options.hideDuration=300]
     * @param {String} [options.hideEasing="ease"]
     * @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}]
     * @param {(Function|Object)} [options.layout]
     * @param {Boolean} [options.layout.fillGaps=false]
     * @param {Boolean} [options.layout.horizontal=false]
     * @param {Boolean} [options.layout.alignRight=false]
     * @param {Boolean} [options.layout.alignBottom=false]
     * @param {Boolean} [options.layout.rounding=false]
     * @param {(Boolean|Number)} [options.layoutOnResize=150]
     * @param {Boolean} [options.layoutOnInit=true]
     * @param {Number} [options.layoutDuration=300]
     * @param {String} [options.layoutEasing="ease"]
     * @param {?Object} [options.sortData=null]
     * @param {Boolean} [options.dragEnabled=false]
     * @param {?String} [options.dragHandle=null]
     * @param {?HtmlElement} [options.dragContainer=null]
     * @param {?Function} [options.dragStartPredicate]
     * @param {Number} [options.dragStartPredicate.distance=0]
     * @param {Number} [options.dragStartPredicate.delay=0]
     * @param {String} [options.dragAxis="xy"]
     * @param {(Boolean|Function)} [options.dragSort=true]
     * @param {Object} [options.dragSortHeuristics]
     * @param {Number} [options.dragSortHeuristics.sortInterval=100]
     * @param {Number} [options.dragSortHeuristics.minDragDistance=10]
     * @param {Number} [options.dragSortHeuristics.minBounceBackAngle=1]
     * @param {(Function|Object)} [options.dragSortPredicate]
     * @param {Number} [options.dragSortPredicate.threshold=50]
     * @param {String} [options.dragSortPredicate.action="move"]
     * @param {String} [options.dragSortPredicate.migrateAction="move"]
     * @param {Object} [options.dragRelease]
     * @param {Number} [options.dragRelease.duration=300]
     * @param {String} [options.dragRelease.easing="ease"]
     * @param {Boolean} [options.dragRelease.useDragContainer=true]
     * @param {Object} [options.dragCssProps]
     * @param {Object} [options.dragPlaceholder]
     * @param {Boolean} [options.dragPlaceholder.enabled=false]
     * @param {?Function} [options.dragPlaceholder.createElement=null]
     * @param {?Function} [options.dragPlaceholder.onCreate=null]
     * @param {?Function} [options.dragPlaceholder.onRemove=null]
     * @param {Object} [options.dragAutoScroll]
     * @param {(Function|Array)} [options.dragAutoScroll.targets=[]]
     * @param {?Function} [options.dragAutoScroll.handle=null]
     * @param {Number} [options.dragAutoScroll.threshold=50]
     * @param {Number} [options.dragAutoScroll.safeZone=0.2]
     * @param {(Function|Number)} [options.dragAutoScroll.speed]
     * @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true]
     * @param {Boolean} [options.dragAutoScroll.smoothStop=false]
     * @param {?Function} [options.dragAutoScroll.onStart=null]
     * @param {?Function} [options.dragAutoScroll.onStop=null]
     * @param {String} [options.containerClass="muuri"]
     * @param {String} [options.itemClass="muuri-item"]
     * @param {String} [options.itemVisibleClass="muuri-item-visible"]
     * @param {String} [options.itemHiddenClass="muuri-item-hidden"]
     * @param {String} [options.itemPositioningClass="muuri-item-positioning"]
     * @param {String} [options.itemDraggingClass="muuri-item-dragging"]
     * @param {String} [options.itemReleasingClass="muuri-item-releasing"]
     * @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"]
     */
    function Grid(element, options) {
        // Allow passing element as selector string
        if (typeof element === STRING_TYPE) {
            element = document.querySelector(element);
        }

        // Throw an error if the container element is not body element or does not
        // exist within the body element.
        var isElementInDom = element.getRootNode
            ? element.getRootNode({ composed: true }) === document
            : document.body.contains(element);
        if (!isElementInDom || element === document.documentElement) {
            throw new Error('Container element must be an existing DOM element.');
        }

        // Create instance settings by merging the options with default options.
        var settings = mergeSettings(Grid.defaultOptions, options);
        settings.visibleStyles = normalizeStyles(settings.visibleStyles);
        settings.hiddenStyles = normalizeStyles(settings.hiddenStyles);
        if (!isFunction(settings.dragSort)) {
            settings.dragSort = !!settings.dragSort;
        }

        this._id = createUid();
        this._element = element;
        this._settings = settings;
        this._isDestroyed = false;
        this._items = [];
        this._layout = {
            id: 0,
            items: [],
            slots: [],
        };
        this._isLayoutFinished = true;
        this._nextLayoutData = null;
        this._emitter = new Emitter();
        this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this);

        // Store grid instance to the grid instances collection.
        GRID_INSTANCES[this._id] = this;

        // Add container element's class name.
        addClass(element, settings.containerClass);

        // If layoutOnResize option is a valid number sanitize it and bind the resize
        // handler.
        bindLayoutOnResize(this, settings.layoutOnResize);

        // Add initial items.
        this.add(getInitialGridElements(element, settings.items), { layout: false });

        // Layout on init if necessary.
        if (settings.layoutOnInit) {
            this.layout(true);
        }
    }

    /**
     * Public properties
     * *****************
     */

    /**
     * @public
     * @static
     * @see Item
     */
    Grid.Item = Item;

    /**
     * @public
     * @static
     * @see ItemLayout
     */
    Grid.ItemLayout = ItemLayout;

    /**
     * @public
     * @static
     * @see ItemVisibility
     */
    Grid.ItemVisibility = ItemVisibility;

    /**
     * @public
     * @static
     * @see ItemMigrate
     */
    Grid.ItemMigrate = ItemMigrate;

    /**
     * @public
     * @static
     * @see ItemDrag
     */
    Grid.ItemDrag = ItemDrag;

    /**
     * @public
     * @static
     * @see ItemDragRelease
     */
    Grid.ItemDragRelease = ItemDragRelease;

    /**
     * @public
     * @static
     * @see ItemDragPlaceholder
     */
    Grid.ItemDragPlaceholder = ItemDragPlaceholder;

    /**
     * @public
     * @static
     * @see Emitter
     */
    Grid.Emitter = Emitter;

    /**
     * @public
     * @static
     * @see Animator
     */
    Grid.Animator = Animator;

    /**
     * @public
     * @static
     * @see Dragger
     */
    Grid.Dragger = Dragger;

    /**
     * @public
     * @static
     * @see Packer
     */
    Grid.Packer = Packer;

    /**
     * @public
     * @static
     * @see AutoScroller
     */
    Grid.AutoScroller = AutoScroller;

    /**
     * The default Packer instance used by default for all layouts.
     *
     * @public
     * @static
     * @type {Packer}
     */
    Grid.defaultPacker = new Packer(2);

    /**
     * Default options for Grid instance.
     *
     * @public
     * @static
     * @type {Object}
     */
    Grid.defaultOptions = {
        // Initial item elements
        items: '*',

        // Default show animation
        showDuration: 300,
        showEasing: 'ease',

        // Default hide animation
        hideDuration: 300,
        hideEasing: 'ease',

        // Item's visible/hidden state styles
        visibleStyles: {
            opacity: '1',
            transform: 'scale(1)',
        },
        hiddenStyles: {
            opacity: '0',
            transform: 'scale(0.5)',
        },

        // Layout
        layout: {
            fillGaps: false,
            horizontal: false,
            alignRight: false,
            alignBottom: false,
            rounding: false,
        },
        layoutOnResize: 150,
        layoutOnInit: true,
        layoutDuration: 300,
        layoutEasing: 'ease',

        // Sorting
        sortData: null,

        // Drag & Drop
        dragEnabled: false,
        dragContainer: null,
        dragHandle: null,
        dragStartPredicate: {
            distance: 0,
            delay: 0,
        },
        dragAxis: 'xy',
        dragSort: true,
        dragSortHeuristics: {
            sortInterval: 100,
            minDragDistance: 10,
            minBounceBackAngle: 1,
        },
        dragSortPredicate: {
            threshold: 50,
            action: ACTION_MOVE,
            migrateAction: ACTION_MOVE,
        },
        dragRelease: {
            duration: 300,
            easing: 'ease',
            useDragContainer: true,
        },
        dragCssProps: {
            touchAction: 'none',
            userSelect: 'none',
            userDrag: 'none',
            tapHighlightColor: 'rgba(0, 0, 0, 0)',
            touchCallout: 'none',
            contentZooming: 'none',
        },
        dragPlaceholder: {
            enabled: false,
            createElement: null,
            onCreate: null,
            onRemove: null,
        },
        dragAutoScroll: {
            targets: [],
            handle: null,
            threshold: 50,
            safeZone: 0.2,
            speed: AutoScroller.smoothSpeed(1000, 2000, 2500),
            sortDuringScroll: true,
            smoothStop: false,
            onStart: null,
            onStop: null,
        },

        // Classnames
        containerClass: 'muuri',
        itemClass: 'muuri-item',
        itemVisibleClass: 'muuri-item-shown',
        itemHiddenClass: 'muuri-item-hidden',
        itemPositioningClass: 'muuri-item-positioning',
        itemDraggingClass: 'muuri-item-dragging',
        itemReleasingClass: 'muuri-item-releasing',
        itemPlaceholderClass: 'muuri-item-placeholder',
    };

    /**
     * Public prototype methods
     * ************************
     */

    /**
     * Bind an event listener.
     *
     * @public
     * @param {String} event
     * @param {Function} listener
     * @returns {Grid}
     */
    Grid.prototype.on = function (event, listener) {
        this._emitter.on(event, listener);
        return this;
    };

    /**
     * Unbind an event listener.
     *
     * @public
     * @param {String} event
     * @param {Function} listener
     * @returns {Grid}
     */
    Grid.prototype.off = function (event, listener) {
        this._emitter.off(event, listener);
        return this;
    };

    /**
     * Get the container element.
     *
     * @public
     * @returns {HTMLElement}
     */
    Grid.prototype.getElement = function () {
        return this._element;
    };

    /**
     * Get instance's item by element or by index. Target can also be an Item
     * instance in which case the function returns the item if it exists within
     * related Grid instance. If nothing is found with the provided target, null
     * is returned.
     *
     * @private
     * @param {(HtmlElement|Number|Item)} [target]
     * @returns {?Item}
     */
    Grid.prototype.getItem = function (target) {
        // If no target is specified or the instance is destroyed, return null.
        if (this._isDestroyed || (!target && target !== 0)) {
            return null;
        }

        // If target is number return the item in that index. If the number is lower
        // than zero look for the item starting from the end of the items array. For
        // example -1 for the last item, -2 for the second last item, etc.
        if (typeof target === NUMBER_TYPE) {
            return this._items[target > -1 ? target : this._items.length + target] || null;
        }

        // If the target is an instance of Item return it if it is attached to this
        // Grid instance, otherwise return null.
        if (target instanceof Item) {
            return target._gridId === this._id ? target : null;
        }

        // In other cases let's assume that the target is an element, so let's try
        // to find an item that matches the element and return it. If item is not
        // found return null.
        if (ITEM_ELEMENT_MAP) {
            var item = ITEM_ELEMENT_MAP.get(target);
            return item && item._gridId === this._id ? item : null;
        } else {
            for (var i = 0; i < this._items.length; i++) {
                if (this._items[i]._element === target) {
                    return this._items[i];
                }
            }
        }

        return null;
    };

    /**
     * Get all items. Optionally you can provide specific targets (elements,
     * indices and item instances). All items that are not found are omitted from
     * the returned array.
     *
     * @public
     * @param {(HtmlElement|Number|Item|Array)} [targets]
     * @returns {Item[]}
     */
    Grid.prototype.getItems = function (targets) {
        // Return all items immediately if no targets were provided or if the
        // instance is destroyed.
        if (this._isDestroyed || targets === undefined) {
            return this._items.slice(0);
        }

        var items = [];
        var i, item;

        if (Array.isArray(targets) || isNodeList(targets)) {
            for (i = 0; i < targets.length; i++) {
                item = this.getItem(targets[i]);
                if (item) items.push(item);
            }
        } else {
            item = this.getItem(targets);
            if (item) items.push(item);
        }

        return items;
    };

    /**
     * Update the cached dimensions of the instance's items. By default all the
     * items are refreshed, but you can also provide an array of target items as the
     * first argument if you want to refresh specific items. Note that all hidden
     * items are not refreshed by default since their "display" property is "none"
     * and their dimensions are therefore not readable from the DOM. However, if you
     * do want to force update hidden item dimensions too you can provide `true`
     * as the second argument, which makes the elements temporarily visible while
     * their dimensions are being read.
     *
     * @public
     * @param {Item[]} [items]
     * @param {Boolean} [force=false]
     * @returns {Grid}
     */
    Grid.prototype.refreshItems = function (items, force) {
        if (this._isDestroyed) return this;

        var targets = items || this._items;
        var i, item, style, hiddenItemStyles;

        if (force === true) {
            hiddenItemStyles = [];
            for (i = 0; i < targets.length; i++) {
                item = targets[i];
                if (!item.isVisible() && !item.isHiding()) {
                    style = item.getElement().style;
                    style.visibility = 'hidden';
                    style.display = '';
                    hiddenItemStyles.push(style);
                }
            }
        }

        for (i = 0; i < targets.length; i++) {
            targets[i]._refreshDimensions(force);
        }

        if (force === true) {
            for (i = 0; i < hiddenItemStyles.length; i++) {
                style = hiddenItemStyles[i];
                style.visibility = '';
                style.display = 'none';
            }
            hiddenItemStyles.length = 0;
        }

        return this;
    };

    /**
     * Update the sort data of the instance's items. By default all the items are
     * refreshed, but you can also provide an array of target items if you want to
     * refresh specific items.
     *
     * @public
     * @param {Item[]} [items]
     * @returns {Grid}
     */
    Grid.prototype.refreshSortData = function (items) {
        if (this._isDestroyed) return this;

        var targets = items || this._items;
        for (var i = 0; i < targets.length; i++) {
            targets[i]._refreshSortData();
        }

        return this;
    };

    /**
     * Synchronize the item elements to match the order of the items in the DOM.
     * This comes handy if you need to keep the DOM structure matched with the
     * order of the items. Note that if an item's element is not currently a child
     * of the container element (if it is dragged for example) it is ignored and
     * left untouched.
     *
     * @public
     * @returns {Grid}
     */
    Grid.prototype.synchronize = function () {
        if (this._isDestroyed) return this;

        var items = this._items;
        if (!items.length) return this;

        var fragment;
        var element;

        for (var i = 0; i < items.length; i++) {
            element = items[i]._element;
            if (element.parentNode === this._element) {
                fragment = fragment || document.createDocumentFragment();
                fragment.appendChild(element);
            }
        }

        if (!fragment) return this;

        this._element.appendChild(fragment);
        this._emit(EVENT_SYNCHRONIZE);

        return this;
    };

    /**
     * Calculate and apply item positions.
     *
     * @public
     * @param {Boolean} [instant=false]
     * @param {Function} [onFinish]
     * @returns {Grid}
     */
    Grid.prototype.layout = function (instant, onFinish) {
        if (this._isDestroyed) return this;

        // Cancel unfinished layout algorithm if possible.
        var unfinishedLayout = this._nextLayoutData;
        if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) {
            unfinishedLayout.cancel();
        }

        // Compute layout id (let's stay in Float32 range).
        layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1;
        var nextLayoutId = layoutId;

        // Store data for next layout.
        this._nextLayoutData = {
            id: nextLayoutId,
            instant: instant,
            onFinish: onFinish,
            cancel: null,
        };

        // Collect layout items (all active grid items).
        var items = this._items;
        var layoutItems = [];
        for (var i = 0; i < items.length; i++) {
            if (items[i]._isActive) layoutItems.push(items[i]);
        }

        // Compute new layout.
        this._refreshDimensions();
        var gridWidth = this._width - this._borderLeft - this._borderRight;
        var gridHeight = this._height - this._borderTop - this._borderBottom;
        var layoutSettings = this._settings.layout;
        var cancelLayout;
        if (isFunction(layoutSettings)) {
            cancelLayout = layoutSettings(
                this,
                nextLayoutId,
                layoutItems,
                gridWidth,
                gridHeight,
                this._onLayoutDataReceived
            );
        } else {
            Grid.defaultPacker.setOptions(layoutSettings);
            cancelLayout = Grid.defaultPacker.createLayout(
                this,
                nextLayoutId,
                layoutItems,
                gridWidth,
                gridHeight,
                this._onLayoutDataReceived
            );
        }

        // Store layout cancel method if available.
        if (
            isFunction(cancelLayout) &&
            this._nextLayoutData &&
            this._nextLayoutData.id === nextLayoutId
        ) {
            this._nextLayoutData.cancel = cancelLayout;
        }

        return this;
    };

    /**
     * Add new items by providing the elements you wish to add to the instance and
     * optionally provide the index where you want the items to be inserted into.
     * All elements that are not already children of the container element will be
     * automatically appended to the container element. If an element has it's CSS
     * display property set to "none" it will be marked as inactive during the
     * initiation process. As long as the item is inactive it will not be part of
     * the layout, but it will retain it's index. You can activate items at any
     * point with grid.show() method. This method will automatically call
     * grid.layout() if one or more of the added elements are visible. If only
     * hidden items are added no layout will be called. All the new visible items
     * are positioned without animation during their first layout.
     *
     * @public
     * @param {(HTMLElement|HTMLElement[])} elements
     * @param {Object} [options]
     * @param {Number} [options.index=-1]
     * @param {Boolean} [options.active]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Item[]}
     */
    Grid.prototype.add = function (elements, options) {
        if (this._isDestroyed || !elements) return [];

        var newItems = toArray(elements);
        if (!newItems.length) return newItems;

        var opts = options || {};
        var layout = opts.layout ? opts.layout : opts.layout === undefined;
        var items = this._items;
        var needsLayout = false;
        var fragment;
        var element;
        var item;
        var i;

        // Collect all the elements that are not child of the grid element into a
        // document fragment.
        for (i = 0; i < newItems.length; i++) {
            element = newItems[i];
            if (element.parentNode !== this._element) {
                fragment = fragment || document.createDocumentFragment();
                fragment.appendChild(element);
            }
        }

        // If we have a fragment, let's append it to the grid element. We could just
        // not do this and the `new Item()` instantiation would handle this for us,
        // but this way we can add the elements into the DOM a bit faster.
        if (fragment) {
            this._element.appendChild(fragment);
        }

        // Map provided elements into new grid items.
        for (i = 0; i < newItems.length; i++) {
            element = newItems[i];
            item = newItems[i] = new Item(this, element, opts.active);

            // If the item to be added is active, we need to do a layout. Also, we
            // need to mark the item with the skipNextAnimation flag to make it
            // position instantly (without animation) during the next layout. Without
            // the hack the item would animate to it's new position from the northwest
            // corner of the grid, which feels a bit buggy (imho).
            if (item._isActive) {
                needsLayout = true;
                item._layout._skipNextAnimation = true;
            }
        }

        // Set up the items' initial dimensions and sort data. This needs to be done
        // in a separate loop to avoid layout thrashing.
        for (i = 0; i < newItems.length; i++) {
            item = newItems[i];
            item._refreshDimensions();
            item._refreshSortData();
        }

        // Add the new items to the items collection to correct index.
        arrayInsert(items, newItems, opts.index);

        // Emit add event.
        if (this._hasListeners(EVENT_ADD)) {
            this._emit(EVENT_ADD, newItems.slice(0));
        }

        // If layout is needed.
        if (needsLayout && layout) {
            this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
        }

        return newItems;
    };

    /**
     * Remove items from the instance.
     *
     * @public
     * @param {Item[]} items
     * @param {Object} [options]
     * @param {Boolean} [options.removeElements=false]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Item[]}
     */
    Grid.prototype.remove = function (items, options) {
        if (this._isDestroyed || !items.length) return [];

        var opts = options || {};
        var layout = opts.layout ? opts.layout : opts.layout === undefined;
        var needsLayout = false;
        var allItems = this.getItems();
        var targetItems = [];
        var indices = [];
        var index;
        var item;
        var i;

        // Remove the individual items.
        for (i = 0; i < items.length; i++) {
            item = items[i];
            if (item._isDestroyed) continue;

            index = this._items.indexOf(item);
            if (index === -1) continue;

            if (item._isActive) needsLayout = true;

            targetItems.push(item);
            indices.push(allItems.indexOf(item));
            item._destroy(opts.removeElements);
            this._items.splice(index, 1);
        }

        // Emit remove event.
        if (this._hasListeners(EVENT_REMOVE)) {
            this._emit(EVENT_REMOVE, targetItems.slice(0), indices);
        }

        // If layout is needed.
        if (needsLayout && layout) {
            this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
        }

        return targetItems;
    };

    /**
     * Show specific instance items.
     *
     * @public
     * @param {Item[]} items
     * @param {Object} [options]
     * @param {Boolean} [options.instant=false]
     * @param {Boolean} [options.syncWithLayout=true]
     * @param {Function} [options.onFinish]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Grid}
     */
    Grid.prototype.show = function (items, options) {
        if (!this._isDestroyed && items.length) {
            this._setItemsVisibility(items, true, options);
        }
        return this;
    };

    /**
     * Hide specific instance items.
     *
     * @public
     * @param {Item[]} items
     * @param {Object} [options]
     * @param {Boolean} [options.instant=false]
     * @param {Boolean} [options.syncWithLayout=true]
     * @param {Function} [options.onFinish]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Grid}
     */
    Grid.prototype.hide = function (items, options) {
        if (!this._isDestroyed && items.length) {
            this._setItemsVisibility(items, false, options);
        }
        return this;
    };

    /**
     * Filter items. Expects at least one argument, a predicate, which should be
     * either a function or a string. The predicate callback is executed for every
     * item in the instance. If the return value of the predicate is truthy the
     * item in question will be shown and otherwise hidden. The predicate callback
     * receives the item instance as it's argument. If the predicate is a string
     * it is considered to be a selector and it is checked against every item
     * element in the instance with the native element.matches() method. All the
     * matching items will be shown and others hidden.
     *
     * @public
     * @param {(Function|String)} predicate
     * @param {Object} [options]
     * @param {Boolean} [options.instant=false]
     * @param {Boolean} [options.syncWithLayout=true]
     * @param {FilterCallback} [options.onFinish]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Grid}
     */
    Grid.prototype.filter = function (predicate, options) {
        if (this._isDestroyed || !this._items.length) return this;

        var itemsToShow = [];
        var itemsToHide = [];
        var isPredicateString = typeof predicate === STRING_TYPE;
        var isPredicateFn = isFunction(predicate);
        var opts = options || {};
        var isInstant = opts.instant === true;
        var syncWithLayout = opts.syncWithLayout;
        var layout = opts.layout ? opts.layout : opts.layout === undefined;
        var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null;
        var tryFinishCounter = -1;
        var tryFinish = noop;
        var item;
        var i;

        // If we have onFinish callback, let's create proper tryFinish callback.
        if (onFinish) {
            tryFinish = function () {
                ++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0));
            };
        }

        // Check which items need to be shown and which hidden.
        if (isPredicateFn || isPredicateString) {
            for (i = 0; i < this._items.length; i++) {
                item = this._items[i];
                if (isPredicateFn ? predicate(item) : elementMatches(item._element, predicate)) {
                    itemsToShow.push(item);
                } else {
                    itemsToHide.push(item);
                }
            }
        }

        // Show items that need to be shown.
        if (itemsToShow.length) {
            this.show(itemsToShow, {
                instant: isInstant,
                syncWithLayout: syncWithLayout,
                onFinish: tryFinish,
                layout: false,
            });
        } else {
            tryFinish();
        }

        // Hide items that need to be hidden.
        if (itemsToHide.length) {
            this.hide(itemsToHide, {
                instant: isInstant,
                syncWithLayout: syncWithLayout,
                onFinish: tryFinish,
                layout: false,
            });
        } else {
            tryFinish();
        }

        // If there are any items to filter.
        if (itemsToShow.length || itemsToHide.length) {
            // Emit filter event.
            if (this._hasListeners(EVENT_FILTER)) {
                this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0));
            }

            // If layout is needed.
            if (layout) {
                this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
            }
        }

        return this;
    };

    /**
     * Sort items. There are three ways to sort the items. The first is simply by
     * providing a function as the comparer which works identically to native
     * array sort. Alternatively you can sort by the sort data you have provided
     * in the instance's options. Just provide the sort data key(s) as a string
     * (separated by space) and the items will be sorted based on the provided
     * sort data keys. Lastly you have the opportunity to provide a presorted
     * array of items which will be used to sync the internal items array in the
     * same order.
     *
     * @public
     * @param {(Function|String|Item[])} comparer
     * @param {Object} [options]
     * @param {Boolean} [options.descending=false]
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Grid}
     */
    Grid.prototype.sort = (function () {
        var sortComparer;
        var isDescending;
        var origItems;
        var indexMap;

        function defaultComparer(a, b) {
            var result = 0;
            var criteriaName;
            var criteriaOrder;
            var valA;
            var valB;

            // Loop through the list of sort criteria.
            for (var i = 0; i < sortComparer.length; i++) {
                // Get the criteria name, which should match an item's sort data key.
                criteriaName = sortComparer[i][0];
                criteriaOrder = sortComparer[i][1];

                // Get items' cached sort values for the criteria. If the item has no sort
                // data let's update the items sort data (this is a lazy load mechanism).
                valA = (a._sortData ? a : a._refreshSortData())._sortData[criteriaName];
                valB = (b._sortData ? b : b._refreshSortData())._sortData[criteriaName];

                // Sort the items in descending order if defined so explicitly. Otherwise
                // sort items in ascending order.
                if (criteriaOrder === 'desc' || (!criteriaOrder && isDescending)) {
                    result = valB < valA ? -1 : valB > valA ? 1 : 0;
                } else {
                    result = valA < valB ? -1 : valA > valB ? 1 : 0;
                }

                // If we have -1 or 1 as the return value, let's return it immediately.
                if (result) return result;
            }

            // If values are equal let's compare the item indices to make sure we
            // have a stable sort. Note that this is not necessary in evergreen browsers
            // because Array.sort() is nowadays stable. However, in order to guarantee
            // same results in older browsers we need this.
            if (!result) {
                if (!indexMap) indexMap = createIndexMap(origItems);
                result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b);
            }
            return result;
        }

        function customComparer(a, b) {
            var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b);
            if (!result) {
                if (!indexMap) indexMap = createIndexMap(origItems);
                result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b);
            }
            return result;
        }

        return function (comparer, options) {
            if (this._isDestroyed || this._items.length < 2) return this;

            var items = this._items;
            var opts = options || {};
            var layout = opts.layout ? opts.layout : opts.layout === undefined;

            // Setup parent scope data.
            isDescending = !!opts.descending;
            origItems = items.slice(0);
            indexMap = null;

            // If function is provided do a native array sort.
            if (isFunction(comparer)) {
                sortComparer = comparer;
                items.sort(customComparer);
            }
                // Otherwise if we got a string, let's sort by the sort data as provided in
            // the instance's options.
            else if (typeof comparer === STRING_TYPE) {
                sortComparer = comparer
                    .trim()
                    .split(' ')
                    .filter(function (val) {
                        return val;
                    })
                    .map(function (val) {
                        return val.split(':');
                    });
                items.sort(defaultComparer);
            }
                // Otherwise if we got an array, let's assume it's a presorted array of the
                // items and order the items based on it. Here we blindly trust that the
                // presorted array consists of the same item instances as the current
            // `gird._items` array.
            else if (Array.isArray(comparer)) {
                items.length = 0;
                items.push.apply(items, comparer);
            }
            // Otherwise let's throw an error.
            else {
                sortComparer = isDescending = origItems = indexMap = null;
                throw new Error('Invalid comparer argument provided.');
            }

            // Emit sort event.
            if (this._hasListeners(EVENT_SORT)) {
                this._emit(EVENT_SORT, items.slice(0), origItems);
            }

            // If layout is needed.
            if (layout) {
                this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
            }

            // Reset data (to avoid mem leaks).
            sortComparer = isDescending = origItems = indexMap = null;

            return this;
        };
    })();

    /**
     * Move item to another index or in place of another item.
     *
     * @public
     * @param {(HtmlElement|Number|Item)} item
     * @param {(HtmlElement|Number|Item)} position
     * @param {Object} [options]
     * @param {String} [options.action="move"]
     *   - Accepts either "move" or "swap".
     *   - "move" moves the item in place of the other item.
     *   - "swap" swaps the position of the items.
     * @param {(Boolean|Function|String)} [options.layout=true]
     * @returns {Grid}
     */
    Grid.prototype.move = function (item, position, options) {
        if (this._isDestroyed || this._items.length < 2) return this;

        var items = this._items;
        var opts = options || {};
        var layout = opts.layout ? opts.layout : opts.layout === undefined;
        var isSwap = opts.action === ACTION_SWAP;
        var action = isSwap ? ACTION_SWAP : ACTION_MOVE;
        var fromItem = this.getItem(item);
        var toItem = this.getItem(position);
        var fromIndex;
        var toIndex;

        // Make sure the items exist and are not the same.
        if (fromItem && toItem && fromItem !== toItem) {
            // Get the indices of the items.
            fromIndex = items.indexOf(fromItem);
            toIndex = items.indexOf(toItem);

            // Do the move/swap.
            if (isSwap) {
                arraySwap(items, fromIndex, toIndex);
            } else {
                arrayMove(items, fromIndex, toIndex);
            }

            // Emit move event.
            if (this._hasListeners(EVENT_MOVE)) {
                this._emit(EVENT_MOVE, {
                    item: fromItem,
                    fromIndex: fromIndex,
                    toIndex: toIndex,
                    action: action,
                });
            }

            // If layout is needed.
            if (layout) {
                this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
            }
        }

        return this;
    };

    /**
     * Send item to another Grid instance.
     *
     * @public
     * @param {(HtmlElement|Number|Item)} item
     * @param {Grid} targetGrid
     * @param {(HtmlElement|Number|Item)} position
     * @param {Object} [options]
     * @param {HTMLElement} [options.appendTo=document.body]
     * @param {(Boolean|Function|String)} [options.layoutSender=true]
     * @param {(Boolean|Function|String)} [options.layoutReceiver=true]
     * @returns {Grid}
     */
    Grid.prototype.send = function (item, targetGrid, position, options) {
        if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this;

        // Make sure we have a valid target item.
        item = this.getItem(item);
        if (!item) return this;

        var opts = options || {};
        var container = opts.appendTo || document.body;
        var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined;
        var layoutReceiver = opts.layoutReceiver
            ? opts.layoutReceiver
            : opts.layoutReceiver === undefined;

        // Start the migration process.
        item._migrate.start(targetGrid, position, container);

        // If migration was started successfully and the item is active, let's layout
        // the grids.
        if (item._migrate._isActive && item._isActive) {
            if (layoutSender) {
                this.layout(
                    layoutSender === INSTANT_LAYOUT,
                    isFunction(layoutSender) ? layoutSender : undefined
                );
            }
            if (layoutReceiver) {
                targetGrid.layout(
                    layoutReceiver === INSTANT_LAYOUT,
                    isFunction(layoutReceiver) ? layoutReceiver : undefined
                );
            }
        }

        return this;
    };

    /**
     * Destroy the instance.
     *
     * @public
     * @param {Boolean} [removeElements=false]
     * @returns {Grid}
     */
    Grid.prototype.destroy = function (removeElements) {
        if (this._isDestroyed) return this;

        var container = this._element;
        var items = this._items.slice(0);
        var layoutStyles = (this._layout && this._layout.styles) || {};
        var i, prop;

        // Unbind window resize event listener.
        unbindLayoutOnResize(this);

        // Destroy items.
        for (i = 0; i < items.length; i++) items[i]._destroy(removeElements);
        this._items.length = 0;

        // Restore container.
        removeClass(container, this._settings.containerClass);
        for (prop in layoutStyles) container.style[prop] = '';

        // Emit destroy event and unbind all events.
        this._emit(EVENT_DESTROY);
        this._emitter.destroy();

        // Remove reference from the grid instances collection.
        delete GRID_INSTANCES[this._id];

        // Flag instance as destroyed.
        this._isDestroyed = true;

        return this;
    };

    /**
     * Private prototype methods
     * *************************
     */

    /**
     * Emit a grid event.
     *
     * @private
     * @param {String} event
     * @param {...*} [arg]
     */
    Grid.prototype._emit = function () {
        if (this._isDestroyed) return;
        this._emitter.emit.apply(this._emitter, arguments);
    };

    /**
     * Check if there are any events listeners for an event.
     *
     * @private
     * @param {String} event
     * @returns {Boolean}
     */
    Grid.prototype._hasListeners = function (event) {
        if (this._isDestroyed) return false;
        return this._emitter.countListeners(event) > 0;
    };

    /**
     * Update container's width, height and offsets.
     *
     * @private
     */
    Grid.prototype._updateBoundingRect = function () {
        var element = this._element;
        var rect = element.getBoundingClientRect();
        this._width = rect.width;
        this._height = rect.height;
        this._left = rect.left;
        this._top = rect.top;
        this._right = rect.right;
        this._bottom = rect.bottom;
    };

    /**
     * Update container's border sizes.
     *
     * @private
     * @param {Boolean} left
     * @param {Boolean} right
     * @param {Boolean} top
     * @param {Boolean} bottom
     */
    Grid.prototype._updateBorders = function (left, right, top, bottom) {
        var element = this._element;
        if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width');
        if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width');
        if (top) this._borderTop = getStyleAsFloat(element, 'border-top-width');
        if (bottom) this._borderBottom = getStyleAsFloat(element, 'border-bottom-width');
    };

    /**
     * Refresh all of container's internal dimensions and offsets.
     *
     * @private
     */
    Grid.prototype._refreshDimensions = function () {
        this._updateBoundingRect();
        this._updateBorders(1, 1, 1, 1);
        this._boxSizing = getStyle(this._element, 'box-sizing');
    };

    /**
     * Calculate and apply item positions.
     *
     * @private
     * @param {Object} layout
     */
    Grid.prototype._onLayoutDataReceived = (function () {
        var itemsToLayout = [];
        return function (layout) {
            if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return;

            var grid = this;
            var instant = this._nextLayoutData.instant;
            var onFinish = this._nextLayoutData.onFinish;
            var numItems = layout.items.length;
            var counter = numItems;
            var item;
            var left;
            var top;
            var i;

            // Reset next layout data.
            this._nextLayoutData = null;

            if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) {
                this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0));
            }

            // Update the layout reference.
            this._layout = layout;

            // Update the item positions and collect all items that need to be laid
            // out. It is critical that we update the item position _before_ the
            // layoutStart event as the new data might be needed in the callback.
            itemsToLayout.length = 0;
            for (i = 0; i < numItems; i++) {
                item = layout.items[i];

                // Make sure we have a matching item.
                if (!item) {
                    --counter;
                    continue;
                }

                // Get the item's new left and top values.
                left = layout.slots[i * 2];
                top = layout.slots[i * 2 + 1];

                // Let's skip the layout process if we can. Possibly avoids a lot of DOM
                // operations which saves us some CPU cycles.
                if (item._canSkipLayout(left, top)) {
                    --counter;
                    continue;
                }

                // Update the item's position.
                item._left = left;
                item._top = top;

                // Only active non-dragged items need to be moved.
                if (item.isActive() && !item.isDragging()) {
                    itemsToLayout.push(item);
                } else {
                    --counter;
                }
            }

            // Set layout styles to the grid element.
            if (layout.styles) {
                setStyles(this._element, layout.styles);
            }

            // layoutStart event is intentionally emitted after the container element's
            // dimensions are set, because otherwise there would be no hook for reacting
            // to container dimension changes.
            if (this._hasListeners(EVENT_LAYOUT_START)) {
                this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true);
                // Let's make sure that the current layout process has not been overridden
                // in the layoutStart event, and if so, let's stop processing the aborted
                // layout.
                if (this._layout.id !== layout.id) return;
            }

            var tryFinish = function () {
                if (--counter > 0) return;

                var hasLayoutChanged = grid._layout.id !== layout.id;
                var callback = isFunction(instant) ? instant : onFinish;

                if (!hasLayoutChanged) {
                    grid._isLayoutFinished = true;
                }

                if (isFunction(callback)) {
                    callback(layout.items.slice(0), hasLayoutChanged);
                }

                if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) {
                    grid._emit(EVENT_LAYOUT_END, layout.items.slice(0));
                }
            };

            if (!itemsToLayout.length) {
                tryFinish();
                return this;
            }

            this._isLayoutFinished = false;

            for (i = 0; i < itemsToLayout.length; i++) {
                if (this._layout.id !== layout.id) break;
                itemsToLayout[i]._layout.start(instant === true, tryFinish);
            }

            if (this._layout.id === layout.id) {
                itemsToLayout.length = 0;
            }

            return this;
        };
    })();

    /**
     * Show or hide Grid instance's items.
     *
     * @private
     * @param {Item[]} items
     * @param {Boolean} toVisible
     * @param {Object} [options]
     * @param {Boolean} [options.instant=false]
     * @param {Boolean} [options.syncWithLayout=true]
     * @param {Function} [options.onFinish]
     * @param {(Boolean|Function|String)} [options.layout=true]
     */
    Grid.prototype._setItemsVisibility = function (items, toVisible, options) {
        var grid = this;
        var targetItems = items.slice(0);
        var opts = options || {};
        var isInstant = opts.instant === true;
        var callback = opts.onFinish;
        var layout = opts.layout ? opts.layout : opts.layout === undefined;
        var counter = targetItems.length;
        var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START;
        var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END;
        var method = toVisible ? 'show' : 'hide';
        var needsLayout = false;
        var completedItems = [];
        var hiddenItems = [];
        var item;
        var i;

        // If there are no items call the callback, but don't emit any events.
        if (!counter) {
            if (isFunction(callback)) callback(targetItems);
            return;
        }

        // Prepare the items.
        for (i = 0; i < targetItems.length; i++) {
            item = targetItems[i];

            // If inactive item is shown or active item is hidden we need to do
            // layout.
            if ((toVisible && !item._isActive) || (!toVisible && item._isActive)) {
                needsLayout = true;
            }

            // If inactive item is shown we also need to do a little hack to make the
            // item not animate it's next positioning (layout).
            item._layout._skipNextAnimation = !!(toVisible && !item._isActive);

            // If a hidden item is being shown we need to refresh the item's
            // dimensions.
            if (toVisible && item._visibility._isHidden) {
                hiddenItems.push(item);
            }

            // Add item to layout or remove it from layout.
            if (toVisible) {
                item._addToLayout();
            } else {
                item._removeFromLayout();
            }
        }

        // Force refresh the dimensions of all hidden items.
        if (hiddenItems.length) {
            this.refreshItems(hiddenItems, true);
            hiddenItems.length = 0;
        }

        // Show the items in sync with the next layout.
        function triggerVisibilityChange() {
            if (needsLayout && opts.syncWithLayout !== false) {
                grid.off(EVENT_LAYOUT_START, triggerVisibilityChange);
            }

            if (grid._hasListeners(startEvent)) {
                grid._emit(startEvent, targetItems.slice(0));
            }

            for (i = 0; i < targetItems.length; i++) {
                // Make sure the item is still in the original grid. There is a chance
                // that the item starts migrating before tiggerVisibilityChange is called.
                if (targetItems[i]._gridId !== grid._id) {
                    if (--counter < 1) {
                        if (isFunction(callback)) callback(completedItems.slice(0));
                        if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
                    }
                    continue;
                }

                targetItems[i]._visibility[method](isInstant, function (interrupted, item) {
                    // If the current item's animation was not interrupted add it to the
                    // completedItems array.
                    if (!interrupted) completedItems.push(item);

                    // If all items have finished their animations call the callback
                    // and emit showEnd/hideEnd event.
                    if (--counter < 1) {
                        if (isFunction(callback)) callback(completedItems.slice(0));
                        if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
                    }
                });
            }
        }

        // Trigger the visibility change, either async with layout or instantly.
        if (needsLayout && opts.syncWithLayout !== false) {
            this.on(EVENT_LAYOUT_START, triggerVisibilityChange);
        } else {
            triggerVisibilityChange();
        }

        // Trigger layout if needed.
        if (needsLayout && layout) {
            this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined);
        }
    };

    /**
     * Private helpers
     * ***************
     */

    /**
     * Merge default settings with user settings. The returned object is a new
     * object with merged values. The merging is a deep merge meaning that all
     * objects and arrays within the provided settings objects will be also merged
     * so that modifying the values of the settings object will have no effect on
     * the returned object.
     *
     * @param {Object} defaultSettings
     * @param {Object} [userSettings]
     * @returns {Object} Returns a new object.
     */
    function mergeSettings(defaultSettings, userSettings) {
        // Create a fresh copy of default settings.
        var settings = mergeObjects({}, defaultSettings);

        // Merge user settings to default settings.
        if (userSettings) {
            settings = mergeObjects(settings, userSettings);
        }

        // Handle visible/hidden styles manually so that the whole object is
        // overridden instead of the props.

        if (userSettings && userSettings.visibleStyles) {
            settings.visibleStyles = userSettings.visibleStyles;
        } else if (defaultSettings && defaultSettings.visibleStyles) {
            settings.visibleStyles = defaultSettings.visibleStyles;
        }

        if (userSettings && userSettings.hiddenStyles) {
            settings.hiddenStyles = userSettings.hiddenStyles;
        } else if (defaultSettings && defaultSettings.hiddenStyles) {
            settings.hiddenStyles = defaultSettings.hiddenStyles;
        }

        return settings;
    }

    /**
     * Merge two objects recursively (deep merge). The source object's properties
     * are merged to the target object.
     *
     * @param {Object} target
     *   - The target object.
     * @param {Object} source
     *   - The source object.
     * @returns {Object} Returns the target object.
     */
    function mergeObjects(target, source) {
        var sourceKeys = Object.keys(source);
        var length = sourceKeys.length;
        var isSourceObject;
        var propName;
        var i;

        for (i = 0; i < length; i++) {
            propName = sourceKeys[i];
            isSourceObject = isPlainObject(source[propName]);

            // If target and source values are both objects, merge the objects and
            // assign the merged value to the target property.
            if (isPlainObject(target[propName]) && isSourceObject) {
                target[propName] = mergeObjects(mergeObjects({}, target[propName]), source[propName]);
                continue;
            }

            // If source's value is object and target's is not let's clone the object as
            // the target's value.
            if (isSourceObject) {
                target[propName] = mergeObjects({}, source[propName]);
                continue;
            }

            // If source's value is an array let's clone the array as the target's
            // value.
            if (Array.isArray(source[propName])) {
                target[propName] = source[propName].slice(0);
                continue;
            }

            // In all other cases let's just directly assign the source's value as the
            // target's value.
            target[propName] = source[propName];
        }

        return target;
    }

    /**
     * Collect and return initial items for grid.
     *
     * @param {HTMLElement} gridElement
     * @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements
     * @returns {(HTMLElement[]|NodeList|HtmlCollection)}
     */
    function getInitialGridElements(gridElement, elements) {
        // If we have a wildcard selector let's return all the children.
        if (elements === '*') {
            return gridElement.children;
        }

        // If we have some more specific selector, let's filter the elements.
        if (typeof elements === STRING_TYPE) {
            var result = [];
            var children = gridElement.children;
            for (var i = 0; i < children.length; i++) {
                if (elementMatches(children[i], elements)) {
                    result.push(children[i]);
                }
            }
            return result;
        }

        // If we have an array of elements or a node list.
        if (Array.isArray(elements) || isNodeList(elements)) {
            return elements;
        }

        // Otherwise just return an empty array.
        return [];
    }

    /**
     * Bind grid's resize handler to window.
     *
     * @param {Grid} grid
     * @param {(Number|Boolean)} delay
     */
    function bindLayoutOnResize(grid, delay) {
        if (typeof delay !== NUMBER_TYPE) {
            delay = delay === true ? 0 : -1;
        }

        if (delay >= 0) {
            grid._resizeHandler = debounce(function () {
                grid.refreshItems().layout();
            }, delay);

            window.addEventListener('resize', grid._resizeHandler);
        }
    }

    /**
     * Unbind grid's resize handler from window.
     *
     * @param {Grid} grid
     */
    function unbindLayoutOnResize(grid) {
        if (grid._resizeHandler) {
            grid._resizeHandler(true);
            window.removeEventListener('resize', grid._resizeHandler);
            grid._resizeHandler = null;
        }
    }

    /**
     * Normalize style declaration object, returns a normalized (new) styles object
     * (prefixed properties and invalid properties removed).
     *
     * @param {Object} styles
     * @returns {Object}
     */
    function normalizeStyles(styles) {
        var normalized = {};
        var docElemStyle = document.documentElement.style;
        var prop, prefixedProp;

        // Normalize visible styles (prefix and remove invalid).
        for (prop in styles) {
            if (!styles[prop]) continue;
            prefixedProp = getPrefixedPropName(docElemStyle, prop);
            if (!prefixedProp) continue;
            normalized[prefixedProp] = styles[prop];
        }

        return normalized;
    }

    /**
     * Create index map from items.
     *
     * @param {Item[]} items
     * @returns {Object}
     */
    function createIndexMap(items) {
        var result = {};
        for (var i = 0; i < items.length; i++) {
            result[items[i]._id] = i;
        }
        return result;
    }

    /**
     * Sort comparer function for items' index map.
     *
     * @param {Object} indexMap
     * @param {Item} itemA
     * @param {Item} itemB
     * @returns {Number}
     */
    function compareIndexMap(indexMap, itemA, itemB) {
        var indexA = indexMap[itemA._id];
        var indexB = indexMap[itemB._id];
        return indexA - indexB;
    }

    return Grid;

})));