OpenXE/classes/Modules/Dashboard/www/js/muuri-0.7.1/muuri.js
2021-05-21 08:49:41 +02:00

5708 lines
162 KiB
JavaScript

/**
* 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ö <inramo@gmail.com>
* @license MIT
*
* Muuri Ticker / Muuri Emitter / Muuri Queue
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* @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;
})));