muuri
Version:
Responsive, sortable, filterable and draggable grid layouts.
1,700 lines (1,455 loc) • 166 kB
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 sor