/** * Muuri v0.7.1 * https://github.com/haltu/muuri * 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ö * @license MIT * * Muuri Ticker / Muuri Emitter / Muuri Queue * Copyright (c) 2018-present, Niklas Rämö * @license MIT */ (function (global, factory) { if (typeof exports === 'object' && typeof module !== 'undefined') { var Hammer; try { Hammer = require('hammerjs') } catch (e) {} module.exports = factory(Hammer); } else { global.Muuri = factory(global.Hammer); } }(this, (function (Hammer) { 'use strict'; var namespace = 'Muuri'; var gridInstances = {}; var eventSynchronize = 'synchronize'; var eventLayoutStart = 'layoutStart'; var eventLayoutEnd = 'layoutEnd'; var eventAdd = 'add'; var eventRemove = 'remove'; var eventShowStart = 'showStart'; var eventShowEnd = 'showEnd'; var eventHideStart = 'hideStart'; var eventHideEnd = 'hideEnd'; var eventFilter = 'filter'; var eventSort = 'sort'; var eventMove = 'move'; var eventSend = 'send'; var eventBeforeSend = 'beforeSend'; var eventReceive = 'receive'; var eventBeforeReceive = 'beforeReceive'; var eventDragInit = 'dragInit'; var eventDragStart = 'dragStart'; var eventDragMove = 'dragMove'; var eventDragScroll = 'dragScroll'; var eventDragEnd = 'dragEnd'; var eventDragReleaseStart = 'dragReleaseStart'; var eventDragReleaseEnd = 'dragReleaseEnd'; var eventDestroy = 'destroy'; /** * Event emitter constructor. * * @class */ function Emitter() { this._events = {}; this._queue = []; this._counter = 0; this._isDestroyed = false; } /** * Public prototype methods * ************************ */ /** * Bind an event listener. * * @public * @memberof Emitter.prototype * @param {String} event * @param {Function} listener * @returns {Emitter} */ Emitter.prototype.on = function(event, listener) { if (this._isDestroyed) 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; }; /** * Bind an event listener that is triggered only once. * * @public * @memberof Emitter.prototype * @param {String} event * @param {Function} listener * @returns {Emitter} */ Emitter.prototype.once = function(event, listener) { if (this._isDestroyed) return this; var callback = function() { this.off(event, callback); listener.apply(null, arguments); }.bind(this); return this.on(event, callback); }; /** * Unbind all event listeners that match the provided listener function. * * @public * @memberof Emitter.prototype * @param {String} event * @param {Function} [listener] * @returns {Emitter} */ Emitter.prototype.off = function(event, listener) { if (this._isDestroyed) return this; // Get listeners and return immediately if none is found. var listeners = this._events[event]; if (!listeners || !listeners.length) return this; // If no specific listener is provided remove all listeners. if (!listener) { listeners.length = 0; return this; } // Remove all matching listeners. var i = listeners.length; while (i--) { if (listener === listeners[i]) listeners.splice(i, 1); } return this; }; /** * Emit all listeners in a specified event with the provided arguments. * * @public * @memberof Emitter.prototype * @param {String} event * @param {*} [arg1] * @param {*} [arg2] * @param {*} [arg3] * @returns {Emitter} */ Emitter.prototype.emit = function(event, arg1, arg2, arg3) { if (this._isDestroyed) return this; // Get event listeners and quit early if there's no listeners. var listeners = this._events[event]; if (!listeners || !listeners.length) return this; var queue = this._queue; var qLength = queue.length; var aLength = arguments.length - 1; var i; // 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. for (i = 0; i < listeners.length; i++) { queue.push(listeners[i]); } // 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). for (i = qLength, qLength = queue.length; i < qLength; i++) { // prettier-ignore aLength === 0 ? queue[i]() : aLength === 1 ? queue[i](arg1) : aLength === 2 ? queue[i](arg1, arg2) : queue[i](arg1, arg2, arg3); // Stop processing if the emitter is destroyed. if (this._isDestroyed) 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; }; /** * Destroy emitter instance. Basically just removes all bound listeners. * * @public * @memberof Emitter.prototype * @returns {Emitter} */ Emitter.prototype.destroy = function() { if (this._isDestroyed) return this; var events = this._events; var event; // Flag as destroyed. this._isDestroyed = true; // Reset queue (if queue is currently processing this will also stop that). this._queue.length = this._counter = 0; // Remove all listeners. for (event in events) { if (events[event]) { events[event].length = 0; events[event] = undefined; } } return this; }; // Set up the default export values. var isTransformSupported = false; var transformStyle = 'transform'; var transformProp = 'transform'; // Find the supported transform prop and style names. var style = 'transform'; var styleCap = 'Transform'; ['', 'Webkit', 'Moz', 'O', 'ms'].forEach(function(prefix) { if (isTransformSupported) return; var propName = prefix ? prefix + styleCap : style; if (document.documentElement.style[propName] !== undefined) { prefix = prefix.toLowerCase(); transformStyle = prefix ? '-' + prefix + '-' + style : style; transformProp = propName; isTransformSupported = true; } }); var stylesCache = 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 = stylesCache && stylesCache.get(element); if (!styles) { styles = window.getComputedStyle(element, null); stylesCache && stylesCache.set(element, styles); } return styles.getPropertyValue(style === 'transform' ? transformStyle : style); } var styleNameRegEx = /([A-Z])/g; /** * Transforms a camel case style property to kebab case style property. * * @param {String} string * @returns {String} */ function getStyleName(string) { return string.replace(styleNameRegEx, '-$1').toLowerCase(); } /** * Set inline styles to an element. * * @param {HTMLElement} element * @param {Object} styles */ function setStyles(element, styles) { for (var prop in styles) { element.style[prop === 'transform' ? transformProp : prop] = styles[prop]; } } /** * Item animation handler powered by Web Animations API. * * @class * @param {HTMLElement} element */ function ItemAnimate(element) { this._element = element; this._animation = null; this._callback = null; this._props = []; this._values = []; this._keyframes = []; this._options = {}; 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 * @memberof ItemAnimate.prototype * @param {Object} propsFrom * @param {Object} propsTo * @param {Object} [options] * @param {Number} [options.duration=300] * @param {String} [options.easing='ease'] * @param {Function} [options.onFinish] */ ItemAnimate.prototype.start = function(propsFrom, propsTo, options) { if (this._isDestroyed) return; var animation = this._animation; var currentProps = this._props; var currentValues = this._values; var opts = options || 0; var cancelAnimation = false; // If we have an existing animation running, let's check if it needs to be // cancelled or if it can continue running. if (animation) { var propCount = 0; var propIndex; // Check if the requested animation target props and values match with the // current props and values. for (var 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 (!cancelAnimation && propCount !== currentProps.length) { cancelAnimation = true; } } // Cancel animation (if required). if (cancelAnimation) animation.cancel(); // Store animation callback. this._callback = typeof opts.onFinish === 'function' ? 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]); } // Set up keyframes. var animKeyframes = this._keyframes; animKeyframes[0] = propsFrom; animKeyframes[1] = propsTo; // Set up options. var animOptions = this._options; animOptions.duration = opts.duration || 300; animOptions.easing = opts.easing || 'ease'; // Start the animation var element = this._element; animation = element.animate(animKeyframes, animOptions); animation.onfinish = this._onFinish; this._animation = animation; // 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 * @memberof ItemAnimate.prototype * @param {Object} [styles] */ ItemAnimate.prototype.stop = function(styles) { if (this._isDestroyed || !this._animation) return; var element = this._element; var currentProps = this._props; var currentValues = this._values; var propName; var propValue; var i; // Calculate (if not provided) and set styles. if (!styles) { for (i = 0; i < currentProps.length; i++) { propName = currentProps[i]; propValue = getStyle(element, getStyleName(propName)); element.style[propName === 'transform' ? transformProp : propName] = propValue; } } else { setStyles(element, styles); } // Cancel animation. this._animation.cancel(); this._animation = this._callback = null; // Reset current props and values. currentProps.length = currentValues.length = 0; }; /** * Check if the item is being animated currently. * * @public * @memberof ItemAnimate.prototype * @return {Boolean} */ ItemAnimate.prototype.isAnimating = function() { return !!this._animation; }; /** * Destroy the instance and stop current animation if it is running. * * @public * @memberof ItemAnimate.prototype */ ItemAnimate.prototype.destroy = function() { if (this._isDestroyed) return; this.stop(); this._element = this._options = this._keyframes = null; this._isDestroyed = true; }; /** * Private prototype methods * ************************* */ /** * Animation end handler. * * @private * @memberof ItemAnimate.prototype */ ItemAnimate.prototype._onFinish = function() { var callback = this._callback; this._animation = this._callback = null; this._props.length = this._values.length = 0; callback && callback(); }; var raf = ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || rafFallback ).bind(window); function rafFallback(cb) { return window.setTimeout(cb, 16); } /** * A ticker system for handling DOM reads and writes in an efficient way. * Contains a read queue and a write queue that are processed on the next * animation frame when needed. * * @class */ function Ticker() { this._nextTick = null; this._queue = []; this._reads = {}; this._writes = {}; this._batch = []; this._batchReads = {}; this._batchWrites = {}; this._flush = this._flush.bind(this); } Ticker.prototype.add = function(id, readCallback, writeCallback, isImportant) { // First, let's check if an item has been added to the queues with the same id // and if so -> remove it. var currentIndex = this._queue.indexOf(id); if (currentIndex > -1) this._queue[currentIndex] = undefined; // Add all important callbacks to the beginning of the queue and other // callbacks to the end of the queue. isImportant ? this._queue.unshift(id) : this._queue.push(id); // Store callbacks. this._reads[id] = readCallback; this._writes[id] = writeCallback; // Finally, let's kick-start the next tick if it is not running yet. if (!this._nextTick) this._nextTick = raf(this._flush); }; Ticker.prototype.cancel = function(id) { var currentIndex = this._queue.indexOf(id); if (currentIndex > -1) { this._queue[currentIndex] = undefined; this._reads[id] = undefined; this._writes[id] = undefined; } }; Ticker.prototype._flush = function() { var queue = this._queue; var reads = this._reads; var writes = this._writes; var batch = this._batch; var batchReads = this._batchReads; var batchWrites = this._batchWrites; var length = queue.length; var id; var i; // Reset ticker. this._nextTick = null; // Setup queues and callback placeholders. for (i = 0; i < length; i++) { id = queue[i]; if (!id) continue; batch.push(id); batchReads[id] = reads[id]; reads[id] = undefined; batchWrites[id] = writes[id]; writes[id] = undefined; } // Reset queue. queue.length = 0; // Process read callbacks. for (i = 0; i < length; i++) { id = batch[i]; if (batchReads[id]) { batchReads[id](); batchReads[id] = undefined; } } // Process write callbacks. for (i = 0; i < length; i++) { id = batch[i]; if (batchWrites[id]) { batchWrites[id](); batchWrites[id] = undefined; } } // Reset batch. batch.length = 0; // Restart the ticker if needed. if (!this._nextTick && queue.length) { this._nextTick = raf(this._flush); } }; var ticker = new Ticker(); var layoutTick = 'layout'; var visibilityTick = 'visibility'; var moveTick = 'move'; var scrollTick = 'scroll'; function addLayoutTick(itemId, readCallback, writeCallback) { return ticker.add(itemId + layoutTick, readCallback, writeCallback); } function cancelLayoutTick(itemId) { return ticker.cancel(itemId + layoutTick); } function addVisibilityTick(itemId, readCallback, writeCallback) { return ticker.add(itemId + visibilityTick, readCallback, writeCallback); } function cancelVisibilityTick(itemId) { return ticker.cancel(itemId + visibilityTick); } function addMoveTick(itemId, readCallback, writeCallback) { return ticker.add(itemId + moveTick, readCallback, writeCallback, true); } function cancelMoveTick(itemId) { return ticker.cancel(itemId + moveTick); } function addScrollTick(itemId, readCallback, writeCallback) { return ticker.add(itemId + scrollTick, readCallback, writeCallback, true); } function cancelScrollTick(itemId) { return ticker.cancel(itemId + scrollTick); } var proto = Element.prototype; var matches = proto.matches || proto.matchesSelector || proto.webkitMatchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || proto.oMatchesSelector; /** * Check if element matches a CSS selector. * * @param {*} val * @returns {Boolean} */ function elementMatches(el, selector) { return matches.call(el, selector); } /** * Add class to an element. * * @param {HTMLElement} element * @param {String} className */ function addClassModern(element, className) { element.classList.add(className); } /** * Add class to an element (legacy version, for IE9 support). * * @param {HTMLElement} element * @param {String} className */ function addClassLegacy(element, className) { if (!elementMatches(element, '.' + className)) { element.className += ' ' + className; } } var addClass = ('classList' in Element.prototype ? addClassModern : addClassLegacy); /** * 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. * * @param {Array} array * @param {Number} index * @param {Boolean} isMigration */ function normalizeArrayIndex(array, index, isMigration) { var length = array.length; var maxIndex = Math.max(0, isMigration ? length : length - 1); 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 actionCancel = 'cancel'; var actionFinish = 'finish'; /** * 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 "finish", calls the debounce function immediately if it is currently * waiting to be called, and when being "cancel" cancels the currently queued * function call. * * @param {Function} fn * @param {Number} wait * @returns {Function} */ function debounce(fn, wait) { var timeout; if (wait > 0) { return function(action) { if (timeout !== undefined) { timeout = window.clearTimeout(timeout); if (action === actionFinish) fn(); } if (action !== actionCancel && action !== actionFinish) { timeout = window.setTimeout(function() { timeout = undefined; fn(); }, wait); } }; } return function(action) { if (action !== actionCancel) fn(); }; } /** * 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, 'transform'); if (!transform || transform === 'none') return false; var display = getStyle(element, 'display'); if (display === 'inline' || display === 'none') 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 * @param {Boolean} [includeSelf=false] * - When this is set to true the containing block checking is started from * the provided element. Otherwise the checking is started from the * provided element's parent element. * @returns {(Document|Element)} */ function getContainingBlock(element, includeSelf) { // 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 ret = (includeSelf ? element : element.parentElement) || document; while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) { ret = ret.parentElement || document; } return ret; } /** * 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 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 ret = offsetData || {}; var rect; // Set up return data. ret.left = 0; ret.top = 0; // Document's offsets are always 0. if (element === document) return ret; // Add viewport scroll left/top to the respective offsets. ret.left = window.pageXOffset || 0; ret.top = window.pageYOffset || 0; // Window's offsets are the viewport scroll left/top values. if (element.self === window.self) return ret; // Add element's client rects to the offsets. rect = element.getBoundingClientRect(); ret.left += rect.left; ret.top += rect.top; // Exclude element's borders from the offset. ret.left += getStyleAsFloat(element, 'border-left-width'); ret.top += getStyleAsFloat(element, 'border-top-width'); return ret; } /** * 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, true); elemB = getContainingBlock(elemB, true); // 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; } var translateData = {}; /** * 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) { translateData.x = 0; translateData.y = 0; var transform = getStyle(element, 'transform'); if (!transform) return translateData; var matrixData = transform.replace('matrix(', '').split(','); translateData.x = parseFloat(matrixData[4]) || 0; translateData.y = parseFloat(matrixData[5]) || 0; return translateData; } /** * 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)'; } var tempArray = []; /** * 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 === 'number' ? index : -1; if (startIndex < 0) startIndex = array.length - startIndex + 1; array.splice.apply(array, tempArray.concat(startIndex, 0, items)); tempArray.length = 0; } var objectType = '[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 === 'object' && toString.call(val) === objectType; } /** * Remove class from an element. * * @param {HTMLElement} element * @param {String} className */ function removeClassModern(element, className) { element.classList.remove(className); } /** * Remove class from an element (legacy version, for IE9 support). * * @param {HTMLElement} element * @param {String} className */ function removeClassLegacy(element, className) { if (elementMatches(element, '.' + className)) { element.className = (' ' + element.className + ' ').replace(' ' + className + ' ', ' ').trim(); } } var removeClass = ('classList' in Element.prototype ? removeClassModern : removeClassLegacy); // To provide consistently correct dragging experience we need to know if // transformed elements leak fixed elements or not. var hasTransformLeak = checkTransformLeak(); // Drag start predicate states. var startPredicateInactive = 0; var startPredicatePending = 1; var startPredicateResolved = 2; var startPredicateRejected = 3; /** * Bind Hammer touch interaction to an item. * * @class * @param {Item} item */ function ItemDrag(item) { if (!Hammer) { throw new Error('[' + namespace + '] required dependency Hammer is not defined.'); } // If we don't have a valid transform leak test result yet, let's run the // test on first ItemDrag init. The test needs body element to be ready and // here we can be sure that it is ready. if (hasTransformLeak === null) { hasTransformLeak = checkTransformLeak(); } var drag = this; var element = item._element; var grid = item.getGrid(); var settings = grid._settings; var hammer; // Start predicate private data. var startPredicate = typeof settings.dragStartPredicate === 'function' ? settings.dragStartPredicate : ItemDrag.defaultStartPredicate; var startPredicateState = startPredicateInactive; var startPredicateResult; // Protected data. this._item = item; this._gridId = grid._id; this._hammer = hammer = new Hammer.Manager(element); this._isDestroyed = false; this._isMigrating = false; // Setup item's initial drag data. this._reset(); // Bind some methods that needs binding. this._onScroll = this._onScroll.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._checkOverlap = this._checkOverlap.bind(this); // Create a private drag start resolver that can be used to resolve the drag // start predicate asynchronously. this._forceResolveStartPredicate = function(event) { if (!this._isDestroyed && startPredicateState === startPredicatePending) { startPredicateState = startPredicateResolved; this._onStart(event); } }; // Create debounce overlap checker function. this._checkOverlapDebounce = debounce(this._checkOverlap, settings.dragSortInterval); // Add drag recognizer to hammer. hammer.add( new Hammer.Pan({ event: 'drag', pointers: 1, threshold: 0, direction: Hammer.DIRECTION_ALL }) ); // Add drag init recognizer to hammer. hammer.add( new Hammer.Press({ event: 'draginit', pointers: 1, threshold: 1000, time: 0 }) ); // Configure the hammer instance. if (isPlainObject(settings.dragHammerSettings)) { hammer.set(settings.dragHammerSettings); } // Bind drag events. hammer .on('draginit dragstart dragmove', function(e) { // Let's activate drag start predicate state. if (startPredicateState === startPredicateInactive) { startPredicateState = startPredicatePending; } // If predicate is pending try to resolve it. if (startPredicateState === startPredicatePending) { startPredicateResult = startPredicate(drag._item, e); if (startPredicateResult === true) { startPredicateState = startPredicateResolved; drag._onStart(e); } else if (startPredicateResult === false) { startPredicateState = startPredicateRejected; } } // Otherwise if predicate is resolved and drag is active, move the item. else if (startPredicateState === startPredicateResolved && drag._isActive) { drag._onMove(e); } }) .on('dragend dragcancel draginitup', function(e) { // Check if the start predicate was resolved during drag. var isResolved = startPredicateState === startPredicateResolved; // 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. startPredicate(drag._item, e); // Reset start predicate state. startPredicateState = startPredicateInactive; // If predicate is resolved and dragging is active, call the end handler. if (isResolved && drag._isActive) drag._onEnd(e); }); // Prevent native link/image dragging for the item and it's ancestors. element.addEventListener('dragstart', preventDefault, false); } /** * 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 * @memberof ItemDrag * @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} */ ItemDrag.defaultStartPredicate = function(item, event, options) { var drag = item._drag; var predicate = drag._startPredicateData || drag._setupStartPredicate(options); // 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; } // Find and store the handle element so we can check later on if the // cursor is within the handle. If we have a handle selector let's find // the corresponding element. Otherwise let's use the item element as the // handle. if (!predicate.handleElement) { predicate.handleElement = drag._getStartPredicateHandle(event); if (!predicate.handleElement) return false; } // 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 * @memberof ItemDrag * @param {Item} item * @param {Object} [options] * @param {Number} [options.threshold=50] * @param {String} [options.action='move'] * @returns {(Boolean|DragSortCommand)} * - Returns false if no valid index was found. Otherwise returns drag sort * command. */ ItemDrag.defaultSortPredicate = (function() { var itemRect = {}; var targetRect = {}; var returnData = {}; var rootGridArray = []; function getTargetGrid(item, rootGrid, threshold) { var target = null; var dragSort = rootGrid._settings.dragSort; var bestScore = -1; var gridScore; var grids; var grid; var i; // Get potential target grids. if (dragSort === true) { rootGridArray[0] = rootGrid; grids = rootGridArray; } else { grids = dragSort.call(rootGrid, item); } // Return immediately if there are no grids. if (!Array.isArray(grids)) 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; // We need to update the grid's offsets and dimensions since they might // have changed (e.g during scrolling). grid._updateBoundingRect(); // Check how much dragged element overlaps the container element. targetRect.width = grid._width; targetRect.height = grid._height; targetRect.left = grid._left; targetRect.top = grid._top; gridScore = getRectOverlapScore(itemRect, targetRect); // Check if this grid is the best match so far. if (gridScore > threshold && gridScore > bestScore) { bestScore = gridScore; target = grid; } } // Always reset root grid array. rootGridArray.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 === 'swap' ? 'swap' : 'move'; // Populate item rect data. itemRect.width = item._width; itemRect.height = item._height; itemRect.left = drag._elementClientX; itemRect.top = drag._elementClientY; // 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 false; var gridOffsetLeft = 0; var gridOffsetTop = 0; var matchScore = -1; var matchIndex; var hasValidTargets; 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 = getRectOverlapScore(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 item is being moved into another // grid. if (matchScore < sortThreshold && item.getGrid() !== grid) { matchIndex = hasValidTargets ? -1 : 0; matchScore = Infinity; } // Check if the best match overlaps enough to justify a placement switch. if (matchScore >= sortThreshold) { returnData.grid = grid; returnData.index = matchIndex; returnData.action = sortAction; return returnData; } return false; }; })(); /** * Public prototype methods * ************************ */ /** * Abort dragging and reset drag data. * * @public * @memberof ItemDrag.prototype * @returns {ItemDrag} */ ItemDrag.prototype.stop = function() { var item = this._item; var element = item._element; var grid = this._getGrid(); if (!this._isActive) return this; // If the item is being dropped into another grid, finish it up and return // immediately. if (this._isMigrating) { this._finishMigration(); return this; } // Cancel queued move and scroll ticks. cancelMoveTick(item._id); cancelScrollTick(item._id); // Remove scroll listeners. this._unbindScrollListeners(); // Cancel overlap check. this._checkOverlapDebounce('cancel'); // 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); element.style[transformProp] = getTranslateString(this._gridX, this._gridY); } // Remove dragging class. removeClass(element, grid._settings.itemDraggingClass); // Reset drag data. this._reset(); return this; }; /** * Destroy instance. * * @public * @memberof ItemDrag.prototype * @returns {ItemDrag} */ ItemDrag.prototype.destroy = function() { if (this._isDestroyed) return this; this.stop(); this._hammer.destroy(); this._item._element.removeEventListener('dragstart', preventDefault, false); this._isDestroyed = true; return this; }; /** * Private prototype methods * ************************* */ /** * Get Grid instance. * * @private * @memberof ItemDrag.prototype * @returns {?Grid} */ ItemDrag.prototype._getGrid = function() { return gridInstances[this._gridId] || null; }; /** * Setup/reset drag data. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._reset = function() { // Is item being dragged? this._isActive = false; // The dragged item's container element. this._container = null; // The dragged item's containing block. this._containingBlock = null; // Hammer event data. this._lastEvent = null; this._lastScrollEvent = 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._elementClientX = 0; this._elementClientY = 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 * @memberof ItemDrag.prototype */ ItemDrag.prototype._bindScrollListeners = function() { var gridContainer = this._getGrid()._element; var dragContainer = this._container; var scrollers = this._scrollers; var containerScrollers; var i; // Get dragged element's scrolling parents. scrollers.length = 0; getScrollParents(this._item._element, 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) { containerScrollers = []; getScrollParents(gridContainer, containerScrollers); containerScrollers.push(gridContainer); for (i = 0; i < containerScrollers.length; i++) { if (scrollers.indexOf(containerScrollers[i]) < 0) { scrollers.push(containerScrollers[i]); } } } // Bind scroll listeners. for (i = 0; i < scrollers.length; i++) { scrollers[i].addEventListener('scroll', this._onScroll); } }; /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._unbindScrollListeners = function() { var scrollers = this._scrollers; var i; for (i = 0; i < scrollers.length; i++) { scrollers[i].removeEventListener('scroll', this._onScroll); } scrollers.length = 0; }; /** * Setup default start predicate. * * @private * @memberof ItemDrag.prototype * @param {Object} [options] * @returns {Object} */ ItemDrag.prototype._setupStartPredicate = function(options) { var config = options || this._getGrid()._settings.dragStartPredicate || 0; return (this._startPredicateData = { distance: Math.abs(config.distance) || 0, delay: Math.max(config.delay, 0) || 0, handle: typeof config.handle === 'string' ? config.handle : false }); }; /** * Setup default start predicate handle. * * @private * @memberof ItemDrag.prototype * @param {Object} event * @returns {?HTMLElement} */ ItemDrag.prototype._getStartPredicateHandle = function(event) { var predicate = this._startPredicateData; var element = this._item._element; var handleElement = element; // No handle, no hassle -> let's use the item element as the handle. if (!predicate.handle) return handleElement; // If there is a specific predicate handle defined, let's try to get it. handleElement = (event.changedPointers[0] || 0).target; while (handleElement && !elementMatches(handleElement, predicate.handle)) { handleElement = handleElement !== element ? handleElement.parentElement : null; } return handleElement || null; }; /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private * @memberof ItemDrag.prototype * @param {Object} event * @returns {Boolean} */ ItemDrag.prototype._resolveStartPredicate = function(event) { var predicate = this._startPredicateData; var pointer = event.changedPointers[0]; var pageX = (pointer && pointer.pageX) || 0; var pageY = (pointer && pointer.pageY) || 0; var handleRect; var handleLeft; var handleTop; var handleWidth; var handleHeight; // If the moved distance is smaller than the threshold distance or there is // some delay left, ignore this predicate cycle. if (event.distance < predicate.distance || predicate.delay) return; // Get handle rect data. handleRect = predicate.handleElement.getBoundingClientRect(); handleLeft = handleRect.left + (window.pageXOffset || 0); handleTop = handleRect.top + (window.pageYOffset || 0); handleWidth = handleRect.width; handleHeight = handleRect.height; // Reset predicate data. this._resetStartPredicate(); // If the cursor is still within the handle let's start the drag. return ( handleWidth && handleHeight && pageX >= handleLeft && pageX < handleLeft + handleWidth && pageY >= handleTop && pageY < handleTop + handleHeight ); }; /** * Finalize start predicate. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._finishStartPredicate = function(event) { var element = this._item._element; // 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(event)) openAnchorHref(element); }; /** * Reset for default drag start predicate function. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._resetStartPredicate = function() { var predicate = this._startPredicateData; if (predicate) { if (predicate.delayTimer) { predicate.delayTimer = window.clearTimeout(predicate.delayTimer); } this._startPredicateData = null; } }; /** * Check (during drag) if an item is overlapping other items and based on * the configuration layout the items. * * @private * @memberof ItemDrag.prototype */ 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 sortAction; var isMigration; // Get overlap check result. if (typeof settings.dragSortPredicate === 'function') { result = settings.dragSortPredicate(item, this._lastEvent); } 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; currentGrid = item.getGrid(); targetGrid = result.grid || currentGrid; isMigration = currentGrid !== targetGrid; currentIndex = currentGrid._items.indexOf(item); targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration); sortAction = result.action === 'swap' ? 'swap' : 'move'; // 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) { // Do the sort. (sortAction === 'swap' ? arraySwap : arrayMove)( currentGrid._items, currentIndex, targetIndex ); // Emit move event. if (currentGrid._hasListeners(eventMove)) { currentGrid._emit(eventMove, { item: item, fromIndex: currentIndex, toIndex: targetIndex, action: sortAction }); } // Layout the grid. currentGrid.layout(); } } // If the item was moved to another grid. else { // Emit beforeSend event. if (currentGrid._hasListeners(eventBeforeSend)) { currentGrid._emit(eventBeforeSend, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit beforeReceive event. if (targetGrid._hasListeners(eventBeforeReceive)) { targetGrid._emit(eventBeforeReceive, { 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); // Set sort data as null, which is an indicator for the item comparison // function that the sort data of this specific item should be fetched // lazily. item._sortData = null; // Emit send event. if (currentGrid._hasListeners(eventSend)) { currentGrid._emit(eventSend, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit receive event. if (targetGrid._hasListeners(eventReceive)) { targetGrid._emit(eventReceive, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Layout both grids. currentGrid.layout(); targetGrid.layout(); } }; /** * If item is dragged into another grid, finish the migration process * gracefully. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._finishMigration = function() { var item = this._item; var release = item._release; 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 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(); // Remove current classnames. removeClass(element, currentSettings.itemClass); removeClass(element, currentSettings.itemVisibleClass); removeClass(element, currentSettings.itemHiddenClass); // Add new classnames. addClass(element, targetSettings.itemClass); addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); // 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 and sort data. item._refreshDimensions(); item._refreshSortData(); // 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) { element.style[transformProp] = getTranslateString(translate.x, translate.y); } // Update child element's styles to reflect the current visibility state. item._child.removeAttribute('style'); setStyles(item._child, isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); // Start the release. release.start(); }; /** * Drag start handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onStart = function(event) { var item = this._item; // If item is not active, don't start the drag. if (!item._isActive) return; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; var release = item._release; var migrate = item._migrate; var gridContainer = grid._element; var dragContainer = settings.dragContainer || gridContainer; var containingBlock = getContainingBlock(dragContainer, true); var translate = getTranslate(element); var currentLeft = translate.x; var currentTop = translate.y; var elementRect = element.getBoundingClientRect(); var hasDragContainer = dragContainer !== gridContainer; var offsetDiff; // If grid container is not the drag container, we need to calculate the // offset difference between grid container and drag container's containing // element. if (hasDragContainer) { offsetDiff = getOffsetDiff(containingBlock, gridContainer); } // Stop current positioning animation. if (item.isPositioning()) { item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); } // Stop current migration animation. if (migrate._isActive) { currentLeft -= migrate._containerDiffX; currentTop -= migrate._containerDiffY; migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); } // If item is being released reset release data. if (item.isReleasing()) release._reset(); // Setup drag data. this._isActive = true; this._lastEvent = event; this._container = dragContainer; this._containingBlock = containingBlock; this._elementClientX = elementRect.left; this._elementClientY = elementRect.top; this._left = this._gridX = currentLeft; this._top = this._gridY = currentTop; // Emit dragInit event. grid._emit(eventDragInit, item, event); // If a specific drag container is set and it is different from the // grid's container element we need to cast some extra spells. if (hasDragContainer) { // Store the container offset diffs to drag data. this._containerDiffX = offsetDiff.left; this._containerDiffY = offsetDiff.top; // 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 === dragContainer) { this._gridX = currentLeft - this._containerDiffX; this._gridY = currentTop - 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 = currentLeft + this._containerDiffX; this._top = currentTop + this._containerDiffY; dragContainer.appendChild(element); element.style[transformProp] = getTranslateString(this._left, this._top); } } // Set drag class and bind scrollers. addClass(element, settings.itemDraggingClass); this._bindScrollListeners(); // Emit dragStart event. grid._emit(eventDragStart, item, event); }; /** * Drag move handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onMove = function(event) { var item = this._item; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } var settings = this._getGrid()._settings; var axis = settings.dragAxis; var xDiff = event.deltaX - this._lastEvent.deltaX; var yDiff = event.deltaY - this._lastEvent.deltaY; // Update last event. this._lastEvent = event; // Update horizontal position data. if (axis !== 'y') { this._left += xDiff; this._gridX += xDiff; this._elementClientX += xDiff; } // Update vertical position data. if (axis !== 'x') { this._top += yDiff; this._gridY += yDiff; this._elementClientY += yDiff; } // Do move prepare/apply handling in the next tick. addMoveTick(item._id, this._prepareMove, this._applyMove); }; /** * Prepare dragged item for moving. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._prepareMove = function() { // Do nothing if item is not active. if (!this._item._isActive) return; // If drag sort is enabled -> check overlap. if (this._getGrid()._settings.dragSort) this._checkOverlapDebounce(); }; /** * Apply movement to dragged item. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._applyMove = function() { var item = this._item; // Do nothing if item is not active. if (!item._isActive) return; // Update element's translateX/Y values. item._element.style[transformProp] = getTranslateString(this._left, this._top); // Emit dragMove event. this._getGrid()._emit(eventDragMove, item, this._lastEvent); }; /** * Drag scroll handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onScroll = function(event) { var item = this._item; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } // Update last scroll event. this._lastScrollEvent = event; // Do scroll prepare/apply handling in the next tick. addScrollTick(item._id, this._prepareScroll, this._applyScroll); }; /** * Prepare dragged item for scrolling. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._prepareScroll = function() { var item = this._item; // If item is not active do nothing. if (!item._isActive) return; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; var axis = settings.dragAxis; var gridContainer = grid._element; var offsetDiff; // Calculate element's rect and x/y diff. var rect = element.getBoundingClientRect(); var xDiff = this._elementClientX - rect.left; var yDiff = this._elementClientY - rect.top; // Update container diff. if (this._container !== gridContainer) { offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); this._containerDiffX = offsetDiff.left; this._containerDiffY = offsetDiff.top; } // Update horizontal position data. if (axis !== 'y') { this._left += xDiff; this._gridX = this._left - this._containerDiffX; } // Update vertical position data. if (axis !== 'x') { this._top += yDiff; this._gridY = this._top - this._containerDiffY; } // Overlap handling. if (settings.dragSort) this._checkOverlapDebounce(); }; /** * Apply scroll to dragged item. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._applyScroll = function() { var item = this._item; // If item is not active do nothing. if (!item._isActive) return; // Update element's translateX/Y values. item._element.style[transformProp] = getTranslateString(this._left, this._top); // Emit dragScroll event. this._getGrid()._emit(eventDragScroll, item, this._lastScrollEvent); }; /** * Drag end handler. * * @private * @memberof ItemDrag.prototype * @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._release; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } // Cancel queued move and scroll ticks. cancelMoveTick(item._id); cancelScrollTick(item._id); // Finish currently queued overlap check. settings.dragSort && this._checkOverlapDebounce('finish'); // 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); // Emit dragEnd event. grid._emit(eventDragEnd, item, event); // Finish up the migration process or start the release process. this._isMigrating ? this._finishMigration() : release.start(); }; /** * Private helpers * *************** */ /** * Prevent default. * * @param {Object} e */ function preventDefault(e) { if (e.preventDefault) e.preventDefault(); } /** * Calculate how many percent the intersection area of two rectangles is from * the maximum potential intersection area between the rectangles. * * @param {Rectangle} a * @param {Rectangle} b * @returns {Number} * - A number between 0-100. */ function getRectOverlapScore(a, b) { // Return 0 immediately if the rectangles do not overlap. if ( a.left + a.width <= b.left || b.left + b.width <= a.left || a.top + a.height <= b.top || b.top + b.height <= a.top ) { return 0; } // Calculate intersection area's width, height, max height and max width. 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); var maxWidth = Math.min(a.width, b.width); var maxHeight = Math.min(a.height, b.height); return ((width * height) / (maxWidth * maxHeight)) * 100; } /** * Get element's scroll parents. * * @param {HTMLElement} element * @param {Array} [data] * @returns {HTMLElement[]} */ function getScrollParents(element, data) { var ret = data || []; var parent = element.parentNode; // // If transformed elements leak fixed elements. // if (hasTransformLeak) { // If the element is fixed it can not have any scroll parents. if (getStyle(element, 'position') === 'fixed') return ret; // Find scroll parents. while (parent && parent !== document && parent !== document.documentElement) { if (isScrollable(parent)) ret.push(parent); parent = getStyle(parent, 'position') === 'fixed' ? null : parent.parentNode; } // If parent is not fixed element, add window object as the last scroll // parent. parent !== null && ret.push(window); return ret; } // // If fixed elements behave as defined in the W3C specification. // // Find scroll parents. while (parent && parent !== document) { // If the currently looped element is fixed ignore all parents that are // not transformed. if (getStyle(element, 'position') === 'fixed' && !isTransformed(parent)) { parent = parent.parentNode; continue; } // Add the parent element to return items if it is scrollable. if (isScrollable(parent)) ret.push(parent); // Update element and parent references. element = parent; parent = parent.parentNode; } // If the last item is the root element, replace it with window. The root // element scroll is propagated to the window. if (ret[ret.length - 1] === document.documentElement) { ret[ret.length - 1] = window; } // Otherwise add window as the last scroll parent. else { ret.push(window); } return ret; } /** * Check if an element is scrollable. * * @param {HTMLElement} element * @returns {Boolean} */ function isScrollable(element) { var overflow = getStyle(element, 'overflow'); if (overflow === 'auto' || overflow === 'scroll') return true; overflow = getStyle(element, 'overflow-x'); if (overflow === 'auto' || overflow === 'scroll') return true; overflow = getStyle(element, 'overflow-y'); if (overflow === 'auto' || overflow === 'scroll') return true; return false; } /** * Check if drag gesture can be interpreted as a click, based on final drag * event data. * * @param {Object} element * @returns {Boolean} */ function isClick(event) { return Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200; } /** * 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; } } /** * Detects if transformed elements leak fixed elements. According W3C * transform rendering spec a transformed element should contain even fixed * elements. Meaning that fixed elements are positioned relative to the * closest transformed ancestor element instead of window. However, not every * browser follows the spec (IE and older Firefox). So we need to test it. * https://www.w3.org/TR/css3-2d-transforms/#transform-rendering * * Borrowed from Mezr (v0.6.1): * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L607 */ function checkTransformLeak() { // No transforms -> definitely leaks. if (!isTransformSupported) return true; // No body available -> can't check it. if (!document.body) return null; // Do the test. var elems = [0, 1].map(function(elem, isInner) { elem = document.createElement('div'); elem.style.position = isInner ? 'fixed' : 'absolute'; elem.style.display = 'block'; elem.style.visibility = 'hidden'; elem.style.left = isInner ? '0px' : '1px'; elem.style[transformProp] = 'none'; return elem; }); var outer = document.body.appendChild(elems[0]); var inner = outer.appendChild(elems[1]); var left = inner.getBoundingClientRect().left; outer.style[transformProp] = 'scale(1)'; var ret = left === inner.getBoundingClientRect().left; document.body.removeChild(outer); return ret; } /** * Queue constructor. * * @class */ function Queue() { this._queue = []; this._isDestroyed = false; } /** * Public prototype methods * ************************ */ /** * Add callback to the queue. * * @public * @memberof Queue.prototype * @param {Function} callback * @returns {Queue} */ Queue.prototype.add = function(callback) { if (this._isDestroyed) return this; this._queue.push(callback); return this; }; /** * Process queue callbacks and reset the queue. * * @public * @memberof Queue.prototype * @param {*} arg1 * @param {*} arg2 * @returns {Queue} */ Queue.prototype.flush = function(arg1, arg2) { if (this._isDestroyed) return this; var queue = this._queue; var length = queue.length; var i; // Quit early if the queue is empty. if (!length) return this; var singleCallback = length === 1; var snapshot = singleCallback ? queue[0] : queue.slice(0); // Reset queue. queue.length = 0; // If we only have a single callback let's just call it. if (singleCallback) { snapshot(arg1, arg2); return this; } // If we have multiple callbacks, let's process them. for (i = 0; i < length; i++) { snapshot[i](arg1, arg2); if (this._isDestroyed) break; } return this; }; /** * Destroy Queue instance. * * @public * @memberof Queue.prototype * @returns {Queue} */ Queue.prototype.destroy = function() { if (this._isDestroyed) return this; this._isDestroyed = true; this._queue.length = 0; return this; }; /** * Layout manager for Item instance. * * @class * @param {Item} item */ function ItemLayout(item) { this._item = item; this._isActive = false; this._isDestroyed = false; this._isInterrupted = false; this._currentStyles = {}; this._targetStyles = {}; this._currentLeft = 0; this._currentTop = 0; this._offsetLeft = 0; this._offsetTop = 0; this._skipNextAnimation = false; this._animateOptions = { onFinish: this._finish.bind(this) }; this._queue = new Queue(); // 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 * @memberof ItemLayout.prototype * @param {Boolean} [instant=false] * @param {Function} [onFinish] * @returns {ItemLayout} */ ItemLayout.prototype.start = function(instant, onFinish) { if (this._isDestroyed) return; var item = this._item; var element = item._element; var release = item._release; var gridSettings = item.getGrid()._settings; var isPositioning = this._isActive; var isJustReleased = release._isActive && release._isPositioningStarted === false; var animDuration = isJustReleased ? gridSettings.dragReleaseDuration : gridSettings.layoutDuration; var animEasing = isJustReleased ? gridSettings.dragReleaseEasing : gridSettings.layoutEasing; var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0; var isAnimating; // If the item is currently positioning process current layout callback // queue with interrupted flag on. if (isPositioning) this._queue.flush(true, item); // Mark release positioning as started. if (isJustReleased) release._isPositioningStarted = true; // Push the callback to the callback queue. if (typeof onFinish === 'function') this._queue.add(onFinish); // If no animations are needed, easy peasy! if (!animEnabled) { this._updateOffsets(); this._updateTargetStyles(); isPositioning && cancelLayoutTick(item._id); isAnimating = item._animate.isAnimating(); this.stop(false, this._targetStyles); !isAnimating && setStyles(element, this._targetStyles); this._skipNextAnimation = false; return this._finish(); } // Set item active and store some data for the animation that is about to be // triggered. this._isActive = true; this._animateOptions.easing = animEasing; this._animateOptions.duration = animDuration; this._isInterrupted = isPositioning; // Start the item's layout animation in the next tick. addLayoutTick(item._id, this._setupAnimation, this._startAnimation); return this; }; /** * Stop item's position animation if it is currently animating. * * @public * @memberof ItemLayout.prototype * @param {Boolean} [processCallbackQueue=false] * @param {Object} [targetStyles] * @returns {ItemLayout} */ ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { if (this._isDestroyed || !this._isActive) return this; var item = this._item; // Cancel animation init. cancelLayoutTick(item._id); // Stop animation. item._animate.stop(targetStyles); // Remove positioning class. removeClass(item._element, item.getGrid()._settings.itemPositioningClass); // Reset active state. this._isActive = false; // Process callback queue if needed. if (processCallbackQueue) this._queue.flush(true, item); return this; }; /** * Destroy the instance and stop current animation if it is running. * * @public * @memberof ItemLayout.prototype * @returns {ItemLayout} */ ItemLayout.prototype.destroy = function() { if (this._isDestroyed) return this; this.stop(true, {}); this._queue.destroy(); this._item = this._currentStyles = this._targetStyles = this._animateOptions = null; this._isDestroyed = true; return this; }; /** * Private prototype methods * ************************* */ /** * Calculate and update item's current layout offset data. * * @private * @memberof ItemLayout.prototype */ ItemLayout.prototype._updateOffsets = function() { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; var release = item._release; this._offsetLeft = release._isActive ? release._containerDiffX : migrate._isActive ? migrate._containerDiffX : 0; this._offsetTop = release._isActive ? release._containerDiffY : migrate._isActive ? migrate._containerDiffY : 0; }; /** * Calculate and update item's layout target styles. * * @private * @memberof ItemLayout.prototype */ ItemLayout.prototype._updateTargetStyles = function() { if (this._isDestroyed) return; var item = this._item; this._targetStyles.transform = getTranslateString( item._left + this._offsetLeft, item._top + this._offsetTop ); }; /** * Finish item layout procedure. * * @private * @memberof ItemLayout.prototype */ ItemLayout.prototype._finish = function() { if (this._isDestroyed) return; var item = this._item; var migrate = item._migrate; var release = item._release; // 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. this._queue.flush(false, item); }; /** * Prepare item for layout animation. * * @private * @memberof ItemLayout.prototype */ ItemLayout.prototype._setupAnimation = function() { var element = this._item._element; var translate = getTranslate(element); this._currentLeft = translate.x; this._currentTop = translate.y; }; /** * Start layout animation. * * @private * @memberof ItemLayout.prototype */ ItemLayout.prototype._startAnimation = function() { var item = this._item; var element = item._element; var grid = item.getGrid(); var settings = grid._settings; // Let's update the offset data and target styles. this._updateOffsets(); this._updateTargetStyles(); // If the item is already in correct position let's quit early. if ( item._left === this._currentLeft - this._offsetLeft && item._top === this._currentTop - this._offsetTop ) { if (this._isInterrupted) this.stop(false, this._targetStyles); this._isActive = false; this._finish(); return; } // Set item's positioning class if needed. !this._isInterrupted && addClass(element, settings.itemPositioningClass); // Get current styles for animation. this._currentStyles.transform = getTranslateString(this._currentLeft, this._currentTop); // Animate. item._animate.start(this._currentStyles, this._targetStyles, this._animateOptions); }; var tempStyles = {}; /** * 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 * @memberof ItemMigrate.prototype * @param {Grid} targetGrid * @param {GridSingleItemQuery} position * @param {HTMLElement} [container] * @returns {ItemMigrate} */ ItemMigrate.prototype.start = function(targetGrid, position, container) { if (this._isDestroyed) return this; var item = this._item; var element = item._element; 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; // Get target index. if (typeof position === 'number') { targetIndex = normalizeArrayIndex(targetItems, position, true); } else { targetItem = targetGrid._getItem(position); /** @todo Consider throwing an error here instead of silently failing. */ if (!targetItem) return this; 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, { transform: getTranslateString(translateX, translateY) }); } // Abort current migration. if (this._isActive) { translateX -= this._containerDiffX; translateY -= this._containerDiffY; this.stop(true, { transform: getTranslateString(translateX, translateY) }); } // Abort current release. if (item.isReleasing()) { translateX -= item._release._containerDiffX; translateY -= item._release._containerDiffY; item._release.stop(true, { transform: getTranslateString(translateX, translateY) }); } // Stop current visibility animations. item._visibility._stopAnimation(); // Destroy current drag. if (item._drag) item._drag.destroy(); // Process current visibility animation queue. item._visibility._queue.flush(true, item); // Emit beforeSend event. if (grid._hasListeners(eventBeforeSend)) { grid._emit(eventBeforeSend, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit beforeReceive event. if (targetGrid._hasListeners(eventBeforeReceive)) { targetGrid._emit(eventBeforeReceive, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Remove current classnames. removeClass(element, settings.itemClass); removeClass(element, settings.itemVisibleClass); removeClass(element, settings.itemHiddenClass); // Add new classnames. addClass(element, targetSettings.itemClass); addClass(element, isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); // 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; // Get current container. currentContainer = element.parentNode; // Move the item inside the target container if it's different than the // current container. if (targetContainer !== currentContainer) { targetContainer.appendChild(element); offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); if (!translate) { translate = getTranslate(element); translateX = translate.x; translateY = translate.y; } element.style[transformProp] = getTranslateString( translateX + offsetDiff.left, translateY + offsetDiff.top ); } // Update child element's styles to reflect the current visibility state. item._child.removeAttribute('style'); setStyles(item._child, isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles); // Update display style. element.style.display = isVisible ? 'block' : 'hidden'; // Get offset diff for the migration data. containerDiff = getOffsetDiff(targetContainer, targetElement, true); // Update item's cached dimensions and sort data. item._refreshDimensions(); item._refreshSortData(); // Create new drag handler. item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null; // Setup migration data. this._isActive = true; this._container = targetContainer; this._containerDiffX = containerDiff.left; this._containerDiffY = containerDiff.top; // Emit send event. if (grid._hasListeners(eventSend)) { grid._emit(eventSend, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit receive event. if (targetGrid._hasListeners(eventReceive)) { targetGrid._emit(eventReceive, { item: item, fromGrid: grid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } return this; }; /** * 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 * @memberof ItemMigrate.prototype * @param {Boolean} [abort=false] * - Should the migration be aborted? * @param {Object} [currentStyles] * - Optional current translateX and translateY styles. * @returns {ItemMigrate} */ ItemMigrate.prototype.stop = function(abort, currentStyles) { if (this._isDestroyed || !this._isActive) return this; var item = this._item; var element = item._element; var grid = item.getGrid(); var gridElement = grid._element; var translate; if (this._container !== gridElement) { if (!currentStyles) { if (abort) { translate = getTranslate(element); tempStyles.transform = getTranslateString( translate.x - this._containerDiffX, translate.y - this._containerDiffY ); } else { tempStyles.transform = getTranslateString(item._left, item._top); } currentStyles = tempStyles; } gridElement.appendChild(element); setStyles(element, currentStyles); } this._isActive = false; this._container = null; this._containerDiffX = 0; this._containerDiffY = 0; return this; }; /** * Destroy instance. * * @public * @memberof ItemMigrate.prototype * @returns {ItemMigrate} */ ItemMigrate.prototype.destroy = function() { if (this._isDestroyed) return this; this.stop(true); this._item = null; this._isDestroyed = true; return this; }; var tempStyles$1 = {}; /** * 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 ItemRelease(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 * @memberof ItemRelease.prototype * @returns {ItemRelease} */ ItemRelease.prototype.start = function() { if (this._isDestroyed || this._isActive) return this; var item = this._item; var grid = item.getGrid(); // Flag release as active. this._isActive = true; // Add release class name to the released element. addClass(item._element, grid._settings.itemReleasingClass); // Emit dragReleaseStart event. grid._emit(eventDragReleaseStart, item); // Position the released item. item._layout.start(false); return this; }; /** * 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 * @memberof ItemRelease.prototype * @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 {Object} [currentStyles] * - Optional current translateX and translateY styles. * @returns {ItemRelease} */ ItemRelease.prototype.stop = function(abort, currentStyles) { if (this._isDestroyed || !this._isActive) return this; var item = this._item; var element = item._element; var grid = item.getGrid(); var container = grid._element; var translate; // Reset data and remove releasing class name from the element. this._reset(); // If the released element is outside the grid's container element put it // back there and adjust position accordingly. if (element.parentNode !== container) { if (!currentStyles) { if (abort) { translate = getTranslate(element); tempStyles$1.transform = getTranslateString( translate.x - this._containerDiffX, translate.y - this._containerDiffY ); } else { tempStyles$1.transform = getTranslateString(item._left, item._top); } currentStyles = tempStyles$1; } container.appendChild(element); setStyles(element, currentStyles); } // Emit dragReleaseEnd event. if (!abort) grid._emit(eventDragReleaseEnd, item); return this; }; /** * Destroy instance. * * @public * @memberof ItemRelease.prototype * @returns {ItemRelease} */ ItemRelease.prototype.destroy = function() { if (this._isDestroyed) return this; this.stop(true); this._item = null; this._isDestroyed = true; return this; }; /** * Private prototype methods * ************************* */ /** * Reset public data and remove releasing class. * * @private * @memberof ItemRelease.prototype */ ItemRelease.prototype._reset = function() { if (this._isDestroyed) return; var item = this._item; this._isActive = false; this._isPositioningStarted = false; this._containerDiffX = 0; this._containerDiffY = 0; removeClass(item._element, item.getGrid()._settings.itemReleasingClass); }; /** * Get current values of the provided styles definition object. * * @param {HTMLElement} element * @param {Object} styles * @return {Object} */ function getCurrentStyles(element, styles) { var current = {}; for (var prop in styles) { current[prop] = getStyle(element, getStyleName(prop)); } return current; } /** * Visibility manager for Item instance. * * @class * @param {Item} item */ function ItemVisibility(item) { var isActive = item._isActive; var element = item._element; var settings = item.getGrid()._settings; this._item = item; this._isDestroyed = false; // Set up visibility states. this._isHidden = !isActive; this._isHiding = false; this._isShowing = false; // Callback queue. this._queue = new Queue(); // Bind show/hide finishers. this._finishShow = this._finishShow.bind(this); this._finishHide = this._finishHide.bind(this); // Force item to be either visible or hidden on init. element.style.display = isActive ? 'block' : 'none'; // Set visible/hidden class. addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass); // Set initial styles for the child element. setStyles(item._child, isActive ? settings.visibleStyles : settings.hiddenStyles); } /** * Public prototype methods * ************************ */ /** * Show item. * * @public * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] * @returns {ItemVisibility} */ ItemVisibility.prototype.show = function(instant, onFinish) { if (this._isDestroyed) return this; var item = this._item; var element = item._element; var queue = this._queue; var callback = typeof onFinish === 'function' ? 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 this; } // 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 && queue.add(callback); return this; } // 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) { queue.flush(true, item); removeClass(element, settings.itemHiddenClass); addClass(element, settings.itemVisibleClass); if (!this._isHiding) element.style.display = 'block'; } // Push callback to the callback queue. callback && queue.add(callback); // Update visibility states. item._isActive = this._isShowing = true; this._isHiding = this._isHidden = false; // Finally let's start show animation. this._startAnimation(true, instant, this._finishShow); return this; }; /** * Hide item. * * @public * @memberof ItemVisibility.prototype * @param {Boolean} instant * @param {Function} [onFinish] * @returns {ItemVisibility} */ ItemVisibility.prototype.hide = function(instant, onFinish) { if (this._isDestroyed) return this; var item = this._item; var element = item._element; var queue = this._queue; var callback = typeof onFinish === 'function' ? 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 this; } // 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 && queue.add(callback); return this; } // 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) { queue.flush(true, item); addClass(element, settings.itemHiddenClass); removeClass(element, settings.itemVisibleClass); } // Push callback to the callback queue. callback && queue.add(callback); // Update visibility states. this._isHidden = this._isHiding = true; item._isActive = this._isShowing = false; // Finally let's start hide animation. this._startAnimation(false, instant, this._finishHide); return this; }; /** * Destroy the instance and stop current animation if it is running. * * @public * @memberof ItemVisibility.prototype * @returns {ItemVisibility} */ ItemVisibility.prototype.destroy = function() { if (this._isDestroyed) return this; var item = this._item; var element = item._element; var grid = item.getGrid(); var queue = this._queue; var settings = grid._settings; // Stop visibility animation. this._stopAnimation({}); // Fire all uncompleted callbacks with interrupted flag and destroy the queue. queue.flush(true, item).destroy(); // Remove visible/hidden classes. removeClass(element, settings.itemVisibleClass); removeClass(element, settings.itemHiddenClass); // Reset state. this._item = null; this._isHiding = this._isShowing = false; this._isDestroyed = this._isHidden = true; return this; }; /** * Private prototype methods * ************************* */ /** * Start visibility animation. * * @private * @memberof ItemVisibility.prototype * @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 settings = item.getGrid()._settings; var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles; var duration = parseInt(toVisible ? settings.showDuration : settings.hideDuration) || 0; var easing = (toVisible ? settings.showEasing : settings.hideEasing) || 'ease'; 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) { if (item._animateChild.isAnimating()) { item._animateChild.stop(targetStyles); } else { setStyles(item._child, targetStyles); } onFinish && onFinish(); return; } // Start the animation in the next tick (to avoid layout thrashing). addVisibilityTick( item._id, function() { currentStyles = getCurrentStyles(item._child, targetStyles); }, function() { item._animateChild.start(currentStyles, targetStyles, { duration: duration, easing: easing, onFinish: onFinish }); } ); }; /** * Stop visibility animation. * * @private * @memberof ItemVisibility.prototype * @param {Object} [targetStyles] */ ItemVisibility.prototype._stopAnimation = function(targetStyles) { if (this._isDestroyed) return; var item = this._item; cancelVisibilityTick(item._id); item._animateChild.stop(targetStyles); }; /** * Finish show procedure. * * @private * @memberof ItemVisibility.prototype */ ItemVisibility.prototype._finishShow = function() { if (this._isHidden) return; this._isShowing = false; this._queue.flush(false, this._item); }; /** * Finish hide procedure. * * @private * @memberof ItemVisibility.prototype */ var finishStyles = {}; ItemVisibility.prototype._finishHide = function() { if (!this._isHidden) return; var item = this._item; this._isHiding = false; finishStyles.transform = getTranslateString(0, 0); item._layout.stop(true, finishStyles); item._element.style.display = 'none'; this._queue.flush(false, item); }; 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; // Create instance id. this._id = createUid(); // Reference to connected Grid instance's id. this._gridId = grid._id; // Destroyed flag. this._isDestroyed = false; // Set up initial positions. this._left = 0; this._top = 0; // The elements. this._element = element; this._child = element.children[0]; // If the provided item element is not a direct child of the grid container // element, append it to the grid container. 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. 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; // Set element's initial position styles. element.style.left = '0'; element.style.top = '0'; element.style[transformProp] = getTranslateString(0, 0); // Initiate item's animation controllers. this._animate = new ItemAnimate(element); this._animateChild = new ItemAnimate(this._child); // 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 release handler this._release = new ItemRelease(this); // Set up drag handler. this._drag = settings.dragEnabled ? new ItemDrag(this) : null; // Set up the initial dimensions and sort data. this._refreshDimensions(); this._refreshSortData(); } /** * Public prototype methods * ************************ */ /** * Get the instance grid reference. * * @public * @memberof Item.prototype * @returns {Grid} */ Item.prototype.getGrid = function() { return gridInstances[this._gridId]; }; /** * Get the instance element. * * @public * @memberof Item.prototype * @returns {HTMLElement} */ Item.prototype.getElement = function() { return this._element; }; /** * Get instance element's cached width. * * @public * @memberof Item.prototype * @returns {Number} */ Item.prototype.getWidth = function() { return this._width; }; /** * Get instance element's cached height. * * @public * @memberof Item.prototype * @returns {Number} */ Item.prototype.getHeight = function() { return this._height; }; /** * Get instance element's cached margins. * * @public * @memberof Item.prototype * @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 * @memberof Item.prototype * @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 * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isActive = function() { return this._isActive; }; /** * Is the item visible? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isVisible = function() { return !!this._visibility && !this._visibility._isHidden; }; /** * Is the item being animated to visible? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isShowing = function() { return !!(this._visibility && this._visibility._isShowing); }; /** * Is the item being animated to hidden? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isHiding = function() { return !!(this._visibility && this._visibility._isHiding); }; /** * Is the item positioning? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isPositioning = function() { return !!(this._layout && this._layout._isActive); }; /** * Is the item being dragged? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isDragging = function() { return !!(this._drag && this._drag._isActive); }; /** * Is the item being released? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isReleasing = function() { return !!(this._release && this._release._isActive); }; /** * Is the item destroyed? * * @public * @memberof Item.prototype * @returns {Boolean} */ Item.prototype.isDestroyed = function() { return this._isDestroyed; }; /** * Private prototype methods * ************************* */ /** * Recalculate item's dimensions. * * @private * @memberof Item.prototype */ Item.prototype._refreshDimensions = function() { if (this._isDestroyed || this._visibility._isHidden) return; var element = this._element; 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')); }; /** * Fetch and store item's sort data. * * @private * @memberof Item.prototype */ 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); } }; /** * Destroy item instance. * * @private * @memberof Item.prototype * @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; var index = grid._items.indexOf(this); // Destroy handlers. this._release.destroy(); this._migrate.destroy(); this._layout.destroy(); this._visibility.destroy(); this._animate.destroy(); this._animateChild.destroy(); this._drag && this._drag.destroy(); // Remove all inline styles. element.removeAttribute('style'); this._child.removeAttribute('style'); // Remove item class. removeClass(element, settings.itemClass); // Remove item from Grid instance if it still exists there. index > -1 && grid._items.splice(index, 1); // Remove element from DOM. removeElement && element.parentNode.removeChild(element); // Reset state. this._isActive = false; this._isDestroyed = true; }; /** * This is the default layout algorithm for Muuri. Based on MAXRECTS approach * as described by Jukka Jylänki in his survey: "A Thousand Ways to Pack the * Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing.". * * @class */ function Packer() { this._slots = []; this._slotSizes = []; this._freeSlots = []; this._newSlots = []; this._rectItem = {}; this._rectStore = []; this._rectId = 0; // The layout return data, which will be populated in getLayout. this._layout = { slots: null, setWidth: false, setHeight: false, width: false, height: false }; // Bind sort handlers. this._sortRectsLeftTop = this._sortRectsLeftTop.bind(this); this._sortRectsTopLeft = this._sortRectsTopLeft.bind(this); } /** * @public * @memberof Packer.prototype * @param {Item[]} items * @param {Number} width * @param {Number} height * @param {Number[]} [slots] * @param {Object} [options] * @param {Boolean} [options.fillGaps=false] * @param {Boolean} [options.horizontal=false] * @param {Boolean} [options.alignRight=false] * @param {Boolean} [options.alignBottom=false] * @returns {LayoutData} */ Packer.prototype.getLayout = function(items, width, height, slots, options) { var layout = this._layout; var fillGaps = !!(options && options.fillGaps); var isHorizontal = !!(options && options.horizontal); var alignRight = !!(options && options.alignRight); var alignBottom = !!(options && options.alignBottom); var rounding = !!(options && options.rounding); var slotSizes = this._slotSizes; var i; // Reset layout data. layout.slots = slots ? slots : this._slots; layout.width = isHorizontal ? 0 : rounding ? Math.round(width) : width; layout.height = !isHorizontal ? 0 : rounding ? Math.round(height) : height; layout.setWidth = isHorizontal; layout.setHeight = !isHorizontal; // Make sure slots and slot size arrays are reset. layout.slots.length = 0; slotSizes.length = 0; // No need to go further if items do not exist. if (!items.length) return layout; // Find slots for items. for (i = 0; i < items.length; i++) { this._addSlot(items[i], isHorizontal, fillGaps, rounding, alignRight || alignBottom); } // If the alignment is set to right we need to adjust the results. if (alignRight) { for (i = 0; i < layout.slots.length; i = i + 2) { layout.slots[i] = layout.width - (layout.slots[i] + slotSizes[i]); } } // If the alignment is set to bottom we need to adjust the results. if (alignBottom) { for (i = 1; i < layout.slots.length; i = i + 2) { layout.slots[i] = layout.height - (layout.slots[i] + slotSizes[i]); } } // Reset slots arrays and rect id. slotSizes.length = 0; this._freeSlots.length = 0; this._newSlots.length = 0; this._rectId = 0; return layout; }; /** * Calculate position for the layout item. Returns the left and top position * of the item in pixels. * * @private * @memberof Packer.prototype * @param {Item} item * @param {Boolean} isHorizontal * @param {Boolean} fillGaps * @param {Boolean} rounding * @returns {Array} */ Packer.prototype._addSlot = (function() { var leeway = 0.001; var itemSlot = {}; return function(item, isHorizontal, fillGaps, rounding, trackSize) { var layout = this._layout; var freeSlots = this._freeSlots; var newSlots = this._newSlots; var rect; var rectId; var potentialSlots; var ignoreCurrentSlots; var i; var ii; // Reset new slots. newSlots.length = 0; // Set item slot initial data. itemSlot.left = null; itemSlot.top = null; itemSlot.width = item._width + item._marginLeft + item._marginRight; itemSlot.height = item._height + item._marginTop + item._marginBottom; // Round item slot width and height if needed. if (rounding) { itemSlot.width = Math.round(itemSlot.width); itemSlot.height = Math.round(itemSlot.height); } // Try to find a slot for the item. for (i = 0; i < freeSlots.length; i++) { rectId = freeSlots[i]; if (!rectId) continue; rect = this._getRect(rectId); if (itemSlot.width <= rect.width + leeway && itemSlot.height <= rect.height + leeway) { itemSlot.left = rect.left; itemSlot.top = rect.top; break; } } // If no slot was found for the item. if (itemSlot.left === null) { // Position the item in to the bottom left (vertical mode) or top right // (horizontal mode) of the grid. itemSlot.left = !isHorizontal ? 0 : layout.width; itemSlot.top = !isHorizontal ? layout.height : 0; // If gaps don't needs filling do not add any current slots to the new // slots array. if (!fillGaps) { ignoreCurrentSlots = true; } } // In vertical mode, if the item's bottom overlaps the grid's bottom. if (!isHorizontal && itemSlot.top + itemSlot.height > layout.height) { // If item is not aligned to the left edge, create a new slot. if (itemSlot.left > 0) { newSlots.push(this._addRect(0, layout.height, itemSlot.left, Infinity)); } // If item is not aligned to the right edge, create a new slot. if (itemSlot.left + itemSlot.width < layout.width) { newSlots.push( this._addRect( itemSlot.left + itemSlot.width, layout.height, layout.width - itemSlot.left - itemSlot.width, Infinity ) ); } // Update grid height. layout.height = itemSlot.top + itemSlot.height; } // In horizontal mode, if the item's right overlaps the grid's right edge. if (isHorizontal && itemSlot.left + itemSlot.width > layout.width) { // If item is not aligned to the top, create a new slot. if (itemSlot.top > 0) { newSlots.push(this._addRect(layout.width, 0, Infinity, itemSlot.top)); } // If item is not aligned to the bottom, create a new slot. if (itemSlot.top + itemSlot.height < layout.height) { newSlots.push( this._addRect( layout.width, itemSlot.top + itemSlot.height, Infinity, layout.height - itemSlot.top - itemSlot.height ) ); } // Update grid width. layout.width = itemSlot.left + itemSlot.width; } // Clean up the current slots making sure there are no old slots that // overlap with the item. If an old slot overlaps with the item, split it // into smaller slots if necessary. for (i = fillGaps ? 0 : ignoreCurrentSlots ? freeSlots.length : i; i < freeSlots.length; i++) { rectId = freeSlots[i]; if (!rectId) continue; rect = this._getRect(rectId); potentialSlots = this._splitRect(rect, itemSlot); for (ii = 0; ii < potentialSlots.length; ii++) { rectId = potentialSlots[ii]; rect = this._getRect(rectId); // Let's make sure here that we have a big enough slot // (width/height > 0.49px) and also let's make sure that the slot is // within the boundaries of the grid. if ( rect.width > 0.49 && rect.height > 0.49 && ((!isHorizontal && rect.top < layout.height) || (isHorizontal && rect.left < layout.width)) ) { newSlots.push(rectId); } } } // Sanitize new slots. if (newSlots.length) { this._purgeRects(newSlots).sort( isHorizontal ? this._sortRectsLeftTop : this._sortRectsTopLeft ); } // Update layout width/height. if (isHorizontal) { layout.width = Math.max(layout.width, itemSlot.left + itemSlot.width); } else { layout.height = Math.max(layout.height, itemSlot.top + itemSlot.height); } // Add item slot data to layout slots (and store the slot size for later // usage too if necessary). layout.slots.push(itemSlot.left, itemSlot.top); if (trackSize) this._slotSizes.push(itemSlot.width, itemSlot.height); // Free/new slots switcheroo! this._freeSlots = newSlots; this._newSlots = freeSlots; }; })(); /** * Add a new rectangle to the rectangle store. Returns the id of the new * rectangle. * * @private * @memberof Packer.prototype * @param {Number} left * @param {Number} top * @param {Number} width * @param {Number} height * @returns {RectId} */ Packer.prototype._addRect = function(left, top, width, height) { var rectId = ++this._rectId; var rectStore = this._rectStore; rectStore[rectId] = left || 0; rectStore[++this._rectId] = top || 0; rectStore[++this._rectId] = width || 0; 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. * * @private * @memberof Packer.prototype * @param {RectId} id * @param {Object} [target] * @returns {Object} */ Packer.prototype._getRect = function(id, target) { var rectItem = target ? target : this._rectItem; var rectStore = this._rectStore; rectItem.left = rectStore[id] || 0; rectItem.top = rectStore[++id] || 0; rectItem.width = rectStore[++id] || 0; rectItem.height = rectStore[++id] || 0; return rectItem; }; /** * Punch a hole into a rectangle and split the remaining area into smaller * rectangles (4 at max). * * @private * @memberof Packer.prototype * @param {Rectangle} rect * @param {Rectangle} hole * @returns {RectId[]} */ Packer.prototype._splitRect = (function() { var results = []; return function(rect, hole) { // Reset old results. results.length = 0; // If the rect does not overlap with the hole add rect to the return data // as is. if (!this._doRectsOverlap(rect, hole)) { results.push(this._addRect(rect.left, rect.top, rect.width, rect.height)); return results; } // Left split. if (rect.left < hole.left) { results.push(this._addRect(rect.left, rect.top, hole.left - rect.left, rect.height)); } // Right split. if (rect.left + rect.width > hole.left + hole.width) { results.push( this._addRect( hole.left + hole.width, rect.top, rect.left + rect.width - (hole.left + hole.width), rect.height ) ); } // Top split. if (rect.top < hole.top) { results.push(this._addRect(rect.left, rect.top, rect.width, hole.top - rect.top)); } // Bottom split. if (rect.top + rect.height > hole.top + hole.height) { results.push( this._addRect( rect.left, hole.top + hole.height, rect.width, rect.top + rect.height - (hole.top + hole.height) ) ); } return results; }; })(); /** * Check if two rectangles overlap. * * @private * @memberof Packer.prototype * @param {Rectangle} a * @param {Rectangle} b * @returns {Boolean} */ Packer.prototype._doRectsOverlap = function(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 ); }; /** * Check if a rectangle is fully within another rectangle. * * @private * @memberof Packer.prototype * @param {Rectangle} a * @param {Rectangle} b * @returns {Boolean} */ Packer.prototype._isRectWithinRect = function(a, b) { return ( a.left >= b.left && a.top >= b.top && a.left + a.width <= b.left + b.width && a.top + a.height <= 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. * * @private * @memberof Packer.prototype * @param {RectId[]} rectIds * @returns {RectId[]} */ Packer.prototype._purgeRects = (function() { var rectA = {}; var rectB = {}; return function(rectIds) { var i = rectIds.length; var ii; while (i--) { ii = rectIds.length; if (!rectIds[i]) continue; this._getRect(rectIds[i], rectA); while (ii--) { if (!rectIds[ii] || i === ii) continue; if (this._isRectWithinRect(rectA, this._getRect(rectIds[ii], rectB))) { rectIds[i] = 0; break; } } } return rectIds; }; })(); /** * Sort rectangles with top-left gravity. * * @private * @memberof Packer.prototype * @param {RectId} aId * @param {RectId} bId * @returns {Number} */ Packer.prototype._sortRectsTopLeft = (function() { var rectA = {}; var rectB = {}; return function(aId, bId) { this._getRect(aId, rectA); this._getRect(bId, rectB); // prettier-ignore return rectA.top < rectB.top ? -1 : rectA.top > rectB.top ? 1 : rectA.left < rectB.left ? -1 : rectA.left > rectB.left ? 1 : 0; }; })(); /** * Sort rectangles with left-top gravity. * * @private * @memberof Packer.prototype * @param {RectId} aId * @param {RectId} bId * @returns {Number} */ Packer.prototype._sortRectsLeftTop = (function() { var rectA = {}; var rectB = {}; return function(aId, bId) { this._getRect(aId, rectA); this._getRect(bId, rectB); // prettier-ignore return rectA.left < rectB.left ? -1 : rectA.left > rectB.left ? 1 : rectA.top < rectB.top ? -1 : rectA.top > rectB.top ? 1 : 0; }; })(); var htmlCollectionType = '[object HTMLCollection]'; var nodeListType = '[object NodeList]'; /** * Check if a value is a node list * * @param {*} val * @returns {Boolean} */ function isNodeList(val) { var type = Object.prototype.toString.call(val); return type === htmlCollectionType || type === nodeListType; } /** * Converts a value to an array or clones an array. * * @param {*} target * @returns {Array} */ function toArray(target) { return isNodeList(target) ? Array.prototype.slice.call(target) : Array.prototype.concat(target); } var packer = new Packer(); var noop = function() {}; /** * Creates a new Grid instance. * * @class * @param {(HTMLElement|String)} element * @param {Object} [options] * @param {(?HTMLElement[]|NodeList|String)} [options.items] * @param {Number} [options.showDuration=300] * @param {String} [options.showEasing="ease"] * @param {Object} [options.visibleStyles] * @param {Number} [options.hideDuration=300] * @param {String} [options.hideEasing="ease"] * @param {Object} [options.hiddenStyles] * @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=true] * @param {(Boolean|Number)} [options.layoutOnResize=100] * @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 {?HtmlElement} [options.dragContainer=null] * @param {?Function} [options.dragStartPredicate] * @param {Number} [options.dragStartPredicate.distance=0] * @param {Number} [options.dragStartPredicate.delay=0] * @param {(Boolean|String)} [options.dragStartPredicate.handle=false] * @param {?String} [options.dragAxis] * @param {(Boolean|Function)} [options.dragSort=true] * @param {Number} [options.dragSortInterval=100] * @param {(Function|Object)} [options.dragSortPredicate] * @param {Number} [options.dragSortPredicate.threshold=50] * @param {String} [options.dragSortPredicate.action="move"] * @param {Number} [options.dragReleaseDuration=300] * @param {String} [options.dragReleaseEasing="ease"] * @param {Object} [options.dragHammerSettings={touchAction: "none"}] * @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"] */ function Grid(element, options) { var inst = this; var settings; var items; var layoutOnResize; // Allow passing element as selector string. Store element for instance. element = this._element = typeof element === 'string' ? document.querySelector(element) : element; // Throw an error if the container element is not body element or does not // exist within the body element. if (!document.body.contains(element)) { throw new Error('Container element must be an existing DOM element'); } // Create instance settings by merging the options with default options. settings = this._settings = mergeSettings(Grid.defaultOptions, options); // Sanitize dragSort setting. if (typeof settings.dragSort !== 'function') { settings.dragSort = !!settings.dragSort; } // Create instance id and store it to the grid instances collection. this._id = createUid(); gridInstances[this._id] = inst; // Destroyed flag. this._isDestroyed = false; // The layout object (mutated on every layout). this._layout = { id: 0, items: [], slots: [], setWidth: false, setHeight: false, width: 0, height: 0 }; // Create private Emitter instance. this._emitter = new Emitter(); // Add container element's class name. addClass(element, settings.containerClass); // Create initial items. this._items = []; items = settings.items; if (typeof items === 'string') { toArray(element.children).forEach(function(itemElement) { if (items === '*' || elementMatches(itemElement, items)) { inst._items.push(new Item(inst, itemElement)); } }); } else if (Array.isArray(items) || isNodeList(items)) { this._items = toArray(items).map(function(itemElement) { return new Item(inst, itemElement); }); } // If layoutOnResize option is a valid number sanitize it and bind the resize // handler. layoutOnResize = settings.layoutOnResize; if (typeof layoutOnResize !== 'number') { layoutOnResize = layoutOnResize === true ? 0 : -1; } if (layoutOnResize >= 0) { window.addEventListener( 'resize', (inst._resizeHandler = debounce(function() { inst.refreshItems().layout(); }, layoutOnResize)) ); } // Layout on init if necessary. if (settings.layoutOnInit) { this.layout(true); } } /** * Public properties * ***************** */ /** * @see Item */ Grid.Item = Item; /** * @see ItemLayout */ Grid.ItemLayout = ItemLayout; /** * @see ItemVisibility */ Grid.ItemVisibility = ItemVisibility; /** * @see ItemRelease */ Grid.ItemRelease = ItemRelease; /** * @see ItemMigrate */ Grid.ItemMigrate = ItemMigrate; /** * @see ItemAnimate */ Grid.ItemAnimate = ItemAnimate; /** * @see ItemDrag */ Grid.ItemDrag = ItemDrag; /** * @see Emitter */ Grid.Emitter = Emitter; /** * Default options for Grid instance. * * @public * @memberof Grid */ Grid.defaultOptions = { // 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: true }, layoutOnResize: 100, layoutOnInit: true, layoutDuration: 300, layoutEasing: 'ease', // Sorting sortData: null, // Drag & Drop dragEnabled: false, dragContainer: null, dragStartPredicate: { distance: 0, delay: 0, handle: false }, dragAxis: null, dragSort: true, dragSortInterval: 100, dragSortPredicate: { threshold: 50, action: 'move' }, dragReleaseDuration: 300, dragReleaseEasing: 'ease', dragHammerSettings: { touchAction: 'none' }, // 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' }; /** * Public prototype methods * ************************ */ /** * Bind an event listener. * * @public * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ Grid.prototype.on = function(event, listener) { this._emitter.on(event, listener); return this; }; /** * Bind an event listener that is triggered only once. * * @public * @memberof Grid.prototype * @param {String} event * @param {Function} listener * @returns {Grid} */ Grid.prototype.once = function(event, listener) { this._emitter.once(event, listener); return this; }; /** * Unbind an event listener. * * @public * @memberof Grid.prototype * @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 * @memberof Grid.prototype * @returns {HTMLElement} */ Grid.prototype.getElement = function() { return this._element; }; /** * Get all items. Optionally you can provide specific targets (elements and * indices). Note that the returned array is not the same object used by the * instance so modifying it will not affect instance's items. All items that * are not found are omitted from the returned array. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} [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 && targets !== 0)) { return this._items.slice(0); } var ret = []; var targetItems = toArray(targets); var item; var i; // If target items are defined return filtered results. for (i = 0; i < targetItems.length; i++) { item = this._getItem(targetItems[i]); item && ret.push(item); } return ret; }; /** * Update the cached dimensions of the instance's items. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} [items] * @returns {Grid} */ Grid.prototype.refreshItems = function(items) { if (this._isDestroyed) return this; var targets = this.getItems(items); var i; for (i = 0; i < targets.length; i++) { targets[i]._refreshDimensions(); } return this; }; /** * Update the sort data of the instance's items. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} [items] * @returns {Grid} */ Grid.prototype.refreshSortData = function(items) { if (this._isDestroyed) return this; var targetItems = this.getItems(items); var i; for (i = 0; i < targetItems.length; i++) { targetItems[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 * @memberof Grid.prototype * @returns {Grid} */ Grid.prototype.synchronize = function() { if (this._isDestroyed) return this; var container = this._element; var items = this._items; var fragment; var element; var i; // Append all elements in order to the container element. if (items.length) { for (i = 0; i < items.length; i++) { element = items[i]._element; if (element.parentNode === container) { fragment = fragment || document.createDocumentFragment(); fragment.appendChild(element); } } if (fragment) container.appendChild(fragment); } // Emit synchronize event. this._emit(eventSynchronize); return this; }; /** * Calculate and apply item positions. * * @public * @memberof Grid.prototype * @param {Boolean} [instant=false] * @param {LayoutCallback} [onFinish] * @returns {Grid} */ Grid.prototype.layout = function(instant, onFinish) { if (this._isDestroyed) return this; var inst = this; var element = this._element; var layout = this._updateLayout(); var layoutId = layout.id; var itemsLength = layout.items.length; var counter = itemsLength; var callback = typeof instant === 'function' ? instant : onFinish; var isCallbackFunction = typeof callback === 'function'; var callbackItems = isCallbackFunction ? layout.items.slice(0) : null; var isBorderBox; var item; var i; // The finish function, which will be used for checking if all the items // have laid out yet. After all items have finished their animations call // callback and emit layoutEnd event. Only emit layoutEnd event if there // hasn't been a new layout call during this layout. function tryFinish() { if (--counter > 0) return; var hasLayoutChanged = inst._layout.id !== layoutId; isCallbackFunction && callback(hasLayoutChanged, callbackItems); if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) { inst._emit(eventLayoutEnd, layout.items.slice(0)); } } // If grid's width or height was modified, we need to update it's cached // dimensions. Also keep in mind that grid's cached width/height should // always equal to what elem.getBoundingClientRect() would return, so // therefore we need to add the grid element's borders to the dimensions if // it's box-sizing is border-box. if ( (layout.setHeight && typeof layout.height === 'number') || (layout.setWidth && typeof layout.width === 'number') ) { isBorderBox = getStyle(element, 'box-sizing') === 'border-box'; } if (layout.setHeight) { if (typeof layout.height === 'number') { element.style.height = (isBorderBox ? layout.height + this._borderTop + this._borderBottom : layout.height) + 'px'; } else { element.style.height = layout.height; } } if (layout.setWidth) { if (typeof layout.width === 'number') { element.style.width = (isBorderBox ? layout.width + this._borderLeft + this._borderRight : layout.width) + 'px'; } else { element.style.width = layout.width; } } // Emit layoutStart event. Note that this 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(eventLayoutStart)) { this._emit(eventLayoutStart, layout.items.slice(0)); } // If there are no items let's finish quickly. if (!itemsLength) { tryFinish(); return this; } // If there are items let's position them. for (i = 0; i < itemsLength; i++) { item = layout.items[i]; if (!item) continue; // Update item's position. item._left = layout.slots[i * 2]; item._top = layout.slots[i * 2 + 1]; // Layout item if it is not dragged. item.isDragging() ? tryFinish() : item._layout.start(instant === true, tryFinish); } 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 * @memberof Grid.prototype * @param {(HTMLElement|HTMLElement[])} elements * @param {Object} [options] * @param {Number} [options.index=-1] * @param {Boolean} [options.isActive] * @param {(Boolean|LayoutCallback|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 || 0; var layout = opts.layout ? opts.layout : opts.layout === undefined; var items = this._items; var needsLayout = false; var item; var i; // Map provided elements into new grid items. for (i = 0; i < newItems.length; i++) { item = new Item(this, newItems[i], opts.isActive); newItems[i] = item; // 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; } } // Add the new items to the items collection to correct index. arrayInsert(items, newItems, opts.index); // Emit add event. if (this._hasListeners(eventAdd)) { this._emit(eventAdd, newItems.slice(0)); } // If layout is needed. if (needsLayout && layout) { this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined); } return newItems; }; /** * Remove items from the instance. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} items * @param {Object} [options] * @param {Boolean} [options.removeElements=false] * @param {(Boolean|LayoutCallback|String)} [options.layout=true] * @returns {Item[]} */ Grid.prototype.remove = function(items, options) { if (this._isDestroyed) return this; var opts = options || 0; var layout = opts.layout ? opts.layout : opts.layout === undefined; var needsLayout = false; var allItems = this.getItems(); var targetItems = this.getItems(items); var indices = []; var item; var i; // Remove the individual items. for (i = 0; i < targetItems.length; i++) { item = targetItems[i]; indices.push(allItems.indexOf(item)); if (item._isActive) needsLayout = true; item._destroy(opts.removeElements); } // Emit remove event. if (this._hasListeners(eventRemove)) { this._emit(eventRemove, targetItems.slice(0), indices); } // If layout is needed. if (needsLayout && layout) { this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined); } return targetItems; }; /** * Show instance items. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} items * @param {Object} [options] * @param {Boolean} [options.instant=false] * @param {ShowCallback} [options.onFinish] * @param {(Boolean|LayoutCallback|String)} [options.layout=true] * @returns {Grid} */ Grid.prototype.show = function(items, options) { if (this._isDestroyed) return this; this._setItemsVisibility(items, true, options); return this; }; /** * Hide instance items. * * @public * @memberof Grid.prototype * @param {GridMultiItemQuery} items * @param {Object} [options] * @param {Boolean} [options.instant=false] * @param {HideCallback} [options.onFinish] * @param {(Boolean|LayoutCallback|String)} [options.layout=true] * @returns {Grid} */ Grid.prototype.hide = function(items, options) { if (this._isDestroyed) return this; 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 * @memberof Grid.prototype * @param {(Function|String)} predicate * @param {Object} [options] * @param {Boolean} [options.instant=false] * @param {FilterCallback} [options.onFinish] * @param {(Boolean|LayoutCallback|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'; var isPredicateFn = typeof predicate === 'function'; var opts = options || 0; var isInstant = opts.instant === true; var layout = opts.layout ? opts.layout : opts.layout === undefined; var onFinish = typeof opts.onFinish === 'function' ? 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, onFinish: tryFinish, layout: false }); } else { tryFinish(); } // Hide items that need to be hidden. if (itemsToHide.length) { this.hide(itemsToHide, { instant: isInstant, onFinish: tryFinish, layout: false }); } else { tryFinish(); } // If there are any items to filter. if (itemsToShow.length || itemsToHide.length) { // Emit filter event. if (this._hasListeners(eventFilter)) { this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0)); } // If layout is needed. if (layout) { this.layout(layout === 'instant', typeof layout === 'function' ? 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 * @memberof Grid.prototype * @param {(Function|Item[]|String|String[])} comparer * @param {Object} [options] * @param {Boolean} [options.descending=false] * @param {(Boolean|LayoutCallback|String)} [options.layout=true] * @returns {Grid} */ Grid.prototype.sort = (function() { var sortComparer; var isDescending; var origItems; var indexMap; function parseCriteria(data) { return data .trim() .split(' ') .map(function(val) { return val.split(':'); }); } function getIndexMap(items) { var ret = {}; for (var i = 0; i < items.length; i++) { ret[items[i]._id] = i; } return ret; } function compareIndices(itemA, itemB) { var indexA = indexMap[itemA._id]; var indexB = indexMap[itemB._id]; return isDescending ? indexB - indexA : indexA - indexB; } 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. if (!result) { if (!indexMap) indexMap = getIndexMap(origItems); result = compareIndices(a, b); } return result; } function customComparer(a, b) { var result = sortComparer(a, b); // If descending let's invert the result value. if (isDescending && result) result = -result; // If we have a valid result (not zero) let's return it right away. if (result) return result; // If result is zero let's compare the item indices to make sure we have a // stable sort. if (!indexMap) indexMap = getIndexMap(origItems); return compareIndices(a, b); } return function(comparer, options) { if (this._isDestroyed || this._items.length < 2) return this; var items = this._items; var opts = options || 0; var layout = opts.layout ? opts.layout : opts.layout === undefined; var i; // Setup parent scope data. sortComparer = comparer; isDescending = !!opts.descending; origItems = items.slice(0); indexMap = null; // If function is provided do a native array sort. if (typeof sortComparer === 'function') { 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 sortComparer === 'string') { sortComparer = parseCriteria(comparer); 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. else if (Array.isArray(sortComparer)) { if (sortComparer.length !== items.length) { throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); } for (i = 0; i < items.length; i++) { if (sortComparer.indexOf(items[i]) < 0) { throw new Error('[' + namespace + '] sort reference items do not match with grid items.'); } items[i] = sortComparer[i]; } if (isDescending) items.reverse(); } // Otherwise let's just skip it, nothing we can do here. else { /** @todo Maybe throw an error here? */ return this; } // Emit sort event. if (this._hasListeners(eventSort)) { this._emit(eventSort, items.slice(0), origItems); } // If layout is needed. if (layout) { this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined); } return this; }; })(); /** * Move item to another index or in place of another item. * * @public * @memberof Grid.prototype * @param {GridSingleItemQuery} item * @param {GridSingleItemQuery} 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|LayoutCallback|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 || 0; var layout = opts.layout ? opts.layout : opts.layout === undefined; var isSwap = opts.action === 'swap'; var action = isSwap ? 'swap' : '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(eventMove)) { this._emit(eventMove, { item: fromItem, fromIndex: fromIndex, toIndex: toIndex, action: action }); } // If layout is needed. if (layout) { this.layout(layout === 'instant', typeof layout === 'function' ? layout : undefined); } } return this; }; /** * Send item to another Grid instance. * * @public * @memberof Grid.prototype * @param {GridSingleItemQuery} item * @param {Grid} grid * @param {GridSingleItemQuery} position * @param {Object} [options] * @param {HTMLElement} [options.appendTo=document.body] * @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true] * @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true] * @returns {Grid} */ Grid.prototype.send = function(item, grid, position, options) { if (this._isDestroyed || grid._isDestroyed || this === grid) return this; // Make sure we have a valid target item. item = this._getItem(item); if (!item) return this; var opts = options || 0; 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(grid, 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', typeof layoutSender === 'function' ? layoutSender : undefined ); } if (layoutReceiver) { grid.layout( layoutReceiver === 'instant', typeof layoutReceiver === 'function' ? layoutReceiver : undefined ); } } return this; }; /** * Destroy the instance. * * @public * @memberof Grid.prototype * @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 i; // Unbind window resize event listener. if (this._resizeHandler) { window.removeEventListener('resize', this._resizeHandler); } // Destroy items. for (i = 0; i < items.length; i++) { items[i]._destroy(removeElements); } // Restore container. removeClass(container, this._settings.containerClass); container.style.height = ''; container.style.width = ''; // Emit destroy event and unbind all events. this._emit(eventDestroy); this._emitter.destroy(); // Remove reference from the grid instances collection. gridInstances[this._id] = undefined; // Flag instance as destroyed. this._isDestroyed = true; return this; }; /** * Private prototype methods * ************************* */ /** * 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 * @memberof Grid.prototype * @param {GridSingleItemQuery} [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') { 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. /** @todo This could be made a lot faster by using Map/WeakMap of elements. */ for (var i = 0; i < this._items.length; i++) { if (this._items[i]._element === target) { return this._items[i]; } } return null; }; /** * Recalculates and updates instance's layout data. * * @private * @memberof Grid.prototype * @returns {LayoutData} */ Grid.prototype._updateLayout = function() { var layout = this._layout; var settings = this._settings.layout; var width; var height; var newLayout; var i; // Let's increment layout id. ++layout.id; // Let's update layout items layout.items.length = 0; for (i = 0; i < this._items.length; i++) { if (this._items[i]._isActive) layout.items.push(this._items[i]); } // Let's make sure we have the correct container dimensions. this._refreshDimensions(); // Calculate container width and height (without borders). width = this._width - this._borderLeft - this._borderRight; height = this._height - this._borderTop - this._borderBottom; // Calculate new layout. if (typeof settings === 'function') { newLayout = settings(layout.items, width, height); } else { newLayout = packer.getLayout(layout.items, width, height, layout.slots, settings); } // Let's update the grid's layout. layout.slots = newLayout.slots; layout.setWidth = Boolean(newLayout.setWidth); layout.setHeight = Boolean(newLayout.setHeight); layout.width = newLayout.width; layout.height = newLayout.height; return layout; }; /** * Emit a grid event. * * @private * @memberof Grid.prototype * @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 * @memberof Grid.prototype * @param {String} event * @returns {Boolean} */ Grid.prototype._hasListeners = function(event) { var listeners = this._emitter._events[event]; return !!(listeners && listeners.length); }; /** * Update container's width, height and offsets. * * @private * @memberof Grid.prototype */ 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; }; /** * Update container's border sizes. * * @private * @memberof Grid.prototype * @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 * @memberof Grid.prototype */ Grid.prototype._refreshDimensions = function() { this._updateBoundingRect(); this._updateBorders(1, 1, 1, 1); }; /** * Show or hide Grid instance's items. * * @private * @memberof Grid.prototype * @param {GridMultiItemQuery} items * @param {Boolean} toVisible * @param {Object} [options] * @param {Boolean} [options.instant=false] * @param {(ShowCallback|HideCallback)} [options.onFinish] * @param {(Boolean|LayoutCallback|String)} [options.layout=true] */ Grid.prototype._setItemsVisibility = function(items, toVisible, options) { var grid = this; var targetItems = this.getItems(items); var opts = options || 0; 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 ? eventShowStart : eventHideStart; var endEvent = toVisible ? eventShowEnd : eventHideEnd; 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 (typeof callback === 'function') callback(targetItems); return; } // Emit showStart/hideStart event. if (this._hasListeners(startEvent)) { this._emit(startEvent, targetItems.slice(0)); } // Show/hide 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). if (toVisible && !item._isActive) { item._layout._skipNextAnimation = true; } // If a hidden item is being shown we need to refresh the item's // dimensions. if (toVisible && item._visibility._isHidden) { hiddenItems.push(item); } // Show/hide the item. item._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 (typeof callback === 'function') callback(completedItems.slice(0)); if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); } }); } // Refresh hidden items. if (hiddenItems.length) this.refreshItems(hiddenItems); // Layout if needed. if (needsLayout && layout) { this.layout(layout === 'instant', typeof layout === 'function' ? 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 ret = mergeObjects({}, defaultSettings); // Merge user settings to default settings. if (userSettings) { ret = mergeObjects(ret, userSettings); } // Handle visible/hidden styles manually so that the whole object is // overridden instead of the props. ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles; ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles; return ret; } /** * 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; } return Grid; })));