Comparing version 0.8.0 to 0.9.0
{ | ||
"name": "muuri", | ||
"version": "0.8.0", | ||
"description": "Responsive, sortable, filterable and draggable grid layouts.", | ||
"version": "0.9.0", | ||
"description": "Responsive, sortable, filterable and draggable layouts", | ||
"keywords": [ | ||
@@ -13,3 +13,3 @@ "grid", | ||
], | ||
"homepage": "https://github.com/haltu/muuri", | ||
"homepage": "https://muuri.dev/", | ||
"license": "MIT", | ||
@@ -26,2 +26,13 @@ "author": { | ||
"main": "dist/muuri.js", | ||
"module": "dist/muuri.module.js", | ||
"types": "src/index.d.ts", | ||
"files": [ | ||
"package.json", | ||
"src", | ||
"dist", | ||
"README.md", | ||
"LICENSE.md", | ||
"AUTHORS.txt", | ||
"CONTRIBUTING.md" | ||
], | ||
"scripts": { | ||
@@ -38,3 +49,4 @@ "test": "./node_modules/.bin/gulp test", | ||
"format": "./node_modules/.bin/prettier --write ./src/**/*.js", | ||
"format-test": "./node_modules/.bin/prettier --list-different ./src/**/*.js", | ||
"format-tests": "./node_modules/.bin/prettier --write ./tests/**/*.js", | ||
"validate-formatting": "./node_modules/.bin/prettier --list-different ./src/**/*.js", | ||
"size": "./node_modules/.bin/gulp size", | ||
@@ -49,25 +61,24 @@ "pre-commit-hook": "./node_modules/.bin/gulp pre-commit" | ||
"devDependencies": { | ||
"dotenv": "^8.0.0", | ||
"dotenv": "^8.2.0", | ||
"gulp": "^4.0.2", | ||
"gulp-eslint": "^6.0.0", | ||
"gulp-size": "^3.0.0", | ||
"husky": "^1.3.1", | ||
"karma": "^4.1.0", | ||
"karma-chrome-launcher": "^2.2.0", | ||
"husky": "^4.2.5", | ||
"karma": "^5.0.9", | ||
"karma-chrome-launcher": "^3.1.0", | ||
"karma-edge-launcher": "^0.4.2", | ||
"karma-firefox-launcher": "^1.1.0", | ||
"karma-qunit": "^3.1.2", | ||
"karma-firefox-launcher": "^1.3.0", | ||
"karma-qunit": "^4.1.1", | ||
"karma-safari-launcher": "^1.0.0", | ||
"karma-sauce-launcher": "^2.0.2", | ||
"karma-sauce-launcher": "^4.1.5", | ||
"karma-story-reporter": "^0.3.1", | ||
"mezr": "^0.6.2", | ||
"prettier": "^1.18.2", | ||
"prettier": "^2.0.5", | ||
"prosthetic-hand": "^1.3.1", | ||
"qunit": "^2.9.2", | ||
"rimraf": "^2.6.3", | ||
"rollup": "^1.16.7", | ||
"terser": "^4.1.2", | ||
"web-animations-js": "^2.3.2", | ||
"yargs": "^13.2.4" | ||
"qunit": "^2.10.0", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.15.0", | ||
"terser": "^4.7.0", | ||
"web-animations-js": "^2.3.2" | ||
} | ||
} |
@@ -8,36 +8,20 @@ /** | ||
import { HAS_TOUCH_EVENTS, HAS_POINTER_EVENTS, HAS_MS_POINTER_EVENTS } from '../constants'; | ||
import Emitter from '../Emitter/Emitter'; | ||
import EdgeHack from './EdgeHack'; | ||
import getPrefixedPropName from '../utils/getPrefixedPropName'; | ||
import raf from '../utils/raf'; | ||
import hasPassiveEvents from '../utils/hasPassiveEvents'; | ||
// Detect support for passive events: | ||
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection | ||
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) {} | ||
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; | ||
// Dragger events. | ||
export var events = { | ||
start: 'start', | ||
move: 'move', | ||
end: 'end', | ||
cancel: 'cancel' | ||
}; | ||
var listenerOptions = hasPassiveEvents() ? { passive: true } : false; | ||
var hasTouchEvents = !!('ontouchstart' in window || window.TouchEvent); | ||
var hasPointerEvents = !!window.PointerEvent; | ||
var hasMsPointerEvents = !!window.navigator.msPointerEnabled; | ||
var isAndroid = /(android)/i.test(window.navigator.userAgent); | ||
var listenerOptions = isPassiveEventsSupported ? { passive: true } : false; | ||
var taProp = 'touchAction'; | ||
var taPropPrefixed = getPrefixedPropName(window.document.documentElement.style, taProp); | ||
var taPropPrefixed = getPrefixedPropName(document.documentElement.style, taProp); | ||
var taDefaultValue = 'auto'; | ||
@@ -59,3 +43,3 @@ | ||
this._touchAction = ''; | ||
this._startEvent = null; | ||
this._isActive = false; | ||
@@ -69,4 +53,2 @@ this._pointerId = null; | ||
this._preStartCheck = this._preStartCheck.bind(this); | ||
this._abortNonCancelable = this._abortNonCancelable.bind(this); | ||
this._onStart = this._onStart.bind(this); | ||
@@ -77,6 +59,12 @@ this._onMove = this._onMove.bind(this); | ||
// Apply initial css props. | ||
// 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 | ||
// If touch action was not provided with initial CSS props let's assume it's | ||
// auto. | ||
@@ -87,14 +75,7 @@ if (!this._touchAction) { | ||
// Prevent native link/image dragging for the item and it's ancestors. | ||
// 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._events.start, this._preStartCheck, listenerOptions); | ||
// If we have touch events, but no pointer events we need to also listen for | ||
// mouse events in addition to touch events for devices which support both | ||
// mouse and touch interaction. | ||
if (hasTouchEvents && !hasPointerEvents && !hasMsPointerEvents) { | ||
element.addEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); | ||
} | ||
element.addEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); | ||
} | ||
@@ -111,3 +92,3 @@ | ||
cancel: 'pointercancel', | ||
end: 'pointerup' | ||
end: 'pointerup', | ||
}; | ||
@@ -119,3 +100,3 @@ | ||
cancel: 'MSPointerCancel', | ||
end: 'MSPointerUp' | ||
end: 'MSPointerUp', | ||
}; | ||
@@ -127,3 +108,3 @@ | ||
cancel: 'touchcancel', | ||
end: 'touchend' | ||
end: 'touchend', | ||
}; | ||
@@ -135,9 +116,9 @@ | ||
cancel: '', | ||
end: 'mouseup' | ||
end: 'mouseup', | ||
}; | ||
Dragger._events = (function() { | ||
if (hasPointerEvents) return Dragger._pointerEvents; | ||
if (hasMsPointerEvents) return Dragger._msPointerEvents; | ||
if (hasTouchEvents) return Dragger._touchEvents; | ||
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; | ||
@@ -148,2 +129,9 @@ })(); | ||
Dragger._emitterEvents = { | ||
start: 'start', | ||
move: 'move', | ||
end: 'end', | ||
cancel: 'cancel', | ||
}; | ||
Dragger._activeInstances = []; | ||
@@ -156,7 +144,7 @@ | ||
Dragger._preventDefault = function(e) { | ||
Dragger._preventDefault = function (e) { | ||
if (e.preventDefault && e.cancelable !== false) e.preventDefault(); | ||
}; | ||
Dragger._activateInstance = function(instance) { | ||
Dragger._activateInstance = function (instance) { | ||
var index = Dragger._activeInstances.indexOf(instance); | ||
@@ -166,5 +154,5 @@ if (index > -1) return; | ||
Dragger._activeInstances.push(instance); | ||
Dragger._emitter.on(events.move, instance._onMove); | ||
Dragger._emitter.on(events.cancel, instance._onCancel); | ||
Dragger._emitter.on(events.end, instance._onEnd); | ||
Dragger._emitter.on(Dragger._emitterEvents.move, instance._onMove); | ||
Dragger._emitter.on(Dragger._emitterEvents.cancel, instance._onCancel); | ||
Dragger._emitter.on(Dragger._emitterEvents.end, instance._onEnd); | ||
@@ -176,3 +164,3 @@ if (Dragger._activeInstances.length === 1) { | ||
Dragger._deactivateInstance = function(instance) { | ||
Dragger._deactivateInstance = function (instance) { | ||
var index = Dragger._activeInstances.indexOf(instance); | ||
@@ -182,5 +170,5 @@ if (index === -1) return; | ||
Dragger._activeInstances.splice(index, 1); | ||
Dragger._emitter.off(events.move, instance._onMove); | ||
Dragger._emitter.off(events.cancel, instance._onCancel); | ||
Dragger._emitter.off(events.end, instance._onEnd); | ||
Dragger._emitter.off(Dragger._emitterEvents.move, instance._onMove); | ||
Dragger._emitter.off(Dragger._emitterEvents.cancel, instance._onCancel); | ||
Dragger._emitter.off(Dragger._emitterEvents.end, instance._onEnd); | ||
@@ -192,17 +180,19 @@ if (!Dragger._activeInstances.length) { | ||
Dragger._bindListeners = function() { | ||
var events = Dragger._events; | ||
window.addEventListener(events.move, Dragger._onMove, listenerOptions); | ||
window.addEventListener(events.end, Dragger._onEnd, listenerOptions); | ||
events.cancel && window.addEventListener(events.cancel, Dragger._onCancel, listenerOptions); | ||
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() { | ||
var events = Dragger._events; | ||
window.removeEventListener(events.move, Dragger._onMove, listenerOptions); | ||
window.removeEventListener(events.end, Dragger._onEnd, listenerOptions); | ||
events.cancel && window.removeEventListener(events.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) { | ||
Dragger._getEventPointerId = function (event) { | ||
// If we have pointer id available let's use it. | ||
@@ -222,3 +212,3 @@ if (typeof event.pointerId === 'number') { | ||
Dragger._getTouchById = function(event, id) { | ||
Dragger._getTouchById = function (event, id) { | ||
// If we have a pointer event return the whole event if there's a match, and | ||
@@ -246,12 +236,12 @@ // null otherwise. | ||
Dragger._onMove = function(e) { | ||
Dragger._emitter.emit(events.move, e); | ||
Dragger._onMove = function (e) { | ||
Dragger._emitter.emit(Dragger._emitterEvents.move, e); | ||
}; | ||
Dragger._onCancel = function(e) { | ||
Dragger._emitter.emit(events.cancel, e); | ||
Dragger._onCancel = function (e) { | ||
Dragger._emitter.emit(Dragger._emitterEvents.cancel, e); | ||
}; | ||
Dragger._onEnd = function(e) { | ||
Dragger._emitter.emit(events.end, e); | ||
Dragger._onEnd = function (e) { | ||
Dragger._emitter.emit(Dragger._emitterEvents.end, e); | ||
}; | ||
@@ -268,7 +258,4 @@ | ||
* @private | ||
* @memberof Dragger.prototype | ||
*/ | ||
Dragger.prototype._reset = function() { | ||
if (this._isDestroyed) return; | ||
Dragger.prototype._reset = function () { | ||
this._pointerId = null; | ||
@@ -280,10 +267,3 @@ this._startTime = 0; | ||
this._currentY = 0; | ||
this._startEvent = null; | ||
this._element.removeEventListener( | ||
Dragger._touchEvents.start, | ||
this._abortNonCancelable, | ||
listenerOptions | ||
); | ||
this._isActive = false; | ||
Dragger._deactivateInstance(this); | ||
@@ -296,8 +276,7 @@ }; | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {String} type | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
* @returns {DraggerEvent} | ||
* @returns {Object} | ||
*/ | ||
Dragger.prototype._createEvent = function(type, e) { | ||
Dragger.prototype._createEvent = function (type, e) { | ||
var touch = this._getTrackedTouch(e); | ||
@@ -311,5 +290,6 @@ return { | ||
deltaY: this.getDeltaY(), | ||
deltaTime: type === events.start ? 0 : this.getDeltaTime(), | ||
isFirst: type === events.start, | ||
isFinal: type === events.end || type === events.cancel, | ||
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. | ||
@@ -323,3 +303,3 @@ identifier: this._pointerId, | ||
pageY: touch.pageY, | ||
target: touch.target | ||
target: touch.target, | ||
}; | ||
@@ -332,7 +312,6 @@ }; | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {String} type | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._emit = function(type, e) { | ||
Dragger.prototype._emit = function (type, e) { | ||
this._emitter.emit(type, this._createEvent(type, e)); | ||
@@ -350,34 +329,22 @@ }; | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
* @returns {?(Touch|PointerEvent|MouseEvent)} | ||
*/ | ||
Dragger.prototype._getTrackedTouch = function(e) { | ||
if (this._pointerId === null) { | ||
return null; | ||
} else { | ||
return Dragger._getTouchById(e, this._pointerId); | ||
} | ||
Dragger.prototype._getTrackedTouch = function (e) { | ||
if (this._pointerId === null) return null; | ||
return Dragger._getTouchById(e, this._pointerId); | ||
}; | ||
/** | ||
* A pre-handler for start event that checks if we can start dragging. | ||
* Handler for start event. | ||
* | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._preStartCheck = function(e) { | ||
Dragger.prototype._onStart = function (e) { | ||
if (this._isDestroyed) return; | ||
// Make sure the element is not being dragged currently. | ||
if (this.isDragging()) return; | ||
// If pointer id is already assigned let's return early. | ||
if (this._pointerId !== null) return; | ||
// Special cancelable check for Android to prevent drag procedure from | ||
// starting if native scrolling is in progress. Part 1. | ||
if (isAndroid && e.cancelable === false) return; | ||
// Make sure left button is pressed on mouse. | ||
if (e.button) return; | ||
// Get (and set) pointer id. | ||
@@ -387,66 +354,15 @@ this._pointerId = Dragger._getEventPointerId(e); | ||
// Store the start event and trigger start (async or sync). Pointer events | ||
// are emitted before touch events if the browser supports both of them. And | ||
// if you try to move an element before `touchstart` is emitted the pointer | ||
// events for that element will be canceled. The fix is to delay the emitted | ||
// pointer events in such a scenario by one frame so that `touchstart` has | ||
// time to be emitted before the element is (potentially) moved. | ||
this._startEvent = e; | ||
if (hasTouchEvents && (hasPointerEvents || hasMsPointerEvents)) { | ||
// Special cancelable check for Android to prevent drag procedure from | ||
// starting if native scrolling is in progress. Part 2. | ||
if (isAndroid) { | ||
this._element.addEventListener( | ||
Dragger._touchEvents.start, | ||
this._abortNonCancelable, | ||
listenerOptions | ||
); | ||
} | ||
raf(this._onStart); | ||
} else { | ||
this._onStart(); | ||
} | ||
}; | ||
/** | ||
* Abort start event if it turns out to be non-cancelable. | ||
* | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._abortNonCancelable = function(e) { | ||
this._element.removeEventListener( | ||
Dragger._touchEvents.start, | ||
this._abortNonCancelable, | ||
listenerOptions | ||
); | ||
if (this._startEvent && e.cancelable === false) { | ||
this._pointerId = null; | ||
this._startEvent = null; | ||
} | ||
}; | ||
/** | ||
* Start the drag procedure if possible. | ||
* | ||
* @private | ||
* @memberof Dragger.prototype | ||
*/ | ||
Dragger.prototype._onStart = function() { | ||
var e = this._startEvent; | ||
if (!e) return; | ||
this._startEvent = null; | ||
// Setup initial data and emit start event. | ||
var touch = this._getTrackedTouch(e); | ||
if (!touch) return; | ||
// Set up init data and emit start event. | ||
this._startX = this._currentX = touch.clientX; | ||
this._startY = this._currentY = touch.clientY; | ||
this._startTime = Date.now(); | ||
this._emit(events.start, e); | ||
Dragger._activateInstance(this); | ||
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); | ||
} | ||
}; | ||
@@ -458,25 +374,21 @@ | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._onMove = function(e) { | ||
Dragger.prototype._onMove = function (e) { | ||
var touch = this._getTrackedTouch(e); | ||
if (!touch) return; | ||
this._currentX = touch.clientX; | ||
this._currentY = touch.clientY; | ||
this._emit(events.move, e); | ||
this._emit(Dragger._emitterEvents.move, e); | ||
}; | ||
/** | ||
* Handler for move cancel event. | ||
* Handler for cancel event. | ||
* | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._onCancel = function(e) { | ||
Dragger.prototype._onCancel = function (e) { | ||
if (!this._getTrackedTouch(e)) return; | ||
this._emit(events.cancel, e); | ||
this._emit(Dragger._emitterEvents.cancel, e); | ||
this._reset(); | ||
@@ -489,9 +401,7 @@ }; | ||
* @private | ||
* @memberof Dragger.prototype | ||
* @param {(PointerEvent|TouchEvent|MouseEvent)} e | ||
*/ | ||
Dragger.prototype._onEnd = function(e) { | ||
Dragger.prototype._onEnd = function (e) { | ||
if (!this._getTrackedTouch(e)) return; | ||
this._emit(events.end, e); | ||
this._emit(Dragger._emitterEvents.end, e); | ||
this._reset(); | ||
@@ -509,7 +419,6 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Dragger.prototype.isDragging = function() { | ||
return this._pointerId !== null; | ||
Dragger.prototype.isActive = function () { | ||
return this._isActive; | ||
}; | ||
@@ -521,6 +430,5 @@ | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @param {String} value | ||
*/ | ||
Dragger.prototype.setTouchAction = function(value) { | ||
Dragger.prototype.setTouchAction = function (value) { | ||
// Store unmodified touch action value (we trust user input here). | ||
@@ -539,7 +447,9 @@ this._touchAction = value; | ||
// unsupported touch action behavior with custom heuristics which sounds like | ||
// a can of worms. | ||
if (hasTouchEvents) { | ||
this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); | ||
if (this._element.style[taPropPrefixed] !== value) { | ||
this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); | ||
// 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); | ||
} | ||
@@ -554,6 +464,5 @@ } | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @param {Object} [newProps] | ||
*/ | ||
Dragger.prototype.setCssProps = function(newProps) { | ||
Dragger.prototype.setCssProps = function (newProps) { | ||
if (!newProps) return; | ||
@@ -598,6 +507,5 @@ | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @returns {Number} | ||
*/ | ||
Dragger.prototype.getDeltaX = function() { | ||
Dragger.prototype.getDeltaX = function () { | ||
return this._currentX - this._startX; | ||
@@ -611,6 +519,5 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @returns {Number} | ||
*/ | ||
Dragger.prototype.getDeltaY = function() { | ||
Dragger.prototype.getDeltaY = function () { | ||
return this._currentY - this._startY; | ||
@@ -623,6 +530,5 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @returns {Number} | ||
*/ | ||
Dragger.prototype.getDistance = function() { | ||
Dragger.prototype.getDistance = function () { | ||
var x = this.getDeltaX(); | ||
@@ -637,6 +543,5 @@ var y = this.getDeltaY(); | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @returns {Number} | ||
*/ | ||
Dragger.prototype.getDeltaTime = function() { | ||
Dragger.prototype.getDeltaTime = function () { | ||
return this._startTime ? Date.now() - this._startTime : 0; | ||
@@ -649,3 +554,2 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @param {String} eventName | ||
@@ -655,3 +559,3 @@ * - 'start', 'move', 'cancel' or 'end'. | ||
*/ | ||
Dragger.prototype.on = function(eventName, listener) { | ||
Dragger.prototype.on = function (eventName, listener) { | ||
this._emitter.on(eventName, listener); | ||
@@ -664,3 +568,2 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
* @param {String} eventName | ||
@@ -670,3 +573,3 @@ * - 'start', 'move', 'cancel' or 'end'. | ||
*/ | ||
Dragger.prototype.off = function(events, listener) { | ||
Dragger.prototype.off = function (eventName, listener) { | ||
this._emitter.off(eventName, listener); | ||
@@ -679,10 +582,10 @@ }; | ||
* @public | ||
* @memberof Dragger.prototype | ||
*/ | ||
Dragger.prototype.destroy = function() { | ||
Dragger.prototype.destroy = function () { | ||
if (this._isDestroyed) return; | ||
var element = this._element; | ||
var events = Dragger._events; | ||
if (this._edgeHack) this._edgeHack.destroy(); | ||
// Reset data and deactivate the instance. | ||
@@ -695,6 +598,5 @@ this._reset(); | ||
// Unbind event handlers. | ||
element.removeEventListener(events.start, this._preStartCheck, listenerOptions); | ||
element.removeEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions); | ||
element.removeEventListener(Dragger._inputEvents.start, this._onStart, listenerOptions); | ||
element.removeEventListener('dragstart', Dragger._preventDefault, false); | ||
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false); | ||
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, true); | ||
@@ -701,0 +603,0 @@ // Reset styles. |
@@ -17,3 +17,3 @@ /** | ||
this._counter = 0; | ||
this._isDestroyed = false; | ||
this._clearOnEmit = false; | ||
} | ||
@@ -30,3 +30,2 @@ | ||
* @public | ||
* @memberof Emitter.prototype | ||
* @param {String} event | ||
@@ -36,4 +35,4 @@ * @param {Function} listener | ||
*/ | ||
Emitter.prototype.on = function(event, listener) { | ||
if (this._isDestroyed) return this; | ||
Emitter.prototype.on = function (event, listener) { | ||
if (!this._events || !event || !listener) return this; | ||
@@ -54,9 +53,8 @@ // Get listeners queue and create it if it does not exist. | ||
* @public | ||
* @memberof Emitter.prototype | ||
* @param {String} event | ||
* @param {Function} [listener] | ||
* @param {Function} listener | ||
* @returns {Emitter} | ||
*/ | ||
Emitter.prototype.off = function(event, listener) { | ||
if (this._isDestroyed) return this; | ||
Emitter.prototype.off = function (event, listener) { | ||
if (!this._events || !event || !listener) return this; | ||
@@ -67,12 +65,25 @@ // Get listeners and return immediately if none is found. | ||
// If no specific listener is provided remove all listeners. | ||
if (!listener) { | ||
listeners.length = 0; | ||
return this; | ||
// Remove all matching listeners. | ||
var index; | ||
while ((index = listeners.indexOf(listener)) !== -1) { | ||
listeners.splice(index, 1); | ||
} | ||
// Remove all matching listeners. | ||
var i = listeners.length; | ||
while (i--) { | ||
if (listener === listeners[i]) listeners.splice(i, 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]; | ||
} | ||
@@ -87,21 +98,32 @@ | ||
* @public | ||
* @memberof Emitter.prototype | ||
* @param {String} event | ||
* @param {*} [arg1] | ||
* @param {*} [arg2] | ||
* @param {*} [arg3] | ||
* @param {...*} [args] | ||
* @returns {Emitter} | ||
*/ | ||
Emitter.prototype.emit = function(event, arg1, arg2, arg3) { | ||
if (this._isDestroyed) return this; | ||
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) return this; | ||
if (!listeners || !listeners.length) { | ||
this._clearOnEmit = false; | ||
return this; | ||
} | ||
var queue = this._queue; | ||
var qLength = queue.length; | ||
var aLength = arguments.length - 1; | ||
var i; | ||
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. | ||
@@ -111,4 +133,8 @@ // This is necessary to guarantee that all of the listeners are called in | ||
// processing and/or events are emitted during processing. | ||
for (i = 0; i < listeners.length; i++) { | ||
queue.push(listeners[i]); | ||
queue.push.apply(queue, listeners); | ||
// Reset the event's listeners if need be. | ||
if (this._clearOnEmit) { | ||
listeners.length = 0; | ||
this._clearOnEmit = false; | ||
} | ||
@@ -123,11 +149,14 @@ | ||
// Process the queue (the specific part of it for this emit). | ||
for (i = qLength, qLength = queue.length; i < qLength; i++) { | ||
var i = startIndex; | ||
var endIndex = queue.length; | ||
for (; i < endIndex; i++) { | ||
// prettier-ignore | ||
aLength === 0 ? queue[i]() : | ||
aLength === 1 ? queue[i](arg1) : | ||
aLength === 2 ? queue[i](arg1, arg2) : | ||
queue[i](arg1, arg2, arg3); | ||
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._isDestroyed) return this; | ||
if (!this._events) return this; | ||
} | ||
@@ -145,28 +174,41 @@ | ||
/** | ||
* Destroy emitter instance. Basically just removes all bound listeners. | ||
* 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 | ||
* @memberof Emitter.prototype | ||
* @param {String} event | ||
* @param {...*} [args] | ||
* @returns {Emitter} | ||
*/ | ||
Emitter.prototype.destroy = function() { | ||
if (this._isDestroyed) return this; | ||
Emitter.prototype.burst = function () { | ||
if (!this._events) return this; | ||
this._clearOnEmit = true; | ||
this.emit.apply(this, arguments); | ||
return this; | ||
}; | ||
var events = this._events; | ||
var event; | ||
/** | ||
* 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; | ||
}; | ||
// Flag as destroyed. | ||
this._isDestroyed = true; | ||
// Reset queue (if queue is currently processing this will also stop that). | ||
/** | ||
* 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; | ||
// Remove all listeners. | ||
for (event in events) { | ||
if (events[event]) { | ||
events[event].length = 0; | ||
events[event] = undefined; | ||
} | ||
} | ||
this._events = null; | ||
return this; | ||
@@ -173,0 +215,0 @@ }; |
1367
src/Grid/Grid.js
@@ -8,24 +8,24 @@ /** | ||
import { | ||
actionMove, | ||
actionSwap, | ||
eventSynchronize, | ||
eventLayoutStart, | ||
eventLayoutEnd, | ||
eventAdd, | ||
eventRemove, | ||
eventShowStart, | ||
eventShowEnd, | ||
eventHideStart, | ||
eventHideEnd, | ||
eventFilter, | ||
eventSort, | ||
eventMove, | ||
eventDestroy, | ||
gridInstances, | ||
namespace | ||
} from '../shared'; | ||
ACTION_MOVE, | ||
ACTION_SWAP, | ||
EVENT_SYNCHRONIZE, | ||
EVENT_LAYOUT_START, | ||
EVENT_LAYOUT_ABORT, | ||
EVENT_LAYOUT_END, | ||
EVENT_ADD, | ||
EVENT_REMOVE, | ||
EVENT_SHOW_START, | ||
EVENT_SHOW_END, | ||
EVENT_HIDE_START, | ||
EVENT_HIDE_END, | ||
EVENT_FILTER, | ||
EVENT_SORT, | ||
EVENT_MOVE, | ||
EVENT_DESTROY, | ||
GRID_INSTANCES, | ||
ITEM_ELEMENT_MAP, | ||
MAX_SAFE_FLOAT32_INTEGER, | ||
} from '../constants'; | ||
import Emitter from '../Emitter/Emitter'; | ||
import Item from '../Item/Item'; | ||
import ItemAnimate from '../Item/ItemAnimate'; | ||
import ItemDrag from '../Item/ItemDrag'; | ||
@@ -35,8 +35,12 @@ import ItemDragPlaceholder from '../Item/ItemDragPlaceholder'; | ||
import ItemMigrate from '../Item/ItemMigrate'; | ||
import ItemRelease from '../Item/ItemRelease'; | ||
import ItemDragRelease from '../Item/ItemDragRelease'; | ||
import ItemVisibility from '../Item/ItemVisibility'; | ||
import Emitter from '../Emitter/Emitter'; | ||
import Animator from '../Animator/Animator'; | ||
import Packer from '../Packer/Packer'; | ||
import Dragger from '../Dragger/Dragger'; | ||
import AutoScroller from '../AutoScroller/AutoScroller'; | ||
import addClass from '../utils/addClass'; | ||
import arrayInsert from '../utils/arrayInsert'; | ||
import arrayMove from '../utils/arrayMove'; | ||
@@ -47,18 +51,18 @@ import arraySwap from '../utils/arraySwap'; | ||
import elementMatches from '../utils/elementMatches'; | ||
import getPrefixedPropName from '../utils/getPrefixedPropName'; | ||
import getStyle from '../utils/getStyle'; | ||
import getStyleAsFloat from '../utils/getStyleAsFloat'; | ||
import arrayInsert from '../utils/arrayInsert'; | ||
import isFunction from '../utils/isFunction'; | ||
import isNodeList from '../utils/isNodeList'; | ||
import isPlainObject from '../utils/isPlainObject'; | ||
import noop from '../utils/noop'; | ||
import removeClass from '../utils/removeClass'; | ||
import setStyles from '../utils/setStyles'; | ||
import toArray from '../utils/toArray'; | ||
var packer = new Packer(); | ||
var noop = function() {}; | ||
var NUMBER_TYPE = 'number'; | ||
var STRING_TYPE = 'string'; | ||
var INSTANT_LAYOUT = 'instant'; | ||
var layoutId = 0; | ||
var numberType = 'number'; | ||
var stringType = 'string'; | ||
var instantLayout = 'instant'; | ||
/** | ||
@@ -70,9 +74,9 @@ * Creates a new Grid instance. | ||
* @param {Object} [options] | ||
* @param {?(HTMLElement[]|NodeList|String)} [options.items] | ||
* @param {(String|HTMLElement[]|NodeList|HTMLCollection)} [options.items="*"] | ||
* @param {Number} [options.showDuration=300] | ||
* @param {String} [options.showEasing="ease"] | ||
* @param {Object} [options.visibleStyles] | ||
* @param {Object} [options.visibleStyles={opacity: "1", transform: "scale(1)"}] | ||
* @param {Number} [options.hideDuration=300] | ||
* @param {String} [options.hideEasing="ease"] | ||
* @param {Object} [options.hiddenStyles] | ||
* @param {Object} [options.hiddenStyles={opacity: "0", transform: "scale(0.5)"}] | ||
* @param {(Function|Object)} [options.layout] | ||
@@ -83,4 +87,4 @@ * @param {Boolean} [options.layout.fillGaps=false] | ||
* @param {Boolean} [options.layout.alignBottom=false] | ||
* @param {Boolean} [options.layout.rounding=true] | ||
* @param {(Boolean|Number)} [options.layoutOnResize=100] | ||
* @param {Boolean} [options.layout.rounding=false] | ||
* @param {(Boolean|Number)} [options.layoutOnResize=150] | ||
* @param {Boolean} [options.layoutOnInit=true] | ||
@@ -91,2 +95,3 @@ * @param {Number} [options.layoutDuration=300] | ||
* @param {Boolean} [options.dragEnabled=false] | ||
* @param {?String} [options.dragHandle=null] | ||
* @param {?HtmlElement} [options.dragContainer=null] | ||
@@ -96,4 +101,3 @@ * @param {?Function} [options.dragStartPredicate] | ||
* @param {Number} [options.dragStartPredicate.delay=0] | ||
* @param {(Boolean|String)} [options.dragStartPredicate.handle=false] | ||
* @param {?String} [options.dragAxis] | ||
* @param {String} [options.dragAxis="xy"] | ||
* @param {(Boolean|Function)} [options.dragSort=true] | ||
@@ -107,12 +111,23 @@ * @param {Object} [options.dragSortHeuristics] | ||
* @param {String} [options.dragSortPredicate.action="move"] | ||
* @param {Number} [options.dragReleaseDuration=300] | ||
* @param {String} [options.dragReleaseEasing="ease"] | ||
* @param {String} [options.dragSortPredicate.migrateAction="move"] | ||
* @param {Object} [options.dragRelease] | ||
* @param {Number} [options.dragRelease.duration=300] | ||
* @param {String} [options.dragRelease.easing="ease"] | ||
* @param {Boolean} [options.dragRelease.useDragContainer=true] | ||
* @param {Object} [options.dragCssProps] | ||
* @param {Object} [options.dragPlaceholder] | ||
* @param {Boolean} [options.dragPlaceholder.enabled=false] | ||
* @param {Number} [options.dragPlaceholder.duration=300] | ||
* @param {String} [options.dragPlaceholder.easing="ease"] | ||
* @param {?Function} [options.dragPlaceholder.createElement=null] | ||
* @param {?Function} [options.dragPlaceholder.onCreate=null] | ||
* @param {?Function} [options.dragPlaceholder.onRemove=null] | ||
* @param {Object} [options.dragAutoScroll] | ||
* @param {(Function|Array)} [options.dragAutoScroll.targets=[]] | ||
* @param {?Function} [options.dragAutoScroll.handle=null] | ||
* @param {Number} [options.dragAutoScroll.threshold=50] | ||
* @param {Number} [options.dragAutoScroll.safeZone=0.2] | ||
* @param {(Function|Number)} [options.dragAutoScroll.speed] | ||
* @param {Boolean} [options.dragAutoScroll.sortDuringScroll=true] | ||
* @param {Boolean} [options.dragAutoScroll.smoothStop=false] | ||
* @param {?Function} [options.dragAutoScroll.onStart=null] | ||
* @param {?Function} [options.dragAutoScroll.onStop=null] | ||
* @param {String} [options.containerClass="muuri"] | ||
@@ -127,13 +142,8 @@ * @param {String} [options.itemClass="muuri-item"] | ||
*/ | ||
function Grid(element, options) { | ||
var inst = this; | ||
var settings; | ||
var items; | ||
var layoutOnResize; | ||
// Allow passing element as selector string | ||
if (typeof element === STRING_TYPE) { | ||
element = document.querySelector(element); | ||
} | ||
// Allow passing element as selector string. Store element for instance. | ||
element = this._element = | ||
typeof element === stringType ? window.document.querySelector(element) : element; | ||
// Throw an error if the container element is not body element or does not | ||
@@ -143,11 +153,11 @@ // exist within the body element. | ||
? element.getRootNode({ composed: true }) === document | ||
: window.document.body.contains(element); | ||
if (!isElementInDom || element === window.document.documentElement) { | ||
throw new Error('Container element must be an existing DOM element'); | ||
: document.body.contains(element); | ||
if (!isElementInDom || element === document.documentElement) { | ||
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. | ||
var settings = mergeSettings(Grid.defaultOptions, options); | ||
settings.visibleStyles = normalizeStyles(settings.visibleStyles); | ||
settings.hiddenStyles = normalizeStyles(settings.hiddenStyles); | ||
if (!isFunction(settings.dragSort)) { | ||
@@ -157,10 +167,7 @@ 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._element = element; | ||
this._settings = settings; | ||
this._isDestroyed = false; | ||
// The layout object (mutated on every layout). | ||
this._items = []; | ||
this._layout = { | ||
@@ -170,44 +177,21 @@ id: 0, | ||
slots: [], | ||
setWidth: false, | ||
setHeight: false, | ||
width: 0, | ||
height: 0 | ||
}; | ||
// Create private Emitter instance. | ||
this._isLayoutFinished = true; | ||
this._nextLayoutData = null; | ||
this._emitter = new Emitter(); | ||
this._onLayoutDataReceived = this._onLayoutDataReceived.bind(this); | ||
// Store grid instance to the grid instances collection. | ||
GRID_INSTANCES[this._id] = this; | ||
// Add container element's class name. | ||
addClass(element, settings.containerClass); | ||
// Create initial items. | ||
this._items = []; | ||
items = settings.items; | ||
if (typeof items === stringType) { | ||
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 !== numberType) { | ||
layoutOnResize = layoutOnResize === true ? 0 : -1; | ||
} | ||
if (layoutOnResize >= 0) { | ||
window.addEventListener( | ||
'resize', | ||
(inst._resizeHandler = debounce(function() { | ||
inst.refreshItems().layout(); | ||
}, layoutOnResize)) | ||
); | ||
} | ||
bindLayoutOnResize(this, settings.layoutOnResize); | ||
// Add initial items. | ||
this.add(getInitialGridElements(element, settings.items), { layout: false }); | ||
// Layout on init if necessary. | ||
@@ -225,2 +209,4 @@ if (settings.layoutOnInit) { | ||
/** | ||
* @public | ||
* @static | ||
* @see Item | ||
@@ -231,2 +217,4 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see ItemLayout | ||
@@ -237,2 +225,4 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see ItemVisibility | ||
@@ -243,2 +233,4 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see ItemMigrate | ||
@@ -249,7 +241,4 @@ */ | ||
/** | ||
* @see ItemAnimate | ||
*/ | ||
Grid.ItemAnimate = ItemAnimate; | ||
/** | ||
* @public | ||
* @static | ||
* @see ItemDrag | ||
@@ -260,7 +249,11 @@ */ | ||
/** | ||
* @see ItemRelease | ||
* @public | ||
* @static | ||
* @see ItemDragRelease | ||
*/ | ||
Grid.ItemRelease = ItemRelease; | ||
Grid.ItemDragRelease = ItemDragRelease; | ||
/** | ||
* @public | ||
* @static | ||
* @see ItemDragPlaceholder | ||
@@ -271,2 +264,4 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see Emitter | ||
@@ -277,2 +272,11 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see Animator | ||
*/ | ||
Grid.Animator = Animator; | ||
/** | ||
* @public | ||
* @static | ||
* @see Dragger | ||
@@ -283,2 +287,4 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see Packer | ||
@@ -289,9 +295,26 @@ */ | ||
/** | ||
* @public | ||
* @static | ||
* @see AutoScroller | ||
*/ | ||
Grid.AutoScroller = AutoScroller; | ||
/** | ||
* The default Packer instance used by default for all layouts. | ||
* | ||
* @public | ||
* @static | ||
* @type {Packer} | ||
*/ | ||
Grid.defaultPacker = new Packer(2); | ||
/** | ||
* Default options for Grid instance. | ||
* | ||
* @public | ||
* @memberof Grid | ||
* @static | ||
* @type {Object} | ||
*/ | ||
Grid.defaultOptions = { | ||
// Item elements | ||
// Initial item elements | ||
items: '*', | ||
@@ -310,7 +333,7 @@ | ||
opacity: '1', | ||
transform: 'scale(1)' | ||
transform: 'scale(1)', | ||
}, | ||
hiddenStyles: { | ||
opacity: '0', | ||
transform: 'scale(0.5)' | ||
transform: 'scale(0.5)', | ||
}, | ||
@@ -324,5 +347,5 @@ | ||
alignBottom: false, | ||
rounding: true | ||
rounding: false, | ||
}, | ||
layoutOnResize: 100, | ||
layoutOnResize: 150, | ||
layoutOnInit: true, | ||
@@ -338,8 +361,8 @@ layoutDuration: 300, | ||
dragContainer: null, | ||
dragHandle: null, | ||
dragStartPredicate: { | ||
distance: 0, | ||
delay: 0, | ||
handle: false | ||
}, | ||
dragAxis: null, | ||
dragAxis: 'xy', | ||
dragSort: true, | ||
@@ -349,10 +372,14 @@ dragSortHeuristics: { | ||
minDragDistance: 10, | ||
minBounceBackAngle: 1 | ||
minBounceBackAngle: 1, | ||
}, | ||
dragSortPredicate: { | ||
threshold: 50, | ||
action: actionMove | ||
action: ACTION_MOVE, | ||
migrateAction: ACTION_MOVE, | ||
}, | ||
dragReleaseDuration: 300, | ||
dragReleaseEasing: 'ease', | ||
dragRelease: { | ||
duration: 300, | ||
easing: 'ease', | ||
useDragContainer: true, | ||
}, | ||
dragCssProps: { | ||
@@ -364,12 +391,21 @@ touchAction: 'none', | ||
touchCallout: 'none', | ||
contentZooming: 'none' | ||
contentZooming: 'none', | ||
}, | ||
dragPlaceholder: { | ||
enabled: false, | ||
duration: 300, | ||
easing: 'ease', | ||
createElement: null, | ||
onCreate: null, | ||
onRemove: null | ||
onRemove: null, | ||
}, | ||
dragAutoScroll: { | ||
targets: [], | ||
handle: null, | ||
threshold: 50, | ||
safeZone: 0.2, | ||
speed: AutoScroller.smoothSpeed(1000, 2000, 2500), | ||
sortDuringScroll: true, | ||
smoothStop: false, | ||
onStart: null, | ||
onStop: null, | ||
}, | ||
@@ -384,3 +420,3 @@ // Classnames | ||
itemReleasingClass: 'muuri-item-releasing', | ||
itemPlaceholderClass: 'muuri-item-placeholder' | ||
itemPlaceholderClass: 'muuri-item-placeholder', | ||
}; | ||
@@ -397,3 +433,2 @@ | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {String} event | ||
@@ -403,3 +438,3 @@ * @param {Function} listener | ||
*/ | ||
Grid.prototype.on = function(event, listener) { | ||
Grid.prototype.on = function (event, listener) { | ||
this._emitter.on(event, listener); | ||
@@ -413,3 +448,2 @@ return this; | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {String} event | ||
@@ -419,3 +453,3 @@ * @param {Function} listener | ||
*/ | ||
Grid.prototype.off = function(event, listener) { | ||
Grid.prototype.off = function (event, listener) { | ||
this._emitter.off(event, listener); | ||
@@ -429,6 +463,5 @@ return this; | ||
* @public | ||
* @memberof Grid.prototype | ||
* @returns {HTMLElement} | ||
*/ | ||
Grid.prototype.getElement = function() { | ||
Grid.prototype.getElement = function () { | ||
return this._element; | ||
@@ -438,51 +471,126 @@ }; | ||
/** | ||
* 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. | ||
* 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 | ||
* @param {(HtmlElement|Number|Item)} [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_TYPE) { | ||
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. | ||
if (ITEM_ELEMENT_MAP) { | ||
var item = ITEM_ELEMENT_MAP.get(target); | ||
return item && item._gridId === this._id ? item : null; | ||
} else { | ||
for (var i = 0; i < this._items.length; i++) { | ||
if (this._items[i]._element === target) { | ||
return this._items[i]; | ||
} | ||
} | ||
} | ||
return null; | ||
}; | ||
/** | ||
* Get all items. Optionally you can provide specific targets (elements, | ||
* indices and item instances). All items that are not found are omitted from | ||
* the returned array. | ||
* | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} [targets] | ||
* @param {(HtmlElement|Number|Item|Array)} [targets] | ||
* @returns {Item[]} | ||
*/ | ||
Grid.prototype.getItems = function(targets) { | ||
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)) { | ||
if (this._isDestroyed || targets === undefined) { | ||
return this._items.slice(0); | ||
} | ||
var ret = []; | ||
var targetItems = toArray(targets); | ||
var item; | ||
var i; | ||
var items = []; | ||
var i, item; | ||
// If target items are defined return filtered results. | ||
for (i = 0; i < targetItems.length; i++) { | ||
item = this._getItem(targetItems[i]); | ||
item && ret.push(item); | ||
if (Array.isArray(targets) || isNodeList(targets)) { | ||
for (i = 0; i < targets.length; i++) { | ||
item = this.getItem(targets[i]); | ||
if (item) items.push(item); | ||
} | ||
} else { | ||
item = this.getItem(targets); | ||
if (item) items.push(item); | ||
} | ||
return ret; | ||
return items; | ||
}; | ||
/** | ||
* Update the cached dimensions of the instance's items. | ||
* Update the cached dimensions of the instance's items. By default all the | ||
* items are refreshed, but you can also provide an array of target items as the | ||
* first argument if you want to refresh specific items. Note that all hidden | ||
* items are not refreshed by default since their "display" property is "none" | ||
* and their dimensions are therefore not readable from the DOM. However, if you | ||
* do want to force update hidden item dimensions too you can provide `true` | ||
* as the second argument, which makes the elements temporarily visible while | ||
* their dimensions are being read. | ||
* | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} [items] | ||
* @param {Item[]} [items] | ||
* @param {Boolean} [force=false] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.refreshItems = function(items) { | ||
Grid.prototype.refreshItems = function (items, force) { | ||
if (this._isDestroyed) return this; | ||
var targets = this.getItems(items); | ||
var i; | ||
var targets = items || this._items; | ||
var i, item, style, hiddenItemStyles; | ||
if (force === true) { | ||
hiddenItemStyles = []; | ||
for (i = 0; i < targets.length; i++) { | ||
item = targets[i]; | ||
if (!item.isVisible() && !item.isHiding()) { | ||
style = item.getElement().style; | ||
style.visibility = 'hidden'; | ||
style.display = ''; | ||
hiddenItemStyles.push(style); | ||
} | ||
} | ||
} | ||
for (i = 0; i < targets.length; i++) { | ||
targets[i]._refreshDimensions(); | ||
targets[i]._refreshDimensions(force); | ||
} | ||
if (force === true) { | ||
for (i = 0; i < hiddenItemStyles.length; i++) { | ||
style = hiddenItemStyles[i]; | ||
style.visibility = ''; | ||
style.display = 'none'; | ||
} | ||
hiddenItemStyles.length = 0; | ||
} | ||
return this; | ||
@@ -492,17 +600,16 @@ }; | ||
/** | ||
* Update the sort data of the instance's items. | ||
* Update the sort data of the instance's items. By default all the items are | ||
* refreshed, but you can also provide an array of target items if you want to | ||
* refresh specific items. | ||
* | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} [items] | ||
* @param {Item[]} [items] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.refreshSortData = function(items) { | ||
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(); | ||
var targets = items || this._items; | ||
for (var i = 0; i < targets.length; i++) { | ||
targets[i]._refreshSortData(); | ||
} | ||
@@ -521,30 +628,26 @@ | ||
* @public | ||
* @memberof Grid.prototype | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.synchronize = function() { | ||
Grid.prototype.synchronize = function () { | ||
if (this._isDestroyed) return this; | ||
var container = this._element; | ||
var items = this._items; | ||
if (!items.length) return this; | ||
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 || window.document.createDocumentFragment(); | ||
fragment.appendChild(element); | ||
} | ||
for (var i = 0; i < items.length; i++) { | ||
element = items[i]._element; | ||
if (element.parentNode === this._element) { | ||
fragment = fragment || document.createDocumentFragment(); | ||
fragment.appendChild(element); | ||
} | ||
if (fragment) container.appendChild(fragment); | ||
} | ||
// Emit synchronize event. | ||
this._emit(eventSynchronize); | ||
if (!fragment) return this; | ||
this._element.appendChild(fragment); | ||
this._emit(EVENT_SYNCHRONIZE); | ||
return this; | ||
@@ -557,95 +660,70 @@ }; | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {Boolean} [instant=false] | ||
* @param {LayoutCallback} [onFinish] | ||
* @param {Function} [onFinish] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.layout = function(instant, onFinish) { | ||
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 isBorderBox; | ||
var item; | ||
var i; | ||
// Cancel unfinished layout algorithm if possible. | ||
var unfinishedLayout = this._nextLayoutData; | ||
if (unfinishedLayout && isFunction(unfinishedLayout.cancel)) { | ||
unfinishedLayout.cancel(); | ||
} | ||
// 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; | ||
// Compute layout id (let's stay in Float32 range). | ||
layoutId = (layoutId % MAX_SAFE_FLOAT32_INTEGER) + 1; | ||
var nextLayoutId = layoutId; | ||
var hasLayoutChanged = inst._layout.id !== layoutId; | ||
var callback = isFunction(instant) ? instant : onFinish; | ||
// Store data for next layout. | ||
this._nextLayoutData = { | ||
id: nextLayoutId, | ||
instant: instant, | ||
onFinish: onFinish, | ||
cancel: null, | ||
}; | ||
if (isFunction(callback)) { | ||
callback(hasLayoutChanged, layout.items.slice(0)); | ||
} | ||
// Collect layout items (all active grid items). | ||
var items = this._items; | ||
var layoutItems = []; | ||
for (var i = 0; i < items.length; i++) { | ||
if (items[i]._isActive) layoutItems.push(items[i]); | ||
} | ||
if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) { | ||
inst._emit(eventLayoutEnd, layout.items.slice(0)); | ||
} | ||
// Compute new layout. | ||
this._refreshDimensions(); | ||
var gridWidth = this._width - this._borderLeft - this._borderRight; | ||
var gridHeight = this._height - this._borderTop - this._borderBottom; | ||
var layoutSettings = this._settings.layout; | ||
var cancelLayout; | ||
if (isFunction(layoutSettings)) { | ||
cancelLayout = layoutSettings( | ||
this, | ||
nextLayoutId, | ||
layoutItems, | ||
gridWidth, | ||
gridHeight, | ||
this._onLayoutDataReceived | ||
); | ||
} else { | ||
Grid.defaultPacker.setOptions(layoutSettings); | ||
cancelLayout = Grid.defaultPacker.createLayout( | ||
this, | ||
nextLayoutId, | ||
layoutItems, | ||
gridWidth, | ||
gridHeight, | ||
this._onLayoutDataReceived | ||
); | ||
} | ||
// 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. Note that we support providing the | ||
// dimensions as a string here too so that one can define the unit of the | ||
// dimensions, in which case we don't do the border-box check. | ||
// Store layout cancel method if available. | ||
if ( | ||
(layout.setHeight && typeof layout.height === numberType) || | ||
(layout.setWidth && typeof layout.width === numberType) | ||
isFunction(cancelLayout) && | ||
this._nextLayoutData && | ||
this._nextLayoutData.id === nextLayoutId | ||
) { | ||
isBorderBox = getStyle(element, 'box-sizing') === 'border-box'; | ||
this._nextLayoutData.cancel = cancelLayout; | ||
} | ||
if (layout.setHeight) { | ||
if (typeof layout.height === numberType) { | ||
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 === numberType) { | ||
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; | ||
@@ -668,11 +746,10 @@ }; | ||
* @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] | ||
* @param {Boolean} [options.active] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Item[]} | ||
*/ | ||
Grid.prototype.add = function(elements, options) { | ||
Grid.prototype.add = function (elements, options) { | ||
if (this._isDestroyed || !elements) return []; | ||
@@ -683,13 +760,32 @@ | ||
var opts = options || 0; | ||
var opts = options || {}; | ||
var layout = opts.layout ? opts.layout : opts.layout === undefined; | ||
var items = this._items; | ||
var needsLayout = false; | ||
var fragment; | ||
var element; | ||
var item; | ||
var i; | ||
// Collect all the elements that are not child of the grid element into a | ||
// document fragment. | ||
for (i = 0; i < newItems.length; i++) { | ||
element = newItems[i]; | ||
if (element.parentNode !== this._element) { | ||
fragment = fragment || document.createDocumentFragment(); | ||
fragment.appendChild(element); | ||
} | ||
} | ||
// If we have a fragment, let's append it to the grid element. We could just | ||
// not do this and the `new Item()` instantiation would handle this for us, | ||
// but this way we can add the elements into the DOM a bit faster. | ||
if (fragment) { | ||
this._element.appendChild(fragment); | ||
} | ||
// 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; | ||
element = newItems[i]; | ||
item = newItems[i] = new Item(this, element, opts.active); | ||
@@ -707,2 +803,10 @@ // If the item to be added is active, we need to do a layout. Also, we | ||
// Set up the items' initial dimensions and sort data. This needs to be done | ||
// in a separate loop to avoid layout thrashing. | ||
for (i = 0; i < newItems.length; i++) { | ||
item = newItems[i]; | ||
item._refreshDimensions(); | ||
item._refreshSortData(); | ||
} | ||
// Add the new items to the items collection to correct index. | ||
@@ -712,4 +816,4 @@ arrayInsert(items, newItems, opts.index); | ||
// Emit add event. | ||
if (this._hasListeners(eventAdd)) { | ||
this._emit(eventAdd, newItems.slice(0)); | ||
if (this._hasListeners(EVENT_ADD)) { | ||
this._emit(EVENT_ADD, newItems.slice(0)); | ||
} | ||
@@ -719,3 +823,3 @@ | ||
if (needsLayout && layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
@@ -730,18 +834,18 @@ | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} items | ||
* @param {Item[]} items | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.removeElements=false] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Item[]} | ||
*/ | ||
Grid.prototype.remove = function(items, options) { | ||
if (this._isDestroyed) return this; | ||
Grid.prototype.remove = function (items, options) { | ||
if (this._isDestroyed || !items.length) return []; | ||
var opts = options || 0; | ||
var opts = options || {}; | ||
var layout = opts.layout ? opts.layout : opts.layout === undefined; | ||
var needsLayout = false; | ||
var allItems = this.getItems(); | ||
var targetItems = this.getItems(items); | ||
var targetItems = []; | ||
var indices = []; | ||
var index; | ||
var item; | ||
@@ -751,12 +855,20 @@ var i; | ||
// Remove the individual items. | ||
for (i = 0; i < targetItems.length; i++) { | ||
item = targetItems[i]; | ||
for (i = 0; i < items.length; i++) { | ||
item = items[i]; | ||
if (item._isDestroyed) continue; | ||
index = this._items.indexOf(item); | ||
if (index === -1) continue; | ||
if (item._isActive) needsLayout = true; | ||
targetItems.push(item); | ||
indices.push(allItems.indexOf(item)); | ||
if (item._isActive) needsLayout = true; | ||
item._destroy(opts.removeElements); | ||
this._items.splice(index, 1); | ||
} | ||
// Emit remove event. | ||
if (this._hasListeners(eventRemove)) { | ||
this._emit(eventRemove, targetItems.slice(0), indices); | ||
if (this._hasListeners(EVENT_REMOVE)) { | ||
this._emit(EVENT_REMOVE, targetItems.slice(0), indices); | ||
} | ||
@@ -766,3 +878,3 @@ | ||
if (needsLayout && layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
@@ -774,16 +886,17 @@ | ||
/** | ||
* Show instance items. | ||
* Show specific instance items. | ||
* | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} items | ||
* @param {Item[]} items | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.instant=false] | ||
* @param {ShowCallback} [options.onFinish] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {Boolean} [options.syncWithLayout=true] | ||
* @param {Function} [options.onFinish] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.show = function(items, options) { | ||
if (this._isDestroyed) return this; | ||
this._setItemsVisibility(items, true, options); | ||
Grid.prototype.show = function (items, options) { | ||
if (!this._isDestroyed && items.length) { | ||
this._setItemsVisibility(items, true, options); | ||
} | ||
return this; | ||
@@ -793,16 +906,17 @@ }; | ||
/** | ||
* Hide instance items. | ||
* Hide specific instance items. | ||
* | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} items | ||
* @param {Item[]} items | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.instant=false] | ||
* @param {HideCallback} [options.onFinish] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {Boolean} [options.syncWithLayout=true] | ||
* @param {Function} [options.onFinish] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.hide = function(items, options) { | ||
if (this._isDestroyed) return this; | ||
this._setItemsVisibility(items, false, options); | ||
Grid.prototype.hide = function (items, options) { | ||
if (!this._isDestroyed && items.length) { | ||
this._setItemsVisibility(items, false, options); | ||
} | ||
return this; | ||
@@ -822,11 +936,11 @@ }; | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {(Function|String)} predicate | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.instant=false] | ||
* @param {Boolean} [options.syncWithLayout=true] | ||
* @param {FilterCallback} [options.onFinish] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.filter = function(predicate, options) { | ||
Grid.prototype.filter = function (predicate, options) { | ||
if (this._isDestroyed || !this._items.length) return this; | ||
@@ -836,6 +950,7 @@ | ||
var itemsToHide = []; | ||
var isPredicateString = typeof predicate === stringType; | ||
var isPredicateString = typeof predicate === STRING_TYPE; | ||
var isPredicateFn = isFunction(predicate); | ||
var opts = options || 0; | ||
var opts = options || {}; | ||
var isInstant = opts.instant === true; | ||
var syncWithLayout = opts.syncWithLayout; | ||
var layout = opts.layout ? opts.layout : opts.layout === undefined; | ||
@@ -850,3 +965,3 @@ var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null; | ||
if (onFinish) { | ||
tryFinish = function() { | ||
tryFinish = function () { | ||
++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0)); | ||
@@ -872,4 +987,5 @@ }; | ||
instant: isInstant, | ||
syncWithLayout: syncWithLayout, | ||
onFinish: tryFinish, | ||
layout: false | ||
layout: false, | ||
}); | ||
@@ -884,4 +1000,5 @@ } else { | ||
instant: isInstant, | ||
syncWithLayout: syncWithLayout, | ||
onFinish: tryFinish, | ||
layout: false | ||
layout: false, | ||
}); | ||
@@ -895,4 +1012,4 @@ } else { | ||
// Emit filter event. | ||
if (this._hasListeners(eventFilter)) { | ||
this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0)); | ||
if (this._hasListeners(EVENT_FILTER)) { | ||
this._emit(EVENT_FILTER, itemsToShow.slice(0), itemsToHide.slice(0)); | ||
} | ||
@@ -902,3 +1019,3 @@ | ||
if (layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
@@ -921,10 +1038,9 @@ } | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {(Function|Item[]|String|String[])} comparer | ||
* @param {(Function|String|Item[])} comparer | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.descending=false] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.sort = (function() { | ||
Grid.prototype.sort = (function () { | ||
var sortComparer; | ||
@@ -935,25 +1051,2 @@ var isDescending; | ||
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) { | ||
@@ -990,6 +1083,8 @@ var result = 0; | ||
// If values are equal let's compare the item indices to make sure we | ||
// have a stable sort. | ||
// have a stable sort. Note that this is not necessary in evergreen browsers | ||
// because Array.sort() is nowadays stable. However, in order to guarantee | ||
// same results in older browsers we need this. | ||
if (!result) { | ||
if (!indexMap) indexMap = getIndexMap(origItems); | ||
result = compareIndices(a, b); | ||
if (!indexMap) indexMap = createIndexMap(origItems); | ||
result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); | ||
} | ||
@@ -1000,23 +1095,18 @@ 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); | ||
var result = isDescending ? -sortComparer(a, b) : sortComparer(a, b); | ||
if (!result) { | ||
if (!indexMap) indexMap = createIndexMap(origItems); | ||
result = isDescending ? compareIndexMap(indexMap, b, a) : compareIndexMap(indexMap, a, b); | ||
} | ||
return result; | ||
} | ||
return function(comparer, options) { | ||
return function (comparer, options) { | ||
if (this._isDestroyed || this._items.length < 2) return this; | ||
var items = this._items; | ||
var opts = options || 0; | ||
var opts = options || {}; | ||
var layout = opts.layout ? opts.layout : opts.layout === undefined; | ||
var i; | ||
// Setup parent scope data. | ||
sortComparer = comparer; | ||
isDescending = !!opts.descending; | ||
@@ -1027,3 +1117,4 @@ origItems = items.slice(0); | ||
// If function is provided do a native array sort. | ||
if (isFunction(sortComparer)) { | ||
if (isFunction(comparer)) { | ||
sortComparer = comparer; | ||
items.sort(customComparer); | ||
@@ -1033,29 +1124,31 @@ } | ||
// the instance's options. | ||
else if (typeof sortComparer === stringType) { | ||
sortComparer = parseCriteria(comparer); | ||
else if (typeof comparer === STRING_TYPE) { | ||
sortComparer = comparer | ||
.trim() | ||
.split(' ') | ||
.filter(function (val) { | ||
return val; | ||
}) | ||
.map(function (val) { | ||
return val.split(':'); | ||
}); | ||
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(); | ||
// items and order the items based on it. Here we blindly trust that the | ||
// presorted array consists of the same item instances as the current | ||
// `gird._items` array. | ||
else if (Array.isArray(comparer)) { | ||
items.length = 0; | ||
items.push.apply(items, comparer); | ||
} | ||
// Otherwise let's just skip it, nothing we can do here. | ||
// Otherwise let's throw an error. | ||
else { | ||
/** @todo Maybe throw an error here? */ | ||
return this; | ||
sortComparer = isDescending = origItems = indexMap = null; | ||
throw new Error('Invalid comparer argument provided.'); | ||
} | ||
// Emit sort event. | ||
if (this._hasListeners(eventSort)) { | ||
this._emit(eventSort, items.slice(0), origItems); | ||
if (this._hasListeners(EVENT_SORT)) { | ||
this._emit(EVENT_SORT, items.slice(0), origItems); | ||
} | ||
@@ -1065,5 +1158,8 @@ | ||
if (layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
// Reset data (to avoid mem leaks). | ||
sortComparer = isDescending = origItems = indexMap = null; | ||
return this; | ||
@@ -1077,5 +1173,4 @@ }; | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridSingleItemQuery} item | ||
* @param {GridSingleItemQuery} position | ||
* @param {(HtmlElement|Number|Item)} item | ||
* @param {(HtmlElement|Number|Item)} position | ||
* @param {Object} [options] | ||
@@ -1086,15 +1181,15 @@ * @param {String} [options.action="move"] | ||
* - "swap" swaps the position of the items. | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.move = function(item, position, options) { | ||
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 opts = options || {}; | ||
var layout = opts.layout ? opts.layout : opts.layout === undefined; | ||
var isSwap = opts.action === actionSwap; | ||
var action = isSwap ? actionSwap : actionMove; | ||
var fromItem = this._getItem(item); | ||
var toItem = this._getItem(position); | ||
var isSwap = opts.action === ACTION_SWAP; | ||
var action = isSwap ? ACTION_SWAP : ACTION_MOVE; | ||
var fromItem = this.getItem(item); | ||
var toItem = this.getItem(position); | ||
var fromIndex; | ||
@@ -1117,8 +1212,8 @@ var toIndex; | ||
// Emit move event. | ||
if (this._hasListeners(eventMove)) { | ||
this._emit(eventMove, { | ||
if (this._hasListeners(EVENT_MOVE)) { | ||
this._emit(EVENT_MOVE, { | ||
item: fromItem, | ||
fromIndex: fromIndex, | ||
toIndex: toIndex, | ||
action: action | ||
action: action, | ||
}); | ||
@@ -1129,3 +1224,3 @@ } | ||
if (layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
@@ -1141,21 +1236,20 @@ } | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {GridSingleItemQuery} item | ||
* @param {Grid} grid | ||
* @param {GridSingleItemQuery} position | ||
* @param {(HtmlElement|Number|Item)} item | ||
* @param {Grid} targetGrid | ||
* @param {(HtmlElement|Number|Item)} position | ||
* @param {Object} [options] | ||
* @param {HTMLElement} [options.appendTo=document.body] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true] | ||
* @param {(Boolean|Function|String)} [options.layoutSender=true] | ||
* @param {(Boolean|Function|String)} [options.layoutReceiver=true] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.send = function(item, grid, position, options) { | ||
if (this._isDestroyed || grid._isDestroyed || this === grid) return this; | ||
Grid.prototype.send = function (item, targetGrid, position, options) { | ||
if (this._isDestroyed || targetGrid._isDestroyed || this === targetGrid) return this; | ||
// Make sure we have a valid target item. | ||
item = this._getItem(item); | ||
item = this.getItem(item); | ||
if (!item) return this; | ||
var opts = options || 0; | ||
var container = opts.appendTo || window.document.body; | ||
var opts = options || {}; | ||
var container = opts.appendTo || document.body; | ||
var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined; | ||
@@ -1167,3 +1261,3 @@ var layoutReceiver = opts.layoutReceiver | ||
// Start the migration process. | ||
item._migrate.start(grid, position, container); | ||
item._migrate.start(targetGrid, position, container); | ||
@@ -1175,3 +1269,3 @@ // If migration was started successfully and the item is active, let's layout | ||
this.layout( | ||
layoutSender === instantLayout, | ||
layoutSender === INSTANT_LAYOUT, | ||
isFunction(layoutSender) ? layoutSender : undefined | ||
@@ -1181,4 +1275,4 @@ ); | ||
if (layoutReceiver) { | ||
grid.layout( | ||
layoutReceiver === instantLayout, | ||
targetGrid.layout( | ||
layoutReceiver === INSTANT_LAYOUT, | ||
isFunction(layoutReceiver) ? layoutReceiver : undefined | ||
@@ -1196,7 +1290,6 @@ ); | ||
* @public | ||
* @memberof Grid.prototype | ||
* @param {Boolean} [removeElements=false] | ||
* @returns {Grid} | ||
*/ | ||
Grid.prototype.destroy = function(removeElements) { | ||
Grid.prototype.destroy = function (removeElements) { | ||
if (this._isDestroyed) return this; | ||
@@ -1206,25 +1299,22 @@ | ||
var items = this._items.slice(0); | ||
var i; | ||
var layoutStyles = (this._layout && this._layout.styles) || {}; | ||
var i, prop; | ||
// Unbind window resize event listener. | ||
if (this._resizeHandler) { | ||
window.removeEventListener('resize', this._resizeHandler); | ||
} | ||
unbindLayoutOnResize(this); | ||
// Destroy items. | ||
for (i = 0; i < items.length; i++) { | ||
items[i]._destroy(removeElements); | ||
} | ||
for (i = 0; i < items.length; i++) items[i]._destroy(removeElements); | ||
this._items.length = 0; | ||
// Restore container. | ||
removeClass(container, this._settings.containerClass); | ||
container.style.height = ''; | ||
container.style.width = ''; | ||
for (prop in layoutStyles) container.style[prop] = ''; | ||
// Emit destroy event and unbind all events. | ||
this._emit(eventDestroy); | ||
this._emit(EVENT_DESTROY); | ||
this._emitter.destroy(); | ||
// Remove reference from the grid instances collection. | ||
gridInstances[this._id] = undefined; | ||
delete GRID_INSTANCES[this._id]; | ||
@@ -1243,101 +1333,9 @@ // Flag instance as destroyed. | ||
/** | ||
* 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 === numberType) { | ||
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 (isFunction(settings)) { | ||
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() { | ||
Grid.prototype._emit = function () { | ||
if (this._isDestroyed) return; | ||
@@ -1351,9 +1349,8 @@ this._emitter.emit.apply(this._emitter, arguments); | ||
* @private | ||
* @memberof Grid.prototype | ||
* @param {String} event | ||
* @returns {Boolean} | ||
*/ | ||
Grid.prototype._hasListeners = function(event) { | ||
var listeners = this._emitter._events[event]; | ||
return !!(listeners && listeners.length); | ||
Grid.prototype._hasListeners = function (event) { | ||
if (this._isDestroyed) return false; | ||
return this._emitter.countListeners(event) > 0; | ||
}; | ||
@@ -1365,5 +1362,4 @@ | ||
* @private | ||
* @memberof Grid.prototype | ||
*/ | ||
Grid.prototype._updateBoundingRect = function() { | ||
Grid.prototype._updateBoundingRect = function () { | ||
var element = this._element; | ||
@@ -1375,2 +1371,4 @@ var rect = element.getBoundingClientRect(); | ||
this._top = rect.top; | ||
this._right = rect.right; | ||
this._bottom = rect.bottom; | ||
}; | ||
@@ -1382,3 +1380,2 @@ | ||
* @private | ||
* @memberof Grid.prototype | ||
* @param {Boolean} left | ||
@@ -1389,3 +1386,3 @@ * @param {Boolean} right | ||
*/ | ||
Grid.prototype._updateBorders = function(left, right, top, bottom) { | ||
Grid.prototype._updateBorders = function (left, right, top, bottom) { | ||
var element = this._element; | ||
@@ -1402,25 +1399,141 @@ if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width'); | ||
* @private | ||
* @memberof Grid.prototype | ||
*/ | ||
Grid.prototype._refreshDimensions = function() { | ||
Grid.prototype._refreshDimensions = function () { | ||
this._updateBoundingRect(); | ||
this._updateBorders(1, 1, 1, 1); | ||
this._boxSizing = getStyle(this._element, 'box-sizing'); | ||
}; | ||
/** | ||
* Calculate and apply item positions. | ||
* | ||
* @private | ||
* @param {Object} layout | ||
*/ | ||
Grid.prototype._onLayoutDataReceived = (function () { | ||
var itemsToLayout = []; | ||
return function (layout) { | ||
if (this._isDestroyed || !this._nextLayoutData || this._nextLayoutData.id !== layout.id) return; | ||
var grid = this; | ||
var instant = this._nextLayoutData.instant; | ||
var onFinish = this._nextLayoutData.onFinish; | ||
var numItems = layout.items.length; | ||
var counter = numItems; | ||
var item; | ||
var left; | ||
var top; | ||
var i; | ||
// Reset next layout data. | ||
this._nextLayoutData = null; | ||
if (!this._isLayoutFinished && this._hasListeners(EVENT_LAYOUT_ABORT)) { | ||
this._emit(EVENT_LAYOUT_ABORT, this._layout.items.slice(0)); | ||
} | ||
// Update the layout reference. | ||
this._layout = layout; | ||
// Update the item positions and collect all items that need to be laid | ||
// out. It is critical that we update the item position _before_ the | ||
// layoutStart event as the new data might be needed in the callback. | ||
itemsToLayout.length = 0; | ||
for (i = 0; i < numItems; i++) { | ||
item = layout.items[i]; | ||
// Make sure we have a matching item. | ||
if (!item) { | ||
--counter; | ||
continue; | ||
} | ||
// Get the item's new left and top values. | ||
left = layout.slots[i * 2]; | ||
top = layout.slots[i * 2 + 1]; | ||
// Let's skip the layout process if we can. Possibly avoids a lot of DOM | ||
// operations which saves us some CPU cycles. | ||
if (item._canSkipLayout(left, top)) { | ||
--counter; | ||
continue; | ||
} | ||
// Update the item's position. | ||
item._left = left; | ||
item._top = top; | ||
// Only active non-dragged items need to be moved. | ||
if (item.isActive() && !item.isDragging()) { | ||
itemsToLayout.push(item); | ||
} | ||
} | ||
// Set layout styles to the grid element. | ||
if (layout.styles) { | ||
setStyles(this._element, layout.styles); | ||
} | ||
// layoutStart event 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(EVENT_LAYOUT_START)) { | ||
this._emit(EVENT_LAYOUT_START, layout.items.slice(0), instant === true); | ||
} | ||
function tryFinish() { | ||
if (--counter > 0) return; | ||
var hasLayoutChanged = grid._layout.id !== layout.id; | ||
var callback = isFunction(instant) ? instant : onFinish; | ||
if (!hasLayoutChanged) { | ||
grid._isLayoutFinished = true; | ||
} | ||
if (isFunction(callback)) { | ||
callback(layout.items.slice(0), hasLayoutChanged); | ||
} | ||
if (!hasLayoutChanged && grid._hasListeners(EVENT_LAYOUT_END)) { | ||
grid._emit(EVENT_LAYOUT_END, layout.items.slice(0)); | ||
} | ||
} | ||
if (!itemsToLayout.length) { | ||
tryFinish(); | ||
return this; | ||
} | ||
this._isLayoutFinished = false; | ||
for (i = 0; i < itemsToLayout.length; i++) { | ||
if (this._layout.id !== layout.id) break; | ||
itemsToLayout[i]._layout.start(instant === true, tryFinish); | ||
} | ||
if (this._layout.id === layout.id) { | ||
itemsToLayout.length = 0; | ||
} | ||
return this; | ||
}; | ||
})(); | ||
/** | ||
* Show or hide Grid instance's items. | ||
* | ||
* @private | ||
* @memberof Grid.prototype | ||
* @param {GridMultiItemQuery} items | ||
* @param {Item[]} items | ||
* @param {Boolean} toVisible | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.instant=false] | ||
* @param {(ShowCallback|HideCallback)} [options.onFinish] | ||
* @param {(Boolean|LayoutCallback|String)} [options.layout=true] | ||
* @param {Boolean} [options.syncWithLayout=true] | ||
* @param {Function} [options.onFinish] | ||
* @param {(Boolean|Function|String)} [options.layout=true] | ||
*/ | ||
Grid.prototype._setItemsVisibility = function(items, toVisible, options) { | ||
Grid.prototype._setItemsVisibility = function (items, toVisible, options) { | ||
var grid = this; | ||
var targetItems = this.getItems(items); | ||
var opts = options || 0; | ||
var targetItems = items.slice(0); | ||
var opts = options || {}; | ||
var isInstant = opts.instant === true; | ||
@@ -1430,4 +1543,4 @@ var callback = opts.onFinish; | ||
var counter = targetItems.length; | ||
var startEvent = toVisible ? eventShowStart : eventHideStart; | ||
var endEvent = toVisible ? eventShowEnd : eventHideEnd; | ||
var startEvent = toVisible ? EVENT_SHOW_START : EVENT_HIDE_START; | ||
var endEvent = toVisible ? EVENT_SHOW_END : EVENT_HIDE_END; | ||
var method = toVisible ? 'show' : 'hide'; | ||
@@ -1446,8 +1559,3 @@ var needsLayout = false; | ||
// Emit showStart/hideStart event. | ||
if (this._hasListeners(startEvent)) { | ||
this._emit(startEvent, targetItems.slice(0)); | ||
} | ||
// Show/hide items. | ||
// Prepare the items. | ||
for (i = 0; i < targetItems.length; i++) { | ||
@@ -1464,5 +1572,3 @@ item = targetItems[i]; | ||
// item not animate it's next positioning (layout). | ||
if (toVisible && !item._isActive) { | ||
item._layout._skipNextAnimation = true; | ||
} | ||
item._layout._skipNextAnimation = !!(toVisible && !item._isActive); | ||
@@ -1475,23 +1581,62 @@ // If a hidden item is being shown we need to refresh the item's | ||
// 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); | ||
// Add item to layout or remove it from layout. | ||
if (toVisible) { | ||
item._addToLayout(); | ||
} else { | ||
item._removeFromLayout(); | ||
} | ||
} | ||
// If all items have finished their animations call the callback | ||
// and emit showEnd/hideEnd event. | ||
if (--counter < 1) { | ||
if (isFunction(callback)) callback(completedItems.slice(0)); | ||
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); | ||
// Force refresh the dimensions of all hidden items. | ||
if (hiddenItems.length) { | ||
this.refreshItems(hiddenItems, true); | ||
hiddenItems.length = 0; | ||
} | ||
// Show the items in sync with the next layout. | ||
function triggerVisibilityChange() { | ||
if (needsLayout && opts.syncWithLayout !== false) { | ||
grid.off(EVENT_LAYOUT_START, triggerVisibilityChange); | ||
} | ||
if (grid._hasListeners(startEvent)) { | ||
grid._emit(startEvent, targetItems.slice(0)); | ||
} | ||
for (i = 0; i < targetItems.length; i++) { | ||
// Make sure the item is still in the original grid. There is a chance | ||
// that the item starts migrating before tiggerVisibilityChange is called. | ||
if (targetItems[i]._gridId !== grid._id) { | ||
if (--counter < 1) { | ||
if (isFunction(callback)) callback(completedItems.slice(0)); | ||
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); | ||
} | ||
continue; | ||
} | ||
}); | ||
targetItems[i]._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 (isFunction(callback)) callback(completedItems.slice(0)); | ||
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0)); | ||
} | ||
}); | ||
} | ||
} | ||
// Refresh hidden items. | ||
if (hiddenItems.length) this.refreshItems(hiddenItems); | ||
// Trigger the visibility change, either async with layout or instantly. | ||
if (needsLayout && opts.syncWithLayout !== false) { | ||
this.on(EVENT_LAYOUT_START, triggerVisibilityChange); | ||
} else { | ||
triggerVisibilityChange(); | ||
} | ||
// Layout if needed. | ||
// Trigger layout if needed. | ||
if (needsLayout && layout) { | ||
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined); | ||
this.layout(layout === INSTANT_LAYOUT, isFunction(layout) ? layout : undefined); | ||
} | ||
@@ -1518,7 +1663,7 @@ }; | ||
// Create a fresh copy of default settings. | ||
var ret = mergeObjects({}, defaultSettings); | ||
var settings = mergeObjects({}, defaultSettings); | ||
// Merge user settings to default settings. | ||
if (userSettings) { | ||
ret = mergeObjects(ret, userSettings); | ||
settings = mergeObjects(settings, userSettings); | ||
} | ||
@@ -1528,6 +1673,16 @@ | ||
// overridden instead of the props. | ||
ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles; | ||
ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles; | ||
return ret; | ||
if (userSettings && userSettings.visibleStyles) { | ||
settings.visibleStyles = userSettings.visibleStyles; | ||
} else if (defaultSettings && defaultSettings.visibleStyles) { | ||
settings.visibleStyles = defaultSettings.visibleStyles; | ||
} | ||
if (userSettings && userSettings.hiddenStyles) { | ||
settings.hiddenStyles = userSettings.hiddenStyles; | ||
} else if (defaultSettings && defaultSettings.hiddenStyles) { | ||
settings.hiddenStyles = defaultSettings.hiddenStyles; | ||
} | ||
return settings; | ||
} | ||
@@ -1585,2 +1740,120 @@ | ||
/** | ||
* Collect and return initial items for grid. | ||
* | ||
* @param {HTMLElement} gridElement | ||
* @param {?(HTMLElement[]|NodeList|HtmlCollection|String)} elements | ||
* @returns {(HTMLElement[]|NodeList|HtmlCollection)} | ||
*/ | ||
function getInitialGridElements(gridElement, elements) { | ||
// If we have a wildcard selector let's return all the children. | ||
if (elements === '*') { | ||
return gridElement.children; | ||
} | ||
// If we have some more specific selector, let's filter the elements. | ||
if (typeof elements === STRING_TYPE) { | ||
var result = []; | ||
var children = gridElement.children; | ||
for (var i = 0; i < children.length; i++) { | ||
if (elementMatches(children[i], elements)) { | ||
result.push(children[i]); | ||
} | ||
} | ||
return result; | ||
} | ||
// If we have an array of elements or a node list. | ||
if (Array.isArray(elements) || isNodeList(elements)) { | ||
return elements; | ||
} | ||
// Otherwise just return an empty array. | ||
return []; | ||
} | ||
/** | ||
* Bind grid's resize handler to window. | ||
* | ||
* @param {Grid} grid | ||
* @param {(Number|Boolean)} delay | ||
*/ | ||
function bindLayoutOnResize(grid, delay) { | ||
if (typeof delay !== NUMBER_TYPE) { | ||
delay = delay === true ? 0 : -1; | ||
} | ||
if (delay >= 0) { | ||
grid._resizeHandler = debounce(function () { | ||
grid.refreshItems().layout(); | ||
}, delay); | ||
window.addEventListener('resize', grid._resizeHandler); | ||
} | ||
} | ||
/** | ||
* Unbind grid's resize handler from window. | ||
* | ||
* @param {Grid} grid | ||
*/ | ||
function unbindLayoutOnResize(grid) { | ||
if (grid._resizeHandler) { | ||
grid._resizeHandler(true); | ||
window.removeEventListener('resize', grid._resizeHandler); | ||
grid._resizeHandler = null; | ||
} | ||
} | ||
/** | ||
* Normalize style declaration object, returns a normalized (new) styles object | ||
* (prefixed properties and invalid properties removed). | ||
* | ||
* @param {Object} styles | ||
* @returns {Object} | ||
*/ | ||
function normalizeStyles(styles) { | ||
var normalized = {}; | ||
var docElemStyle = document.documentElement.style; | ||
var prop, prefixedProp; | ||
// Normalize visible styles (prefix and remove invalid). | ||
for (prop in styles) { | ||
if (!styles[prop]) continue; | ||
prefixedProp = getPrefixedPropName(docElemStyle, prop); | ||
if (!prefixedProp) continue; | ||
normalized[prefixedProp] = styles[prop]; | ||
} | ||
return normalized; | ||
} | ||
/** | ||
* Create index map from items. | ||
* | ||
* @param {Item[]} items | ||
* @returns {Object} | ||
*/ | ||
function createIndexMap(items) { | ||
var result = {}; | ||
for (var i = 0; i < items.length; i++) { | ||
result[items[i]._id] = i; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Sort comparer function for items' index map. | ||
* | ||
* @param {Object} indexMap | ||
* @param {Item} itemA | ||
* @param {Item} itemB | ||
* @returns {Number} | ||
*/ | ||
function compareIndexMap(indexMap, itemA, itemB) { | ||
var indexA = indexMap[itemA._id]; | ||
var indexB = indexMap[itemB._id]; | ||
return indexA - indexB; | ||
} | ||
export default Grid; |
@@ -7,3 +7,2 @@ /** | ||
import Grid from './Grid/Grid'; | ||
export default Grid; | ||
export { default } from './Grid/Grid'; |
@@ -7,11 +7,11 @@ /** | ||
import { gridInstances } from '../shared'; | ||
import { GRID_INSTANCES, ITEM_ELEMENT_MAP } from '../constants'; | ||
import ItemAnimate from './ItemAnimate'; | ||
import ItemDrag from './ItemDrag'; | ||
import ItemDragPlaceholder from './ItemDragPlaceholder'; | ||
import ItemDragRelease from './ItemDragRelease'; | ||
import ItemLayout from './ItemLayout'; | ||
import ItemMigrate from './ItemMigrate'; | ||
import ItemRelease from './ItemRelease'; | ||
import ItemVisibility from './ItemVisibility'; | ||
import Emitter from '../Emitter/Emitter'; | ||
@@ -24,3 +24,3 @@ import addClass from '../utils/addClass'; | ||
import removeClass from '../utils/removeClass'; | ||
import { transformProp } from '../utils/supportedTransform'; | ||
import transformProp from '../utils/transformProp'; | ||
@@ -38,21 +38,31 @@ /** | ||
// Create instance id. | ||
// Store item/element pair to a map (for faster item querying by element). | ||
if (ITEM_ELEMENT_MAP) { | ||
if (ITEM_ELEMENT_MAP.has(element)) { | ||
throw new Error('You can only create one Muuri Item per element!'); | ||
} else { | ||
ITEM_ELEMENT_MAP.set(element, this); | ||
} | ||
} | ||
this._id = createUid(); | ||
// Reference to connected Grid instance's id. | ||
this._gridId = grid._id; | ||
// Destroyed flag. | ||
this._element = element; | ||
this._isDestroyed = false; | ||
// Set up initial positions. | ||
this._left = 0; | ||
this._top = 0; | ||
this._width = 0; | ||
this._height = 0; | ||
this._marginLeft = 0; | ||
this._marginRight = 0; | ||
this._marginTop = 0; | ||
this._marginBottom = 0; | ||
this._tX = undefined; | ||
this._tY = undefined; | ||
this._sortData = null; | ||
this._emitter = new Emitter(); | ||
// 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. | ||
// element, append it to the grid container. Note, we are indeed reading the | ||
// DOM here but it's a property that does not cause reflowing. | ||
if (element.parentNode !== grid._element) { | ||
@@ -65,3 +75,5 @@ grid._element.appendChild(element); | ||
// If isActive is not defined, let's try to auto-detect it. | ||
// If isActive is not defined, let's try to auto-detect it. Note, we are | ||
// indeed reading the DOM here but it's a property that does not cause | ||
// reflowing. | ||
if (typeof isActive !== 'boolean') { | ||
@@ -75,11 +87,2 @@ isActive = getStyle(element, 'display') !== 'none'; | ||
// 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. | ||
@@ -94,6 +97,9 @@ this._visibility = new ItemVisibility(this); | ||
// Set up drag handler. | ||
this._drag = settings.dragEnabled ? new ItemDrag(this) : null; | ||
// Set up release handler. Note that although this is fully linked to dragging | ||
// this still needs to be always instantiated to handle migration scenarios | ||
// correctly. | ||
this._release = new ItemRelease(this); | ||
this._dragRelease = new ItemDragRelease(this); | ||
@@ -105,8 +111,8 @@ // Set up drag placeholder handler. Note that although this is fully linked to | ||
// Set up drag handler. | ||
this._drag = settings.dragEnabled ? new ItemDrag(this) : null; | ||
// Set up the initial dimensions and sort data. | ||
this._refreshDimensions(); | ||
this._refreshSortData(); | ||
// Note! You must call the following methods before you start using the | ||
// instance. They are deliberately not called in the end as it would cause | ||
// potentially a massive amount of reflows if multiple items were instantiated | ||
// in a loop. | ||
// this._refreshDimensions(); | ||
// this._refreshSortData(); | ||
} | ||
@@ -123,7 +129,6 @@ | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Grid} | ||
*/ | ||
Item.prototype.getGrid = function() { | ||
return gridInstances[this._gridId]; | ||
Item.prototype.getGrid = function () { | ||
return GRID_INSTANCES[this._gridId]; | ||
}; | ||
@@ -135,6 +140,5 @@ | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {HTMLElement} | ||
*/ | ||
Item.prototype.getElement = function() { | ||
Item.prototype.getElement = function () { | ||
return this._element; | ||
@@ -147,6 +151,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Number} | ||
*/ | ||
Item.prototype.getWidth = function() { | ||
Item.prototype.getWidth = function () { | ||
return this._width; | ||
@@ -159,6 +162,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Number} | ||
*/ | ||
Item.prototype.getHeight = function() { | ||
Item.prototype.getHeight = function () { | ||
return this._height; | ||
@@ -171,3 +173,2 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Object} | ||
@@ -177,3 +178,3 @@ * - The returned object contains left, right, top and bottom properties | ||
*/ | ||
Item.prototype.getMargin = function() { | ||
Item.prototype.getMargin = function () { | ||
return { | ||
@@ -183,3 +184,3 @@ left: this._marginLeft, | ||
top: this._marginTop, | ||
bottom: this._marginBottom | ||
bottom: this._marginBottom, | ||
}; | ||
@@ -192,3 +193,2 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Object} | ||
@@ -198,6 +198,6 @@ * - The returned object contains left and top properties which indicate the | ||
*/ | ||
Item.prototype.getPosition = function() { | ||
Item.prototype.getPosition = function () { | ||
return { | ||
left: this._left, | ||
top: this._top | ||
top: this._top, | ||
}; | ||
@@ -210,6 +210,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isActive = function() { | ||
Item.prototype.isActive = function () { | ||
return this._isActive; | ||
@@ -222,6 +221,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isVisible = function() { | ||
Item.prototype.isVisible = function () { | ||
return !!this._visibility && !this._visibility._isHidden; | ||
@@ -234,6 +232,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isShowing = function() { | ||
Item.prototype.isShowing = function () { | ||
return !!(this._visibility && this._visibility._isShowing); | ||
@@ -246,6 +243,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isHiding = function() { | ||
Item.prototype.isHiding = function () { | ||
return !!(this._visibility && this._visibility._isHiding); | ||
@@ -258,6 +254,5 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isPositioning = function() { | ||
Item.prototype.isPositioning = function () { | ||
return !!(this._layout && this._layout._isActive); | ||
@@ -267,9 +262,8 @@ }; | ||
/** | ||
* Is the item being dragged? | ||
* Is the item being dragged (or queued for dragging)? | ||
* | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isDragging = function() { | ||
Item.prototype.isDragging = function () { | ||
return !!(this._drag && this._drag._isActive); | ||
@@ -282,7 +276,6 @@ }; | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isReleasing = function() { | ||
return !!(this._release && this._release._isActive); | ||
Item.prototype.isReleasing = function () { | ||
return !!(this._dragRelease && this._dragRelease._isActive); | ||
}; | ||
@@ -294,6 +287,5 @@ | ||
* @public | ||
* @memberof Item.prototype | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype.isDestroyed = function() { | ||
Item.prototype.isDestroyed = function () { | ||
return this._isDestroyed; | ||
@@ -311,6 +303,7 @@ }; | ||
* @private | ||
* @memberof Item.prototype | ||
* @param {Boolean} [force=false] | ||
*/ | ||
Item.prototype._refreshDimensions = function() { | ||
if (this._isDestroyed || this._visibility._isHidden) return; | ||
Item.prototype._refreshDimensions = function (force) { | ||
if (this._isDestroyed) return; | ||
if (force !== true && this._visibility._isHidden) return; | ||
@@ -332,5 +325,3 @@ var element = this._element; | ||
// Keep drag placeholder's dimensions synced with the item's. | ||
if (dragPlaceholder) { | ||
dragPlaceholder.updateDimensions(this._width, this._height); | ||
} | ||
if (dragPlaceholder) dragPlaceholder.updateDimensions(); | ||
}; | ||
@@ -342,5 +333,4 @@ | ||
* @private | ||
* @memberof Item.prototype | ||
*/ | ||
Item.prototype._refreshSortData = function() { | ||
Item.prototype._refreshSortData = function () { | ||
if (this._isDestroyed) return; | ||
@@ -358,9 +348,70 @@ | ||
/** | ||
* Add item to layout. | ||
* | ||
* @private | ||
*/ | ||
Item.prototype._addToLayout = function (left, top) { | ||
if (this._isActive === true) return; | ||
this._isActive = true; | ||
this._left = left || 0; | ||
this._top = top || 0; | ||
}; | ||
/** | ||
* Remove item from layout. | ||
* | ||
* @private | ||
*/ | ||
Item.prototype._removeFromLayout = function () { | ||
if (this._isActive === false) return; | ||
this._isActive = false; | ||
this._left = 0; | ||
this._top = 0; | ||
}; | ||
/** | ||
* Check if the layout procedure can be skipped for the item. | ||
* | ||
* @private | ||
* @param {Number} left | ||
* @param {Number} top | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype._canSkipLayout = function (left, top) { | ||
return ( | ||
this._left === left && | ||
this._top === top && | ||
!this._migrate._isActive && | ||
!this._layout._skipNextAnimation && | ||
!this._dragRelease.isJustReleased() | ||
); | ||
}; | ||
/** | ||
* Set the provided left and top arguments as the item element's translate | ||
* values in the DOM. This method keeps track of the currently applied | ||
* translate values and skips the update operation if the provided values are | ||
* identical to the currently applied values. Returns `false` if there was no | ||
* need for update and `true` if the translate value was updated. | ||
* | ||
* @private | ||
* @param {Number} left | ||
* @param {Number} top | ||
* @returns {Boolean} | ||
*/ | ||
Item.prototype._setTranslate = function (left, top) { | ||
if (this._tX === left && this._tY === top) return false; | ||
this._tX = left; | ||
this._tY = top; | ||
this._element.style[transformProp] = getTranslateString(left, top); | ||
return true; | ||
}; | ||
/** | ||
* Destroy item instance. | ||
* | ||
* @private | ||
* @memberof Item.prototype | ||
* @param {Boolean} [removeElement=false] | ||
*/ | ||
Item.prototype._destroy = function(removeElement) { | ||
Item.prototype._destroy = function (removeElement) { | ||
if (this._isDestroyed) return; | ||
@@ -371,17 +422,13 @@ | ||
var settings = grid._settings; | ||
var index = grid._items.indexOf(this); | ||
// Destroy handlers. | ||
this._release.destroy(); | ||
this._dragPlaceholder.destroy(); | ||
this._dragRelease.destroy(); | ||
this._migrate.destroy(); | ||
this._layout.destroy(); | ||
this._visibility.destroy(); | ||
this._animate.destroy(); | ||
this._animateChild.destroy(); | ||
this._dragPlaceholder.destroy(); | ||
this._drag && this._drag.destroy(); | ||
if (this._drag) this._drag.destroy(); | ||
// Remove all inline styles. | ||
element.removeAttribute('style'); | ||
this._child.removeAttribute('style'); | ||
// Destroy emitter. | ||
this._emitter.destroy(); | ||
@@ -391,8 +438,8 @@ // Remove item class. | ||
// 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); | ||
if (removeElement) element.parentNode.removeChild(element); | ||
// Remove item/element pair from map. | ||
if (ITEM_ELEMENT_MAP) ITEM_ELEMENT_MAP.delete(element); | ||
// Reset state. | ||
@@ -399,0 +446,0 @@ this._isActive = false; |
@@ -8,20 +8,30 @@ /** | ||
import { | ||
actionMove, | ||
actionSwap, | ||
eventMove, | ||
eventSend, | ||
eventBeforeSend, | ||
eventReceive, | ||
eventBeforeReceive, | ||
eventDragInit, | ||
eventDragStart, | ||
eventDragMove, | ||
eventDragScroll, | ||
eventDragEnd, | ||
gridInstances | ||
} from '../shared'; | ||
ACTION_MOVE, | ||
ACTION_SWAP, | ||
EVENT_MOVE, | ||
EVENT_SEND, | ||
EVENT_BEFORE_SEND, | ||
EVENT_RECEIVE, | ||
EVENT_BEFORE_RECEIVE, | ||
EVENT_DRAG_INIT, | ||
EVENT_DRAG_START, | ||
EVENT_DRAG_MOVE, | ||
EVENT_DRAG_SCROLL, | ||
EVENT_DRAG_END, | ||
GRID_INSTANCES, | ||
} from '../constants'; | ||
import Dragger from '../Dragger/Dragger'; | ||
import AutoScroller from '../AutoScroller/AutoScroller'; | ||
import { addMoveTick, cancelMoveTick, addScrollTick, cancelScrollTick } from '../ticker'; | ||
import { | ||
addDragStartTick, | ||
cancelDragStartTick, | ||
addDragMoveTick, | ||
cancelDragMoveTick, | ||
addDragScrollTick, | ||
cancelDragScrollTick, | ||
addDragSortTick, | ||
cancelDragSortTick, | ||
} from '../ticker'; | ||
@@ -32,20 +42,17 @@ import addClass from '../utils/addClass'; | ||
import arraySwap from '../utils/arraySwap'; | ||
import debounce from '../utils/debounce'; | ||
import elementMatches from '../utils/elementMatches'; | ||
import getContainingBlock from '../utils/getContainingBlock'; | ||
import getIntersectionScore from '../utils/getIntersectionScore'; | ||
import getOffsetDiff from '../utils/getOffsetDiff'; | ||
import getScrollableAncestors from '../utils/getScrollableAncestors'; | ||
import getStyle from '../utils/getStyle'; | ||
import getTranslate from '../utils/getTranslate'; | ||
import getTranslateString from '../utils/getTranslateString'; | ||
import hasPassiveEvents from '../utils/hasPassiveEvents'; | ||
import isFunction from '../utils/isFunction'; | ||
import normalizeArrayIndex from '../utils/normalizeArrayIndex'; | ||
import removeClass from '../utils/removeClass'; | ||
import setStyles from '../utils/setStyles'; | ||
import { transformProp } from '../utils/supportedTransform'; | ||
// Drag start predicate states. | ||
var startPredicateInactive = 0; | ||
var startPredicatePending = 1; | ||
var startPredicateResolved = 2; | ||
var startPredicateRejected = 3; | ||
var START_PREDICATE_INACTIVE = 0; | ||
var START_PREDICATE_PENDING = 1; | ||
var START_PREDICATE_RESOLVED = 2; | ||
var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false; | ||
@@ -72,11 +79,13 @@ /** | ||
: ItemDrag.defaultStartPredicate; | ||
this._startPredicateState = startPredicateInactive; | ||
this._startPredicateState = START_PREDICATE_INACTIVE; | ||
this._startPredicateResult = undefined; | ||
// Data for drag sort predicate heuristics. | ||
this._hBlockedIndex = null; | ||
this._hX1 = 0; | ||
this._hX2 = 0; | ||
this._hY1 = 0; | ||
this._hY2 = 0; | ||
this._isSortNeeded = false; | ||
this._sortTimer = undefined; | ||
this._blockedSortIndex = null; | ||
this._sortX1 = 0; | ||
this._sortX2 = 0; | ||
this._sortY1 = 0; | ||
this._sortY2 = 0; | ||
@@ -90,2 +99,4 @@ // Setup item's initial drag data. | ||
this._onScroll = this._onScroll.bind(this); | ||
this._prepareStart = this._prepareStart.bind(this); | ||
this._applyStart = this._applyStart.bind(this); | ||
this._prepareMove = this._prepareMove.bind(this); | ||
@@ -95,10 +106,10 @@ this._applyMove = this._applyMove.bind(this); | ||
this._applyScroll = this._applyScroll.bind(this); | ||
this._checkOverlap = this._checkOverlap.bind(this); | ||
this._handleSort = this._handleSort.bind(this); | ||
this._handleSortDelayed = this._handleSortDelayed.bind(this); | ||
// Create debounce overlap checker function. | ||
var sortInterval = settings.dragSortHeuristics.sortInterval; | ||
this._checkOverlapDebounce = debounce(this._checkOverlap, sortInterval); | ||
// Get drag handle element. | ||
this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element; | ||
// Init dragger. | ||
this._dragger = new Dragger(element, settings.dragCssProps); | ||
this._dragger = new Dragger(this._handle, settings.dragCssProps); | ||
this._dragger.on('start', this._preStartCheck); | ||
@@ -111,2 +122,14 @@ this._dragger.on('move', this._preStartCheck); | ||
/** | ||
* Public properties | ||
* ***************** | ||
*/ | ||
/** | ||
* @public | ||
* @static | ||
* @type {AutoScroller} | ||
*/ | ||
ItemDrag.autoScroller = new AutoScroller(); | ||
/** | ||
* Public static methods | ||
@@ -124,5 +147,5 @@ * ********************* | ||
* @public | ||
* @memberof ItemDrag | ||
* @static | ||
* @param {Item} item | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
* @param {Object} [options] | ||
@@ -132,8 +155,25 @@ * - An optional options object which can be used to pass the predicate | ||
* from the grid's settings. | ||
* @returns {Boolean} | ||
* @returns {(Boolean|undefined)} | ||
*/ | ||
ItemDrag.defaultStartPredicate = function(item, event, options) { | ||
ItemDrag.defaultStartPredicate = function (item, event, options) { | ||
var drag = item._drag; | ||
var predicate = drag._startPredicateData || drag._setupStartPredicate(options); | ||
// Make sure left button is pressed on mouse. | ||
if (event.isFirst && event.srcEvent.button) { | ||
return false; | ||
} | ||
// If the start event is trusted, non-cancelable and it's default action has | ||
// not been prevented it is in most cases a sign that the gesture would be | ||
// cancelled anyways right after it has started (e.g. starting drag while | ||
// the page is scrolling). | ||
if ( | ||
event.isFirst && | ||
event.srcEvent.isTrusted === true && | ||
event.srcEvent.defaultPrevented === false && | ||
event.srcEvent.cancelable === false | ||
) { | ||
return false; | ||
} | ||
// Final event logic. At this stage return value does not matter anymore, | ||
@@ -148,9 +188,10 @@ // the predicate is either resolved or it's not and there's nothing to do | ||
// 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; | ||
// Setup predicate data from options if not already set. | ||
var predicate = drag._startPredicateData; | ||
if (!predicate) { | ||
var config = options || drag._getGrid()._settings.dragStartPredicate || {}; | ||
drag._startPredicateData = predicate = { | ||
distance: Math.max(config.distance, 0) || 0, | ||
delay: Math.max(config.delay, 0) || 0, | ||
}; | ||
} | ||
@@ -163,3 +204,3 @@ | ||
if (!predicate.delayTimer) { | ||
predicate.delayTimer = window.setTimeout(function() { | ||
predicate.delayTimer = window.setTimeout(function () { | ||
predicate.delay = 0; | ||
@@ -181,3 +222,3 @@ if (drag._resolveStartPredicate(predicate.event)) { | ||
* @public | ||
* @memberof ItemDrag | ||
* @static | ||
* @param {Item} item | ||
@@ -187,11 +228,13 @@ * @param {Object} [options] | ||
* @param {String} [options.action='move'] | ||
* @returns {(Boolean|DragSortCommand)} | ||
* - Returns false if no valid index was found. Otherwise returns drag sort | ||
* @returns {?Object} | ||
* - Returns `null` if no valid index was found. Otherwise returns drag sort | ||
* command. | ||
*/ | ||
ItemDrag.defaultSortPredicate = (function() { | ||
ItemDrag.defaultSortPredicate = (function () { | ||
var itemRect = {}; | ||
var targetRect = {}; | ||
var returnData = {}; | ||
var rootGridArray = []; | ||
var gridsArray = []; | ||
var minThreshold = 1; | ||
var maxThreshold = 100; | ||
@@ -205,2 +248,8 @@ function getTargetGrid(item, rootGrid, threshold) { | ||
var grid; | ||
var container; | ||
var containerRect; | ||
var left; | ||
var top; | ||
var right; | ||
var bottom; | ||
var i; | ||
@@ -210,5 +259,5 @@ | ||
if (dragSort === true) { | ||
rootGridArray[0] = rootGrid; | ||
grids = rootGridArray; | ||
} else { | ||
gridsArray[0] = rootGrid; | ||
grids = gridsArray; | ||
} else if (isFunction(dragSort)) { | ||
grids = dragSort.call(rootGrid, item); | ||
@@ -218,3 +267,5 @@ } | ||
// Return immediately if there are no grids. | ||
if (!Array.isArray(grids)) return target; | ||
if (!grids || !Array.isArray(grids) || !grids.length) { | ||
return target; | ||
} | ||
@@ -228,12 +279,49 @@ // Loop through the grids and get the best match. | ||
// We need to update the grid's offsets and dimensions since they might | ||
// have changed (e.g during scrolling). | ||
// Compute the grid's client rect an clamp the initial boundaries to | ||
// viewport dimensions. | ||
grid._updateBoundingRect(); | ||
left = Math.max(0, grid._left); | ||
top = Math.max(0, grid._top); | ||
right = Math.min(window.innerWidth, grid._right); | ||
bottom = Math.min(window.innerHeight, grid._bottom); | ||
// The grid might be inside one or more elements that clip it's visibility | ||
// (e.g overflow scroll/hidden) so we want to find out the visible portion | ||
// of the grid in the viewport and use that in our calculations. | ||
container = grid._element.parentNode; | ||
while ( | ||
container && | ||
container !== document && | ||
container !== document.documentElement && | ||
container !== document.body | ||
) { | ||
if (container.getRootNode && container instanceof DocumentFragment) { | ||
container = container.getRootNode().host; | ||
continue; | ||
} | ||
if (getStyle(container, 'overflow') !== 'visible') { | ||
containerRect = container.getBoundingClientRect(); | ||
left = Math.max(left, containerRect.left); | ||
top = Math.max(top, containerRect.top); | ||
right = Math.min(right, containerRect.right); | ||
bottom = Math.min(bottom, containerRect.bottom); | ||
} | ||
if (getStyle(container, 'position') === 'fixed') { | ||
break; | ||
} | ||
container = container.parentNode; | ||
} | ||
// No need to go further if target rect does not have visible area. | ||
if (left >= right || top >= bottom) continue; | ||
// 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); | ||
targetRect.left = left; | ||
targetRect.top = top; | ||
targetRect.width = right - left; | ||
targetRect.height = bottom - top; | ||
gridScore = getIntersectionScore(itemRect, targetRect); | ||
@@ -247,4 +335,4 @@ // Check if this grid is the best match so far. | ||
// Always reset root grid array. | ||
rootGridArray.length = 0; | ||
// Always reset grids array. | ||
gridsArray.length = 0; | ||
@@ -254,3 +342,3 @@ return target; | ||
return function(item, options) { | ||
return function (item, options) { | ||
var drag = item._drag; | ||
@@ -261,9 +349,16 @@ var rootGrid = drag._getGrid(); | ||
var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50; | ||
var sortAction = options && options.action === actionSwap ? actionSwap : actionMove; | ||
var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; | ||
var migrateAction = | ||
options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; | ||
// Sort threshold must be a positive number capped to a max value of 100. If | ||
// that's not the case this function will not work correctly. So let's clamp | ||
// the threshold just in case. | ||
sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold); | ||
// Populate item rect data. | ||
itemRect.width = item._width; | ||
itemRect.height = item._height; | ||
itemRect.left = drag._elementClientX; | ||
itemRect.top = drag._elementClientY; | ||
itemRect.left = drag._clientX; | ||
itemRect.top = drag._clientY; | ||
@@ -275,9 +370,10 @@ // Calculate the target grid. | ||
// dragged item enough. | ||
if (!grid) return false; | ||
if (!grid) return null; | ||
var isMigration = item.getGrid() !== grid; | ||
var gridOffsetLeft = 0; | ||
var gridOffsetTop = 0; | ||
var matchScore = -1; | ||
var matchIndex; | ||
var hasValidTargets; | ||
var matchScore = 0; | ||
var matchIndex = -1; | ||
var hasValidTargets = false; | ||
var target; | ||
@@ -317,3 +413,3 @@ var score; | ||
targetRect.top = target._top + target._marginTop + gridOffsetTop; | ||
score = getRectOverlapScore(itemRect, targetRect); | ||
score = getIntersectionScore(itemRect, targetRect); | ||
@@ -328,7 +424,14 @@ // Update best match index and score if the target's overlap score with | ||
// 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; | ||
// If there is no valid match and the dragged item is being moved into | ||
// another grid we need to do some guess work here. If there simply are no | ||
// valid targets (which means that the dragged item will be the only active | ||
// item in the new grid) we can just add it as the first item. If we have | ||
// valid items in the new grid and the dragged item is overlapping one or | ||
// more of the items in the new grid let's make an exception with the | ||
// threshold and just pick the item which the dragged item is overlapping | ||
// most. However, if the dragged item is not overlapping any of the valid | ||
// items in the new grid let's position it as the last item in the grid. | ||
if (isMigration && matchScore < sortThreshold) { | ||
matchIndex = hasValidTargets ? matchIndex : 0; | ||
matchScore = sortThreshold; | ||
} | ||
@@ -340,7 +443,7 @@ | ||
returnData.index = matchIndex; | ||
returnData.action = sortAction; | ||
returnData.action = isMigration ? migrateAction : sortAction; | ||
return returnData; | ||
} | ||
return false; | ||
return null; | ||
}; | ||
@@ -358,12 +461,6 @@ })(); | ||
* @public | ||
* @memberof ItemDrag.prototype | ||
* @returns {ItemDrag} | ||
*/ | ||
ItemDrag.prototype.stop = function() { | ||
var item = this._item; | ||
var element = item._element; | ||
var grid = this._getGrid(); | ||
ItemDrag.prototype.stop = function () { | ||
if (!this._isActive) return; | ||
if (!this._isActive) return this; | ||
// If the item is being dropped into another grid, finish it up and return | ||
@@ -373,29 +470,59 @@ // immediately. | ||
this._finishMigration(); | ||
return this; | ||
return; | ||
} | ||
// Cancel queued move and scroll ticks. | ||
cancelMoveTick(item._id); | ||
cancelScrollTick(item._id); | ||
// Cancel queued ticks. | ||
var itemId = this._item._id; | ||
cancelDragStartTick(itemId); | ||
cancelDragMoveTick(itemId); | ||
cancelDragScrollTick(itemId); | ||
// Remove scroll listeners. | ||
this._unbindScrollListeners(); | ||
// Cancel sort procedure. | ||
this._cancelSort(); | ||
// Cancel overlap check. | ||
this._checkOverlapDebounce('cancel'); | ||
if (this._isStarted) { | ||
// Remove scroll listeners. | ||
this._unbindScrollListeners(); | ||
// 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); | ||
var element = item._element; | ||
var grid = this._getGrid(); | ||
var draggingClass = grid._settings.itemDraggingClass; | ||
// 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); | ||
item._setTranslate(this._gridX, this._gridY); | ||
// We need to do forced reflow to make sure the dragging class is removed | ||
// gracefully. | ||
// eslint-disable-next-line | ||
if (draggingClass) element.clientWidth; | ||
} | ||
// Remove dragging class. | ||
removeClass(element, draggingClass); | ||
} | ||
// Remove dragging class. | ||
removeClass(element, grid._settings.itemDraggingClass); | ||
// Reset drag data. | ||
this._reset(); | ||
}; | ||
return this; | ||
/** | ||
* Manually trigger drag sort. This is only needed for special edge cases where | ||
* e.g. you have disabled sort and want to trigger a sort right after enabling | ||
* it (and don't want to wait for the next move/scroll event). | ||
* | ||
* @private | ||
* @param {Boolean} [force=false] | ||
*/ | ||
ItemDrag.prototype.sort = function (force) { | ||
var item = this._item; | ||
if (item._isActive && this._dragMoveEvent) { | ||
if (force === true) { | ||
this._handleSort(); | ||
} else { | ||
addDragSortTick(item._id, this._handleSort); | ||
} | ||
} | ||
}; | ||
@@ -407,11 +534,9 @@ | ||
* @public | ||
* @memberof ItemDrag.prototype | ||
* @returns {ItemDrag} | ||
*/ | ||
ItemDrag.prototype.destroy = function() { | ||
if (this._isDestroyed) return this; | ||
ItemDrag.prototype.destroy = function () { | ||
if (this._isDestroyed) return; | ||
this.stop(); | ||
this._dragger.destroy(); | ||
ItemDrag.autoScroller.removeItem(this._item); | ||
this._isDestroyed = true; | ||
return this; | ||
}; | ||
@@ -428,7 +553,6 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @returns {?Grid} | ||
*/ | ||
ItemDrag.prototype._getGrid = function() { | ||
return gridInstances[this._gridId] || null; | ||
ItemDrag.prototype._getGrid = function () { | ||
return GRID_INSTANCES[this._gridId] || null; | ||
}; | ||
@@ -440,7 +564,6 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._reset = function() { | ||
// Is item being dragged? | ||
ItemDrag.prototype._reset = function () { | ||
this._isActive = false; | ||
this._isStarted = false; | ||
@@ -454,3 +577,5 @@ // The dragged item's container element. | ||
// Drag/scroll event data. | ||
this._dragEvent = null; | ||
this._dragStartEvent = null; | ||
this._dragMoveEvent = null; | ||
this._dragPrevMoveEvent = null; | ||
this._scrollEvent = null; | ||
@@ -472,5 +597,13 @@ | ||
// not account for element's margins. | ||
this._elementClientX = 0; | ||
this._elementClientY = 0; | ||
this._clientX = 0; | ||
this._clientY = 0; | ||
// Keep track of the clientX/Y diff for scrolling. | ||
this._scrollDiffX = 0; | ||
this._scrollDiffY = 0; | ||
// Keep track of the clientX/Y diff for moving. | ||
this._moveDiffX = 0; | ||
this._moveDiffY = 0; | ||
// Offset difference between the dragged element's temporary drag | ||
@@ -487,5 +620,4 @@ // container and it's original container. | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._bindScrollListeners = function() { | ||
ItemDrag.prototype._bindScrollListeners = function () { | ||
var gridContainer = this._getGrid()._element; | ||
@@ -499,3 +631,3 @@ var dragContainer = this._container; | ||
scrollers.length = 0; | ||
getScrollableAncestors(this._item._element, false, scrollers); | ||
getScrollableAncestors(this._item._element.parentNode, scrollers); | ||
@@ -507,3 +639,3 @@ // If drag container is defined and it's not the same element as grid | ||
gridScrollers = []; | ||
getScrollableAncestors(gridContainer, true, gridScrollers); | ||
getScrollableAncestors(gridContainer, gridScrollers); | ||
for (i = 0; i < gridScrollers.length; i++) { | ||
@@ -518,3 +650,3 @@ if (scrollers.indexOf(gridScrollers[i]) < 0) { | ||
for (i = 0; i < scrollers.length; i++) { | ||
scrollers[i].addEventListener('scroll', this._onScroll); | ||
scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); | ||
} | ||
@@ -528,5 +660,4 @@ }; | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._unbindScrollListeners = function() { | ||
ItemDrag.prototype._unbindScrollListeners = function () { | ||
var scrollers = this._scrollers; | ||
@@ -536,3 +667,3 @@ var i; | ||
for (i = 0; i < scrollers.length; i++) { | ||
scrollers[i].removeEventListener('scroll', this._onScroll); | ||
scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS); | ||
} | ||
@@ -544,43 +675,2 @@ | ||
/** | ||
* 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 {DraggerEvent} 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.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 | ||
@@ -590,32 +680,10 @@ * elements of the dragged element and the drag container element. | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
* @returns {Boolean} | ||
*/ | ||
ItemDrag.prototype._resolveStartPredicate = function(event) { | ||
ItemDrag.prototype._resolveStartPredicate = function (event) { | ||
var predicate = this._startPredicateData; | ||
// 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. | ||
var handleRect = predicate.handleElement.getBoundingClientRect(); | ||
var handleLeft = handleRect.left + (window.pageXOffset || 0); | ||
var handleTop = handleRect.top + (window.pageYOffset || 0); | ||
var handleWidth = handleRect.width; | ||
var handleHeight = handleRect.height; | ||
// Reset predicate data. | ||
this._resetStartPredicate(); | ||
// If the cursor is still within the handle let's start the drag. | ||
return ( | ||
handleWidth && | ||
handleHeight && | ||
event.pageX >= handleLeft && | ||
event.pageX < handleLeft + handleWidth && | ||
event.pageY >= handleTop && | ||
event.pageY < handleTop + handleHeight | ||
); | ||
return true; | ||
}; | ||
@@ -627,8 +695,7 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._forceResolveStartPredicate = function(event) { | ||
if (!this._isDestroyed && this._startPredicateState === startPredicatePending) { | ||
this._startPredicateState = startPredicateResolved; | ||
ItemDrag.prototype._forceResolveStartPredicate = function (event) { | ||
if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) { | ||
this._startPredicateState = START_PREDICATE_RESOLVED; | ||
this._onStart(event); | ||
@@ -642,6 +709,5 @@ } | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._finishStartPredicate = function(event) { | ||
ItemDrag.prototype._finishStartPredicate = function (event) { | ||
var element = this._item._element; | ||
@@ -664,9 +730,9 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Number} x | ||
* @param {Number} y | ||
*/ | ||
ItemDrag.prototype._resetHeuristics = function(event) { | ||
this._hBlockedIndex = null; | ||
this._hX1 = this._hX2 = event.clientX; | ||
this._hY1 = this._hY2 = event.clientY; | ||
ItemDrag.prototype._resetHeuristics = function (x, y) { | ||
this._blockedSortIndex = null; | ||
this._sortX1 = this._sortX2 = x; | ||
this._sortY1 = this._sortY2 = y; | ||
}; | ||
@@ -679,7 +745,7 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Number} x | ||
* @param {Number} y | ||
* @returns {Boolean} | ||
*/ | ||
ItemDrag.prototype._checkHeuristics = function(event) { | ||
ItemDrag.prototype._checkHeuristics = function (x, y) { | ||
var settings = this._getGrid()._settings.dragSortHeuristics; | ||
@@ -690,10 +756,8 @@ var minDist = settings.minDragDistance; | ||
if (minDist <= 0) { | ||
this._hBlockedIndex = null; | ||
this._blockedSortIndex = null; | ||
return true; | ||
} | ||
var x = event.clientX; | ||
var y = event.clientY; | ||
var diffX = x - this._hX2; | ||
var diffY = y - this._hY2; | ||
var diffX = x - this._sortX2; | ||
var diffY = y - this._sortY2; | ||
@@ -704,3 +768,3 @@ // If we can't do proper bounce back check make sure that the blocked index | ||
if (!canCheckBounceBack) { | ||
this._hBlockedIndex = null; | ||
this._blockedSortIndex = null; | ||
} | ||
@@ -713,6 +777,6 @@ | ||
var angle = Math.atan2(diffX, diffY); | ||
var prevAngle = Math.atan2(this._hX2 - this._hX1, this._hY2 - this._hY1); | ||
var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1); | ||
var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle)); | ||
if (Math.abs(deltaAngle) > settings.minBounceBackAngle) { | ||
this._hBlockedIndex = null; | ||
this._blockedSortIndex = null; | ||
} | ||
@@ -722,6 +786,6 @@ } | ||
// Update points. | ||
this._hX1 = this._hX2; | ||
this._hY1 = this._hY2; | ||
this._hX2 = x; | ||
this._hY2 = y; | ||
this._sortX1 = this._sortX2; | ||
this._sortY1 = this._sortY2; | ||
this._sortX2 = x; | ||
this._sortY2 = y; | ||
@@ -738,5 +802,4 @@ return true; | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._resetStartPredicate = function() { | ||
ItemDrag.prototype._resetStartPredicate = function () { | ||
var predicate = this._startPredicateData; | ||
@@ -752,2 +815,86 @@ if (predicate) { | ||
/** | ||
* Handle the sorting procedure. Manage drag sort heuristics/interval and | ||
* check overlap when necessary. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._handleSort = function () { | ||
var settings = this._getGrid()._settings; | ||
// No sorting when drag sort is disabled. Also, account for the scenario where | ||
// dragSort is temporarily disabled during drag procedure so we need to reset | ||
// sort timer heuristics state too. | ||
if ( | ||
!settings.dragSort || | ||
(!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item)) | ||
) { | ||
this._sortX1 = this._sortX2 = this._gridX; | ||
this._sortY1 = this._sortY2 = this._gridY; | ||
// We set this to true intentionally so that overlap check would be | ||
// triggered as soon as possible after sort becomes enabled again. | ||
this._isSortNeeded = true; | ||
if (this._sortTimer !== undefined) { | ||
this._sortTimer = window.clearTimeout(this._sortTimer); | ||
} | ||
return; | ||
} | ||
// If sorting is enabled we always need to run the heuristics check to keep | ||
// the tracked coordinates updated. We also allow an exception when the sort | ||
// timer is finished because the heuristics are intended to prevent overlap | ||
// checks based on the dragged element's immediate movement and a delayed | ||
// overlap check is valid if it comes through, because it was valid when it | ||
// was invoked. | ||
var shouldSort = this._checkHeuristics(this._gridX, this._gridY); | ||
if (!this._isSortNeeded && !shouldSort) return; | ||
var sortInterval = settings.dragSortHeuristics.sortInterval; | ||
if (sortInterval <= 0 || this._isSortNeeded) { | ||
this._isSortNeeded = false; | ||
if (this._sortTimer !== undefined) { | ||
this._sortTimer = window.clearTimeout(this._sortTimer); | ||
} | ||
this._checkOverlap(); | ||
} else if (this._sortTimer === undefined) { | ||
this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval); | ||
} | ||
}; | ||
/** | ||
* Delayed sort handler. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._handleSortDelayed = function () { | ||
this._isSortNeeded = true; | ||
this._sortTimer = undefined; | ||
addDragSortTick(this._item._id, this._handleSort); | ||
}; | ||
/** | ||
* Cancel and reset sort procedure. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._cancelSort = function () { | ||
this._isSortNeeded = false; | ||
if (this._sortTimer !== undefined) { | ||
this._sortTimer = window.clearTimeout(this._sortTimer); | ||
} | ||
cancelDragSortTick(this._item._id); | ||
}; | ||
/** | ||
* Handle the ending of the drag procedure for sorting. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._finishSort = function () { | ||
var isSortEnabled = this._getGrid()._settings.dragSort; | ||
var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined); | ||
this._cancelSort(); | ||
if (needsFinalCheck) this._checkOverlap(); | ||
}; | ||
/** | ||
* Check (during drag) if an item is overlapping other items and based on | ||
@@ -757,5 +904,4 @@ * the configuration layout the items. | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._checkOverlap = function() { | ||
ItemDrag.prototype._checkOverlap = function () { | ||
if (!this._isActive) return; | ||
@@ -770,2 +916,3 @@ | ||
var targetIndex; | ||
var targetItem; | ||
var sortAction; | ||
@@ -776,3 +923,3 @@ var isMigration; | ||
if (isFunction(settings.dragSortPredicate)) { | ||
result = settings.dragSortPredicate(item, this._dragEvent); | ||
result = settings.dragSortPredicate(item, this._dragMoveEvent); | ||
} else { | ||
@@ -785,2 +932,3 @@ result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate); | ||
sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE; | ||
currentGrid = item.getGrid(); | ||
@@ -790,7 +938,10 @@ targetGrid = result.grid || currentGrid; | ||
currentIndex = currentGrid._items.indexOf(item); | ||
targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration); | ||
sortAction = result.action === actionSwap ? actionSwap : actionMove; | ||
targetIndex = normalizeArrayIndex( | ||
targetGrid._items, | ||
result.index, | ||
isMigration && sortAction === ACTION_MOVE ? 1 : 0 | ||
); | ||
// Prevent position bounce. | ||
if (!isMigration && targetIndex === this._hBlockedIndex) { | ||
if (!isMigration && targetIndex === this._blockedSortIndex) { | ||
return; | ||
@@ -803,6 +954,6 @@ } | ||
if (currentIndex !== targetIndex) { | ||
this._hBlockedIndex = currentIndex; | ||
this._blockedSortIndex = currentIndex; | ||
// Do the sort. | ||
(sortAction === actionSwap ? arraySwap : arrayMove)( | ||
(sortAction === ACTION_SWAP ? arraySwap : arrayMove)( | ||
currentGrid._items, | ||
@@ -814,8 +965,8 @@ currentIndex, | ||
// Emit move event. | ||
if (currentGrid._hasListeners(eventMove)) { | ||
currentGrid._emit(eventMove, { | ||
if (currentGrid._hasListeners(EVENT_MOVE)) { | ||
currentGrid._emit(EVENT_MOVE, { | ||
item: item, | ||
fromIndex: currentIndex, | ||
toIndex: targetIndex, | ||
action: sortAction | ||
action: sortAction, | ||
}); | ||
@@ -831,7 +982,10 @@ } | ||
else { | ||
this._hBlockedIndex = null; | ||
this._blockedSortIndex = null; | ||
// Let's fetch the target item when it's still in it's original index. | ||
targetItem = targetGrid._items[targetIndex]; | ||
// Emit beforeSend event. | ||
if (currentGrid._hasListeners(eventBeforeSend)) { | ||
currentGrid._emit(eventBeforeSend, { | ||
if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) { | ||
currentGrid._emit(EVENT_BEFORE_SEND, { | ||
item: item, | ||
@@ -841,3 +995,3 @@ fromGrid: currentGrid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
@@ -847,4 +1001,4 @@ } | ||
// Emit beforeReceive event. | ||
if (targetGrid._hasListeners(eventBeforeReceive)) { | ||
targetGrid._emit(eventBeforeReceive, { | ||
if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { | ||
targetGrid._emit(EVENT_BEFORE_RECEIVE, { | ||
item: item, | ||
@@ -854,3 +1008,3 @@ fromGrid: currentGrid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
@@ -869,10 +1023,8 @@ } | ||
// 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. | ||
// Reset sort data. | ||
item._sortData = null; | ||
// Emit send event. | ||
if (currentGrid._hasListeners(eventSend)) { | ||
currentGrid._emit(eventSend, { | ||
if (currentGrid._hasListeners(EVENT_SEND)) { | ||
currentGrid._emit(EVENT_SEND, { | ||
item: item, | ||
@@ -882,3 +1034,3 @@ fromGrid: currentGrid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
@@ -888,4 +1040,4 @@ } | ||
// Emit receive event. | ||
if (targetGrid._hasListeners(eventReceive)) { | ||
targetGrid._emit(eventReceive, { | ||
if (targetGrid._hasListeners(EVENT_RECEIVE)) { | ||
targetGrid._emit(EVENT_RECEIVE, { | ||
item: item, | ||
@@ -895,6 +1047,23 @@ fromGrid: currentGrid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
} | ||
// If the sort action is "swap" let's respect it and send the target item | ||
// (if it exists) from the target grid to the originating grid. This process | ||
// is done on purpose after the dragged item placed within the target grid | ||
// so that we can keep this implementation as simple as possible utilizing | ||
// the existing API. | ||
if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) { | ||
// Sanity check to make sure that the target item is still part of the | ||
// target grid. It could have been manipulated in the event handlers. | ||
if (targetGrid._items.indexOf(targetItem) > -1) { | ||
targetGrid.send(targetItem, currentGrid, currentIndex, { | ||
appendTo: this._container || document.body, | ||
layoutSender: false, | ||
layoutReceiver: false, | ||
}); | ||
} | ||
} | ||
// Layout both grids. | ||
@@ -911,7 +1080,6 @@ currentGrid.layout(); | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._finishMigration = function() { | ||
ItemDrag.prototype._finishMigration = function () { | ||
var item = this._item; | ||
var release = item._release; | ||
var release = item._dragRelease; | ||
var element = item._element; | ||
@@ -925,2 +1093,6 @@ var isActive = item._isActive; | ||
var currentContainer = element.parentNode; | ||
var currentVisClass = isActive | ||
? currentSettings.itemVisibleClass | ||
: currentSettings.itemHiddenClass; | ||
var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; | ||
var translate; | ||
@@ -935,10 +1107,13 @@ var offsetDiff; | ||
// Remove current classnames. | ||
removeClass(element, currentSettings.itemClass); | ||
removeClass(element, currentSettings.itemVisibleClass); | ||
removeClass(element, currentSettings.itemHiddenClass); | ||
// Update item class. | ||
if (currentSettings.itemClass !== targetSettings.itemClass) { | ||
removeClass(element, currentSettings.itemClass); | ||
addClass(element, targetSettings.itemClass); | ||
} | ||
// Add new classnames. | ||
addClass(element, targetSettings.itemClass); | ||
addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); | ||
// Update visibility class. | ||
if (currentVisClass !== nextVisClass) { | ||
removeClass(element, currentVisClass); | ||
addClass(element, nextVisClass); | ||
} | ||
@@ -955,5 +1130,4 @@ // Move the item inside the target container if it's different than the | ||
// Update item's cached dimensions and sort data. | ||
// Update item's cached dimensions. | ||
item._refreshDimensions(); | ||
item._refreshSortData(); | ||
@@ -973,8 +1147,7 @@ // Calculate the offset difference between target's drag container (if any) | ||
if (targetContainer !== currentContainer) { | ||
element.style[transformProp] = getTranslateString(translate.x, translate.y); | ||
item._setTranslate(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); | ||
item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); | ||
@@ -989,19 +1162,20 @@ // Start the release. | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._preStartCheck = function(event) { | ||
ItemDrag.prototype._preStartCheck = function (event) { | ||
// Let's activate drag start predicate state. | ||
if (this._startPredicateState === startPredicateInactive) { | ||
this._startPredicateState = startPredicatePending; | ||
if (this._startPredicateState === START_PREDICATE_INACTIVE) { | ||
this._startPredicateState = START_PREDICATE_PENDING; | ||
} | ||
// If predicate is pending try to resolve it. | ||
if (this._startPredicateState === startPredicatePending) { | ||
if (this._startPredicateState === START_PREDICATE_PENDING) { | ||
this._startPredicateResult = this._startPredicate(this._item, event); | ||
if (this._startPredicateResult === true) { | ||
this._startPredicateState = startPredicateResolved; | ||
this._startPredicateState = START_PREDICATE_RESOLVED; | ||
this._onStart(event); | ||
} else if (this._startPredicateResult === false) { | ||
this._startPredicateState = startPredicateRejected; | ||
this._resetStartPredicate(event); | ||
this._dragger._reset(); | ||
this._startPredicateState = START_PREDICATE_INACTIVE; | ||
} | ||
@@ -1011,3 +1185,3 @@ } | ||
// Otherwise if predicate is resolved and drag is active, move the item. | ||
else if (this._startPredicateState === startPredicateResolved && this._isActive) { | ||
else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) { | ||
this._onMove(event); | ||
@@ -1021,8 +1195,6 @@ } | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._preEndCheck = function(event) { | ||
// Check if the start predicate was resolved during drag. | ||
var isResolved = this._startPredicateState === startPredicateResolved; | ||
ItemDrag.prototype._preEndCheck = function (event) { | ||
var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED; | ||
@@ -1034,7 +1206,11 @@ // Do final predicate check to allow user to unbind stuff for the current | ||
// Reset start predicate state. | ||
this._startPredicateState = startPredicateInactive; | ||
this._startPredicateState = START_PREDICATE_INACTIVE; | ||
// If predicate is resolved and dragging is active, call the end handler. | ||
if (isResolved && this._isActive) this._onEnd(event); | ||
if (!isResolved || !this._isActive) return; | ||
if (this._isStarted) { | ||
this._onEnd(event); | ||
} else { | ||
this.stop(); | ||
} | ||
}; | ||
@@ -1046,9 +1222,23 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._onStart = function(event) { | ||
ItemDrag.prototype._onStart = function (event) { | ||
var item = this._item; | ||
if (!item._isActive) return; | ||
// If item is not active, don't start the drag. | ||
this._isActive = true; | ||
this._dragStartEvent = event; | ||
ItemDrag.autoScroller.addItem(item); | ||
addDragStartTick(item._id, this._prepareStart, this._applyStart); | ||
}; | ||
/** | ||
* Prepare item to be dragged. | ||
* | ||
* @private | ||
* ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._prepareStart = function () { | ||
var item = this._item; | ||
if (!item._isActive) return; | ||
@@ -1059,71 +1249,75 @@ | ||
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 containingBlock = getContainingBlock(dragContainer); | ||
var translate = getTranslate(element); | ||
var currentLeft = translate.x; | ||
var currentTop = translate.y; | ||
var elementRect = element.getBoundingClientRect(); | ||
var hasDragContainer = dragContainer !== gridContainer; | ||
var offsetDiff; | ||
// Reset heuristics data. | ||
this._resetHeuristics(event); | ||
this._container = dragContainer; | ||
this._containingBlock = containingBlock; | ||
this._clientX = elementRect.left; | ||
this._clientY = elementRect.top; | ||
this._left = this._gridX = translate.x; | ||
this._top = this._gridY = translate.y; | ||
this._scrollDiffX = this._scrollDiffY = 0; | ||
this._moveDiffX = this._moveDiffY = 0; | ||
// If grid container is not the drag container, we need to calculate the | ||
// offset difference between grid container and drag container's containing | ||
// element. | ||
this._resetHeuristics(this._gridX, this._gridY); | ||
// If a specific drag container is set and it is different from the | ||
// grid's container element we store the offset between containers. | ||
if (hasDragContainer) { | ||
offsetDiff = getOffsetDiff(containingBlock, gridContainer); | ||
var offsetDiff = getOffsetDiff(containingBlock, gridContainer); | ||
this._containerDiffX = offsetDiff.left; | ||
this._containerDiffY = offsetDiff.top; | ||
} | ||
}; | ||
// Stop current positioning animation. | ||
/** | ||
* Start drag for the item. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._applyStart = function () { | ||
var item = this._item; | ||
if (!item._isActive) return; | ||
var grid = this._getGrid(); | ||
var element = item._element; | ||
var release = item._dragRelease; | ||
var migrate = item._migrate; | ||
var hasDragContainer = this._container !== grid._element; | ||
if (item.isPositioning()) { | ||
item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); | ||
item._layout.stop(true, this._left, this._top); | ||
} | ||
// Stop current migration animation. | ||
if (migrate._isActive) { | ||
currentLeft -= migrate._containerDiffX; | ||
currentTop -= migrate._containerDiffY; | ||
migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); | ||
this._left -= migrate._containerDiffX; | ||
this._top -= migrate._containerDiffY; | ||
this._gridX -= migrate._containerDiffX; | ||
this._gridY -= migrate._containerDiffY; | ||
migrate.stop(true, this._left, this._top); | ||
} | ||
// If item is being released reset release data. | ||
if (item.isReleasing()) release._reset(); | ||
if (item.isReleasing()) { | ||
release._reset(); | ||
} | ||
// Setup drag data. | ||
this._isActive = true; | ||
this._dragEvent = 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; | ||
// Create placeholder (if necessary). | ||
if (settings.dragPlaceholder.enabled) { | ||
if (grid._settings.dragPlaceholder.enabled) { | ||
item._dragPlaceholder.create(); | ||
} | ||
// Emit dragInit event. | ||
grid._emit(eventDragInit, item, event); | ||
this._isStarted = true; | ||
// If a specific drag container is set and it is different from the | ||
// grid's container element we need to cast some extra spells. | ||
grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent); | ||
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; | ||
if (element.parentNode === this._container) { | ||
this._gridX -= this._containerDiffX; | ||
this._gridY -= this._containerDiffY; | ||
} | ||
// Otherwise we need to append the element inside the correct container, | ||
@@ -1133,15 +1327,12 @@ // setup the actual drag position data and adjust the element's translate | ||
else { | ||
this._left = currentLeft + this._containerDiffX; | ||
this._top = currentTop + this._containerDiffY; | ||
dragContainer.appendChild(element); | ||
element.style[transformProp] = getTranslateString(this._left, this._top); | ||
this._left += this._containerDiffX; | ||
this._top += this._containerDiffY; | ||
this._container.appendChild(element); | ||
item._setTranslate(this._left, this._top); | ||
} | ||
} | ||
// Set drag class and bind scrollers. | ||
addClass(element, settings.itemDraggingClass); | ||
addClass(element, grid._settings.itemDraggingClass); | ||
this._bindScrollListeners(); | ||
// Emit dragStart event. | ||
grid._emit(eventDragStart, item, event); | ||
grid._emit(EVENT_DRAG_START, item, this._dragStartEvent); | ||
}; | ||
@@ -1153,9 +1344,7 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._onMove = function(event) { | ||
ItemDrag.prototype._onMove = function (event) { | ||
var item = this._item; | ||
// If item is not active, reset drag. | ||
if (!item._isActive) { | ||
@@ -1166,11 +1355,29 @@ this.stop(); | ||
this._dragMoveEvent = event; | ||
addDragMoveTick(item._id, this._prepareMove, this._applyMove); | ||
addDragSortTick(item._id, this._handleSort); | ||
}; | ||
/** | ||
* Prepare dragged item for moving. | ||
* | ||
* @private | ||
*/ | ||
ItemDrag.prototype._prepareMove = function () { | ||
var item = this._item; | ||
if (!item._isActive) return; | ||
var settings = this._getGrid()._settings; | ||
var axis = settings.dragAxis; | ||
var nextEvent = this._dragMoveEvent; | ||
var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent; | ||
// Update horizontal position data. | ||
if (axis !== 'y') { | ||
var xDiff = event.clientX - this._dragEvent.clientX; | ||
this._left += xDiff; | ||
this._gridX += xDiff; | ||
this._elementClientX += xDiff; | ||
var moveDiffX = nextEvent.clientX - prevEvent.clientX; | ||
this._left = this._left - this._moveDiffX + moveDiffX; | ||
this._gridX = this._gridX - this._moveDiffX + moveDiffX; | ||
this._clientX = this._clientX - this._moveDiffX + moveDiffX; | ||
this._moveDiffX = moveDiffX; | ||
} | ||
@@ -1180,50 +1387,25 @@ | ||
if (axis !== 'x') { | ||
var yDiff = event.clientY - this._dragEvent.clientY; | ||
this._top += yDiff; | ||
this._gridY += yDiff; | ||
this._elementClientY += yDiff; | ||
var moveDiffY = nextEvent.clientY - prevEvent.clientY; | ||
this._top = this._top - this._moveDiffY + moveDiffY; | ||
this._gridY = this._gridY - this._moveDiffY + moveDiffY; | ||
this._clientY = this._clientY - this._moveDiffY + moveDiffY; | ||
this._moveDiffY = moveDiffY; | ||
} | ||
// Update event data. | ||
this._dragEvent = event; | ||
// Do move prepare/apply handling in the next tick. | ||
addMoveTick(item._id, this._prepareMove, this._applyMove); | ||
this._dragPrevMoveEvent = nextEvent; | ||
}; | ||
/** | ||
* 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) { | ||
if (this._checkHeuristics(this._dragEvent)) { | ||
this._checkOverlapDebounce(); | ||
} | ||
} | ||
}; | ||
/** | ||
* Apply movement to dragged item. | ||
* | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._applyMove = function() { | ||
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._dragEvent); | ||
this._moveDiffX = this._moveDiffY = 0; | ||
item._setTranslate(this._left, this._top); | ||
this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent); | ||
ItemDrag.autoScroller.updateItem(item); | ||
}; | ||
@@ -1235,9 +1417,7 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {Event} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._onScroll = function(event) { | ||
ItemDrag.prototype._onScroll = function (event) { | ||
var item = this._item; | ||
// If item is not active, reset drag. | ||
if (!item._isActive) { | ||
@@ -1248,7 +1428,5 @@ this.stop(); | ||
// Update last scroll event. | ||
this._scrollEvent = event; | ||
// Do scroll prepare/apply handling in the next tick. | ||
addScrollTick(item._id, this._prepareScroll, this._applyScroll); | ||
addDragScrollTick(item._id, this._prepareScroll, this._applyScroll); | ||
addDragSortTick(item._id, this._handleSort); | ||
}; | ||
@@ -1260,5 +1438,4 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._prepareScroll = function() { | ||
ItemDrag.prototype._prepareScroll = function () { | ||
var item = this._item; | ||
@@ -1271,15 +1448,11 @@ | ||
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 axis = grid._settings.dragAxis; | ||
var moveX = axis !== 'y'; | ||
var moveY = axis !== 'x'; | ||
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); | ||
var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); | ||
this._containerDiffX = offsetDiff.left; | ||
@@ -1290,15 +1463,18 @@ this._containerDiffY = offsetDiff.top; | ||
// Update horizontal position data. | ||
if (axis !== 'y') { | ||
this._left += xDiff; | ||
this._gridX = this._left - this._containerDiffX; | ||
if (moveX) { | ||
var scrollDiffX = this._clientX - this._moveDiffX - this._scrollDiffX - rect.left; | ||
this._left = this._left - this._scrollDiffX + scrollDiffX; | ||
this._scrollDiffX = scrollDiffX; | ||
} | ||
// Update vertical position data. | ||
if (axis !== 'x') { | ||
this._top += yDiff; | ||
this._gridY = this._top - this._containerDiffY; | ||
if (moveY) { | ||
var scrollDiffY = this._clientY - this._moveDiffY - this._scrollDiffY - rect.top; | ||
this._top = this._top - this._scrollDiffY + scrollDiffY; | ||
this._scrollDiffY = scrollDiffY; | ||
} | ||
// Overlap handling. | ||
if (settings.dragSort) this._checkOverlapDebounce(); | ||
// Update grid position. | ||
this._gridX = this._left - this._containerDiffX; | ||
this._gridY = this._top - this._containerDiffY; | ||
}; | ||
@@ -1310,15 +1486,10 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
*/ | ||
ItemDrag.prototype._applyScroll = function() { | ||
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._scrollEvent); | ||
this._scrollDiffX = this._scrollDiffY = 0; | ||
item._setTranslate(this._left, this._top); | ||
this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent); | ||
}; | ||
@@ -1330,6 +1501,5 @@ | ||
* @private | ||
* @memberof ItemDrag.prototype | ||
* @param {DraggerEvent} event | ||
* @param {Object} event | ||
*/ | ||
ItemDrag.prototype._onEnd = function(event) { | ||
ItemDrag.prototype._onEnd = function (event) { | ||
var item = this._item; | ||
@@ -1339,3 +1509,3 @@ var element = item._element; | ||
var settings = grid._settings; | ||
var release = item._release; | ||
var release = item._dragRelease; | ||
@@ -1348,8 +1518,9 @@ // If item is not active, reset drag. | ||
// Cancel queued move and scroll ticks. | ||
cancelMoveTick(item._id); | ||
cancelScrollTick(item._id); | ||
// Cancel queued ticks. | ||
cancelDragStartTick(item._id); | ||
cancelDragMoveTick(item._id); | ||
cancelDragScrollTick(item._id); | ||
// Finish currently queued overlap check. | ||
settings.dragSort && this._checkOverlapDebounce('finish'); | ||
// Finish sort procedure (does final overlap check if needed). | ||
this._finishSort(); | ||
@@ -1369,4 +1540,7 @@ // Remove scroll listeners. | ||
// Stop auto-scroll. | ||
ItemDrag.autoScroller.removeItem(item); | ||
// Emit dragEnd event. | ||
grid._emit(eventDragEnd, item, event); | ||
grid._emit(EVENT_DRAG_END, item, event); | ||
@@ -1383,31 +1557,2 @@ // Finish up the migration process or start the release process. | ||
/** | ||
* 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; | ||
} | ||
/** | ||
* Check if an element is an anchor element and open the href url if possible. | ||
@@ -1414,0 +1559,0 @@ * |
@@ -7,7 +7,17 @@ /** | ||
import { addPlaceholderTick, cancelPlaceholderTick } from '../ticker'; | ||
import { | ||
addPlaceholderLayoutTick, | ||
cancelPlaceholderLayoutTick, | ||
addPlaceholderResizeTick, | ||
cancelPlaceholderResizeTick, | ||
} from '../ticker'; | ||
import { eventBeforeSend, eventDragReleaseEnd, eventLayoutStart } from '../shared'; | ||
import { | ||
EVENT_BEFORE_SEND, | ||
EVENT_DRAG_RELEASE_END, | ||
EVENT_LAYOUT_START, | ||
EVENT_HIDE_START, | ||
} from '../constants'; | ||
import ItemAnimate from '../Item/ItemAnimate'; | ||
import Animator from '../Animator/Animator'; | ||
@@ -20,2 +30,3 @@ import addClass from '../utils/addClass'; | ||
import removeClass from '../utils/removeClass'; | ||
import transformProp from '../utils/transformProp'; | ||
@@ -30,3 +41,3 @@ /** | ||
this._item = item; | ||
this._animate = new ItemAnimate(); | ||
this._animation = new Animator(); | ||
this._element = null; | ||
@@ -36,6 +47,8 @@ this._className = ''; | ||
this._resetAfterLayout = false; | ||
this._currentLeft = 0; | ||
this._currentTop = 0; | ||
this._nextLeft = 0; | ||
this._nextTop = 0; | ||
this._left = 0; | ||
this._top = 0; | ||
this._transX = 0; | ||
this._transY = 0; | ||
this._nextTransX = 0; | ||
this._nextTransY = 0; | ||
@@ -45,2 +58,3 @@ // Bind animation handlers. | ||
this._startAnimation = this._startAnimation.bind(this); | ||
this._updateDimensions = this._updateDimensions.bind(this); | ||
@@ -52,2 +66,3 @@ // Bind event handlers. | ||
this._onMigrate = this._onMigrate.bind(this); | ||
this._onHide = this._onHide.bind(this); | ||
} | ||
@@ -61,19 +76,42 @@ | ||
/** | ||
* Update placeholder's dimensions to match the item's dimensions. | ||
* | ||
* @private | ||
*/ | ||
ItemDragPlaceholder.prototype._updateDimensions = function () { | ||
if (!this.isActive()) return; | ||
setStyles(this._element, { | ||
width: this._item._width + 'px', | ||
height: this._item._height + 'px', | ||
}); | ||
}; | ||
/** | ||
* Move placeholder to a new position. | ||
* | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
* @param {Item[]} items | ||
* @param {Boolean} isInstant | ||
*/ | ||
ItemDragPlaceholder.prototype._onLayoutStart = function() { | ||
ItemDragPlaceholder.prototype._onLayoutStart = function (items, isInstant) { | ||
var item = this._item; | ||
var grid = item.getGrid(); | ||
// Find out the item's new (unapplied) left and top position. | ||
var itemIndex = grid._items.indexOf(item); | ||
var nextLeft = grid._layout.slots[itemIndex * 2]; | ||
var nextTop = grid._layout.slots[itemIndex * 2 + 1]; | ||
// If the item is not part of the layout anymore reset placeholder. | ||
if (items.indexOf(item) === -1) { | ||
this.reset(); | ||
return; | ||
} | ||
// If item's position did not change and the item did not migrate we can | ||
// safely skip layout. | ||
if (!this._didMigrate && item._left === nextLeft && item._top === nextTop) { | ||
var nextLeft = item._left; | ||
var nextTop = item._top; | ||
var currentLeft = this._left; | ||
var currentTop = this._top; | ||
// Keep track of item layout position. | ||
this._left = nextLeft; | ||
this._top = nextTop; | ||
// If item's position did not change, and the item did not migrate and the | ||
// layout is not instant and we can safely skip layout. | ||
if (!isInstant && !this._didMigrate && currentLeft === nextLeft && currentTop === nextTop) { | ||
return; | ||
@@ -85,19 +123,16 @@ } | ||
// next position. | ||
nextLeft += item._marginLeft; | ||
nextTop += item._marginTop; | ||
var nextX = nextLeft + item._marginLeft; | ||
var nextY = nextTop + item._marginTop; | ||
// Just snap to new position without any animations if no animation is | ||
// required or if placeholder moves between grids. | ||
var animEnabled = grid._settings.dragPlaceholder.duration > 0; | ||
var grid = item.getGrid(); | ||
var animEnabled = !isInstant && grid._settings.layoutDuration > 0; | ||
if (!animEnabled || this._didMigrate) { | ||
// Cancel potential (queued) layout tick. | ||
cancelPlaceholderTick(item._id); | ||
cancelPlaceholderLayoutTick(item._id); | ||
// Snap placeholder to correct position. | ||
var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; | ||
if (this._animate.isAnimating()) { | ||
this._animate.stop(targetStyles); | ||
} else { | ||
setStyles(this._element, targetStyles); | ||
} | ||
this._element.style[transformProp] = getTranslateString(nextX, nextY); | ||
this._animation.stop(); | ||
@@ -115,5 +150,5 @@ // Move placeholder inside correct container after migration. | ||
// avoid layout thrashing. | ||
this._nextLeft = nextLeft; | ||
this._nextTop = nextTop; | ||
addPlaceholderTick(item._id, this._setupAnimation, this._startAnimation); | ||
this._nextTransX = nextX; | ||
this._nextTransY = nextY; | ||
addPlaceholderLayoutTick(item._id, this._setupAnimation, this._startAnimation); | ||
}; | ||
@@ -125,10 +160,9 @@ | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype._setupAnimation = function() { | ||
ItemDragPlaceholder.prototype._setupAnimation = function () { | ||
if (!this.isActive()) return; | ||
var translate = getTranslate(this._element); | ||
this._currentLeft = translate.x; | ||
this._currentTop = translate.y; | ||
this._transX = translate.x; | ||
this._transY = translate.y; | ||
}; | ||
@@ -140,18 +174,19 @@ | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype._startAnimation = function() { | ||
ItemDragPlaceholder.prototype._startAnimation = function () { | ||
if (!this.isActive()) return; | ||
var animation = this._animate; | ||
var currentLeft = this._currentLeft; | ||
var currentTop = this._currentTop; | ||
var nextLeft = this._nextLeft; | ||
var nextTop = this._nextTop; | ||
var targetStyles = { transform: getTranslateString(nextLeft, nextTop) }; | ||
var animation = this._animation; | ||
var currentX = this._transX; | ||
var currentY = this._transY; | ||
var nextX = this._nextTransX; | ||
var nextY = this._nextTransY; | ||
// If placeholder is already in correct position let's just stop animation | ||
// and be done with it. | ||
if (currentLeft === nextLeft && currentTop === nextTop) { | ||
if (animation.isAnimating()) animation.stop(targetStyles); | ||
if (currentX === nextX && currentY === nextY) { | ||
if (animation.isAnimating()) { | ||
this._element.style[transformProp] = getTranslateString(nextX, nextY); | ||
animation.stop(); | ||
} | ||
return; | ||
@@ -161,8 +196,11 @@ } | ||
// Otherwise let's start the animation. | ||
var settings = this._item.getGrid()._settings.dragPlaceholder; | ||
var currentStyles = { transform: getTranslateString(currentLeft, currentTop) }; | ||
var settings = this._item.getGrid()._settings; | ||
var currentStyles = {}; | ||
var targetStyles = {}; | ||
currentStyles[transformProp] = getTranslateString(currentX, currentY); | ||
targetStyles[transformProp] = getTranslateString(nextX, nextY); | ||
animation.start(currentStyles, targetStyles, { | ||
duration: settings.duration, | ||
easing: settings.easing, | ||
onFinish: this._onLayoutEnd | ||
duration: settings.layoutDuration, | ||
easing: settings.layoutEasing, | ||
onFinish: this._onLayoutEnd, | ||
}); | ||
@@ -175,5 +213,4 @@ }; | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype._onLayoutEnd = function() { | ||
ItemDragPlaceholder.prototype._onLayoutEnd = function () { | ||
if (this._resetAfterLayout) { | ||
@@ -189,9 +226,8 @@ this.reset(); | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
* @param {Item} item | ||
*/ | ||
ItemDragPlaceholder.prototype._onReleaseEnd = function(item) { | ||
ItemDragPlaceholder.prototype._onReleaseEnd = function (item) { | ||
if (item._id === this._item._id) { | ||
// If the placeholder is not animating anymore we can safely reset it. | ||
if (!this._animate.isAnimating()) { | ||
if (!this._animation.isAnimating()) { | ||
this.reset(); | ||
@@ -212,3 +248,2 @@ return; | ||
* @private | ||
* @memberof ItemDragPlaceholder.prototype | ||
* @param {Object} data | ||
@@ -221,3 +256,3 @@ * @param {Item} data.item | ||
*/ | ||
ItemDragPlaceholder.prototype._onMigrate = function(data) { | ||
ItemDragPlaceholder.prototype._onMigrate = function (data) { | ||
// Make sure we have a matching item. | ||
@@ -230,10 +265,12 @@ if (data.item !== this._item) return; | ||
// Unbind listeners from current grid. | ||
grid.off(eventDragReleaseEnd, this._onReleaseEnd); | ||
grid.off(eventLayoutStart, this._onLayoutStart); | ||
grid.off(eventBeforeSend, this._onMigrate); | ||
grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); | ||
grid.off(EVENT_LAYOUT_START, this._onLayoutStart); | ||
grid.off(EVENT_BEFORE_SEND, this._onMigrate); | ||
grid.off(EVENT_HIDE_START, this._onHide); | ||
// Bind listeners to the next grid. | ||
nextGrid.on(eventDragReleaseEnd, this._onReleaseEnd); | ||
nextGrid.on(eventLayoutStart, this._onLayoutStart); | ||
nextGrid.on(eventBeforeSend, this._onMigrate); | ||
nextGrid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); | ||
nextGrid.on(EVENT_LAYOUT_START, this._onLayoutStart); | ||
nextGrid.on(EVENT_BEFORE_SEND, this._onMigrate); | ||
nextGrid.on(EVENT_HIDE_START, this._onHide); | ||
@@ -245,2 +282,12 @@ // Mark the item as migrated. | ||
/** | ||
* Reset placeholder if the associated item is hidden. | ||
* | ||
* @private | ||
* @param {Item[]} items | ||
*/ | ||
ItemDragPlaceholder.prototype._onHide = function (items) { | ||
if (items.indexOf(this._item) > -1) this.reset(); | ||
}; | ||
/** | ||
* Public prototype methods | ||
@@ -256,5 +303,4 @@ * ************************ | ||
* @public | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype.create = function() { | ||
ItemDragPlaceholder.prototype.create = function () { | ||
// If we already have placeholder set up we can skip the initiation logic. | ||
@@ -269,4 +315,8 @@ if (this.isActive()) { | ||
var settings = grid._settings; | ||
var animation = this._animate; | ||
var animation = this._animation; | ||
// Keep track of layout position. | ||
this._left = item._left; | ||
this._top = item._top; | ||
// Create placeholder element. | ||
@@ -277,3 +327,3 @@ var element; | ||
} else { | ||
element = window.document.createElement('div'); | ||
element = document.createElement('div'); | ||
} | ||
@@ -291,19 +341,22 @@ this._element = element; | ||
// Position the placeholder item correctly. | ||
var left = item._left + item._marginLeft; | ||
var top = item._top + item._marginTop; | ||
// Set initial styles. | ||
setStyles(element, { | ||
display: 'block', | ||
position: 'absolute', | ||
left: '0', | ||
top: '0', | ||
left: '0px', | ||
top: '0px', | ||
width: item._width + 'px', | ||
height: item._height + 'px', | ||
transform: getTranslateString(left, top) | ||
}); | ||
// Set initial position. | ||
element.style[transformProp] = getTranslateString( | ||
item._left + item._marginLeft, | ||
item._top + item._marginTop | ||
); | ||
// Bind event listeners. | ||
grid.on(eventLayoutStart, this._onLayoutStart); | ||
grid.on(eventDragReleaseEnd, this._onReleaseEnd); | ||
grid.on(eventBeforeSend, this._onMigrate); | ||
grid.on(EVENT_LAYOUT_START, this._onLayoutStart); | ||
grid.on(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); | ||
grid.on(EVENT_BEFORE_SEND, this._onMigrate); | ||
grid.on(EVENT_HIDE_START, this._onHide); | ||
@@ -323,5 +376,4 @@ // onCreate hook. | ||
* @public | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype.reset = function() { | ||
ItemDragPlaceholder.prototype.reset = function () { | ||
if (!this.isActive()) return; | ||
@@ -333,3 +385,3 @@ | ||
var settings = grid._settings; | ||
var animation = this._animate; | ||
var animation = this._animation; | ||
@@ -340,3 +392,4 @@ // Reset flag. | ||
// Cancel potential (queued) layout tick. | ||
cancelPlaceholderTick(item._id); | ||
cancelPlaceholderLayoutTick(item._id); | ||
cancelPlaceholderResizeTick(item._id); | ||
@@ -348,5 +401,6 @@ // Reset animation instance. | ||
// Unbind event listeners. | ||
grid.off(eventDragReleaseEnd, this._onReleaseEnd); | ||
grid.off(eventLayoutStart, this._onLayoutStart); | ||
grid.off(eventBeforeSend, this._onMigrate); | ||
grid.off(EVENT_DRAG_RELEASE_END, this._onReleaseEnd); | ||
grid.off(EVENT_LAYOUT_START, this._onLayoutStart); | ||
grid.off(EVENT_BEFORE_SEND, this._onMigrate); | ||
grid.off(EVENT_HIDE_START, this._onHide); | ||
@@ -372,41 +426,44 @@ // Remove placeholder class from the placeholder element. | ||
/** | ||
* Update placeholder's dimensions. | ||
* Check if placeholder is currently active (visible). | ||
* | ||
* @public | ||
* @memberof ItemDragPlaceholder.prototype | ||
* @param {Number} width | ||
* @param {height} height | ||
* @returns {Boolean} | ||
*/ | ||
ItemDragPlaceholder.prototype.updateDimensions = function(width, height) { | ||
if (this.isActive()) { | ||
setStyles(this._element, { | ||
width: width + 'px', | ||
height: height + 'px' | ||
}); | ||
} | ||
ItemDragPlaceholder.prototype.isActive = function () { | ||
return !!this._element; | ||
}; | ||
/** | ||
* Check if placeholder is currently active (visible). | ||
* Get placeholder element. | ||
* | ||
* @public | ||
* @memberof ItemDragPlaceholder.prototype | ||
* @returns {Boolean} | ||
* @returns {?HTMLElement} | ||
*/ | ||
ItemDragPlaceholder.prototype.isActive = function() { | ||
return !!this._element; | ||
ItemDragPlaceholder.prototype.getElement = function () { | ||
return this._element; | ||
}; | ||
/** | ||
* Update placeholder's dimensions to match the item's dimensions. Note that | ||
* the updating is done asynchronously in the next tick to avoid layout | ||
* thrashing. | ||
* | ||
* @public | ||
*/ | ||
ItemDragPlaceholder.prototype.updateDimensions = function () { | ||
if (!this.isActive()) return; | ||
addPlaceholderResizeTick(this._item._id, this._updateDimensions); | ||
}; | ||
/** | ||
* Destroy placeholder instance. | ||
* | ||
* @public | ||
* @memberof ItemDragPlaceholder.prototype | ||
*/ | ||
ItemDragPlaceholder.prototype.destroy = function() { | ||
ItemDragPlaceholder.prototype.destroy = function () { | ||
this.reset(); | ||
this._animate.destroy(); | ||
this._item = this._animate = null; | ||
this._animation.destroy(); | ||
this._item = this._animation = null; | ||
}; | ||
export default ItemDragPlaceholder; |
@@ -9,3 +9,3 @@ /** | ||
import Queue from '../Queue/Queue'; | ||
import Animator from '../Animator/Animator'; | ||
@@ -17,6 +17,8 @@ import addClass from '../utils/addClass'; | ||
import removeClass from '../utils/removeClass'; | ||
import setStyles from '../utils/setStyles'; | ||
import transformProp from '../utils/transformProp'; | ||
var MIN_ANIMATION_DISTANCE = 2; | ||
/** | ||
* Layout manager for Item instance. | ||
* Layout manager for Item instance, handles the positioning of an item. | ||
* | ||
@@ -27,2 +29,5 @@ * @class | ||
function ItemLayout(item) { | ||
var element = item._element; | ||
var elementStyle = element.style; | ||
this._item = item; | ||
@@ -34,12 +39,21 @@ this._isActive = false; | ||
this._targetStyles = {}; | ||
this._currentLeft = 0; | ||
this._currentTop = 0; | ||
this._nextLeft = 0; | ||
this._nextTop = 0; | ||
this._offsetLeft = 0; | ||
this._offsetTop = 0; | ||
this._skipNextAnimation = false; | ||
this._animateOptions = { | ||
onFinish: this._finish.bind(this) | ||
this._animOptions = { | ||
onFinish: this._finish.bind(this), | ||
duration: 0, | ||
easing: 0, | ||
}; | ||
this._queue = new Queue(); | ||
// Set element's initial position styles. | ||
elementStyle.left = '0px'; | ||
elementStyle.top = '0px'; | ||
item._setTranslate(0, 0); | ||
this._animation = new Animator(element); | ||
this._queue = 'layout-' + item._id; | ||
// Bind animation handlers and finish method. | ||
@@ -59,26 +73,25 @@ this._setupAnimation = this._setupAnimation.bind(this); | ||
* @public | ||
* @memberof ItemLayout.prototype | ||
* @param {Boolean} [instant=false] | ||
* @param {Boolean} instant | ||
* @param {Function} [onFinish] | ||
* @returns {ItemLayout} | ||
*/ | ||
ItemLayout.prototype.start = function(instant, onFinish) { | ||
ItemLayout.prototype.start = function (instant, onFinish) { | ||
if (this._isDestroyed) return; | ||
var item = this._item; | ||
var element = item._element; | ||
var release = item._release; | ||
var release = item._dragRelease; | ||
var gridSettings = item.getGrid()._settings; | ||
var isPositioning = this._isActive; | ||
var isJustReleased = release._isActive && release._isPositioningStarted === false; | ||
var isJustReleased = release.isJustReleased(); | ||
var animDuration = isJustReleased | ||
? gridSettings.dragReleaseDuration | ||
? gridSettings.dragRelease.duration | ||
: gridSettings.layoutDuration; | ||
var animEasing = isJustReleased ? gridSettings.dragReleaseEasing : gridSettings.layoutEasing; | ||
var animEasing = isJustReleased ? gridSettings.dragRelease.easing : 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); | ||
// If the item is currently positioning cancel potential queued layout tick | ||
// and process current layout callback queue with interrupted flag on. | ||
if (isPositioning) { | ||
cancelLayoutTick(item._id); | ||
item._emitter.burst(this._queue, true, item); | ||
} | ||
@@ -89,26 +102,24 @@ // Mark release positioning as started. | ||
// Push the callback to the callback queue. | ||
if (isFunction(onFinish)) this._queue.add(onFinish); | ||
if (isFunction(onFinish)) { | ||
item._emitter.on(this._queue, onFinish); | ||
} | ||
// Reset animation skipping flag. | ||
this._skipNextAnimation = false; | ||
// If no animations are needed, easy peasy! | ||
if (!animEnabled) { | ||
this._updateOffsets(); | ||
this._updateTargetStyles(); | ||
isAnimating = item._animate.isAnimating(); | ||
this.stop(false, this._targetStyles); | ||
!isAnimating && setStyles(element, this._targetStyles); | ||
this._skipNextAnimation = false; | ||
return this._finish(); | ||
item._setTranslate(this._nextLeft, this._nextTop); | ||
this._animation.stop(); | ||
this._finish(); | ||
return; | ||
} | ||
// Set item active and store some data for the animation that is about to be | ||
// triggered. | ||
// Kick off animation to be started in the next tick. | ||
this._isActive = true; | ||
this._animateOptions.easing = animEasing; | ||
this._animateOptions.duration = animDuration; | ||
this._animOptions.easing = animEasing; | ||
this._animOptions.duration = animDuration; | ||
this._isInterrupted = isPositioning; | ||
// Start the item's layout animation in the next tick. | ||
addLayoutTick(item._id, this._setupAnimation, this._startAnimation); | ||
return this; | ||
}; | ||
@@ -120,9 +131,8 @@ | ||
* @public | ||
* @memberof ItemLayout.prototype | ||
* @param {Boolean} [processCallbackQueue=false] | ||
* @param {Object} [targetStyles] | ||
* @returns {ItemLayout} | ||
* @param {Boolean} processCallbackQueue | ||
* @param {Number} [left] | ||
* @param {Number} [top] | ||
*/ | ||
ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) { | ||
if (this._isDestroyed || !this._isActive) return this; | ||
ItemLayout.prototype.stop = function (processCallbackQueue, left, top) { | ||
if (this._isDestroyed || !this._isActive) return; | ||
@@ -135,3 +145,11 @@ var item = this._item; | ||
// Stop animation. | ||
item._animate.stop(targetStyles); | ||
if (this._animation.isAnimating()) { | ||
if (left === undefined || top === undefined) { | ||
var translate = getTranslate(item._element); | ||
left = translate.x; | ||
top = translate.y; | ||
} | ||
item._setTranslate(left, top); | ||
this._animation.stop(); | ||
} | ||
@@ -145,5 +163,5 @@ // Remove positioning class. | ||
// Process callback queue if needed. | ||
if (processCallbackQueue) this._queue.flush(true, item); | ||
return this; | ||
if (processCallbackQueue) { | ||
item._emitter.burst(this._queue, true, item); | ||
} | ||
}; | ||
@@ -155,12 +173,21 @@ | ||
* @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; | ||
ItemLayout.prototype.destroy = function () { | ||
if (this._isDestroyed) return; | ||
var elementStyle = this._item._element.style; | ||
this.stop(true, 0, 0); | ||
this._item._emitter.clear(this._queue); | ||
this._animation.destroy(); | ||
elementStyle[transformProp] = ''; | ||
elementStyle.left = ''; | ||
elementStyle.top = ''; | ||
this._item = null; | ||
this._currentStyles = null; | ||
this._targetStyles = null; | ||
this._animOptions = null; | ||
this._isDestroyed = true; | ||
return this; | ||
}; | ||
@@ -177,5 +204,4 @@ | ||
* @private | ||
* @memberof ItemLayout.prototype | ||
*/ | ||
ItemLayout.prototype._updateOffsets = function() { | ||
ItemLayout.prototype._updateOffsets = function () { | ||
if (this._isDestroyed) return; | ||
@@ -185,3 +211,3 @@ | ||
var migrate = item._migrate; | ||
var release = item._release; | ||
var release = item._dragRelease; | ||
@@ -199,16 +225,5 @@ this._offsetLeft = release._isActive | ||
: 0; | ||
}; | ||
/** | ||
* Calculate and update item's layout target styles. | ||
* | ||
* @private | ||
* @memberof ItemLayout.prototype | ||
*/ | ||
ItemLayout.prototype._updateTargetStyles = function() { | ||
if (this._isDestroyed) return; | ||
this._targetStyles.transform = getTranslateString( | ||
this._item._left + this._offsetLeft, | ||
this._item._top + this._offsetTop | ||
); | ||
this._nextLeft = this._item._left + this._offsetLeft; | ||
this._nextTop = this._item._top + this._offsetTop; | ||
}; | ||
@@ -220,5 +235,4 @@ | ||
* @private | ||
* @memberof ItemLayout.prototype | ||
*/ | ||
ItemLayout.prototype._finish = function() { | ||
ItemLayout.prototype._finish = function () { | ||
if (this._isDestroyed) return; | ||
@@ -228,4 +242,8 @@ | ||
var migrate = item._migrate; | ||
var release = item._release; | ||
var release = item._dragRelease; | ||
// Update internal translate values. | ||
item._tX = this._nextLeft; | ||
item._tY = this._nextTop; | ||
// Mark the item as inactive and remove positioning classes. | ||
@@ -242,3 +260,3 @@ if (this._isActive) { | ||
// Process the callback queue. | ||
this._queue.flush(false, item); | ||
item._emitter.burst(this._queue, false, item); | ||
}; | ||
@@ -250,8 +268,10 @@ | ||
* @private | ||
* @memberof ItemLayout.prototype | ||
*/ | ||
ItemLayout.prototype._setupAnimation = function() { | ||
var translate = getTranslate(this._item._element); | ||
this._currentLeft = translate.x; | ||
this._currentTop = translate.y; | ||
ItemLayout.prototype._setupAnimation = function () { | ||
var item = this._item; | ||
if (item._tX === undefined || item._tY === undefined) { | ||
var translate = getTranslate(item._element); | ||
item._tX = translate.x; | ||
item._tY = translate.y; | ||
} | ||
}; | ||
@@ -263,19 +283,21 @@ | ||
* @private | ||
* @memberof ItemLayout.prototype | ||
*/ | ||
ItemLayout.prototype._startAnimation = function() { | ||
ItemLayout.prototype._startAnimation = function () { | ||
var item = this._item; | ||
var settings = item.getGrid()._settings; | ||
var isInstant = this._animOptions.duration <= 0; | ||
// 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; | ||
var xDiff = Math.abs(item._left - (item._tX - this._offsetLeft)); | ||
var yDiff = Math.abs(item._top - (item._tY - this._offsetTop)); | ||
// If there is no need for animation or if the item is already in correct | ||
// position (or near it) let's finish the process early. | ||
if (isInstant || (xDiff < MIN_ANIMATION_DISTANCE && yDiff < MIN_ANIMATION_DISTANCE)) { | ||
if (xDiff || yDiff || this._isInterrupted) { | ||
item._setTranslate(this._nextLeft, this._nextTop); | ||
} | ||
this._animation.stop(); | ||
this._finish(); | ||
@@ -290,9 +312,16 @@ return; | ||
// Get current styles for animation. | ||
this._currentStyles.transform = getTranslateString(this._currentLeft, this._currentTop); | ||
// Get current/next styles for animation. | ||
this._currentStyles[transformProp] = getTranslateString(item._tX, item._tY); | ||
this._targetStyles[transformProp] = getTranslateString(this._nextLeft, this._nextTop); | ||
// Animate. | ||
item._animate.start(this._currentStyles, this._targetStyles, this._animateOptions); | ||
// Set internal translation values to undefined for the duration of the | ||
// animation since they will be changing on each animation frame for the | ||
// duration of the animation and tracking them would mean reading the DOM on | ||
// each frame, which is pretty darn expensive. | ||
item._tX = item._tY = undefined; | ||
// Start animation. | ||
this._animation.start(this._currentStyles, this._targetStyles, this._animOptions); | ||
}; | ||
export default ItemLayout; |
@@ -7,3 +7,3 @@ /** | ||
import { eventBeforeSend, eventBeforeReceive, eventSend, eventReceive } from '../shared'; | ||
import { EVENT_BEFORE_SEND, EVENT_BEFORE_RECEIVE, EVENT_SEND, EVENT_RECEIVE } from '../constants'; | ||
@@ -15,11 +15,6 @@ import ItemDrag from './ItemDrag'; | ||
import getTranslate from '../utils/getTranslate'; | ||
import getTranslateString from '../utils/getTranslateString'; | ||
import arrayInsert from '../utils/arrayInsert'; | ||
import normalizeArrayIndex from '../utils/normalizeArrayIndex'; | ||
import removeClass from '../utils/removeClass'; | ||
import setStyles from '../utils/setStyles'; | ||
import { transformProp } from '../utils/supportedTransform'; | ||
var tempStyles = {}; | ||
/** | ||
@@ -50,13 +45,12 @@ * The migrate process handler constructor. | ||
* @public | ||
* @memberof ItemMigrate.prototype | ||
* @param {Grid} targetGrid | ||
* @param {GridSingleItemQuery} position | ||
* @param {(HTMLElement|Number|Item)} position | ||
* @param {HTMLElement} [container] | ||
* @returns {ItemMigrate} | ||
*/ | ||
ItemMigrate.prototype.start = function(targetGrid, position, container) { | ||
if (this._isDestroyed) return this; | ||
ItemMigrate.prototype.start = function (targetGrid, position, container) { | ||
if (this._isDestroyed) return; | ||
var item = this._item; | ||
var element = item._element; | ||
var isActive = item.isActive(); | ||
var isVisible = item.isVisible(); | ||
@@ -69,3 +63,3 @@ var grid = item.getGrid(); | ||
var currentIndex = grid._items.indexOf(item); | ||
var targetContainer = container || window.document.body; | ||
var targetContainer = container || document.body; | ||
var targetIndex; | ||
@@ -79,10 +73,11 @@ var targetItem; | ||
var translateY; | ||
var currentVisClass; | ||
var nextVisClass; | ||
// Get target index. | ||
if (typeof position === 'number') { | ||
targetIndex = normalizeArrayIndex(targetItems, position, true); | ||
targetIndex = normalizeArrayIndex(targetItems, position, 1); | ||
} else { | ||
targetItem = targetGrid._getItem(position); | ||
/** @todo Consider throwing an error here instead of silently failing. */ | ||
if (!targetItem) return this; | ||
targetItem = targetGrid.getItem(position); | ||
if (!targetItem) return; | ||
targetIndex = targetItems.indexOf(targetItem); | ||
@@ -100,3 +95,3 @@ } | ||
if (item.isPositioning()) { | ||
item._layout.stop(true, { transform: getTranslateString(translateX, translateY) }); | ||
item._layout.stop(true, translateX, translateY); | ||
} | ||
@@ -108,3 +103,3 @@ | ||
translateY -= this._containerDiffY; | ||
this.stop(true, { transform: getTranslateString(translateX, translateY) }); | ||
this.stop(true, translateX, translateY); | ||
} | ||
@@ -114,9 +109,9 @@ | ||
if (item.isReleasing()) { | ||
translateX -= item._release._containerDiffX; | ||
translateY -= item._release._containerDiffY; | ||
item._release.stop(true, { transform: getTranslateString(translateX, translateY) }); | ||
translateX -= item._dragRelease._containerDiffX; | ||
translateY -= item._dragRelease._containerDiffY; | ||
item._dragRelease.stop(true, translateX, translateY); | ||
} | ||
// Stop current visibility animations. | ||
item._visibility._stopAnimation(); | ||
// Stop current visibility animation. | ||
item._visibility.stop(true); | ||
@@ -126,8 +121,5 @@ // Destroy current drag. | ||
// Process current visibility animation queue. | ||
item._visibility._queue.flush(true, item); | ||
// Emit beforeSend event. | ||
if (grid._hasListeners(eventBeforeSend)) { | ||
grid._emit(eventBeforeSend, { | ||
if (grid._hasListeners(EVENT_BEFORE_SEND)) { | ||
grid._emit(EVENT_BEFORE_SEND, { | ||
item: item, | ||
@@ -137,3 +129,3 @@ fromGrid: grid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
@@ -143,4 +135,4 @@ } | ||
// Emit beforeReceive event. | ||
if (targetGrid._hasListeners(eventBeforeReceive)) { | ||
targetGrid._emit(eventBeforeReceive, { | ||
if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) { | ||
targetGrid._emit(EVENT_BEFORE_RECEIVE, { | ||
item: item, | ||
@@ -150,14 +142,19 @@ fromGrid: grid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
} | ||
// Remove current classnames. | ||
removeClass(element, settings.itemClass); | ||
removeClass(element, settings.itemVisibleClass); | ||
removeClass(element, settings.itemHiddenClass); | ||
// Update item class. | ||
if (settings.itemClass !== targetSettings.itemClass) { | ||
removeClass(element, settings.itemClass); | ||
addClass(element, targetSettings.itemClass); | ||
} | ||
// Add new classnames. | ||
addClass(element, targetSettings.itemClass); | ||
addClass(element, isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); | ||
// Update visibility class. | ||
currentVisClass = isVisible ? settings.itemVisibleClass : settings.itemHiddenClass; | ||
nextVisClass = isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass; | ||
if (currentVisClass !== nextVisClass) { | ||
removeClass(element, currentVisClass); | ||
addClass(element, nextVisClass); | ||
} | ||
@@ -171,35 +168,39 @@ // Move item instance from current grid to target grid. | ||
// Get current container. | ||
currentContainer = element.parentNode; | ||
// Move the item inside the target container if it's different than the | ||
// If item is active we need to move the item inside the target container for | ||
// the duration of the (potential) animation 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; | ||
if (isActive) { | ||
currentContainer = element.parentNode; | ||
if (targetContainer !== currentContainer) { | ||
targetContainer.appendChild(element); | ||
offsetDiff = getOffsetDiff(targetContainer, currentContainer, true); | ||
if (!translate) { | ||
translate = getTranslate(element); | ||
translateX = translate.x; | ||
translateY = translate.y; | ||
} | ||
item._setTranslate(translateX + offsetDiff.left, translateY + offsetDiff.top); | ||
} | ||
element.style[transformProp] = getTranslateString( | ||
translateX + offsetDiff.left, | ||
translateY + offsetDiff.top | ||
); | ||
} | ||
// If item is not active let's just append it to the target grid's element. | ||
else { | ||
targetElement.appendChild(element); | ||
} | ||
// Update child element's styles to reflect the current visibility state. | ||
item._child.removeAttribute('style'); | ||
setStyles(item._child, isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles); | ||
item._visibility.setStyles( | ||
isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles | ||
); | ||
// Update display style. | ||
element.style.display = isVisible ? 'block' : 'hidden'; | ||
// Get offset diff for the migration data, if the item is active. | ||
if (isActive) { | ||
containerDiff = getOffsetDiff(targetContainer, targetElement, true); | ||
} | ||
// Get offset diff for the migration data. | ||
containerDiff = getOffsetDiff(targetContainer, targetElement, true); | ||
// Update item's cached dimensions and sort data. | ||
// Update item's cached dimensions. | ||
item._refreshDimensions(); | ||
item._refreshSortData(); | ||
// Reset item's sort data. | ||
item._sortData = null; | ||
// Create new drag handler. | ||
@@ -209,10 +210,17 @@ 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; | ||
if (isActive) { | ||
this._isActive = true; | ||
this._container = targetContainer; | ||
this._containerDiffX = containerDiff.left; | ||
this._containerDiffY = containerDiff.top; | ||
} else { | ||
this._isActive = false; | ||
this._container = null; | ||
this._containerDiffX = 0; | ||
this._containerDiffY = 0; | ||
} | ||
// Emit send event. | ||
if (grid._hasListeners(eventSend)) { | ||
grid._emit(eventSend, { | ||
if (grid._hasListeners(EVENT_SEND)) { | ||
grid._emit(EVENT_SEND, { | ||
item: item, | ||
@@ -222,3 +230,3 @@ fromGrid: grid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
@@ -228,4 +236,4 @@ } | ||
// Emit receive event. | ||
if (targetGrid._hasListeners(eventReceive)) { | ||
targetGrid._emit(eventReceive, { | ||
if (targetGrid._hasListeners(EVENT_RECEIVE)) { | ||
targetGrid._emit(EVENT_RECEIVE, { | ||
item: item, | ||
@@ -235,7 +243,5 @@ fromGrid: grid, | ||
toGrid: targetGrid, | ||
toIndex: targetIndex | ||
toIndex: targetIndex, | ||
}); | ||
} | ||
return this; | ||
}; | ||
@@ -248,11 +254,11 @@ | ||
* @public | ||
* @memberof ItemMigrate.prototype | ||
* @param {Boolean} [abort=false] | ||
* - Should the migration be aborted? | ||
* @param {Object} [currentStyles] | ||
* - Optional current translateX and translateY styles. | ||
* @returns {ItemMigrate} | ||
* @param {Number} [left] | ||
* - The element's current translateX value (optional). | ||
* @param {Number} [top] | ||
* - The element's current translateY value (optional). | ||
*/ | ||
ItemMigrate.prototype.stop = function(abort, currentStyles) { | ||
if (this._isDestroyed || !this._isActive) return this; | ||
ItemMigrate.prototype.stop = function (abort, left, top) { | ||
if (this._isDestroyed || !this._isActive) return; | ||
@@ -266,16 +272,15 @@ var item = this._item; | ||
if (this._container !== gridElement) { | ||
if (!currentStyles) { | ||
if (left === undefined || top === undefined) { | ||
if (abort) { | ||
translate = getTranslate(element); | ||
tempStyles.transform = getTranslateString( | ||
translate.x - this._containerDiffX, | ||
translate.y - this._containerDiffY | ||
); | ||
left = translate.x - this._containerDiffX; | ||
top = translate.y - this._containerDiffY; | ||
} else { | ||
tempStyles.transform = getTranslateString(item._left, item._top); | ||
left = item._left; | ||
top = item._top; | ||
} | ||
currentStyles = tempStyles; | ||
} | ||
gridElement.appendChild(element); | ||
setStyles(element, currentStyles); | ||
item._setTranslate(left, top); | ||
} | ||
@@ -287,4 +292,2 @@ | ||
this._containerDiffY = 0; | ||
return this; | ||
}; | ||
@@ -296,13 +299,10 @@ | ||
* @public | ||
* @memberof ItemMigrate.prototype | ||
* @returns {ItemMigrate} | ||
*/ | ||
ItemMigrate.prototype.destroy = function() { | ||
if (this._isDestroyed) return this; | ||
ItemMigrate.prototype.destroy = function () { | ||
if (this._isDestroyed) return; | ||
this.stop(true); | ||
this._item = null; | ||
this._isDestroyed = true; | ||
return this; | ||
}; | ||
export default ItemMigrate; |
@@ -9,7 +9,6 @@ /** | ||
import Queue from '../Queue/Queue'; | ||
import Animator from '../Animator/Animator'; | ||
import addClass from '../utils/addClass'; | ||
import getCurrentStyles from '../utils/getCurrentStyles'; | ||
import getTranslateString from '../utils/getTranslateString'; | ||
import isFunction from '../utils/isFunction'; | ||
@@ -20,3 +19,3 @@ import removeClass from '../utils/removeClass'; | ||
/** | ||
* Visibility manager for Item instance. | ||
* Visibility manager for Item instance, handles visibility of an item. | ||
* | ||
@@ -29,27 +28,24 @@ * @class | ||
var element = item._element; | ||
var childElement = element.children[0]; | ||
var settings = item.getGrid()._settings; | ||
if (!childElement) { | ||
throw new Error('No valid child element found within item element.'); | ||
} | ||
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._childElement = childElement; | ||
this._currentStyleProps = []; | ||
this._animation = new Animator(childElement); | ||
this._queue = 'visibility-' + item._id; | ||
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. | ||
element.style.display = isActive ? '' : 'none'; | ||
addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass); | ||
// Set initial styles for the child element. | ||
setStyles(item._child, isActive ? settings.visibleStyles : settings.hiddenStyles); | ||
this.setStyles(isActive ? settings.visibleStyles : settings.hiddenStyles); | ||
} | ||
@@ -66,13 +62,10 @@ | ||
* @public | ||
* @memberof ItemVisibility.prototype | ||
* @param {Boolean} instant | ||
* @param {Function} [onFinish] | ||
* @returns {ItemVisibility} | ||
*/ | ||
ItemVisibility.prototype.show = function(instant, onFinish) { | ||
if (this._isDestroyed) return this; | ||
ItemVisibility.prototype.show = function (instant, onFinish) { | ||
if (this._isDestroyed) return; | ||
var item = this._item; | ||
var element = item._element; | ||
var queue = this._queue; | ||
var callback = isFunction(onFinish) ? onFinish : null; | ||
@@ -85,3 +78,3 @@ var grid = item.getGrid(); | ||
callback && callback(false, item); | ||
return this; | ||
return; | ||
} | ||
@@ -92,4 +85,4 @@ | ||
if (this._isShowing && !instant) { | ||
callback && queue.add(callback); | ||
return this; | ||
callback && item._emitter.on(this._queue, callback); | ||
return; | ||
} | ||
@@ -101,13 +94,13 @@ | ||
if (!this._isShowing) { | ||
queue.flush(true, item); | ||
item._emitter.burst(this._queue, true, item); | ||
removeClass(element, settings.itemHiddenClass); | ||
addClass(element, settings.itemVisibleClass); | ||
if (!this._isHiding) element.style.display = 'block'; | ||
if (!this._isHiding) element.style.display = ''; | ||
} | ||
// Push callback to the callback queue. | ||
callback && queue.add(callback); | ||
callback && item._emitter.on(this._queue, callback); | ||
// Update visibility states. | ||
item._isActive = this._isShowing = true; | ||
this._isShowing = true; | ||
this._isHiding = this._isHidden = false; | ||
@@ -117,4 +110,2 @@ | ||
this._startAnimation(true, instant, this._finishShow); | ||
return this; | ||
}; | ||
@@ -126,13 +117,10 @@ | ||
* @public | ||
* @memberof ItemVisibility.prototype | ||
* @param {Boolean} instant | ||
* @param {Function} [onFinish] | ||
* @returns {ItemVisibility} | ||
*/ | ||
ItemVisibility.prototype.hide = function(instant, onFinish) { | ||
if (this._isDestroyed) return this; | ||
ItemVisibility.prototype.hide = function (instant, onFinish) { | ||
if (this._isDestroyed) return; | ||
var item = this._item; | ||
var element = item._element; | ||
var queue = this._queue; | ||
var callback = isFunction(onFinish) ? onFinish : null; | ||
@@ -145,3 +133,3 @@ var grid = item.getGrid(); | ||
callback && callback(false, item); | ||
return this; | ||
return; | ||
} | ||
@@ -152,4 +140,4 @@ | ||
if (this._isHiding && !instant) { | ||
callback && queue.add(callback); | ||
return this; | ||
callback && item._emitter.on(this._queue, callback); | ||
return; | ||
} | ||
@@ -161,3 +149,3 @@ | ||
if (!this._isHiding) { | ||
queue.flush(true, item); | ||
item._emitter.burst(this._queue, true, item); | ||
addClass(element, settings.itemHiddenClass); | ||
@@ -168,23 +156,57 @@ removeClass(element, settings.itemVisibleClass); | ||
// Push callback to the callback queue. | ||
callback && queue.add(callback); | ||
callback && item._emitter.on(this._queue, callback); | ||
// Update visibility states. | ||
this._isHidden = this._isHiding = true; | ||
item._isActive = this._isShowing = false; | ||
this._isShowing = false; | ||
// Finally let's start hide animation. | ||
this._startAnimation(false, instant, this._finishHide); | ||
}; | ||
return this; | ||
/** | ||
* Stop current hiding/showing process. | ||
* | ||
* @public | ||
* @param {Boolean} processCallbackQueue | ||
*/ | ||
ItemVisibility.prototype.stop = function (processCallbackQueue) { | ||
if (this._isDestroyed) return; | ||
if (!this._isHiding && !this._isShowing) return; | ||
var item = this._item; | ||
cancelVisibilityTick(item._id); | ||
this._animation.stop(); | ||
if (processCallbackQueue) { | ||
item._emitter.burst(this._queue, true, item); | ||
} | ||
}; | ||
/** | ||
* Reset all existing visibility styles and apply new visibility styles to the | ||
* visibility element. This method should be used to set styles when there is a | ||
* chance that the current style properties differ from the new ones (basically | ||
* on init and on migrations). | ||
* | ||
* @public | ||
* @param {Object} styles | ||
*/ | ||
ItemVisibility.prototype.setStyles = function (styles) { | ||
var childElement = this._childElement; | ||
var currentStyleProps = this._currentStyleProps; | ||
this._removeCurrentStyles(); | ||
for (var prop in styles) { | ||
currentStyleProps.push(prop); | ||
childElement.style[prop] = styles[prop]; | ||
} | ||
}; | ||
/** | ||
* 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; | ||
ItemVisibility.prototype.destroy = function () { | ||
if (this._isDestroyed) return; | ||
@@ -194,21 +216,15 @@ var item = this._item; | ||
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. | ||
this.stop(true); | ||
item._emitter.clear(this._queue); | ||
this._animation.destroy(); | ||
this._removeCurrentStyles(); | ||
removeClass(element, settings.itemVisibleClass); | ||
removeClass(element, settings.itemHiddenClass); | ||
element.style.display = ''; | ||
// Reset state. | ||
this._item = null; | ||
this._isHiding = this._isShowing = false; | ||
this._isDestroyed = this._isHidden = true; | ||
return this; | ||
}; | ||
@@ -225,3 +241,2 @@ | ||
* @private | ||
* @memberof ItemVisibility.prototype | ||
* @param {Boolean} toVisible | ||
@@ -231,10 +246,12 @@ * @param {Boolean} [instant] | ||
*/ | ||
ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish) { | ||
ItemVisibility.prototype._startAnimation = function (toVisible, instant, onFinish) { | ||
if (this._isDestroyed) return; | ||
var item = this._item; | ||
var animation = this._animation; | ||
var childElement = this._childElement; | ||
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 duration = toVisible ? settings.showDuration : settings.hideDuration; | ||
var easing = toVisible ? settings.showEasing : settings.hideEasing; | ||
var isInstant = instant || duration <= 0; | ||
@@ -254,7 +271,4 @@ var currentStyles; | ||
if (isInstant) { | ||
if (item._animateChild.isAnimating()) { | ||
item._animateChild.stop(targetStyles); | ||
} else { | ||
setStyles(item._child, targetStyles); | ||
} | ||
setStyles(childElement, targetStyles); | ||
animation.stop(); | ||
onFinish && onFinish(); | ||
@@ -267,10 +281,10 @@ return; | ||
item._id, | ||
function() { | ||
currentStyles = getCurrentStyles(item._child, targetStyles); | ||
function () { | ||
currentStyles = getCurrentStyles(childElement, targetStyles); | ||
}, | ||
function() { | ||
item._animateChild.start(currentStyles, targetStyles, { | ||
function () { | ||
animation.start(currentStyles, targetStyles, { | ||
duration: duration, | ||
easing: easing, | ||
onFinish: onFinish | ||
onFinish: onFinish, | ||
}); | ||
@@ -282,25 +296,10 @@ } | ||
/** | ||
* 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() { | ||
ItemVisibility.prototype._finishShow = function () { | ||
if (this._isHidden) return; | ||
this._isShowing = false; | ||
this._queue.flush(false, this._item); | ||
this._item._emitter.burst(this._queue, false, this._item); | ||
}; | ||
@@ -312,15 +311,28 @@ | ||
* @private | ||
* @memberof ItemVisibility.prototype | ||
*/ | ||
var finishStyles = {}; | ||
ItemVisibility.prototype._finishHide = function() { | ||
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._layout.stop(true, 0, 0); | ||
item._element.style.display = 'none'; | ||
this._queue.flush(false, item); | ||
item._emitter.burst(this._queue, false, item); | ||
}; | ||
/** | ||
* Remove currently applied visibility related inline style properties. | ||
* | ||
* @private | ||
*/ | ||
ItemVisibility.prototype._removeCurrentStyles = function () { | ||
var childElement = this._childElement; | ||
var currentStyleProps = this._currentStyleProps; | ||
for (var i = 0; i < currentStyleProps.length; i++) { | ||
childElement.style[currentStyleProps[i]] = ''; | ||
} | ||
currentStyleProps.length = 0; | ||
}; | ||
export default ItemVisibility; |
@@ -8,39 +8,22 @@ /** | ||
/** | ||
* 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; | ||
import PackerProcessor, { | ||
createWorkerProcessors, | ||
destroyWorkerProcessors, | ||
isWorkerProcessorsSupported, | ||
} from './PackerProcessor'; | ||
// The layout return data, which will be populated in getLayout. | ||
this._layout = { | ||
slots: null, | ||
setWidth: false, | ||
setHeight: false, | ||
width: false, | ||
height: false | ||
}; | ||
export var FILL_GAPS = 1; | ||
export var HORIZONTAL = 2; | ||
export var ALIGN_RIGHT = 4; | ||
export var ALIGN_BOTTOM = 8; | ||
export var ROUNDING = 16; | ||
export var PACKET_INDEX_ID = 0; | ||
export var PACKET_INDEX_WIDTH = 1; | ||
export var PACKET_INDEX_HEIGHT = 2; | ||
export var PACKET_INDEX_OPTIONS = 3; | ||
export var PACKET_HEADER_SLOTS = 4; | ||
// 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] | ||
* @class | ||
* @param {Number} [numWorkers=0] | ||
* @param {Object} [options] | ||
@@ -51,443 +34,252 @@ * @param {Boolean} [options.fillGaps=false] | ||
* @param {Boolean} [options.alignBottom=false] | ||
* @returns {LayoutData} | ||
* @param {Boolean} [options.rounding=false] | ||
*/ | ||
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; | ||
function Packer(numWorkers, options) { | ||
this._options = 0; | ||
this._processor = null; | ||
this._layoutQueue = []; | ||
this._layouts = {}; | ||
this._layoutCallbacks = {}; | ||
this._layoutWorkers = {}; | ||
this._layoutWorkerData = {}; | ||
this._workers = []; | ||
this._onWorkerMessage = this._onWorkerMessage.bind(this); | ||
// 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; | ||
// Set initial options. | ||
this.setOptions(options); | ||
// 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]); | ||
// Init the worker(s) or the processor if workers can't be used. | ||
numWorkers = typeof numWorkers === 'number' ? Math.max(0, numWorkers) : 0; | ||
if (numWorkers && isWorkerProcessorsSupported()) { | ||
try { | ||
this._workers = createWorkerProcessors(numWorkers, this._onWorkerMessage); | ||
} catch (e) { | ||
this._processor = new PackerProcessor(); | ||
} | ||
} else { | ||
this._processor = new PackerProcessor(); | ||
} | ||
} | ||
// 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]); | ||
} | ||
} | ||
Packer.prototype._sendToWorker = function () { | ||
if (!this._layoutQueue.length || !this._workers.length) return; | ||
// Reset slots arrays and rect id. | ||
slotSizes.length = 0; | ||
this._freeSlots.length = 0; | ||
this._newSlots.length = 0; | ||
this._rectId = 0; | ||
var layoutId = this._layoutQueue.shift(); | ||
var worker = this._workers.pop(); | ||
var data = this._layoutWorkerData[layoutId]; | ||
return layout; | ||
delete this._layoutWorkerData[layoutId]; | ||
this._layoutWorkers[layoutId] = worker; | ||
worker.postMessage(data.buffer, [data.buffer]); | ||
}; | ||
/** | ||
* 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 eps = 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; | ||
Packer.prototype._onWorkerMessage = function (msg) { | ||
var data = new Float32Array(msg.data); | ||
var layoutId = data[PACKET_INDEX_ID]; | ||
var layout = this._layouts[layoutId]; | ||
var callback = this._layoutCallbacks[layoutId]; | ||
var worker = this._layoutWorkers[layoutId]; | ||
// Reset new slots. | ||
newSlots.length = 0; | ||
if (layout) delete this._layoutCallbacks[layoutId]; | ||
if (callback) delete this._layoutCallbacks[layoutId]; | ||
if (worker) delete this._layoutWorkers[layoutId]; | ||
// 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; | ||
if (layout && callback) { | ||
layout.width = data[PACKET_INDEX_WIDTH]; | ||
layout.height = data[PACKET_INDEX_HEIGHT]; | ||
layout.slots = data.subarray(PACKET_HEADER_SLOTS, data.length); | ||
this._finalizeLayout(layout); | ||
callback(layout); | ||
} | ||
// Round item slot width and height if needed. | ||
if (rounding) { | ||
itemSlot.width = Math.round(itemSlot.width); | ||
itemSlot.height = Math.round(itemSlot.height); | ||
} | ||
if (worker) { | ||
this._workers.push(worker); | ||
this._sendToWorker(); | ||
} | ||
}; | ||
// 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 + eps && itemSlot.height <= rect.height + eps) { | ||
itemSlot.left = rect.left; | ||
itemSlot.top = rect.top; | ||
break; | ||
} | ||
} | ||
Packer.prototype._finalizeLayout = function (layout) { | ||
var grid = layout._grid; | ||
var isHorizontal = layout._settings & HORIZONTAL; | ||
var isBorderBox = grid._boxSizing === 'border-box'; | ||
// 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; | ||
delete layout._grid; | ||
delete layout._settings; | ||
// If gaps don't need filling do not add any current slots to the new | ||
// slots array. | ||
if (!fillGaps) { | ||
ignoreCurrentSlots = true; | ||
} | ||
} | ||
layout.styles = {}; | ||
// 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 (isHorizontal) { | ||
layout.styles.width = | ||
(isBorderBox ? layout.width + grid._borderLeft + grid._borderRight : layout.width) + 'px'; | ||
} else { | ||
layout.styles.height = | ||
(isBorderBox ? layout.height + grid._borderTop + grid._borderBottom : layout.height) + 'px'; | ||
} | ||
// 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 | ||
) | ||
); | ||
} | ||
return layout; | ||
}; | ||
// Update grid height. | ||
layout.height = itemSlot.top + itemSlot.height; | ||
} | ||
/** | ||
* @public | ||
* @param {Object} [options] | ||
* @param {Boolean} [options.fillGaps] | ||
* @param {Boolean} [options.horizontal] | ||
* @param {Boolean} [options.alignRight] | ||
* @param {Boolean} [options.alignBottom] | ||
* @param {Boolean} [options.rounding] | ||
*/ | ||
Packer.prototype.setOptions = function (options) { | ||
if (!options) return; | ||
// 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)); | ||
} | ||
var fillGaps; | ||
if (typeof options.fillGaps === 'boolean') { | ||
fillGaps = options.fillGaps ? FILL_GAPS : 0; | ||
} else { | ||
fillGaps = this._options & FILL_GAPS; | ||
} | ||
// 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 | ||
) | ||
); | ||
} | ||
var horizontal; | ||
if (typeof options.horizontal === 'boolean') { | ||
horizontal = options.horizontal ? HORIZONTAL : 0; | ||
} else { | ||
horizontal = this._options & HORIZONTAL; | ||
} | ||
// Update grid width. | ||
layout.width = itemSlot.left + itemSlot.width; | ||
} | ||
var alignRight; | ||
if (typeof options.alignRight === 'boolean') { | ||
alignRight = options.alignRight ? ALIGN_RIGHT : 0; | ||
} else { | ||
alignRight = this._options & ALIGN_RIGHT; | ||
} | ||
// 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); | ||
} | ||
} | ||
} | ||
var alignBottom; | ||
if (typeof options.alignBottom === 'boolean') { | ||
alignBottom = options.alignBottom ? ALIGN_BOTTOM : 0; | ||
} else { | ||
alignBottom = this._options & ALIGN_BOTTOM; | ||
} | ||
// Sanitize new slots. | ||
if (newSlots.length) { | ||
this._purgeRects(newSlots).sort( | ||
isHorizontal ? this._sortRectsLeftTop : this._sortRectsTopLeft | ||
); | ||
} | ||
var rounding; | ||
if (typeof options.rounding === 'boolean') { | ||
rounding = options.rounding ? ROUNDING : 0; | ||
} else { | ||
rounding = this._options & ROUNDING; | ||
} | ||
// 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); | ||
} | ||
this._options = fillGaps | horizontal | alignRight | alignBottom | rounding; | ||
}; | ||
// 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 | ||
* @public | ||
* @param {Grid} grid | ||
* @param {Number} layoutId | ||
* @param {Item[]} items | ||
* @param {Number} width | ||
* @param {Number} height | ||
* @returns {RectId} | ||
* @param {Function} callback | ||
* @returns {?Function} | ||
*/ | ||
Packer.prototype._addRect = function(left, top, width, height) { | ||
var rectId = ++this._rectId; | ||
var rectStore = this._rectStore; | ||
Packer.prototype.createLayout = function (grid, layoutId, items, width, height, callback) { | ||
if (this._layouts[layoutId]) { | ||
throw new Error('A layout with the provided id is currently being processed.'); | ||
} | ||
rectStore[rectId] = left || 0; | ||
rectStore[++this._rectId] = top || 0; | ||
rectStore[++this._rectId] = width || 0; | ||
rectStore[++this._rectId] = height || 0; | ||
var horizontal = this._options & HORIZONTAL; | ||
var layout = { | ||
id: layoutId, | ||
items: items, | ||
slots: null, | ||
width: horizontal ? 0 : width, | ||
height: !horizontal ? 0 : height, | ||
// Temporary data, which will be removed before sending the layout data | ||
// outside of Packer's context. | ||
_grid: grid, | ||
_settings: this._options, | ||
}; | ||
return rectId; | ||
}; | ||
// If there are no items let's call the callback immediately. | ||
if (!items.length) { | ||
layout.slots = []; | ||
this._finalizeLayout(layout); | ||
callback(layout); | ||
return; | ||
} | ||
/** | ||
* 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; | ||
// Create layout synchronously if needed. | ||
if (this._processor) { | ||
layout.slots = window.Float32Array | ||
? new Float32Array(items.length * 2) | ||
: new Array(items.length * 2); | ||
this._processor.computeLayout(layout, layout._settings); | ||
this._finalizeLayout(layout); | ||
callback(layout); | ||
return; | ||
} | ||
rectItem.left = rectStore[id] || 0; | ||
rectItem.top = rectStore[++id] || 0; | ||
rectItem.width = rectStore[++id] || 0; | ||
rectItem.height = rectStore[++id] || 0; | ||
// Worker data. | ||
var data = new Float32Array(PACKET_HEADER_SLOTS + items.length * 2); | ||
return rectItem; | ||
}; | ||
// Worker data header. | ||
data[PACKET_INDEX_ID] = layoutId; | ||
data[PACKET_INDEX_WIDTH] = layout.width; | ||
data[PACKET_INDEX_HEIGHT] = layout.height; | ||
data[PACKET_INDEX_OPTIONS] = layout._settings; | ||
/** | ||
* 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; | ||
// Worker data items. | ||
var i, j, item; | ||
for (i = 0, j = PACKET_HEADER_SLOTS - 1, item; i < items.length; i++) { | ||
item = items[i]; | ||
data[++j] = item._width + item._marginLeft + item._marginRight; | ||
data[++j] = item._height + item._marginTop + item._marginBottom; | ||
} | ||
// 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; | ||
} | ||
this._layoutQueue.push(layoutId); | ||
this._layouts[layoutId] = layout; | ||
this._layoutCallbacks[layoutId] = callback; | ||
this._layoutWorkerData[layoutId] = data; | ||
// Left split. | ||
if (rect.left < hole.left) { | ||
results.push(this._addRect(rect.left, rect.top, hole.left - rect.left, rect.height)); | ||
} | ||
this._sendToWorker(); | ||
// 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 | ||
) | ||
); | ||
} | ||
return this.cancelLayout.bind(this, layoutId); | ||
}; | ||
// 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} | ||
* @public | ||
* @param {Number} layoutId | ||
*/ | ||
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 | ||
); | ||
}; | ||
Packer.prototype.cancelLayout = function (layoutId) { | ||
var layout = this._layouts[layoutId]; | ||
if (!layout) return; | ||
/** | ||
* 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 | ||
); | ||
delete this._layouts[layoutId]; | ||
delete this._layoutCallbacks[layoutId]; | ||
if (this._layoutWorkerData[layoutId]) { | ||
delete this._layoutWorkerData[layoutId]; | ||
var queueIndex = this._layoutQueue.indexOf(layoutId); | ||
if (queueIndex > -1) this._layoutQueue.splice(queueIndex, 1); | ||
} | ||
}; | ||
/** | ||
* 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[]} | ||
* @public | ||
*/ | ||
Packer.prototype._purgeRects = (function() { | ||
var rectA = {}; | ||
var rectB = {}; | ||
return function(rectIds) { | ||
var i = rectIds.length; | ||
var ii; | ||
Packer.prototype.destroy = function () { | ||
// Move all currently used workers back in the workers array. | ||
for (var key in this._layoutWorkers) { | ||
this._workers.push(this._layoutWorkers[key]); | ||
} | ||
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; | ||
} | ||
} | ||
} | ||
// Destroy all instance's workers. | ||
destroyWorkerProcessors(this._workers); | ||
return rectIds; | ||
}; | ||
})(); | ||
// Reset data. | ||
this._workers.length = 0; | ||
this._layoutQueue.length = 0; | ||
this._layouts = {}; | ||
this._layoutCallbacks = {}; | ||
this._layoutWorkers = {}; | ||
this._layoutWorkerData = {}; | ||
}; | ||
/** | ||
* 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; | ||
}; | ||
})(); | ||
export default Packer; |
@@ -9,50 +9,119 @@ /** | ||
var ticker = new Ticker(); | ||
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 layoutTick = 'layout'; | ||
var visibilityTick = 'visibility'; | ||
var moveTick = 'move'; | ||
var scrollTick = 'scroll'; | ||
var placeholderTick = 'placeholder'; | ||
var LANE_READ = 0; | ||
var LANE_READ_TAIL = 1; | ||
var LANE_WRITE = 2; | ||
var ticker = new Ticker(3); | ||
export default ticker; | ||
export function addLayoutTick(itemId, readCallback, writeCallback) { | ||
return ticker.add(itemId + layoutTick, readCallback, writeCallback); | ||
export function addLayoutTick(itemId, read, write) { | ||
ticker.add(LANE_READ, LAYOUT_READ + itemId, read); | ||
ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write); | ||
} | ||
export function cancelLayoutTick(itemId) { | ||
return ticker.cancel(itemId + layoutTick); | ||
ticker.remove(LANE_READ, LAYOUT_READ + itemId); | ||
ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId); | ||
} | ||
export function addVisibilityTick(itemId, readCallback, writeCallback) { | ||
return ticker.add(itemId + visibilityTick, readCallback, writeCallback); | ||
export function addVisibilityTick(itemId, read, write) { | ||
ticker.add(LANE_READ, VISIBILITY_READ + itemId, read); | ||
ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write); | ||
} | ||
export function cancelVisibilityTick(itemId) { | ||
return ticker.cancel(itemId + visibilityTick); | ||
ticker.remove(LANE_READ, VISIBILITY_READ + itemId); | ||
ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId); | ||
} | ||
export function addMoveTick(itemId, readCallback, writeCallback) { | ||
return ticker.add(itemId + moveTick, readCallback, writeCallback, true); | ||
export function addDragStartTick(itemId, read, write) { | ||
ticker.add(LANE_READ, DRAG_START_READ + itemId, read); | ||
ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write); | ||
} | ||
export function cancelMoveTick(itemId) { | ||
return ticker.cancel(itemId + moveTick); | ||
export function cancelDragStartTick(itemId) { | ||
ticker.remove(LANE_READ, DRAG_START_READ + itemId); | ||
ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId); | ||
} | ||
export function addScrollTick(itemId, readCallback, writeCallback) { | ||
return ticker.add(itemId + scrollTick, readCallback, writeCallback, true); | ||
export function addDragMoveTick(itemId, read, write) { | ||
ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read); | ||
ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write); | ||
} | ||
export function cancelScrollTick(itemId) { | ||
return ticker.cancel(itemId + scrollTick); | ||
export function cancelDragMoveTick(itemId) { | ||
ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId); | ||
ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId); | ||
} | ||
export function addPlaceholderTick(itemId, readCallback, writeCallback) { | ||
return ticker.add(itemId + placeholderTick, readCallback, writeCallback); | ||
export function addDragScrollTick(itemId, read, write) { | ||
ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read); | ||
ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write); | ||
} | ||
export function cancelPlaceholderTick(itemId) { | ||
return ticker.cancel(itemId + placeholderTick); | ||
export function cancelDragScrollTick(itemId) { | ||
ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId); | ||
ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId); | ||
} | ||
export function addDragSortTick(itemId, read) { | ||
ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read); | ||
} | ||
export function cancelDragSortTick(itemId) { | ||
ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId); | ||
} | ||
export function addPlaceholderLayoutTick(itemId, read, write) { | ||
ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read); | ||
ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write); | ||
} | ||
export function cancelPlaceholderLayoutTick(itemId) { | ||
ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId); | ||
ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId); | ||
} | ||
export function addPlaceholderResizeTick(itemId, write) { | ||
ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write); | ||
} | ||
export function cancelPlaceholderResizeTick(itemId) { | ||
ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId); | ||
} | ||
export function addAutoScrollTick(read, write) { | ||
ticker.add(LANE_READ, AUTO_SCROLL_READ, read); | ||
ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write); | ||
} | ||
export function cancelAutoScrollTick() { | ||
ticker.remove(LANE_READ, AUTO_SCROLL_READ); | ||
ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE); | ||
} | ||
export function addDebounceTick(debounceId, read) { | ||
ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read); | ||
} | ||
export function cancelDebounceTick(debounceId) { | ||
ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId); | ||
} |
@@ -12,103 +12,84 @@ /** | ||
* 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() { | ||
function Ticker(numLanes) { | ||
this._nextStep = null; | ||
this._queue = []; | ||
this._reads = {}; | ||
this._writes = {}; | ||
this._batch = []; | ||
this._batchReads = {}; | ||
this._batchWrites = {}; | ||
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.add = function(id, readOperation, writeOperation, isPrioritized) { | ||
// 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; | ||
Ticker.prototype._step = function (time) { | ||
var lanes = this._lanes; | ||
var stepQueue = this._stepQueue; | ||
var stepCallbacks = this._stepCallbacks; | ||
var i, j, id, laneQueue, laneCallbacks, laneIndices; | ||
// Add entry. | ||
isPrioritized ? this._queue.unshift(id) : this._queue.push(id); | ||
this._reads[id] = readOperation; | ||
this._writes[id] = writeOperation; | ||
this._nextStep = null; | ||
// Finally, let's kick-start the next tick if it is not running yet. | ||
if (!this._nextStep) this._nextStep = raf(this._step); | ||
}; | ||
Ticker.prototype.cancel = function(id) { | ||
var currentIndex = this._queue.indexOf(id); | ||
if (currentIndex > -1) { | ||
this._queue[currentIndex] = undefined; | ||
delete this._reads[id]; | ||
delete this._writes[id]; | ||
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; | ||
} | ||
}; | ||
Ticker.prototype._step = 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._nextStep = 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]; | ||
delete reads[id]; | ||
batchWrites[id] = writes[id]; | ||
delete writes[id]; | ||
for (i = 0; i < stepQueue.length; i++) { | ||
id = stepQueue[i]; | ||
if (stepCallbacks[id]) stepCallbacks[id](time); | ||
delete stepCallbacks[id]; | ||
} | ||
// Reset queue. | ||
queue.length = 0; | ||
stepQueue.length = 0; | ||
}; | ||
// Process read callbacks. | ||
for (i = 0; i < length; i++) { | ||
id = batch[i]; | ||
if (batchReads[id]) { | ||
batchReads[id](); | ||
delete batchReads[id]; | ||
} | ||
} | ||
Ticker.prototype.add = function (laneIndex, id, callback) { | ||
this._lanes[laneIndex].add(id, callback); | ||
if (!this._nextStep) this._nextStep = raf(this._step); | ||
}; | ||
// Process write callbacks. | ||
for (i = 0; i < length; i++) { | ||
id = batch[i]; | ||
if (batchWrites[id]) { | ||
batchWrites[id](); | ||
delete batchWrites[id]; | ||
} | ||
} | ||
Ticker.prototype.remove = function (laneIndex, id) { | ||
this._lanes[laneIndex].remove(id); | ||
}; | ||
// Reset batch. | ||
batch.length = 0; | ||
/** | ||
* A lane for ticker. | ||
* | ||
* @class | ||
*/ | ||
function TickerLane() { | ||
this.queue = []; | ||
this.indices = {}; | ||
this.callbacks = {}; | ||
} | ||
// Restart the ticker if needed. | ||
if (!this._nextStep && queue.length) { | ||
this._nextStep = raf(this._step); | ||
} | ||
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]; | ||
}; | ||
export default Ticker; |
@@ -16,2 +16,4 @@ /** | ||
export default function addClass(element, className) { | ||
if (!className) return; | ||
if (element.classList) { | ||
@@ -18,0 +20,0 @@ element.classList.add(className); |
@@ -7,7 +7,4 @@ /** | ||
import ticker from '../ticker'; | ||
import { addDebounceTick, cancelDebounceTick } from '../ticker'; | ||
var actionCancel = 'cancel'; | ||
var actionFinish = 'finish'; | ||
var debounceTick = 'debounce'; | ||
var debounceId = 0; | ||
@@ -19,34 +16,51 @@ | ||
* 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. | ||
* being `true`, cancels the debounce function immediately. When the debounce | ||
* function is canceled it cannot be invoked again. | ||
* | ||
* @param {Function} fn | ||
* @param {Number} wait | ||
* @param {Number} durationMs | ||
* @returns {Function} | ||
*/ | ||
export default function debounce(fn, wait) { | ||
var timeout; | ||
var tickerId = ++debounceId + debounceTick; | ||
export default function debounce(fn, durationMs) { | ||
var id = ++debounceId; | ||
var timer = 0; | ||
var lastTime = 0; | ||
var isCanceled = false; | ||
var tick = function (time) { | ||
if (isCanceled) return; | ||
if (wait > 0) { | ||
return function(action) { | ||
if (timeout !== undefined) { | ||
timeout = window.clearTimeout(timeout); | ||
ticker.cancel(tickerId); | ||
if (action === actionFinish) fn(); | ||
} | ||
if (lastTime) timer -= time - lastTime; | ||
lastTime = time; | ||
if (action !== actionCancel && action !== actionFinish) { | ||
timeout = window.setTimeout(function() { | ||
timeout = undefined; | ||
ticker.add(tickerId, fn, null, true); | ||
}, wait); | ||
} | ||
}; | ||
} | ||
if (timer > 0) { | ||
addDebounceTick(id, tick); | ||
} else { | ||
timer = lastTime = 0; | ||
fn(); | ||
} | ||
}; | ||
return function(action) { | ||
if (action !== actionCancel) fn(); | ||
return function (cancel) { | ||
if (isCanceled) return; | ||
if (durationMs <= 0) { | ||
if (cancel !== true) fn(); | ||
return; | ||
} | ||
if (cancel === true) { | ||
isCanceled = true; | ||
timer = lastTime = 0; | ||
tick = undefined; | ||
cancelDebounceTick(id); | ||
return; | ||
} | ||
if (timer <= 0) { | ||
timer = durationMs; | ||
tick(0); | ||
} else { | ||
timer = durationMs; | ||
} | ||
}; | ||
} |
@@ -15,3 +15,3 @@ /** | ||
ElProto.oMatchesSelector || | ||
function() { | ||
function () { | ||
return false; | ||
@@ -18,0 +18,0 @@ }; |
@@ -17,18 +17,14 @@ /** | ||
* @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)} | ||
*/ | ||
export default function getContainingBlock(element, includeSelf) { | ||
export default function getContainingBlock(element) { | ||
// 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 document = window.document; | ||
var ret = (includeSelf ? element : element.parentElement) || document; | ||
while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) { | ||
ret = ret.parentElement || document; | ||
var doc = document; | ||
var res = element || doc; | ||
while (res && res !== doc && getStyle(res, 'position') === 'static' && !isTransformed(res)) { | ||
res = res.parentElement || doc; | ||
} | ||
return ret; | ||
return res; | ||
} |
@@ -11,14 +11,24 @@ /** | ||
/** | ||
* Get current values of the provided styles definition object. | ||
* Get current values of the provided styles definition object or array. | ||
* | ||
* @param {HTMLElement} element | ||
* @param {Object} styles | ||
* @param {(Object|Array} styles | ||
* @return {Object} | ||
*/ | ||
export default function getCurrentStyles(element, styles) { | ||
var current = {}; | ||
for (var prop in styles) { | ||
current[prop] = getStyle(element, getStyleName(prop)); | ||
var result = {}; | ||
var prop, i; | ||
if (Array.isArray(styles)) { | ||
for (i = 0; i < styles.length; i++) { | ||
prop = styles[i]; | ||
result[prop] = getStyle(element, getStyleName(prop)); | ||
} | ||
} else { | ||
for (prop in styles) { | ||
result[prop] = getStyle(element, getStyleName(prop)); | ||
} | ||
} | ||
return current; | ||
return result; | ||
} |
@@ -27,29 +27,29 @@ /** | ||
function getOffset(element, offsetData) { | ||
var ret = offsetData || {}; | ||
var offset = offsetData || {}; | ||
var rect; | ||
// Set up return data. | ||
ret.left = 0; | ||
ret.top = 0; | ||
offset.left = 0; | ||
offset.top = 0; | ||
// Document's offsets are always 0. | ||
if (element === document) return ret; | ||
if (element === document) return offset; | ||
// Add viewport scroll left/top to the respective offsets. | ||
ret.left = window.pageXOffset || 0; | ||
ret.top = window.pageYOffset || 0; | ||
offset.left = window.pageXOffset || 0; | ||
offset.top = window.pageYOffset || 0; | ||
// Window's offsets are the viewport scroll left/top values. | ||
if (element.self === window.self) return ret; | ||
if (element.self === window.self) return offset; | ||
// Add element's client rects to the offsets. | ||
rect = element.getBoundingClientRect(); | ||
ret.left += rect.left; | ||
ret.top += rect.top; | ||
offset.left += rect.left; | ||
offset.top += rect.top; | ||
// Exclude element's borders from the offset. | ||
ret.left += getStyleAsFloat(element, 'border-left-width'); | ||
ret.top += getStyleAsFloat(element, 'border-top-width'); | ||
offset.left += getStyleAsFloat(element, 'border-left-width'); | ||
offset.top += getStyleAsFloat(element, 'border-top-width'); | ||
return ret; | ||
return offset; | ||
} | ||
@@ -77,4 +77,4 @@ | ||
if (compareContainingBlocks) { | ||
elemA = getContainingBlock(elemA, true); | ||
elemB = getContainingBlock(elemB, true); | ||
elemA = getContainingBlock(elemA); | ||
elemB = getContainingBlock(elemB); | ||
@@ -81,0 +81,0 @@ // If containing blocks are identical, let's return early. |
@@ -6,24 +6,30 @@ /** | ||
// Playing it safe here, test all potential prefixes capitalized and lowercase. | ||
var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O']; | ||
var cache = {}; | ||
/** | ||
* Get prefixed CSS property name when given a non-prefixed CSS property name. | ||
* @param {Object} elemStyle | ||
* @param {String} propName | ||
* @returns {!String} | ||
* Returns null if the property is not supported at all. | ||
* | ||
* @param {CSSStyleDeclaration} style | ||
* @param {String} prop | ||
* @returns {String} | ||
*/ | ||
export default function getPrefixedPropName(elemStyle, propName) { | ||
var camelPropName = propName[0].toUpperCase() + propName.slice(1); | ||
export default function getPrefixedPropName(style, prop) { | ||
var prefixedProp = cache[prop] || ''; | ||
if (prefixedProp) return prefixedProp; | ||
var camelProp = prop[0].toUpperCase() + prop.slice(1); | ||
var i = 0; | ||
var prefix; | ||
var prefixedPropName; | ||
while (i < vendorPrefixes.length) { | ||
prefix = vendorPrefixes[i]; | ||
prefixedPropName = prefix ? prefix + camelPropName : propName; | ||
if (prefixedPropName in elemStyle) return prefixedPropName; | ||
prefixedProp = vendorPrefixes[i] ? vendorPrefixes[i] + camelProp : prop; | ||
if (prefixedProp in style) { | ||
cache[prop] = prefixedProp; | ||
return prefixedProp; | ||
} | ||
++i; | ||
} | ||
return null; | ||
return ''; | ||
} |
@@ -10,19 +10,19 @@ /** | ||
/** | ||
* Collect element's ancestors that are potentially scrollable elements. | ||
* Collect element's ancestors that are potentially scrollable elements. The | ||
* provided element is also also included in the check, meaning that if it is | ||
* scrollable it is added to the result array. | ||
* | ||
* @param {HTMLElement} element | ||
* @param {Boolean} [includeSelf=false] | ||
* @param {Array} [data] | ||
* @param {Array} [result] | ||
* @returns {Array} | ||
*/ | ||
export default function getScrollableAncestors(element, includeSelf, data) { | ||
var ret = data || []; | ||
var parent = includeSelf ? element : element.parentNode; | ||
export default function getScrollableAncestors(element, result) { | ||
result = result || []; | ||
// Find scroll parents. | ||
while (parent && parent !== document) { | ||
while (element && element !== document) { | ||
// If element is inside ShadowDOM let's get it's host node from the real | ||
// DOM and continue looping. | ||
if (parent.getRootNode && parent instanceof DocumentFragment) { | ||
parent = parent.getRootNode().host; | ||
if (element.getRootNode && element instanceof DocumentFragment) { | ||
element = element.getRootNode().host; | ||
continue; | ||
@@ -32,13 +32,13 @@ } | ||
// If element is scrollable let's add it to the scrollable list. | ||
if (isScrollable(parent)) { | ||
ret.push(parent); | ||
if (isScrollable(element)) { | ||
result.push(element); | ||
} | ||
parent = parent.parentNode; | ||
element = element.parentNode; | ||
} | ||
// Always add window to the results. | ||
ret.push(window); | ||
result.push(window); | ||
return ret; | ||
return result; | ||
} |
@@ -6,7 +6,16 @@ /** | ||
*/ | ||
var isWeakMapSupported = typeof WeakMap === 'function'; | ||
var cache = isWeakMapSupported ? new WeakMap() : null; | ||
var cacheInterval = 3000; | ||
var cacheTimer; | ||
var canClearCache = true; | ||
var clearCache = function () { | ||
if (canClearCache) { | ||
cacheTimer = window.clearInterval(cacheTimer); | ||
cache = isWeakMapSupported ? new WeakMap() : null; | ||
} else { | ||
canClearCache = true; | ||
} | ||
}; | ||
import { transformStyle } from './supportedTransform'; | ||
var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null; | ||
/** | ||
@@ -20,8 +29,18 @@ * Returns the computed value of an element's style property as a string. | ||
export default function getStyle(element, style) { | ||
var styles = stylesCache && stylesCache.get(element); | ||
var styles = cache && cache.get(element); | ||
if (!styles) { | ||
styles = window.getComputedStyle(element, null); | ||
if (stylesCache) stylesCache.set(element, styles); | ||
if (cache) cache.set(element, styles); | ||
} | ||
return styles.getPropertyValue(style === 'transform' ? transformStyle : style); | ||
if (cache) { | ||
if (!cacheTimer) { | ||
cacheTimer = window.setInterval(clearCache, cacheInterval); | ||
} else { | ||
canClearCache = false; | ||
} | ||
} | ||
return styles.getPropertyValue(style); | ||
} |
@@ -8,11 +8,26 @@ /** | ||
var styleNameRegEx = /([A-Z])/g; | ||
var prefixRegex = /^(webkit-|moz-|ms-|o-)/; | ||
var msPrefixRegex = /^(-m-s-)/; | ||
/** | ||
* Transforms a camel case style property to kebab case style property. | ||
* Transforms a camel case style property to kebab case style property. Handles | ||
* vendor prefixed properties elegantly as well, e.g. "WebkitTransform" and | ||
* "webkitTransform" are both transformed into "-webkit-transform". | ||
* | ||
* @param {String} string | ||
* @param {String} property | ||
* @returns {String} | ||
*/ | ||
export default function getStyleName(string) { | ||
return string.replace(styleNameRegEx, '-$1').toLowerCase(); | ||
export default function getStyleName(property) { | ||
// Initial slicing, turns "fooBarProp" into "foo-bar-prop". | ||
var styleName = property.replace(styleNameRegEx, '-$1').toLowerCase(); | ||
// Handle properties that start with "webkit", "moz", "ms" or "o" prefix (we | ||
// need to add an extra '-' to the beginnig). | ||
styleName = styleName.replace(prefixRegex, '-$1'); | ||
// Handle properties that start with "MS" prefix (we need to transform the | ||
// "-m-s-" into "-ms-"). | ||
styleName = styleName.replace(msPrefixRegex, '-ms-'); | ||
return styleName; | ||
} |
@@ -8,5 +8,5 @@ /** | ||
import getStyle from './getStyle'; | ||
import transformStyle from './transformStyle'; | ||
var translateValue = {}; | ||
var transformStyle = 'transform'; | ||
var transformNone = 'none'; | ||
@@ -13,0 +13,0 @@ var rxMat3d = /^matrix3d/; |
@@ -7,3 +7,3 @@ /** | ||
var strFunction = 'function'; | ||
var functionType = 'function'; | ||
@@ -17,3 +17,3 @@ /** | ||
export default function isFunction(val) { | ||
return typeof val === strFunction; | ||
return typeof val === functionType; | ||
} |
@@ -11,3 +11,3 @@ /** | ||
/** | ||
* Check if a value is a node list | ||
* Check if a value is a node list or a html collection. | ||
* | ||
@@ -14,0 +14,0 @@ * @param {*} val |
@@ -9,7 +9,11 @@ /** | ||
var styleOverflow = 'overflow'; | ||
var styleOverflowX = 'overflow-x'; | ||
var styleOverflowY = 'overflow-y'; | ||
var overflowAuto = 'auto'; | ||
var overflowScroll = 'scroll'; | ||
/** | ||
* Check if overflow style value is scrollable. | ||
* | ||
* @param {String} value | ||
* @returns {Boolean} | ||
*/ | ||
function isScrollableOverflow(value) { | ||
return value === 'auto' || value === 'scroll' || value === 'overlay'; | ||
} | ||
@@ -23,12 +27,7 @@ /** | ||
export default function isScrollable(element) { | ||
var overflow = getStyle(element, styleOverflow); | ||
if (overflow === overflowAuto || overflow === overflowScroll) return true; | ||
overflow = getStyle(element, styleOverflowX); | ||
if (overflow === overflowAuto || overflow === overflowScroll) return true; | ||
overflow = getStyle(element, styleOverflowY); | ||
if (overflow === overflowAuto || overflow === overflowScroll) return true; | ||
return false; | ||
return ( | ||
isScrollableOverflow(getStyle(element, 'overflow')) || | ||
isScrollableOverflow(getStyle(element, 'overflow-x')) || | ||
isScrollableOverflow(getStyle(element, 'overflow-y')) | ||
); | ||
} |
@@ -8,3 +8,9 @@ /** | ||
import getStyle from './getStyle'; | ||
import transformStyle from './transformStyle'; | ||
var transformNone = 'none'; | ||
var displayInline = 'inline'; | ||
var displayNone = 'none'; | ||
var displayStyle = 'display'; | ||
/** | ||
@@ -23,9 +29,9 @@ * Returns true if element is transformed, false if not. In practice the | ||
export default function isTransformed(element) { | ||
var transform = getStyle(element, 'transform'); | ||
if (!transform || transform === 'none') return false; | ||
var transform = getStyle(element, transformStyle); | ||
if (!transform || transform === transformNone) return false; | ||
var display = getStyle(element, 'display'); | ||
if (display === 'inline' || display === 'none') return false; | ||
var display = getStyle(element, displayStyle); | ||
if (display === displayInline || display === displayNone) return false; | ||
return true; | ||
} |
@@ -10,12 +10,13 @@ /** | ||
* array index is within the bounds of the provided array and also transforms | ||
* negative index to the matching positive index. | ||
* negative index to the matching positive index. The third (optional) argument | ||
* allows you to define offset for array's length in case you are adding items | ||
* to the array or removing items from the array. | ||
* | ||
* @param {Array} array | ||
* @param {Number} index | ||
* @param {Boolean} isMigration | ||
* @param {Number} [sizeOffset] | ||
*/ | ||
export default function normalizeArrayIndex(array, index, isMigration) { | ||
var length = array.length; | ||
var maxIndex = Math.max(0, isMigration ? length : length - 1); | ||
export default function normalizeArrayIndex(array, index, sizeOffset) { | ||
var maxIndex = Math.max(0, array.length - 1 + (sizeOffset || 0)); | ||
return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index; | ||
} |
@@ -14,5 +14,5 @@ /** | ||
window.msRequestAnimationFrame || | ||
function(callback) { | ||
return this.setTimeout(function() { | ||
callback(dt); | ||
function (callback) { | ||
return this.setTimeout(function () { | ||
callback(Date.now()); | ||
}, dt); | ||
@@ -19,0 +19,0 @@ } |
@@ -16,2 +16,4 @@ /** | ||
export default function removeClass(element, className) { | ||
if (!className) return; | ||
if (element.classList) { | ||
@@ -18,0 +20,0 @@ element.classList.remove(className); |
@@ -7,6 +7,2 @@ /** | ||
import { transformProp } from './supportedTransform'; | ||
var transformStyle = 'transform'; | ||
/** | ||
@@ -20,4 +16,4 @@ * Set inline styles to an element. | ||
for (var prop in styles) { | ||
element.style[prop === transformStyle ? transformProp : prop] = styles[prop]; | ||
element.style[prop] = styles[prop]; | ||
} | ||
} |
@@ -12,7 +12,7 @@ /** | ||
* | ||
* @param {*} target | ||
* @param {*} val | ||
* @returns {Array} | ||
*/ | ||
export default function toArray(target) { | ||
return isNodeList(target) ? Array.prototype.slice.call(target) : Array.prototype.concat(target); | ||
export default function toArray(val) { | ||
return isNodeList(val) ? Array.prototype.slice.call(val) : Array.prototype.concat(val); | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
999909
21
24420
2833
0
74