muuri
Version:
Responsive, sortable, filterable and draggable layouts
1,783 lines (1,548 loc) • 264 kB
JavaScript
/**
* Muuri v0.9.5
* https://muuri.dev/
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
* @license MIT
*
* Muuri Packer
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*
* Muuri Ticker / Muuri Emitter / Muuri Dragger
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*
* Muuri AutoScroller
* Copyright (c) 2019-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Muuri = factory());
}(this, (function () { 'use strict';
var GRID_INSTANCES = {};
var ITEM_ELEMENT_MAP = typeof Map === 'function' ? new Map() : null;
var ACTION_SWAP = 'swap';
var ACTION_MOVE = 'move';
var EVENT_SYNCHRONIZE = 'synchronize';
var EVENT_LAYOUT_START = 'layoutStart';
var EVENT_LAYOUT_END = 'layoutEnd';
var EVENT_LAYOUT_ABORT = 'layoutAbort';
var EVENT_ADD = 'add';
var EVENT_REMOVE = 'remove';
var EVENT_SHOW_START = 'showStart';
var EVENT_SHOW_END = 'showEnd';
var EVENT_HIDE_START = 'hideStart';
var EVENT_HIDE_END = 'hideEnd';
var EVENT_FILTER = 'filter';
var EVENT_SORT = 'sort';
var EVENT_MOVE = 'move';
var EVENT_SEND = 'send';
var EVENT_BEFORE_SEND = 'beforeSend';
var EVENT_RECEIVE = 'receive';
var EVENT_BEFORE_RECEIVE = 'beforeReceive';
var EVENT_DRAG_INIT = 'dragInit';
var EVENT_DRAG_START = 'dragStart';
var EVENT_DRAG_MOVE = 'dragMove';
var EVENT_DRAG_SCROLL = 'dragScroll';
var EVENT_DRAG_END = 'dragEnd';
var EVENT_DRAG_RELEASE_START = 'dragReleaseStart';
var EVENT_DRAG_RELEASE_END = 'dragReleaseEnd';
var EVENT_DESTROY = 'destroy';
var HAS_TOUCH_EVENTS = 'ontouchstart' in window;
var HAS_POINTER_EVENTS = !!window.PointerEvent;
var HAS_MS_POINTER_EVENTS = !!window.navigator.msPointerEnabled;
var MAX_SAFE_FLOAT32_INTEGER = 16777216;
/**
* Event emitter constructor.
*
* @class
*/
function Emitter() {
this._events = {};
this._queue = [];
this._counter = 0;
this._clearOnEmit = false;
}
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.on = function (event, listener) {
if (!this._events || !event || !listener) return this;
// Get listeners queue and create it if it does not exist.
var listeners = this._events[event];
if (!listeners) listeners = this._events[event] = [];
// Add the listener to the queue.
listeners.push(listener);
return this;
};
/**
* Unbind all event listeners that match the provided listener function.
*
* @public
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.off = function (event, listener) {
if (!this._events || !event || !listener) return this;
// Get listeners and return immediately if none is found.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
// Remove all matching listeners.
var index;
while ((index = listeners.indexOf(listener)) !== -1) {
listeners.splice(index, 1);
}
return this;
};
/**
* Unbind all listeners of the provided event.
*
* @public
* @param {String} event
* @returns {Emitter}
*/
Emitter.prototype.clear = function (event) {
if (!this._events || !event) return this;
var listeners = this._events[event];
if (listeners) {
listeners.length = 0;
delete this._events[event];
}
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments.
*
* @public
* @param {String} event
* @param {...*} [args]
* @returns {Emitter}
*/
Emitter.prototype.emit = function (event) {
if (!this._events || !event) {
this._clearOnEmit = false;
return this;
}
// Get event listeners and quit early if there's no listeners.
var listeners = this._events[event];
if (!listeners || !listeners.length) {
this._clearOnEmit = false;
return this;
}
var queue = this._queue;
var startIndex = queue.length;
var argsLength = arguments.length - 1;
var args;
// If we have more than 3 arguments let's put the arguments in an array and
// apply it to the listeners.
if (argsLength > 3) {
args = [];
args.push.apply(args, arguments);
args.shift();
}
// Add the current listeners to the callback queue before we process them.
// This is necessary to guarantee that all of the listeners are called in
// correct order even if new event listeners are removed/added during
// processing and/or events are emitted during processing.
queue.push.apply(queue, listeners);
// Reset the event's listeners if need be.
if (this._clearOnEmit) {
listeners.length = 0;
this._clearOnEmit = false;
}
// Increment queue counter. This is needed for the scenarios where emit is
// triggered while the queue is already processing. We need to keep track of
// how many "queue processors" there are active so that we can safely reset
// the queue in the end when the last queue processor is finished.
++this._counter;
// Process the queue (the specific part of it for this emit).
var i = startIndex;
var endIndex = queue.length;
for (; i < endIndex; i++) {
// prettier-ignore
argsLength === 0 ? queue[i]() :
argsLength === 1 ? queue[i](arguments[1]) :
argsLength === 2 ? queue[i](arguments[1], arguments[2]) :
argsLength === 3 ? queue[i](arguments[1], arguments[2], arguments[3]) :
queue[i].apply(null, args);
// Stop processing if the emitter is destroyed.
if (!this._events) return this;
}
// Decrement queue process counter.
--this._counter;
// Reset the queue if there are no more queue processes running.
if (!this._counter) queue.length = 0;
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments and
* remove the event's listeners just before calling the them. This method allows
* the emitter to serve as a queue where all listeners are called only once.
*
* @public
* @param {String} event
* @param {...*} [args]
* @returns {Emitter}
*/
Emitter.prototype.burst = function () {
if (!this._events) return this;
this._clearOnEmit = true;
this.emit.apply(this, arguments);
return this;
};
/**
* Check how many listeners there are for a specific event.
*
* @public
* @param {String} event
* @returns {Boolean}
*/
Emitter.prototype.countListeners = function (event) {
if (!this._events) return 0;
var listeners = this._events[event];
return listeners ? listeners.length : 0;
};
/**
* Destroy emitter instance. Basically just removes all bound listeners.
*
* @public
* @returns {Emitter}
*/
Emitter.prototype.destroy = function () {
if (!this._events) return this;
this._queue.length = this._counter = 0;
this._events = null;
return this;
};
var pointerout = HAS_POINTER_EVENTS ? 'pointerout' : HAS_MS_POINTER_EVENTS ? 'MSPointerOut' : '';
var waitDuration = 100;
/**
* If you happen to use Edge or IE on a touch capable device there is a
* a specific case where pointercancel and pointerend events are never emitted,
* even though one them should always be emitted when you release your finger
* from the screen. The bug appears specifically when Muuri shifts the dragged
* element's position in the DOM after pointerdown event, IE and Edge don't like
* that behaviour and quite often forget to emit the pointerend/pointercancel
* event. But, they do emit pointerout event so we utilize that here.
* Specifically, if there has been no pointermove event within 100 milliseconds
* since the last pointerout event we force cancel the drag operation. This hack
* works surprisingly well 99% of the time. There is that 1% chance there still
* that dragged items get stuck but it is what it is.
*
* @class
* @param {Dragger} dragger
*/
function EdgeHack(dragger) {
if (!pointerout) return;
this._dragger = dragger;
this._timeout = null;
this._outEvent = null;
this._isActive = false;
this._addBehaviour = this._addBehaviour.bind(this);
this._removeBehaviour = this._removeBehaviour.bind(this);
this._onTimeout = this._onTimeout.bind(this);
this._resetData = this._resetData.bind(this);
this._onStart = this._onStart.bind(this);
this._onOut = this._onOut.bind(this);
this._dragger.on('start', this._onStart);
}
/**
* @private
*/
EdgeHack.prototype._addBehaviour = function () {
if (this._isActive) return;
this._isActive = true;
this._dragger.on('move', this._resetData);
this._dragger.on('cancel', this._removeBehaviour);
this._dragger.on('end', this._removeBehaviour);
window.addEventListener(pointerout, this._onOut);
};
/**
* @private
*/
EdgeHack.prototype._removeBehaviour = function () {
if (!this._isActive) return;
this._dragger.off('move', this._resetData);
this._dragger.off('cancel', this._removeBehaviour);
this._dragger.off('end', this._removeBehaviour);
window.removeEventListener(pointerout, this._onOut);
this._resetData();
this._isActive = false;
};
/**
* @private
*/
EdgeHack.prototype._resetData = function () {
window.clearTimeout(this._timeout);
this._timeout = null;
this._outEvent = null;
};
/**
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
EdgeHack.prototype._onStart = function (e) {
if (e.pointerType === 'mouse') return;
this._addBehaviour();
};
/**
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
EdgeHack.prototype._onOut = function (e) {
if (!this._dragger._getTrackedTouch(e)) return;
this._resetData();
this._outEvent = e;
this._timeout = window.setTimeout(this._onTimeout, waitDuration);
};
/**
* @private
*/
EdgeHack.prototype._onTimeout = function () {
var e = this._outEvent;
this._resetData();
if (this._dragger.isActive()) this._dragger._onCancel(e);
};
/**
* @public
*/
EdgeHack.prototype.destroy = function () {
if (!pointerout) return;
this._dragger.off('start', this._onStart);
this._removeBehaviour();
};
// Playing it safe here, test all potential prefixes capitalized and lowercase.
var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O'];
var cache$2 = {};
/**
* Get prefixed CSS property name when given a non-prefixed CSS property name.
* Returns null if the property is not supported at all.
*
* @param {CSSStyleDeclaration} style
* @param {String} prop
* @returns {String}
*/
function getPrefixedPropName(style, prop) {
var prefixedProp = cache$2[prop] || '';
if (prefixedProp) return prefixedProp;
var camelProp = prop[0].toUpperCase() + prop.slice(1);
var i = 0;
while (i < vendorPrefixes.length) {
prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop;
if (prefixedProp in style) {
cache$2[prop] = prefixedProp;
return prefixedProp;
}
++i;
}
return '';
}
/**
* Check if passive events are supported.
* https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
*
* @returns {Boolean}
*/
function hasPassiveEvents() {
var isPassiveEventsSupported = false;
try {
var passiveOpts = Object.defineProperty({}, 'passive', {
get: function () {
isPassiveEventsSupported = true;
},
});
window.addEventListener('testPassive', null, passiveOpts);
window.removeEventListener('testPassive', null, passiveOpts);
} catch (e) {}
return isPassiveEventsSupported;
}
var ua = window.navigator.userAgent.toLowerCase();
var isEdge = ua.indexOf('edge') > -1;
var isIE = ua.indexOf('trident') > -1;
var isFirefox = ua.indexOf('firefox') > -1;
var isAndroid = ua.indexOf('android') > -1;
var listenerOptions = hasPassiveEvents() ? { passive: true } : false;
var taProp = 'touchAction';
var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp);
var taDefaultValue = 'auto';
/**
* Creates a new Dragger instance for an element.
*
* @public
* @class
* @param {HTMLElement} element
* @param {Object} [cssProps]
*/
function Dragger(element, cssProps) {
this._element = element;
this._emitter = new Emitter();
this._isDestroyed = false;
this._cssProps = {};
this._touchAction = '';
this._isActive = false;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._onStart = this._onStart.bind(this);
this._onMove = this._onMove.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onEnd = this._onEnd.bind(this);
// Can't believe had to build a freaking class for a hack!
this._edgeHack = null;
if ((isEdge || isIE) && (HAS_POINTER_EVENTS || HAS_MS_POINTER_EVENTS)) {
this._edgeHack = new EdgeHack(this);
}
// Apply initial CSS props.
this.setCssProps(cssProps);
// If touch action was not provided with initial CSS props let's assume it's
// auto.
if (!this._touchAction) {
this.setTouchAction(taDefaultValue);
}
// Prevent native link/image dragging for the item and it's children.
element.addEventListener('dragstart', Dragger._preventDefault, false);
// Listen to start event.
element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
}
/**
* Protected properties
* ********************
*/
Dragger._pointerEvents = {
start: 'pointerdown',
move: 'pointermove',
cancel: 'pointercancel',
end: 'pointerup',
};
Dragger._msPointerEvents = {
start: 'MSPointerDown',
move: 'MSPointerMove',
cancel: 'MSPointerCancel',
end: 'MSPointerUp',
};
Dragger._touchEvents = {
start: 'touchstart',
move: 'touchmove',
cancel: 'touchcancel',
end: 'touchend',
};
Dragger._mouseEvents = {
start: 'mousedown',
move: 'mousemove',
cancel: '',
end: 'mouseup',
};
Dragger._inputEvents = (function () {
if (HAS_TOUCH_EVENTS) return Dragger._touchEvents;
if (HAS_POINTER_EVENTS) return Dragger._pointerEvents;
if (HAS_MS_POINTER_EVENTS) return Dragger._msPointerEvents;
return Dragger._mouseEvents;
})();
Dragger._emitter = new Emitter();
Dragger._emitterEvents = {
start: 'start',
move: 'move',
end: 'end',
cancel: 'cancel',
};
Dragger._activeInstances = [];
/**
* Protected static methods
* ************************
*/
Dragger._preventDefault = function (e) {
if (e.preventDefault && e.cancelable !== false) e.preventDefault();
};
Dragger._activateInstance = function (instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index > -1) return;
Dragger._activeInstances.push(instance);
Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove);
Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel);
Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd);
if (Dragger._activeInstances.length === 1) {
Dragger._bindListeners();
}
};
Dragger._deactivateInstance = function (instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index === -1) return;
Dragger._activeInstances.splice(index, 1);
Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove);
Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel);
Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd);
if (!Dragger._activeInstances.length) {
Dragger._unbindListeners();
}
};
Dragger._bindListeners = function () {
window.addEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
window.addEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
if (Dragger._inputEvents.cancel) {
window.addEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
}
};
Dragger._unbindListeners = function () {
window.removeEventListener(Dragger._inputEvents.move, Dragger._onMove, listenerOptions);
window.removeEventListener(Dragger._inputEvents.end, Dragger._onEnd, listenerOptions);
if (Dragger._inputEvents.cancel) {
window.removeEventListener(Dragger._inputEvents.cancel, Dragger._onCancel, listenerOptions);
}
};
Dragger._getEventPointerId = function (event) {
// If we have pointer id available let's use it.
if (typeof event.pointerId === 'number') {
return event.pointerId;
}
// For touch events let's get the first changed touch's identifier.
if (event.changedTouches) {
return event.changedTouches[0] ? event.changedTouches[0].identifier : null;
}
// For mouse/other events let's provide a static id.
return 1;
};
Dragger._getTouchById = function (event, id) {
// If we have a pointer event return the whole event if there's a match, and
// null otherwise.
if (typeof event.pointerId === 'number') {
return event.pointerId === id ? event : null;
}
// For touch events let's check if there's a changed touch object that matches
// the pointerId in which case return the touch object.
if (event.changedTouches) {
for (var i = 0; i < event.changedTouches.length; i++) {
if (event.changedTouches[i].identifier === id) {
return event.changedTouches[i];
}
}
return null;
}
// For mouse/other events let's assume there's only one pointer and just
// return the event.
return event;
};
Dragger._onMove = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.move, e);
};
Dragger._onCancel = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.cancel, e);
};
Dragger._onEnd = function (e) {
Dragger._emitter.emit(Dragger._emitterEvents.end, e);
};
/**
* Private prototype methods
* *************************
*/
/**
* Reset current drag operation (if any).
*
* @private
*/
Dragger.prototype._reset = function () {
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._isActive = false;
Dragger._deactivateInstance(this);
};
/**
* Create a custom dragger event from a raw event.
*
* @private
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {Object}
*/
Dragger.prototype._createEvent = function (type, e) {
var touch = this._getTrackedTouch(e);
return {
// Hammer.js compatibility interface.
type: type,
srcEvent: e,
distance: this.getDistance(),
deltaX: this.getDeltaX(),
deltaY: this.getDeltaY(),
deltaTime: type === Dragger._emitterEvents.start ? 0 : this.getDeltaTime(),
isFirst: type === Dragger._emitterEvents.start,
isFinal: type === Dragger._emitterEvents.end || type === Dragger._emitterEvents.cancel,
pointerType: e.pointerType || (e.touches ? 'touch' : 'mouse'),
// Partial Touch API interface.
identifier: this._pointerId,
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target,
};
};
/**
* Emit a raw event as dragger event internally.
*
* @private
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._emit = function (type, e) {
this._emitter.emit(type, this._createEvent(type, e));
};
/**
* If the provided event is a PointerEvent this method will return it if it has
* the same pointerId as the instance. If the provided event is a TouchEvent
* this method will try to look for a Touch instance in the changedTouches that
* has an identifier matching this instance's pointerId. If the provided event
* is a MouseEvent (or just any other event than PointerEvent or TouchEvent)
* it will be returned immediately.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {?(Touch|PointerEvent|MouseEvent)}
*/
Dragger.prototype._getTrackedTouch = function (e) {
if (this._pointerId === null) return null;
return Dragger._getTouchById(e, this._pointerId);
};
/**
* Handler for start event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onStart = function (e) {
if (this._isDestroyed) return;
// If pointer id is already assigned let's return early.
if (this._pointerId !== null) return;
// Get (and set) pointer id.
this._pointerId = Dragger._getEventPointerId(e);
if (this._pointerId === null) return;
// Setup initial data and emit start event.
var touch = this._getTrackedTouch(e);
this._startX = this._currentX = touch.clientX;
this._startY = this._currentY = touch.clientY;
this._startTime = Date.now();
this._isActive = true;
this._emit(Dragger._emitterEvents.start, e);
// If the drag procedure was not reset within the start procedure let's
// activate the instance (start listening to move/cancel/end events).
if (this._isActive) {
Dragger._activateInstance(this);
}
};
/**
* Handler for move event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onMove = function (e) {
var touch = this._getTrackedTouch(e);
if (!touch) return;
this._currentX = touch.clientX;
this._currentY = touch.clientY;
this._emit(Dragger._emitterEvents.move, e);
};
/**
* Handler for cancel event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onCancel = function (e) {
if (!this._getTrackedTouch(e)) return;
this._emit(Dragger._emitterEvents.cancel, e);
this._reset();
};
/**
* Handler for end event.
*
* @private
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onEnd = function (e) {
if (!this._getTrackedTouch(e)) return;
this._emit(Dragger._emitterEvents.end, e);
this._reset();
};
/**
* Public prototype methods
* ************************
*/
/**
* Check if the element is being dragged at the moment.
*
* @public
* @returns {Boolean}
*/
Dragger.prototype.isActive = function () {
return this._isActive;
};
/**
* Set element's touch-action CSS property.
*
* @public
* @param {String} value
*/
Dragger.prototype.setTouchAction = function (value) {
// Store unmodified touch action value (we trust user input here).
this._touchAction = value;
// Set touch-action style.
if (taPropPrefixed) {
this._cssProps[taPropPrefixed] = '';
this._element.style[taPropPrefixed] = value;
}
// If we have an unsupported touch-action value let's add a special listener
// that prevents default action on touch start event. A dirty hack, but best
// we can do for now. The other options would be to somehow polyfill the
// unsupported touch action behavior with custom heuristics which sounds like
// a can of worms. We do a special exception here for Firefox Android which's
// touch-action does not work properly if the dragged element is moved in the
// the DOM tree on touchstart.
if (HAS_TOUCH_EVENTS) {
this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
if (this._element.style[taPropPrefixed] !== value || (isFirefox && isAndroid)) {
this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
}
}
};
/**
* Update element's CSS properties. Accepts an object with camel cased style
* props with value pairs as it's first argument.
*
* @public
* @param {Object} [newProps]
*/
Dragger.prototype.setCssProps = function (newProps) {
if (!newProps) return;
var currentProps = this._cssProps;
var element = this._element;
var prop;
var prefixedProp;
// Reset current props.
for (prop in currentProps) {
element.style[prop] = currentProps[prop];
delete currentProps[prop];
}
// Set new props.
for (prop in newProps) {
// Make sure we have a value for the prop.
if (!newProps[prop]) continue;
// Special handling for touch-action.
if (prop === taProp) {
this.setTouchAction(newProps[prop]);
continue;
}
// Get prefixed prop and skip if it does not exist.
prefixedProp = getPrefixedPropName(element.style, prop);
if (!prefixedProp) continue;
// Store the prop and add the style.
currentProps[prefixedProp] = '';
element.style[prefixedProp] = newProps[prop];
}
};
/**
* How much the pointer has moved on x-axis from start position, in pixels.
* Positive value indicates movement from left to right.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaX = function () {
return this._currentX - this._startX;
};
/**
* How much the pointer has moved on y-axis from start position, in pixels.
* Positive value indicates movement from top to bottom.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaY = function () {
return this._currentY - this._startY;
};
/**
* How far (in pixels) has pointer moved from start position.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDistance = function () {
var x = this.getDeltaX();
var y = this.getDeltaY();
return Math.sqrt(x * x + y * y);
};
/**
* How long has pointer been dragged.
*
* @public
* @returns {Number}
*/
Dragger.prototype.getDeltaTime = function () {
return this._startTime ? Date.now() - this._startTime : 0;
};
/**
* Bind drag event listeners.
*
* @public
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.on = function (eventName, listener) {
this._emitter.on(eventName, listener);
};
/**
* Unbind drag event listeners.
*
* @public
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.off = function (eventName, listener) {
this._emitter.off(eventName, listener);
};
/**
* Destroy the instance and unbind all drag event listeners.
*
* @public
*/
Dragger.prototype.destroy = function () {
if (this._isDestroyed) return;
var element = this._element;
if (this._edgeHack) this._edgeHack.destroy();
// Reset data and deactivate the instance.
this._reset();
// Destroy emitter.
this._emitter.destroy();
// Unbind event handlers.
element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions);
element.removeEventListener('dragstart', Dragger._preventDefault, false);
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true);
// Reset styles.
for (var prop in this._cssProps) {
element.style[prop] = this._cssProps[prop];
delete this._cssProps[prop];
}
// Reset data.
this._element = null;
// Mark as destroyed.
this._isDestroyed = true;
};
var dt = 1000 / 60;
var raf = (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return this.setTimeout(function () {
callback(Date.now());
}, dt);
}
).bind(window);
/**
* A ticker system for handling DOM reads and writes in an efficient way.
*
* @class
*/
function Ticker(numLanes) {
this._nextStep = null;
this._lanes = [];
this._stepQueue = [];
this._stepCallbacks = {};
this._step = this._step.bind(this);
for (var i = 0; i < numLanes; i++) {
this._lanes.push(new TickerLane());
}
}
Ticker.prototype._step = function (time) {
var lanes = this._lanes;
var stepQueue = this._stepQueue;
var stepCallbacks = this._stepCallbacks;
var i, j, id, laneQueue, laneCallbacks, laneIndices;
this._nextStep = null;
for (i = 0; i < lanes.length; i++) {
laneQueue = lanes[i].queue;
laneCallbacks = lanes[i].callbacks;
laneIndices = lanes[i].indices;
for (j = 0; j < laneQueue.length; j++) {
id = laneQueue[j];
if (!id) continue;
stepQueue.push(id);
stepCallbacks[id] = laneCallbacks[id];
delete laneCallbacks[id];
delete laneIndices[id];
}
laneQueue.length = 0;
}
for (i = 0; i < stepQueue.length; i++) {
id = stepQueue[i];
if (stepCallbacks[id]) stepCallbacks[id](time);
delete stepCallbacks[id];
}
stepQueue.length = 0;
};
Ticker.prototype.add = function (laneIndex, id, callback) {
this._lanes[laneIndex].add(id, callback);
if (!this._nextStep) this._nextStep = raf(this._step);
};
Ticker.prototype.remove = function (laneIndex, id) {
this._lanes[laneIndex].remove(id);
};
/**
* A lane for ticker.
*
* @class
*/
function TickerLane() {
this.queue = [];
this.indices = {};
this.callbacks = {};
}
TickerLane.prototype.add = function (id, callback) {
var index = this.indices[id];
if (index !== undefined) this.queue[index] = undefined;
this.queue.push(id);
this.callbacks[id] = callback;
this.indices[id] = this.queue.length - 1;
};
TickerLane.prototype.remove = function (id) {
var index = this.indices[id];
if (index === undefined) return;
this.queue[index] = undefined;
delete this.callbacks[id];
delete this.indices[id];
};
var LAYOUT_READ = 'layoutRead';
var LAYOUT_WRITE = 'layoutWrite';
var VISIBILITY_READ = 'visibilityRead';
var VISIBILITY_WRITE = 'visibilityWrite';
var DRAG_START_READ = 'dragStartRead';
var DRAG_START_WRITE = 'dragStartWrite';
var DRAG_MOVE_READ = 'dragMoveRead';
var DRAG_MOVE_WRITE = 'dragMoveWrite';
var DRAG_SCROLL_READ = 'dragScrollRead';
var DRAG_SCROLL_WRITE = 'dragScrollWrite';
var DRAG_SORT_READ = 'dragSortRead';
var PLACEHOLDER_LAYOUT_READ = 'placeholderLayoutRead';
var PLACEHOLDER_LAYOUT_WRITE = 'placeholderLayoutWrite';
var PLACEHOLDER_RESIZE_WRITE = 'placeholderResizeWrite';
var AUTO_SCROLL_READ = 'autoScrollRead';
var AUTO_SCROLL_WRITE = 'autoScrollWrite';
var DEBOUNCE_READ = 'debounceRead';
var LANE_READ = 0;
var LANE_READ_TAIL = 1;
var LANE_WRITE = 2;
var ticker = new Ticker(3);
function addLayoutTick(itemId, read, write) {
ticker.add(LANE_READ, LAYOUT_READ + itemId, read);
ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write);
}
function cancelLayoutTick(itemId) {
ticker.remove(LANE_READ, LAYOUT_READ + itemId);
ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId);
}
function addVisibilityTick(itemId, read, write) {
ticker.add(LANE_READ, VISIBILITY_READ + itemId, read);
ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write);
}
function cancelVisibilityTick(itemId) {
ticker.remove(LANE_READ, VISIBILITY_READ + itemId);
ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId);
}
function addDragStartTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_START_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write);
}
function cancelDragStartTick(itemId) {
ticker.remove(LANE_READ, DRAG_START_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId);
}
function addDragMoveTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write);
}
function cancelDragMoveTick(itemId) {
ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId);
}
function addDragScrollTick(itemId, read, write) {
ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read);
ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write);
}
function cancelDragScrollTick(itemId) {
ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId);
ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId);
}
function addDragSortTick(itemId, read) {
ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read);
}
function cancelDragSortTick(itemId) {
ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId);
}
function addPlaceholderLayoutTick(itemId, read, write) {
ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read);
ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write);
}
function cancelPlaceholderLayoutTick(itemId) {
ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId);
ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId);
}
function addPlaceholderResizeTick(itemId, write) {
ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write);
}
function cancelPlaceholderResizeTick(itemId) {
ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId);
}
function addAutoScrollTick(read, write) {
ticker.add(LANE_READ, AUTO_SCROLL_READ, read);
ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write);
}
function cancelAutoScrollTick() {
ticker.remove(LANE_READ, AUTO_SCROLL_READ);
ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE);
}
function addDebounceTick(debounceId, read) {
ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read);
}
function cancelDebounceTick(debounceId) {
ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId);
}
var AXIS_X = 1;
var AXIS_Y = 2;
var FORWARD = 4;
var BACKWARD = 8;
var LEFT = AXIS_X | BACKWARD;
var RIGHT = AXIS_X | FORWARD;
var UP = AXIS_Y | BACKWARD;
var DOWN = AXIS_Y | FORWARD;
var functionType = 'function';
/**
* Check if a value is a function.
*
* @param {*} val
* @returns {Boolean}
*/
function isFunction(val) {
return typeof val === functionType;
}
var cache$1 = typeof WeakMap === 'function' ? new WeakMap() : null;
/**
* Returns the computed value of an element's style property as a string.
*
* @param {HTMLElement} element
* @param {String} style
* @returns {String}
*/
function getStyle(element, style) {
var styles = cache$1 && cache$1.get(element);
if (!styles) {
styles = window.getComputedStyle(element, null);
if (cache$1) cache$1.set(element, styles);
}
return styles.getPropertyValue(style);
}
/**
* Returns the computed value of an element's style property transformed into
* a float value.
*
* @param {HTMLElement} el
* @param {String} style
* @returns {Number}
*/
function getStyleAsFloat(el, style) {
return parseFloat(getStyle(el, style)) || 0;
}
var DOC_ELEM = document.documentElement;
var BODY = document.body;
var THRESHOLD_DATA = { value: 0, offset: 0 };
/**
* @param {HTMLElement|Window} element
* @returns {HTMLElement|Window}
*/
function getScrollElement(element) {
if (element === window || element === DOC_ELEM || element === BODY) {
return window;
} else {
return element;
}
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
function getScrollLeft(element) {
return element === window ? element.pageXOffset : element.scrollLeft;
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
function getScrollTop(element) {
return element === window ? element.pageYOffset : element.scrollTop;
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
function getScrollLeftMax(element) {
if (element === window) {
return DOC_ELEM.scrollWidth - DOC_ELEM.clientWidth;
} else {
return element.scrollWidth - element.clientWidth;
}
}
/**
* @param {HTMLElement|Window} element
* @returns {Number}
*/
function getScrollTopMax(element) {
if (element === window) {
return DOC_ELEM.scrollHeight - DOC_ELEM.clientHeight;
} else {
return element.scrollHeight - element.clientHeight;
}
}
/**
* Get window's or element's client rectangle data relative to the element's
* content dimensions (includes inner size + padding, excludes scrollbars,
* borders and margins).
*
* @param {HTMLElement|Window} element
* @returns {Rectangle}
*/
function getContentRect(element, result) {
result = result || {};
if (element === window) {
result.width = DOC_ELEM.clientWidth;
result.height = DOC_ELEM.clientHeight;
result.left = 0;
result.right = result.width;
result.top = 0;
result.bottom = result.height;
} else {
var bcr = element.getBoundingClientRect();
var borderLeft = element.clientLeft || getStyleAsFloat(element, 'border-left-width');
var borderTop = element.clientTop || getStyleAsFloat(element, 'border-top-width');
result.width = element.clientWidth;
result.height = element.clientHeight;
result.left = bcr.left + borderLeft;
result.right = result.left + result.width;
result.top = bcr.top + borderTop;
result.bottom = result.top + result.height;
}
return result;
}
/**
* @param {Item} item
* @returns {Object}
*/
function getItemAutoScrollSettings(item) {
return item._drag._getGrid()._settings.dragAutoScroll;
}
/**
* @param {Item} item
*/
function prepareItemScrollSync(item) {
if (!item._drag) return;
item._drag._prepareScroll();
}
/**
* @param {Item} item
*/
function applyItemScrollSync(item) {
if (!item._drag || !item._isActive) return;
var drag = item._drag;
drag._scrollDiffX = drag._scrollDiffY = 0;
item._setTranslate(drag._left, drag._top);
}
/**
* Compute threshold value and edge offset.
*
* @param {Number} threshold
* @param {Number} safeZone
* @param {Number} itemSize
* @param {Number} targetSize
* @returns {Object}
*/
function computeThreshold(threshold, safeZone, itemSize, targetSize) {
THRESHOLD_DATA.value = Math.min(targetSize / 2, threshold);
THRESHOLD_DATA.offset =
Math.max(0, itemSize + THRESHOLD_DATA.value * 2 + targetSize * safeZone - targetSize) / 2;
return THRESHOLD_DATA;
}
function ScrollRequest() {
this.reset();
}
ScrollRequest.prototype.reset = function () {
if (this.isActive) this.onStop();
this.item = null;
this.element = null;
this.isActive = false;
this.isEnding = false;
this.direction = null;
this.value = null;
this.maxValue = 0;
this.threshold = 0;
this.distance = 0;
this.speed = 0;
this.duration = 0;
this.action = null;
};
ScrollRequest.prototype.hasReachedEnd = function () {
return FORWARD & this.direction ? this.value >= this.maxValue : this.value <= 0;
};
ScrollRequest.prototype.computeCurrentScrollValue = function () {
if (this.value === null) {
return AXIS_X & this.direction ? getScrollLeft(this.element) : getScrollTop(this.element);
}
return Math.max(0, Math.min(this.value, this.maxValue));
};
ScrollRequest.prototype.computeNextScrollValue = function (deltaTime) {
var delta = this.speed * (deltaTime / 1000);
var nextValue = FORWARD & this.direction ? this.value + delta : this.value - delta;
return Math.max(0, Math.min(nextValue, this.maxValue));
};
ScrollRequest.prototype.computeSpeed = (function () {
var data = {
direction: null,
threshold: 0,
distance: 0,
value: 0,
maxValue: 0,
deltaTime: 0,
duration: 0,
isEnding: false,
};
return function (deltaTime) {
var item = this.item;
var speed = getItemAutoScrollSettings(item).speed;
if (isFunction(speed)) {
data.direction = this.direction;
data.threshold = this.threshold;
data.distance = this.distance;
data.value = this.value;
data.maxValue = this.maxValue;
data.duration = this.duration;
data.speed = this.speed;
data.deltaTime = deltaTime;
data.isEnding = this.isEnding;
return speed(item, this.element, data);
} else {
return speed;
}
};
})();
ScrollRequest.prototype.tick = function (deltaTime) {
if (!this.isActive) {
this.isActive = true;
this.onStart();
}
this.value = this.computeCurrentScrollValue();
this.speed = this.computeSpeed(deltaTime);
this.value = this.computeNextScrollValue(deltaTime);
this.duration += deltaTime;
return this.value;
};
ScrollRequest.prototype.onStart = function () {
var item = this.item;
var onStart = getItemAutoScrollSettings(item).onStart;
if (isFunction(onStart)) onStart(item, this.element, this.direction);
};
ScrollRequest.prototype.onStop = function () {
var item = this.item;
var onStop = getItemAutoScrollSettings(item).onStop;
if (isFunction(onStop)) onStop(item, this.element, this.direction);
// Manually nudge sort to happen. There's a good chance that the item is still
// after the scroll stops which means that the next sort will be triggered
// only after the item is moved or it's parent scrolled.
if (item._drag) item._drag.sort();
};
function ScrollAction() {
this.element = null;
this.requestX = null;
this.requestY = null;
this.scrollLeft = 0;
this.scrollTop = 0;
}
ScrollAction.prototype.reset = function () {
if (this.requestX) this.requestX.action = null;
if (this.requestY) this.requestY.action = null;
this.element = null;
this.requestX = null;
this.requestY = null;
this.scrollLeft = 0;
this.scrollTop = 0;
};
ScrollAction.prototype.addRequest = function (request) {
if (AXIS_X & request.direction) {
this.removeRequest(this.requestX);
this.requestX = request;
} else {
this.removeRequest(this.requestY);
this.requestY = request;
}
request.action = this;
};
ScrollAction.prototype.removeRequest = function (request) {
if (!request) return;
if (this.requestX === request) {
this.requestX = null;
request.action = null;
} else if (this.requestY === request) {
this.requestY = null;
request.action = null;
}
};
ScrollAction.prototype.computeScrollValues = function () {
this.scrollLeft = this.requestX ? this.requestX.value : getScrollLeft(this.element);
this.scrollTop = this.requestY ? this.requestY.value : getScrollTop(this.element);
};
ScrollAction.prototype.scroll = function () {
var element = this.element;
if (!element) return;
if (element.scrollTo) {
element.scrollTo(this.scrollLeft, this.scrollTop);
} else {
element.scrollLeft = this.scrollLeft;
element.scrollTop = this.scrollTop;
}
};
function Pool(createItem, releaseItem) {
this.pool = [];
this.createItem = createItem;
this.releaseItem = releaseItem;
}
Pool.prototype.pick = function () {
return this.pool.pop() || this.createItem();
};
Pool.prototype.release = function (item) {
this.releaseItem(item);
if (this.pool.indexOf(item) !== -1) return;
this.pool.push(item);
};
Pool.prototype.reset = function () {
this.pool.length = 0;
};
/**
* Check if two rectangles are overlapping.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
function isOverlapping(a, b) {
return !(
a.left + a.width <= b.left ||
b.left + b.width <= a.left ||
a.top + a.height <= b.top ||
b.top + b.height <= a.top
);
}
/**
* Calculate intersection area between two rectangle.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
function getIntersectionArea(a, b) {
if (!isOverlapping(a, b)) return 0;
var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left);
var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
return width * height;
}
/**
* Calculate how many percent the intersection area of two rectangles is from
* the maximum potential intersection area between the rectangles.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
function getIntersectionScore(a, b) {
var area = getIntersectionArea(a, b);
if (!area) return 0;
var maxArea = Math.min(a.width, b.width) * Math.min(a.height, b.height);
return (area / maxArea) * 100;
}
var RECT_1 = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
};
var RECT_2 = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
};
function AutoScroller() {
this._isDestroyed = false;
this._isTicking = false;
this._tickTime = 0;
this._tickDeltaTime = 0;
this._items = [];
this._actions = [];
this._requests = {};
this._requests[AXIS_X] = {};
this._requests[AXIS_Y] = {};
this._requestOverlapCheck = {};
this._dragPositions = {};
this._dragDirections = {};
this._overlapCheckInterval = 150;
this._requestPool = new Pool(
function () {
return new ScrollRequest();
},
function (request) {
request.reset();
}
);
this._actionPool = new Pool(
function () {
return new ScrollAction();
},
function (action) {
action.reset();
}
);
this._readTick = this._readTick.bind(this);
this._writeTick = this._writeTick.bind(this);
}
AutoScroller.AXIS_X = AXIS_X;
AutoScroller.AXIS_Y = AXIS_Y;
AutoScroller.FORWARD = FORWARD;
AutoScroller.BACKWARD = BACKWARD;
AutoScroller.LEFT = LEFT;
AutoScroller.RIGHT = RIGHT;
AutoScroller.UP = UP;
AutoScroller.DOWN = DOWN;
AutoScroller.smoothSpeed = function (maxSpeed, acceleration, deceleration) {
return function (item, element, data) {
var targetSpeed = 0;
if (!data.isEnding) {
if (data.threshold > 0) {
var factor = data.threshold - Math.max(0, data.distance);
targetSpeed = (maxSpeed / data.threshold) * factor;
} else {
targetSpeed = maxSpeed;
}
}
var currentSpeed = data.speed;
var nextSpeed = targetSpeed;
if (currentSpeed === targetSpeed) {
return nextSpeed;
}
if (currentSpeed < targetSpeed) {
nextSpeed = currentSpeed + acceleration * (data.deltaTime / 1000);
return Math.min(targetSpeed, nextSpeed);
} else {
nextSpeed = currentSpeed - deceleration * (data.deltaTime / 1000);
return Math.max(targetSpeed, nextSpeed);
}
};
};
AutoScroller.pointerHandle = function (pointerSize) {
var rect = { left: 0, top: 0, width: 0, height: 0 };
var size = pointerSize || 1;
return function (item, x, y, w, h, pX, pY) {
rect.left = pX - size * 0.5;
rect.top = pY - size * 0.5;
rect.width = size;
rect.height = size;
return rect;
};
};
AutoScroller.prototype._readTick = function (time) {
if (this._isDestroyed) return;
if (time && this._tickTime) {
this._tickDeltaTime = time - this._tickTime;
this._tickTime = time;
this._updateRequests();
this._updateActions();
} else {
this._tickTime = time;
this._tickDeltaTime = 0;
}
};
AutoScroller.prototype._writeTick = function () {
if (this._isDestroyed) return;
this._applyActions();
addAutoScrollTick(this._readTick, this._writeTick);
};
AutoScroller.prototype._startTicking = function () {
this._isTicking = true;
addAutoScrollTick(this._readTick, this._writeTick);
};
AutoScroller.prototype._stopTicking = function () {
this._isTicking = false;
this._tickTime = 0;
this._tickDeltaTime = 0;
cancelAutoScrollTick();
};
AutoScroller.prototype._getItemHandleRect = function (item, handle, rect) {
var itemDrag = item._drag;
if (handle) {
var ev = itemDrag._dragMoveEvent || itemDrag._dragStartEvent;
var data = handle(
item,
itemDrag._clientX,
itemDrag._clientY,
item._width,
item._height,
ev.clientX,
ev.clientY
);
rect.left = data.left;
rect.top = data.top;
rect.width = data.width;
rect.height = data.height;
} else {
rect.left = itemDrag._clientX;
rect.top = itemDrag._clientY;
rect.width = item._width;
rect.height = item._height;
}
rect.right = rect.left + rect.width;
rect.bottom = rect.top + rect.height;
return rect;
};
AutoScroller.prototype._requestItemScroll = function (
item,
axis,
element,
direction,
threshold,
distance,
maxValue
) {
var reqMap = this._requests[axis];
var request = reqMap[ite