mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-28 15:40:28 +01:00
5708 lines
162 KiB
JavaScript
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;
|
|
|
|
})));
|