From cddc0217724f1a7661014d50e4c940e623a0c2dc Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 3 Aug 2021 07:12:35 +0000 Subject: Apps drag and drop feature --- library/Sortable/src/Animation.js | 175 +++ library/Sortable/src/BrowserInfo.js | 12 + library/Sortable/src/EventDispatcher.js | 57 + library/Sortable/src/PluginManager.js | 94 ++ library/Sortable/src/Sortable.js | 2001 +++++++++++++++++++++++++++++++ library/Sortable/src/utils.js | 556 +++++++++ 6 files changed, 2895 insertions(+) create mode 100644 library/Sortable/src/Animation.js create mode 100644 library/Sortable/src/BrowserInfo.js create mode 100644 library/Sortable/src/EventDispatcher.js create mode 100644 library/Sortable/src/PluginManager.js create mode 100644 library/Sortable/src/Sortable.js create mode 100644 library/Sortable/src/utils.js (limited to 'library/Sortable/src') diff --git a/library/Sortable/src/Animation.js b/library/Sortable/src/Animation.js new file mode 100644 index 000000000..6aa8e3ef8 --- /dev/null +++ b/library/Sortable/src/Animation.js @@ -0,0 +1,175 @@ +import { getRect, css, matrix, isRectEqual, indexOfObject } from './utils.js'; +import Sortable from './Sortable.js'; + +export default function AnimationStateManager() { + let animationStates = [], + animationCallbackId; + + return { + captureAnimationState() { + animationStates = []; + if (!this.options.animation) return; + let children = [].slice.call(this.el.children); + + children.forEach(child => { + if (css(child, 'display') === 'none' || child === Sortable.ghost) return; + animationStates.push({ + target: child, + rect: getRect(child) + }); + let fromRect = { ...animationStates[animationStates.length - 1].rect }; + + // If animating: compensate for current animation + if (child.thisAnimationDuration) { + let childMatrix = matrix(child, true); + if (childMatrix) { + fromRect.top -= childMatrix.f; + fromRect.left -= childMatrix.e; + } + } + + child.fromRect = fromRect; + }); + }, + + addAnimationState(state) { + animationStates.push(state); + }, + + removeAnimationState(target) { + animationStates.splice(indexOfObject(animationStates, { target }), 1); + }, + + animateAll(callback) { + if (!this.options.animation) { + clearTimeout(animationCallbackId); + if (typeof(callback) === 'function') callback(); + return; + } + + let animating = false, + animationTime = 0; + + animationStates.forEach((state) => { + let time = 0, + animatingThis = false, + target = state.target, + fromRect = target.fromRect, + toRect = getRect(target), + prevFromRect = target.prevFromRect, + prevToRect = target.prevToRect, + animatingRect = state.rect, + targetMatrix = matrix(target, true); + + + if (targetMatrix) { + // Compensate for current animation + toRect.top -= targetMatrix.f; + toRect.left -= targetMatrix.e; + } + + target.toRect = toRect; + + if (target.thisAnimationDuration) { + // Could also check if animatingRect is between fromRect and toRect + if ( + isRectEqual(prevFromRect, toRect) && + !isRectEqual(fromRect, toRect) && + // Make sure animatingRect is on line between toRect & fromRect + (animatingRect.top - toRect.top) / + (animatingRect.left - toRect.left) === + (fromRect.top - toRect.top) / + (fromRect.left - toRect.left) + ) { + // If returning to same place as started from animation and on same axis + time = calculateRealTime(animatingRect, prevFromRect, prevToRect, this.options); + } + } + + // if fromRect != toRect: animate + if (!isRectEqual(toRect, fromRect)) { + target.prevFromRect = fromRect; + target.prevToRect = toRect; + + if (!time) { + time = this.options.animation; + } + this.animate( + target, + animatingRect, + toRect, + time + ); + } + + if (time) { + animating = true; + animationTime = Math.max(animationTime, time); + clearTimeout(target.animationResetTimer); + target.animationResetTimer = setTimeout(function() { + target.animationTime = 0; + target.prevFromRect = null; + target.fromRect = null; + target.prevToRect = null; + target.thisAnimationDuration = null; + }, time); + target.thisAnimationDuration = time; + } + }); + + + clearTimeout(animationCallbackId); + if (!animating) { + if (typeof(callback) === 'function') callback(); + } else { + animationCallbackId = setTimeout(function() { + if (typeof(callback) === 'function') callback(); + }, animationTime); + } + animationStates = []; + }, + + animate(target, currentRect, toRect, duration) { + if (duration) { + css(target, 'transition', ''); + css(target, 'transform', ''); + let elMatrix = matrix(this.el), + scaleX = elMatrix && elMatrix.a, + scaleY = elMatrix && elMatrix.d, + translateX = (currentRect.left - toRect.left) / (scaleX || 1), + translateY = (currentRect.top - toRect.top) / (scaleY || 1); + + target.animatingX = !!translateX; + target.animatingY = !!translateY; + + css(target, 'transform', 'translate3d(' + translateX + 'px,' + translateY + 'px,0)'); + + this.forRepaintDummy = repaint(target); // repaint + + css(target, 'transition', 'transform ' + duration + 'ms' + (this.options.easing ? ' ' + this.options.easing : '')); + css(target, 'transform', 'translate3d(0,0,0)'); + (typeof target.animated === 'number') && clearTimeout(target.animated); + target.animated = setTimeout(function () { + css(target, 'transition', ''); + css(target, 'transform', ''); + target.animated = false; + + target.animatingX = false; + target.animatingY = false; + }, duration); + } + } + }; +} + +function repaint(target) { + return target.offsetWidth; +} + + +function calculateRealTime(animatingRect, fromRect, toRect, options) { + return ( + Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / + Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) + ) * options.animation; +} diff --git a/library/Sortable/src/BrowserInfo.js b/library/Sortable/src/BrowserInfo.js new file mode 100644 index 000000000..304a853a2 --- /dev/null +++ b/library/Sortable/src/BrowserInfo.js @@ -0,0 +1,12 @@ +function userAgent(pattern) { + if (typeof window !== 'undefined' && window.navigator) { + return !!/*@__PURE__*/navigator.userAgent.match(pattern); + } +} + +export const IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i); +export const Edge = userAgent(/Edge/i); +export const FireFox = userAgent(/firefox/i); +export const Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i); +export const IOS = userAgent(/iP(ad|od|hone)/i); +export const ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i); diff --git a/library/Sortable/src/EventDispatcher.js b/library/Sortable/src/EventDispatcher.js new file mode 100644 index 000000000..e47cb5809 --- /dev/null +++ b/library/Sortable/src/EventDispatcher.js @@ -0,0 +1,57 @@ +import { IE11OrLess, Edge } from './BrowserInfo.js'; +import { expando } from './utils.js'; +import PluginManager from './PluginManager.js'; + +export default function dispatchEvent( + { + sortable, rootEl, name, + targetEl, cloneEl, toEl, fromEl, + oldIndex, newIndex, + oldDraggableIndex, newDraggableIndex, + originalEvent, putSortable, extraEventProperties + } +) { + sortable = (sortable || (rootEl && rootEl[expando])); + if (!sortable) return; + + let evt, + options = sortable.options, + onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); + // Support for new CustomEvent feature + if (window.CustomEvent && !IE11OrLess && !Edge) { + evt = new CustomEvent(name, { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + } + + evt.to = toEl || rootEl; + evt.from = fromEl || rootEl; + evt.item = targetEl || rootEl; + evt.clone = cloneEl; + + evt.oldIndex = oldIndex; + evt.newIndex = newIndex; + + evt.oldDraggableIndex = oldDraggableIndex; + evt.newDraggableIndex = newDraggableIndex; + + evt.originalEvent = originalEvent; + evt.pullMode = putSortable ? putSortable.lastPutMode : undefined; + + let allEventProperties = { ...extraEventProperties, ...PluginManager.getEventProperties(name, sortable) }; + for (let option in allEventProperties) { + evt[option] = allEventProperties[option]; + } + + if (rootEl) { + rootEl.dispatchEvent(evt); + } + + if (options[onName]) { + options[onName].call(sortable, evt); + } +} diff --git a/library/Sortable/src/PluginManager.js b/library/Sortable/src/PluginManager.js new file mode 100644 index 000000000..db0a0f238 --- /dev/null +++ b/library/Sortable/src/PluginManager.js @@ -0,0 +1,94 @@ +let plugins = []; + +const defaults = { + initializeByDefault: true +}; + +export default { + mount(plugin) { + // Set default static properties + for (let option in defaults) { + if (defaults.hasOwnProperty(option) && !(option in plugin)) { + plugin[option] = defaults[option]; + } + } + + plugins.forEach(p => { + if (p.pluginName === plugin.pluginName) { + throw (`Sortable: Cannot mount plugin ${ plugin.pluginName } more than once`); + } + }); + + plugins.push(plugin); + }, + pluginEvent(eventName, sortable, evt) { + this.eventCanceled = false; + evt.cancel = () => { + this.eventCanceled = true; + }; + const eventNameGlobal = eventName + 'Global'; + plugins.forEach(plugin => { + if (!sortable[plugin.pluginName]) return; + // Fire global events if it exists in this sortable + if ( + sortable[plugin.pluginName][eventNameGlobal] + ) { + sortable[plugin.pluginName][eventNameGlobal]({ sortable, ...evt }); + } + + // Only fire plugin event if plugin is enabled in this sortable, + // and plugin has event defined + if ( + sortable.options[plugin.pluginName] && + sortable[plugin.pluginName][eventName] + ) { + sortable[plugin.pluginName][eventName]({ sortable, ...evt }); + } + }); + }, + initializePlugins(sortable, el, defaults, options) { + plugins.forEach(plugin => { + const pluginName = plugin.pluginName; + if (!sortable.options[pluginName] && !plugin.initializeByDefault) return; + + let initialized = new plugin(sortable, el, sortable.options); + initialized.sortable = sortable; + initialized.options = sortable.options; + sortable[pluginName] = initialized; + + // Add default options from plugin + Object.assign(defaults, initialized.defaults); + }); + + for (let option in sortable.options) { + if (!sortable.options.hasOwnProperty(option)) continue; + let modified = this.modifyOption(sortable, option, sortable.options[option]); + if (typeof(modified) !== 'undefined') { + sortable.options[option] = modified; + } + } + }, + getEventProperties(name, sortable) { + let eventProperties = {}; + plugins.forEach(plugin => { + if (typeof(plugin.eventProperties) !== 'function') return; + Object.assign(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name)); + }); + + return eventProperties; + }, + modifyOption(sortable, name, value) { + let modifiedValue; + plugins.forEach(plugin => { + // Plugin must exist on the Sortable + if (!sortable[plugin.pluginName]) return; + + // If static option listener exists for this option, call in the context of the Sortable's instance of this plugin + if (plugin.optionListeners && typeof(plugin.optionListeners[name]) === 'function') { + modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value); + } + }); + + return modifiedValue; + } +}; diff --git a/library/Sortable/src/Sortable.js b/library/Sortable/src/Sortable.js new file mode 100644 index 000000000..d66b8e7f6 --- /dev/null +++ b/library/Sortable/src/Sortable.js @@ -0,0 +1,2001 @@ +/**! + * Sortable + * @author RubaXa + * @author owenm + * @license MIT + */ + +import { version } from '../package.json'; + +import { IE11OrLess, Edge, FireFox, Safari, IOS, ChromeForAndroid } from './BrowserInfo.js'; + +import AnimationStateManager from './Animation.js'; + +import PluginManager from './PluginManager.js'; + +import dispatchEvent from './EventDispatcher.js'; + +import { + on, + off, + closest, + toggleClass, + css, + matrix, + find, + getWindowScrollingElement, + getRect, + isScrolledPast, + getChild, + lastChild, + index, + getRelativeScrollOffset, + extend, + throttle, + scrollBy, + clone, + expando +} from './utils.js'; + + +let pluginEvent = function(eventName, sortable, { evt: originalEvent, ...data } = {}) { + PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, { + dragEl, + parentEl, + ghostEl, + rootEl, + nextEl, + lastDownEl, + cloneEl, + cloneHidden, + dragStarted: moved, + putSortable, + activeSortable: Sortable.active, + originalEvent, + + oldIndex, + oldDraggableIndex, + newIndex, + newDraggableIndex, + + hideGhostForTarget: _hideGhostForTarget, + unhideGhostForTarget: _unhideGhostForTarget, + + + cloneNowHidden() { + cloneHidden = true; + }, + cloneNowShown() { + cloneHidden = false; + }, + + dispatchSortableEvent(name) { + _dispatchEvent({ sortable, name, originalEvent }); + }, + + ...data + }); +}; + +function _dispatchEvent(info) { + dispatchEvent({ + putSortable, + cloneEl, + targetEl: dragEl, + rootEl, + oldIndex, + oldDraggableIndex, + newIndex, + newDraggableIndex, + ...info + }); +} + + +let dragEl, + parentEl, + ghostEl, + rootEl, + nextEl, + lastDownEl, + + cloneEl, + cloneHidden, + + oldIndex, + newIndex, + oldDraggableIndex, + newDraggableIndex, + + activeGroup, + putSortable, + + awaitingDragStarted = false, + ignoreNextClick = false, + sortables = [], + + tapEvt, + touchEvt, + lastDx, + lastDy, + tapDistanceLeft, + tapDistanceTop, + + moved, + + lastTarget, + lastDirection, + pastFirstInvertThresh = false, + isCircumstantialInvert = false, + + targetMoveDistance, + + // For positioning ghost absolutely + ghostRelativeParent, + ghostRelativeParentInitialScroll = [], // (left, top) + + _silent = false, + savedInputChecked = []; + + /** @const */ + const documentExists = typeof document !== 'undefined', + + PositionGhostAbsolutely = IOS, + + CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float', + + // This will not pass for IE9, because IE9 DnD only works on anchors + supportDraggable = documentExists && !ChromeForAndroid && !IOS && ('draggable' in document.createElement('div')), + + supportCssPointerEvents = (function() { + if (!documentExists) return; + // false when <= IE11 + if (IE11OrLess) { + return false; + } + let el = document.createElement('x'); + el.style.cssText = 'pointer-events:auto'; + return el.style.pointerEvents === 'auto'; + })(), + + _detectDirection = function(el, options) { + let elCSS = css(el), + elWidth = parseInt(elCSS.width) + - parseInt(elCSS.paddingLeft) + - parseInt(elCSS.paddingRight) + - parseInt(elCSS.borderLeftWidth) + - parseInt(elCSS.borderRightWidth), + child1 = getChild(el, 0, options), + child2 = getChild(el, 1, options), + firstChildCSS = child1 && css(child1), + secondChildCSS = child2 && css(child2), + firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + getRect(child1).width, + secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + getRect(child2).width; + + if (elCSS.display === 'flex') { + return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' + ? 'vertical' : 'horizontal'; + } + + if (elCSS.display === 'grid') { + return elCSS.gridTemplateColumns.split(' ').length <= 1 ? 'vertical' : 'horizontal'; + } + + if (child1 && firstChildCSS.float && firstChildCSS.float !== 'none') { + let touchingSideChild2 = firstChildCSS.float === 'left' ? 'left' : 'right'; + + return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ? + 'vertical' : 'horizontal'; + } + + return (child1 && + ( + firstChildCSS.display === 'block' || + firstChildCSS.display === 'flex' || + firstChildCSS.display === 'table' || + firstChildCSS.display === 'grid' || + firstChildWidth >= elWidth && + elCSS[CSSFloatProperty] === 'none' || + child2 && + elCSS[CSSFloatProperty] === 'none' && + firstChildWidth + secondChildWidth > elWidth + ) ? + 'vertical' : 'horizontal' + ); + }, + + _dragElInRowColumn = function(dragRect, targetRect, vertical) { + let dragElS1Opp = vertical ? dragRect.left : dragRect.top, + dragElS2Opp = vertical ? dragRect.right : dragRect.bottom, + dragElOppLength = vertical ? dragRect.width : dragRect.height, + targetS1Opp = vertical ? targetRect.left : targetRect.top, + targetS2Opp = vertical ? targetRect.right : targetRect.bottom, + targetOppLength = vertical ? targetRect.width : targetRect.height; + + return ( + dragElS1Opp === targetS1Opp || + dragElS2Opp === targetS2Opp || + (dragElS1Opp + dragElOppLength / 2) === (targetS1Opp + targetOppLength / 2) + ); + }, + + /** + * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold. + * @param {Number} x X position + * @param {Number} y Y position + * @return {HTMLElement} Element of the first found nearest Sortable + */ + _detectNearestEmptySortable = function(x, y) { + let ret; + sortables.some((sortable) => { + const threshold = sortable[expando].options.emptyInsertThreshold; + if (!threshold || lastChild(sortable)) return; + + const rect = getRect(sortable), + insideHorizontally = x >= (rect.left - threshold) && x <= (rect.right + threshold), + insideVertically = y >= (rect.top - threshold) && y <= (rect.bottom + threshold); + + if (insideHorizontally && insideVertically) { + return (ret = sortable); + } + }); + return ret; + }, + + _prepareGroup = function (options) { + function toFn(value, pull) { + return function(to, from, dragEl, evt) { + let sameGroup = to.options.group.name && + from.options.group.name && + to.options.group.name === from.options.group.name; + + if (value == null && (pull || sameGroup)) { + // Default pull value + // Default pull and put value if same group + return true; + } else if (value == null || value === false) { + return false; + } else if (pull && value === 'clone') { + return value; + } else if (typeof value === 'function') { + return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt); + } else { + let otherGroup = (pull ? to : from).options.group.name; + + return (value === true || + (typeof value === 'string' && value === otherGroup) || + (value.join && value.indexOf(otherGroup) > -1)); + } + }; + } + + let group = {}; + let originalGroup = options.group; + + if (!originalGroup || typeof originalGroup != 'object') { + originalGroup = {name: originalGroup}; + } + + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + group.revertClone = originalGroup.revertClone; + + options.group = group; + }, + + _hideGhostForTarget = function() { + if (!supportCssPointerEvents && ghostEl) { + css(ghostEl, 'display', 'none'); + } + }, + + _unhideGhostForTarget = function() { + if (!supportCssPointerEvents && ghostEl) { + css(ghostEl, 'display', ''); + } + }; + + +// #1184 fix - Prevent click event on fallback if dragged but item not changed position +if (documentExists) { + document.addEventListener('click', function(evt) { + if (ignoreNextClick) { + evt.preventDefault(); + evt.stopPropagation && evt.stopPropagation(); + evt.stopImmediatePropagation && evt.stopImmediatePropagation(); + ignoreNextClick = false; + return false; + } + }, true); +} + +let nearestEmptyInsertDetectEvent = function(evt) { + if (dragEl) { + evt = evt.touches ? evt.touches[0] : evt; + let nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY); + + if (nearest) { + // Create imitation event + let event = {}; + for (let i in evt) { + if (evt.hasOwnProperty(i)) { + event[i] = evt[i]; + } + } + event.target = event.rootEl = nearest; + event.preventDefault = void 0; + event.stopPropagation = void 0; + nearest[expando]._onDragOver(event); + } + } +}; + + +let _checkOutsideTargetEl = function(evt) { + if (dragEl) { + dragEl.parentNode[expando]._isOutsideThisEl(evt.target); + } +}; + + +/** + * @class Sortable + * @param {HTMLElement} el + * @param {Object} [options] + */ +function Sortable(el, options) { + if (!(el && el.nodeType && el.nodeType === 1)) { + throw `Sortable: \`el\` must be an HTMLElement, not ${ {}.toString.call(el) }`; + } + + this.el = el; // root element + this.options = options = Object.assign({}, options); + + + // Export instance + el[expando] = this; + + let defaults = { + group: null, + sort: true, + disabled: false, + store: null, + handle: null, + draggable: /^[uo]l$/i.test(el.nodeName) ? '>li' : '>*', + swapThreshold: 1, // percentage; 0 <= x <= 1 + invertSwap: false, // invert always + invertedSwapThreshold: null, // will be set to same as swapThreshold if default + removeCloneOnHide: true, + direction: function() { + return _detectDirection(el, this.options); + }, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + ignore: 'a, img', + filter: null, + preventOnFilter: true, + animation: 0, + easing: null, + setData: function (dataTransfer, dragEl) { + dataTransfer.setData('Text', dragEl.textContent); + }, + dropBubble: false, + dragoverBubble: false, + dataIdAttr: 'data-id', + delay: 0, + delayOnTouchOnly: false, + touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1, + forceFallback: false, + fallbackClass: 'sortable-fallback', + fallbackOnBody: false, + fallbackTolerance: 0, + fallbackOffset: {x: 0, y: 0}, + supportPointer: Sortable.supportPointer !== false && ('PointerEvent' in window) && !Safari, + emptyInsertThreshold: 5 + }; + + PluginManager.initializePlugins(this, el, defaults); + + // Set default options + for (let name in defaults) { + !(name in options) && (options[name] = defaults[name]); + } + + _prepareGroup(options); + + // Bind all private methods + for (let fn in this) { + if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { + this[fn] = this[fn].bind(this); + } + } + + // Setup drag mode + this.nativeDraggable = options.forceFallback ? false : supportDraggable; + + if (this.nativeDraggable) { + // Touch start threshold cannot be greater than the native dragstart threshold + this.options.touchStartThreshold = 1; + } + + // Bind events + if (options.supportPointer) { + on(el, 'pointerdown', this._onTapStart); + } else { + on(el, 'mousedown', this._onTapStart); + on(el, 'touchstart', this._onTapStart); + } + + if (this.nativeDraggable) { + on(el, 'dragover', this); + on(el, 'dragenter', this); + } + + sortables.push(this.el); + + // Restore sorting + options.store && options.store.get && this.sort(options.store.get(this) || []); + + // Add animation state manager + Object.assign(this, AnimationStateManager()); +} + +Sortable.prototype = /** @lends Sortable.prototype */ { + constructor: Sortable, + + _isOutsideThisEl: function(target) { + if (!this.el.contains(target) && target !== this.el) { + lastTarget = null; + } + }, + + _getDirection: function(evt, target) { + return (typeof this.options.direction === 'function') ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction; + }, + + _onTapStart: function (/** Event|TouchEvent */evt) { + if (!evt.cancelable) return; + let _this = this, + el = this.el, + options = this.options, + preventOnFilter = options.preventOnFilter, + type = evt.type, + touch = (evt.touches && evt.touches[0]) || (evt.pointerType && evt.pointerType === 'touch' && evt), + target = (touch || evt).target, + originalTarget = evt.target.shadowRoot && ((evt.path && evt.path[0]) || (evt.composedPath && evt.composedPath()[0])) || target, + filter = options.filter; + + _saveInputCheckedState(el); + + + // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. + if (dragEl) { + return; + } + + if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { + return; // only left button and enabled + } + + // cancel dnd if original target is content editable + if (originalTarget.isContentEditable) { + return; + } + + // Safari ignores further event handling after mousedown + if (!this.nativeDraggable && Safari && target && target.tagName.toUpperCase() === 'SELECT') { + return; + } + + target = closest(target, options.draggable, el, false); + + + if (target && target.animated) { + return; + } + + if (lastDownEl === target) { + // Ignoring duplicate `down` + return; + } + + // Get the index of the dragged element within its parent + oldIndex = index(target); + oldDraggableIndex = index(target, options.draggable); + + // Check filter + if (typeof filter === 'function') { + if (filter.call(this, evt, target, this)) { + _dispatchEvent({ + sortable: _this, + rootEl: originalTarget, + name: 'filter', + targetEl: target, + toEl: el, + fromEl: el + }); + pluginEvent('filter', _this, { evt }); + preventOnFilter && evt.cancelable && evt.preventDefault(); + return; // cancel dnd + } + } + else if (filter) { + filter = filter.split(',').some(function (criteria) { + criteria = closest(originalTarget, criteria.trim(), el, false); + + if (criteria) { + _dispatchEvent({ + sortable: _this, + rootEl: criteria, + name: 'filter', + targetEl: target, + fromEl: el, + toEl: el + }); + pluginEvent('filter', _this, { evt }); + return true; + } + }); + + if (filter) { + preventOnFilter && evt.cancelable && evt.preventDefault(); + return; // cancel dnd + } + } + + if (options.handle && !closest(originalTarget, options.handle, el, false)) { + return; + } + + // Prepare `dragstart` + this._prepareDragStart(evt, touch, target); + }, + + _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) { + let _this = this, + el = _this.el, + options = _this.options, + ownerDocument = el.ownerDocument, + dragStartFn; + + if (target && !dragEl && (target.parentNode === el)) { + let dragRect = getRect(target); + rootEl = el; + dragEl = target; + parentEl = dragEl.parentNode; + nextEl = dragEl.nextSibling; + lastDownEl = target; + activeGroup = options.group; + + Sortable.dragged = dragEl; + + tapEvt = { + target: dragEl, + clientX: (touch || evt).clientX, + clientY: (touch || evt).clientY + }; + + tapDistanceLeft = tapEvt.clientX - dragRect.left; + tapDistanceTop = tapEvt.clientY - dragRect.top; + + this._lastX = (touch || evt).clientX; + this._lastY = (touch || evt).clientY; + + dragEl.style['will-change'] = 'all'; + + dragStartFn = function () { + pluginEvent('delayEnded', _this, { evt }); + if (Sortable.eventCanceled) { + _this._onDrop(); + return; + } + // Delayed drag has been triggered + // we can re-enable the events: touchmove/mousemove + _this._disableDelayedDragEvents(); + + if (!FireFox && _this.nativeDraggable) { + dragEl.draggable = true; + } + + // Bind the events: dragstart/dragend + _this._triggerDragStart(evt, touch); + + // Drag start event + _dispatchEvent({ + sortable: _this, + name: 'choose', + originalEvent: evt + }); + + // Chosen item + toggleClass(dragEl, options.chosenClass, true); + }; + + // Disable "draggable" + options.ignore.split(',').forEach(function (criteria) { + find(dragEl, criteria.trim(), _disableDraggable); + }); + + on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent); + on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent); + on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent); + + on(ownerDocument, 'mouseup', _this._onDrop); + on(ownerDocument, 'touchend', _this._onDrop); + on(ownerDocument, 'touchcancel', _this._onDrop); + + // Make dragEl draggable (must be before delay for FireFox) + if (FireFox && this.nativeDraggable) { + this.options.touchStartThreshold = 4; + dragEl.draggable = true; + } + + pluginEvent('delayStart', this, { evt }); + + // Delay is impossible for native DnD in Edge or IE + if (options.delay && (!options.delayOnTouchOnly || touch) && (!this.nativeDraggable || !(Edge || IE11OrLess))) { + if (Sortable.eventCanceled) { + this._onDrop(); + return; + } + // If the user moves the pointer or let go the click or touch + // before the delay has been reached: + // disable the delayed drag + on(ownerDocument, 'mouseup', _this._disableDelayedDrag); + on(ownerDocument, 'touchend', _this._disableDelayedDrag); + on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); + on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler); + on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); + options.supportPointer && on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); + + _this._dragStartTimer = setTimeout(dragStartFn, options.delay); + } else { + dragStartFn(); + } + } + }, + + _delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) { + let touch = e.touches ? e.touches[0] : e; + if (Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) + >= Math.floor(this.options.touchStartThreshold / (this.nativeDraggable && window.devicePixelRatio || 1)) + ) { + this._disableDelayedDrag(); + } + }, + + _disableDelayedDrag: function () { + dragEl && _disableDraggable(dragEl); + clearTimeout(this._dragStartTimer); + + this._disableDelayedDragEvents(); + }, + + _disableDelayedDragEvents: function () { + let ownerDocument = this.el.ownerDocument; + off(ownerDocument, 'mouseup', this._disableDelayedDrag); + off(ownerDocument, 'touchend', this._disableDelayedDrag); + off(ownerDocument, 'touchcancel', this._disableDelayedDrag); + off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler); + off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler); + off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler); + }, + + _triggerDragStart: function (/** Event */evt, /** Touch */touch) { + touch = touch || (evt.pointerType == 'touch' && evt); + + if (!this.nativeDraggable || touch) { + if (this.options.supportPointer) { + on(document, 'pointermove', this._onTouchMove); + } else if (touch) { + on(document, 'touchmove', this._onTouchMove); + } else { + on(document, 'mousemove', this._onTouchMove); + } + } else { + on(dragEl, 'dragend', this); + on(rootEl, 'dragstart', this._onDragStart); + } + + try { + if (document.selection) { + // Timeout neccessary for IE9 + _nextTick(function () { + document.selection.empty(); + }); + } else { + window.getSelection().removeAllRanges(); + } + } catch (err) { + } + }, + + _dragStarted: function (fallback, evt) { + let _this = this; + awaitingDragStarted = false; + if (rootEl && dragEl) { + pluginEvent('dragStarted', this, { evt }); + + if (this.nativeDraggable) { + on(document, 'dragover', _checkOutsideTargetEl); + } + let options = this.options; + + // Apply effect + !fallback && toggleClass(dragEl, options.dragClass, false); + toggleClass(dragEl, options.ghostClass, true); + + Sortable.active = this; + + fallback && this._appendGhost(); + + // Drag start event + _dispatchEvent({ + sortable: this, + name: 'start', + originalEvent: evt + }); + } else { + this._nulling(); + } + }, + + _emulateDragOver: function () { + if (touchEvt) { + this._lastX = touchEvt.clientX; + this._lastY = touchEvt.clientY; + + _hideGhostForTarget(); + + let target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + let parent = target; + + while (target && target.shadowRoot) { + target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + if (target === parent) break; + parent = target; + } + + dragEl.parentNode[expando]._isOutsideThisEl(target); + + if (parent) { + do { + if (parent[expando]) { + let inserted; + + inserted = parent[expando]._onDragOver({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target: target, + rootEl: parent + }); + + if (inserted && !this.options.dragoverBubble) { + break; + } + } + + target = parent; // store last element + } + /* jshint boss:true */ + while (parent = parent.parentNode); + } + + _unhideGhostForTarget(); + } + }, + + + _onTouchMove: function (/**TouchEvent*/evt) { + if (tapEvt) { + let options = this.options, + fallbackTolerance = options.fallbackTolerance, + fallbackOffset = options.fallbackOffset, + touch = evt.touches ? evt.touches[0] : evt, + ghostMatrix = ghostEl && matrix(ghostEl, true), + scaleX = ghostEl && ghostMatrix && ghostMatrix.a, + scaleY = ghostEl && ghostMatrix && ghostMatrix.d, + relativeScrollOffset = PositionGhostAbsolutely && ghostRelativeParent && getRelativeScrollOffset(ghostRelativeParent), + dx = ((touch.clientX - tapEvt.clientX) + + fallbackOffset.x) / (scaleX || 1) + + (relativeScrollOffset ? (relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0]) : 0) / (scaleX || 1), + dy = ((touch.clientY - tapEvt.clientY) + + fallbackOffset.y) / (scaleY || 1) + + (relativeScrollOffset ? (relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1]) : 0) / (scaleY || 1); + + // only set the status to dragging, when we are actually dragging + if (!Sortable.active && !awaitingDragStarted) { + if (fallbackTolerance && + Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) < fallbackTolerance + ) { + return; + } + this._onDragStart(evt, true); + } + + if (ghostEl) { + if (ghostMatrix) { + ghostMatrix.e += dx - (lastDx || 0); + ghostMatrix.f += dy - (lastDy || 0); + } else { + ghostMatrix = { + a: 1, + b: 0, + c: 0, + d: 1, + e: dx, + f: dy + }; + } + + let cssMatrix = `matrix(${ghostMatrix.a},${ghostMatrix.b},${ghostMatrix.c},${ghostMatrix.d},${ghostMatrix.e},${ghostMatrix.f})`; + + css(ghostEl, 'webkitTransform', cssMatrix); + css(ghostEl, 'mozTransform', cssMatrix); + css(ghostEl, 'msTransform', cssMatrix); + css(ghostEl, 'transform', cssMatrix); + + lastDx = dx; + lastDy = dy; + + touchEvt = touch; + } + + evt.cancelable && evt.preventDefault(); + } + }, + + _appendGhost: function () { + // Bug if using scale(): https://stackoverflow.com/questions/2637058 + // Not being adjusted for + if (!ghostEl) { + let container = this.options.fallbackOnBody ? document.body : rootEl, + rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container), + options = this.options; + + // Position absolutely + if (PositionGhostAbsolutely) { + // Get relatively positioned parent + ghostRelativeParent = container; + + while ( + css(ghostRelativeParent, 'position') === 'static' && + css(ghostRelativeParent, 'transform') === 'none' && + ghostRelativeParent !== document + ) { + ghostRelativeParent = ghostRelativeParent.parentNode; + } + + if (ghostRelativeParent !== document.body && ghostRelativeParent !== document.documentElement) { + if (ghostRelativeParent === document) ghostRelativeParent = getWindowScrollingElement(); + + rect.top += ghostRelativeParent.scrollTop; + rect.left += ghostRelativeParent.scrollLeft; + } else { + ghostRelativeParent = getWindowScrollingElement(); + } + ghostRelativeParentInitialScroll = getRelativeScrollOffset(ghostRelativeParent); + } + + + ghostEl = dragEl.cloneNode(true); + + toggleClass(ghostEl, options.ghostClass, false); + toggleClass(ghostEl, options.fallbackClass, true); + toggleClass(ghostEl, options.dragClass, true); + + css(ghostEl, 'transition', ''); + css(ghostEl, 'transform', ''); + + css(ghostEl, 'box-sizing', 'border-box'); + css(ghostEl, 'margin', 0); + css(ghostEl, 'top', rect.top); + css(ghostEl, 'left', rect.left); + css(ghostEl, 'width', rect.width); + css(ghostEl, 'height', rect.height); + css(ghostEl, 'opacity', '0.8'); + css(ghostEl, 'position', (PositionGhostAbsolutely ? 'absolute' : 'fixed')); + css(ghostEl, 'zIndex', '100000'); + css(ghostEl, 'pointerEvents', 'none'); + + + Sortable.ghost = ghostEl; + + container.appendChild(ghostEl); + + // Set transform-origin + css(ghostEl, 'transform-origin', (tapDistanceLeft / parseInt(ghostEl.style.width) * 100) + '% ' + (tapDistanceTop / parseInt(ghostEl.style.height) * 100) + '%'); + } + }, + + _onDragStart: function (/**Event*/evt, /**boolean*/fallback) { + let _this = this; + let dataTransfer = evt.dataTransfer; + let options = _this.options; + + pluginEvent('dragStart', this, { evt }); + if (Sortable.eventCanceled) { + this._onDrop(); + return; + } + + pluginEvent('setupClone', this); + if (!Sortable.eventCanceled) { + cloneEl = clone(dragEl); + + cloneEl.draggable = false; + cloneEl.style['will-change'] = ''; + + this._hideClone(); + + toggleClass(cloneEl, this.options.chosenClass, false); + Sortable.clone = cloneEl; + } + + + // #1143: IFrame support workaround + _this.cloneId = _nextTick(function() { + pluginEvent('clone', _this); + if (Sortable.eventCanceled) return; + + if (!_this.options.removeCloneOnHide) { + rootEl.insertBefore(cloneEl, dragEl); + } + _this._hideClone(); + + _dispatchEvent({ + sortable: _this, + name: 'clone' + }); + }); + + + !fallback && toggleClass(dragEl, options.dragClass, true); + + // Set proper drop events + if (fallback) { + ignoreNextClick = true; + _this._loopId = setInterval(_this._emulateDragOver, 50); + } else { + // Undo what was set in _prepareDragStart before drag started + off(document, 'mouseup', _this._onDrop); + off(document, 'touchend', _this._onDrop); + off(document, 'touchcancel', _this._onDrop); + + if (dataTransfer) { + dataTransfer.effectAllowed = 'move'; + options.setData && options.setData.call(_this, dataTransfer, dragEl); + } + + on(document, 'drop', _this); + + // #1276 fix: + css(dragEl, 'transform', 'translateZ(0)'); + } + + awaitingDragStarted = true; + + _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback, evt)); + on(document, 'selectstart', _this); + + moved = true; + + if (Safari) { + css(document.body, 'user-select', 'none'); + } + }, + + + // Returns true - if no further action is needed (either inserted or another condition) + _onDragOver: function (/**Event*/evt) { + let el = this.el, + target = evt.target, + dragRect, + targetRect, + revert, + options = this.options, + group = options.group, + activeSortable = Sortable.active, + isOwner = (activeGroup === group), + canSort = options.sort, + fromSortable = (putSortable || activeSortable), + vertical, + _this = this, + completedFired = false; + + if (_silent) return; + + function dragOverEvent(name, extra) { + pluginEvent(name, _this, { + evt, + isOwner, + axis: vertical ? 'vertical' : 'horizontal', + revert, + dragRect, + targetRect, + canSort, + fromSortable, + target, + completed, + onMove(target, after) { + return onMove(rootEl, el, dragEl, dragRect, target, getRect(target), evt, after); + }, + changed, + ...extra + }); + } + + // Capture animation state + function capture() { + dragOverEvent('dragOverAnimationCapture'); + + _this.captureAnimationState(); + if (_this !== fromSortable) { + fromSortable.captureAnimationState(); + } + } + + // Return invocation when dragEl is inserted (or completed) + function completed(insertion) { + dragOverEvent('dragOverCompleted', { insertion }); + + if (insertion) { + // Clones must be hidden before folding animation to capture dragRectAbsolute properly + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(_this); + } + + if (_this !== fromSortable) { + // Set ghost class to new sortable's ghost class + toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false); + toggleClass(dragEl, options.ghostClass, true); + } + + if (putSortable !== _this && _this !== Sortable.active) { + putSortable = _this; + } else if (_this === Sortable.active && putSortable) { + putSortable = null; + } + + // Animation + if (fromSortable === _this) { + _this._ignoreWhileAnimating = target; + } + _this.animateAll(function() { + dragOverEvent('dragOverAnimationComplete'); + _this._ignoreWhileAnimating = null; + }); + if (_this !== fromSortable) { + fromSortable.animateAll(); + fromSortable._ignoreWhileAnimating = null; + } + } + + + // Null lastTarget if it is not inside a previously swapped element + if ((target === dragEl && !dragEl.animated) || (target === el && !target.animated)) { + lastTarget = null; + } + + // no bubbling and not fallback + if (!options.dragoverBubble && !evt.rootEl && target !== document) { + dragEl.parentNode[expando]._isOutsideThisEl(evt.target); + + // Do not detect for empty insert if already inserted + !insertion && nearestEmptyInsertDetectEvent(evt); + } + + !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation(); + + return (completedFired = true); + } + + // Call when dragEl has been inserted + function changed() { + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + _dispatchEvent({ + sortable: _this, + name: 'change', + toEl: el, + newIndex, + newDraggableIndex, + originalEvent: evt + }); + } + + + if (evt.preventDefault !== void 0) { + evt.cancelable && evt.preventDefault(); + } + + + target = closest(target, options.draggable, el, true); + + dragOverEvent('dragOver'); + if (Sortable.eventCanceled) return completedFired; + + if ( + dragEl.contains(evt.target) || + target.animated && target.animatingX && target.animatingY || + _this._ignoreWhileAnimating === target + ) { + return completed(false); + } + + ignoreNextClick = false; + + if (activeSortable && !options.disabled && + (isOwner + ? canSort || (revert = parentEl !== rootEl) // Reverting item into the original list + : ( + putSortable === this || + ( + (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && + group.checkPut(this, activeSortable, dragEl, evt) + ) + ) + ) + ) { + vertical = this._getDirection(evt, target) === 'vertical'; + + dragRect = getRect(dragEl); + + dragOverEvent('dragOverValid'); + if (Sortable.eventCanceled) return completedFired; + + if (revert) { + parentEl = rootEl; // actualization + capture(); + + this._hideClone(); + + dragOverEvent('revert'); + + if (!Sortable.eventCanceled) { + if (nextEl) { + rootEl.insertBefore(dragEl, nextEl); + } else { + rootEl.appendChild(dragEl); + } + } + + return completed(true); + } + + let elLastChild = lastChild(el, options.draggable); + + if (!elLastChild || _ghostIsLast(evt, vertical, this) && !elLastChild.animated) { + // Insert to end of list + + // If already at end of list: Do not insert + if (elLastChild === dragEl) { + return completed(false); + } + + // if there is a last element, it is the target + if (elLastChild && el === evt.target) { + target = elLastChild; + } + + if (target) { + targetRect = getRect(target); + } + + if (onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { + capture(); + el.appendChild(dragEl); + parentEl = el; // actualization + + changed(); + return completed(true); + } + } + else if (elLastChild && _ghostIsFirst(evt, vertical, this)) { + // Insert to start of list + let firstChild = getChild(el, 0, options, true); + if (firstChild === dragEl) { + return completed(false); + } + target = firstChild; + targetRect = getRect(target); + + if (onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { + capture(); + el.insertBefore(dragEl, firstChild); + parentEl = el; // actualization + + changed(); + return completed(true); + } + } + else if (target.parentNode === el) { + targetRect = getRect(target); + let direction = 0, + targetBeforeFirstSwap, + differentLevel = dragEl.parentNode !== el, + differentRowCol = !_dragElInRowColumn(dragEl.animated && dragEl.toRect || dragRect, target.animated && target.toRect || targetRect, vertical), + side1 = vertical ? 'top' : 'left', + scrolledPastTop = isScrolledPast(target, 'top', 'top') || isScrolledPast(dragEl, 'top', 'top'), + scrollBefore = scrolledPastTop ? scrolledPastTop.scrollTop : void 0; + + + if (lastTarget !== target) { + targetBeforeFirstSwap = targetRect[side1]; + pastFirstInvertThresh = false; + isCircumstantialInvert = (!differentRowCol && options.invertSwap) || differentLevel; + } + + direction = _getSwapDirection( + evt, target, targetRect, vertical, + differentRowCol ? 1 : options.swapThreshold, + options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, + isCircumstantialInvert, + lastTarget === target + ); + + let sibling; + + if (direction !== 0) { + // Check if target is beside dragEl in respective direction (ignoring hidden elements) + let dragIndex = index(dragEl); + + do { + dragIndex -= direction; + sibling = parentEl.children[dragIndex]; + } while (sibling && (css(sibling, 'display') === 'none' || sibling === ghostEl)); + } + // If dragEl is already beside target: Do not insert + if ( + direction === 0 || + sibling === target + ) { + return completed(false); + } + + lastTarget = target; + + lastDirection = direction; + + let nextSibling = target.nextElementSibling, + after = false; + + after = direction === 1; + + let moveVector = onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); + + if (moveVector !== false) { + if (moveVector === 1 || moveVector === -1) { + after = (moveVector === 1); + } + + _silent = true; + setTimeout(_unsilent, 30); + + capture(); + + if (after && !nextSibling) { + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); + } + + // Undo chrome's scroll adjustment (has no effect on other browsers) + if (scrolledPastTop) { + scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop); + } + + parentEl = dragEl.parentNode; // actualization + + // must be done before animation + if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) { + targetMoveDistance = Math.abs(targetBeforeFirstSwap - getRect(target)[side1]); + } + changed(); + + return completed(true); + } + } + + if (el.contains(dragEl)) { + return completed(false); + } + } + + return false; + }, + + _ignoreWhileAnimating: null, + + _offMoveEvents: function() { + off(document, 'mousemove', this._onTouchMove); + off(document, 'touchmove', this._onTouchMove); + off(document, 'pointermove', this._onTouchMove); + off(document, 'dragover', nearestEmptyInsertDetectEvent); + off(document, 'mousemove', nearestEmptyInsertDetectEvent); + off(document, 'touchmove', nearestEmptyInsertDetectEvent); + }, + + _offUpEvents: function () { + let ownerDocument = this.el.ownerDocument; + + off(ownerDocument, 'mouseup', this._onDrop); + off(ownerDocument, 'touchend', this._onDrop); + off(ownerDocument, 'pointerup', this._onDrop); + off(ownerDocument, 'touchcancel', this._onDrop); + off(document, 'selectstart', this); + }, + + _onDrop: function (/**Event*/evt) { + let el = this.el, + options = this.options; + + // Get the index of the dragged element within its parent + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + + pluginEvent('drop', this, { + evt + }); + + parentEl = dragEl && dragEl.parentNode; + + // Get again after plugin event + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + + if (Sortable.eventCanceled) { + this._nulling(); + return; + } + + awaitingDragStarted = false; + isCircumstantialInvert = false; + pastFirstInvertThresh = false; + + clearInterval(this._loopId); + + clearTimeout(this._dragStartTimer); + + _cancelNextTick(this.cloneId); + _cancelNextTick(this._dragStartId); + + // Unbind events + if (this.nativeDraggable) { + off(document, 'drop', this); + off(el, 'dragstart', this._onDragStart); + } + this._offMoveEvents(); + this._offUpEvents(); + + + if (Safari) { + css(document.body, 'user-select', ''); + } + + css(dragEl, 'transform', ''); + + if (evt) { + if (moved) { + evt.cancelable && evt.preventDefault(); + !options.dropBubble && evt.stopPropagation(); + } + + ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); + + if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) { + // Remove clone(s) + cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); + } + + if (dragEl) { + if (this.nativeDraggable) { + off(dragEl, 'dragend', this); + } + + _disableDraggable(dragEl); + dragEl.style['will-change'] = ''; + + // Remove classes + // ghostClass is added in dragStarted + if (moved && !awaitingDragStarted) { + toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false); + } + toggleClass(dragEl, this.options.chosenClass, false); + + // Drag stop event + _dispatchEvent({ + sortable: this, + name: 'unchoose', + toEl: parentEl, + newIndex: null, + newDraggableIndex: null, + originalEvent: evt + }); + + + if (rootEl !== parentEl) { + + if (newIndex >= 0) { + // Add event + _dispatchEvent({ + rootEl: parentEl, + name: 'add', + toEl: parentEl, + fromEl: rootEl, + originalEvent: evt + }); + + // Remove event + _dispatchEvent({ + sortable: this, + name: 'remove', + toEl: parentEl, + originalEvent: evt + }); + + // drag from one list and drop into another + _dispatchEvent({ + rootEl: parentEl, + name: 'sort', + toEl: parentEl, + fromEl: rootEl, + originalEvent: evt + }); + + _dispatchEvent({ + sortable: this, + name: 'sort', + toEl: parentEl, + originalEvent: evt + }); + } + + putSortable && putSortable.save(); + } else { + if (newIndex !== oldIndex) { + if (newIndex >= 0) { + // drag & drop within the same list + _dispatchEvent({ + sortable: this, + name: 'update', + toEl: parentEl, + originalEvent: evt + }); + + _dispatchEvent({ + sortable: this, + name: 'sort', + toEl: parentEl, + originalEvent: evt + }); + } + } + } + + if (Sortable.active) { + /* jshint eqnull:true */ + if (newIndex == null || newIndex === -1) { + newIndex = oldIndex; + newDraggableIndex = oldDraggableIndex; + } + + _dispatchEvent({ + sortable: this, + name: 'end', + toEl: parentEl, + originalEvent: evt + }); + + // Save sorting + this.save(); + } + } + + } + this._nulling(); + }, + + _nulling: function() { + pluginEvent('nulling', this); + + rootEl = + dragEl = + parentEl = + ghostEl = + nextEl = + cloneEl = + lastDownEl = + cloneHidden = + + tapEvt = + touchEvt = + + moved = + newIndex = + newDraggableIndex = + oldIndex = + oldDraggableIndex = + + lastTarget = + lastDirection = + + putSortable = + activeGroup = + Sortable.dragged = + Sortable.ghost = + Sortable.clone = + Sortable.active = null; + + savedInputChecked.forEach(function (el) { + el.checked = true; + }); + + savedInputChecked.length = + lastDx = + lastDy = 0; + }, + + handleEvent: function (/**Event*/evt) { + switch (evt.type) { + case 'drop': + case 'dragend': + this._onDrop(evt); + break; + + case 'dragenter': + case 'dragover': + if (dragEl) { + this._onDragOver(evt); + _globalDragOver(evt); + } + break; + + case 'selectstart': + evt.preventDefault(); + break; + } + }, + + + /** + * Serializes the item into an array of string. + * @returns {String[]} + */ + toArray: function () { + let order = [], + el, + children = this.el.children, + i = 0, + n = children.length, + options = this.options; + + for (; i < n; i++) { + el = children[i]; + if (closest(el, options.draggable, this.el, false)) { + order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); + } + } + + return order; + }, + + + /** + * Sorts the elements according to the array. + * @param {String[]} order order of the items + */ + sort: function (order, useAnimation) { + let items = {}, rootEl = this.el; + + this.toArray().forEach(function (id, i) { + let el = rootEl.children[i]; + + if (closest(el, this.options.draggable, rootEl, false)) { + items[id] = el; + } + }, this); + + useAnimation && this.captureAnimationState(); + order.forEach(function (id) { + if (items[id]) { + rootEl.removeChild(items[id]); + rootEl.appendChild(items[id]); + } + }); + useAnimation && this.animateAll(); + }, + + + /** + * Save the current sorting + */ + save: function () { + let store = this.options.store; + store && store.set && store.set(this); + }, + + + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * @param {HTMLElement} el + * @param {String} [selector] default: `options.draggable` + * @returns {HTMLElement|null} + */ + closest: function (el, selector) { + return closest(el, selector || this.options.draggable, this.el, false); + }, + + + /** + * Set/get option + * @param {string} name + * @param {*} [value] + * @returns {*} + */ + option: function (name, value) { + let options = this.options; + + if (value === void 0) { + return options[name]; + } else { + let modifiedValue = PluginManager.modifyOption(this, name, value); + if (typeof modifiedValue !== 'undefined') { + options[name] = modifiedValue; + } else { + options[name] = value; + } + + if (name === 'group') { + _prepareGroup(options); + } + } + }, + + + /** + * Destroy + */ + destroy: function () { + pluginEvent('destroy', this); + let el = this.el; + + el[expando] = null; + + off(el, 'mousedown', this._onTapStart); + off(el, 'touchstart', this._onTapStart); + off(el, 'pointerdown', this._onTapStart); + + if (this.nativeDraggable) { + off(el, 'dragover', this); + off(el, 'dragenter', this); + } + // Remove draggable attributes + Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { + el.removeAttribute('draggable'); + }); + + this._onDrop(); + + this._disableDelayedDragEvents(); + + sortables.splice(sortables.indexOf(this.el), 1); + + this.el = el = null; + }, + + _hideClone: function() { + if (!cloneHidden) { + pluginEvent('hideClone', this); + if (Sortable.eventCanceled) return; + + + css(cloneEl, 'display', 'none'); + if (this.options.removeCloneOnHide && cloneEl.parentNode) { + cloneEl.parentNode.removeChild(cloneEl); + } + cloneHidden = true; + } + }, + + _showClone: function(putSortable) { + if (putSortable.lastPutMode !== 'clone') { + this._hideClone(); + return; + } + + + if (cloneHidden) { + pluginEvent('showClone', this); + if (Sortable.eventCanceled) return; + + // show clone at dragEl or original position + if (dragEl.parentNode == rootEl && !this.options.group.revertClone) { + rootEl.insertBefore(cloneEl, dragEl); + } else if (nextEl) { + rootEl.insertBefore(cloneEl, nextEl); + } else { + rootEl.appendChild(cloneEl); + } + + if (this.options.group.revertClone) { + this.animate(dragEl, cloneEl); + } + + css(cloneEl, 'display', ''); + cloneHidden = false; + } + } +}; + +function _globalDragOver(/**Event*/evt) { + if (evt.dataTransfer) { + evt.dataTransfer.dropEffect = 'move'; + } + evt.cancelable && evt.preventDefault(); +} + +function onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvent, willInsertAfter) { + let evt, + sortable = fromEl[expando], + onMoveFn = sortable.options.onMove, + retVal; + // Support for new CustomEvent feature + if (window.CustomEvent && !IE11OrLess && !Edge) { + evt = new CustomEvent('move', { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent('Event'); + evt.initEvent('move', true, true); + } + + evt.to = toEl; + evt.from = fromEl; + evt.dragged = dragEl; + evt.draggedRect = dragRect; + evt.related = targetEl || toEl; + evt.relatedRect = targetRect || getRect(toEl); + evt.willInsertAfter = willInsertAfter; + + evt.originalEvent = originalEvent; + + fromEl.dispatchEvent(evt); + + if (onMoveFn) { + retVal = onMoveFn.call(sortable, evt, originalEvent); + } + + return retVal; +} + +function _disableDraggable(el) { + el.draggable = false; +} + +function _unsilent() { + _silent = false; +} + +function _ghostIsFirst(evt, vertical, sortable) { + let rect = getRect(getChild(sortable.el, 0, sortable.options, true)); + const spacer = 10; + + return vertical ? + ((evt.clientX < rect.left - spacer) || (evt.clientY < rect.top && evt.clientX < rect.right)) : + ((evt.clientY < rect.top - spacer) || (evt.clientY < rect.bottom && evt.clientX < rect.left)) +} + +function _ghostIsLast(evt, vertical, sortable) { + let rect = getRect(lastChild(sortable.el, sortable.options.draggable)); + const spacer = 10; + + return vertical ? + (evt.clientX > rect.right + spacer || evt.clientX <= rect.right && evt.clientY > rect.bottom && evt.clientX >= rect.left) : + (evt.clientX > rect.right && evt.clientY > rect.top || evt.clientX <= rect.right && evt.clientY > rect.bottom + spacer); +} + +function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) { + let mouseOnAxis = vertical ? evt.clientY : evt.clientX, + targetLength = vertical ? targetRect.height : targetRect.width, + targetS1 = vertical ? targetRect.top : targetRect.left, + targetS2 = vertical ? targetRect.bottom : targetRect.right, + invert = false; + + + if (!invertSwap) { + // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold + if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) { // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2 + // check if past first invert threshold on side opposite of lastDirection + if (!pastFirstInvertThresh && + (lastDirection === 1 ? + ( + mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 + ) : + ( + mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2 + ) + ) + ) + { + // past first invert threshold, do not restrict inverted threshold to dragEl shadow + pastFirstInvertThresh = true; + } + + if (!pastFirstInvertThresh) { + // dragEl shadow (target move distance shadow) + if ( + lastDirection === 1 ? + ( + mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow + ) : + ( + mouseOnAxis > targetS2 - targetMoveDistance + ) + ) + { + return -lastDirection; + } + } else { + invert = true; + } + } else { + // Regular + if ( + mouseOnAxis > targetS1 + (targetLength * (1 - swapThreshold) / 2) && + mouseOnAxis < targetS2 - (targetLength * (1 - swapThreshold) / 2) + ) { + return _getInsertDirection(target); + } + } + } + + invert = invert || invertSwap; + + if (invert) { + // Invert of regular + if ( + mouseOnAxis < targetS1 + (targetLength * invertedSwapThreshold / 2) || + mouseOnAxis > targetS2 - (targetLength * invertedSwapThreshold / 2) + ) + { + return ((mouseOnAxis > targetS1 + targetLength / 2) ? 1 : -1); + } + } + + return 0; +} + +/** + * Gets the direction dragEl must be swapped relative to target in order to make it + * seem that dragEl has been "inserted" into that element's position + * @param {HTMLElement} target The target whose position dragEl is being inserted at + * @return {Number} Direction dragEl must be swapped + */ +function _getInsertDirection(target) { + if (index(dragEl) < index(target)) { + return 1; + } else { + return -1; + } +} + + +/** + * Generate id + * @param {HTMLElement} el + * @returns {String} + * @private + */ +function _generateId(el) { + let str = el.tagName + el.className + el.src + el.href + el.textContent, + i = str.length, + sum = 0; + + while (i--) { + sum += str.charCodeAt(i); + } + + return sum.toString(36); +} + +function _saveInputCheckedState(root) { + savedInputChecked.length = 0; + + let inputs = root.getElementsByTagName('input'); + let idx = inputs.length; + + while (idx--) { + let el = inputs[idx]; + el.checked && savedInputChecked.push(el); + } +} + +function _nextTick(fn) { + return setTimeout(fn, 0); +} + +function _cancelNextTick(id) { + return clearTimeout(id); +} + +// Fixed #973: +if (documentExists) { + on(document, 'touchmove', function(evt) { + if ((Sortable.active || awaitingDragStarted) && evt.cancelable) { + evt.preventDefault(); + } + }); +} + + +// Export utils +Sortable.utils = { + on: on, + off: off, + css: css, + find: find, + is: function (el, selector) { + return !!closest(el, selector, el, false); + }, + extend: extend, + throttle: throttle, + closest: closest, + toggleClass: toggleClass, + clone: clone, + index: index, + nextTick: _nextTick, + cancelNextTick: _cancelNextTick, + detectDirection: _detectDirection, + getChild: getChild +}; + + +/** + * Get the Sortable instance of an element + * @param {HTMLElement} element The element + * @return {Sortable|undefined} The instance of Sortable + */ +Sortable.get = function(element) { + return element[expando]; +}; + +/** + * Mount a plugin to Sortable + * @param {...SortablePlugin|SortablePlugin[]} plugins Plugins being mounted + */ +Sortable.mount = function(...plugins) { + if (plugins[0].constructor === Array) plugins = plugins[0]; + + plugins.forEach((plugin) => { + if (!plugin.prototype || !plugin.prototype.constructor) { + throw `Sortable: Mounted plugin must be a constructor function, not ${ {}.toString.call(plugin) }`; + } + if (plugin.utils) Sortable.utils = { ...Sortable.utils, ...plugin.utils }; + + PluginManager.mount(plugin); + }); +}; + + + +/** + * Create sortable instance + * @param {HTMLElement} el + * @param {Object} [options] + */ +Sortable.create = function (el, options) { + return new Sortable(el, options); +}; + + +// Export +Sortable.version = version; + + +export default Sortable; diff --git a/library/Sortable/src/utils.js b/library/Sortable/src/utils.js new file mode 100644 index 000000000..cdb8cc774 --- /dev/null +++ b/library/Sortable/src/utils.js @@ -0,0 +1,556 @@ +import { IE11OrLess } from './BrowserInfo.js'; +import Sortable from './Sortable.js'; + +const captureMode = { + capture: false, + passive: false +}; + +function on(el, event, fn) { + el.addEventListener(event, fn, !IE11OrLess && captureMode); +} + + +function off(el, event, fn) { + el.removeEventListener(event, fn, !IE11OrLess && captureMode); +} + +function matches(/**HTMLElement*/el, /**String*/selector) { + if (!selector) return; + + selector[0] === '>' && (selector = selector.substring(1)); + + if (el) { + try { + if (el.matches) { + return el.matches(selector); + } else if (el.msMatchesSelector) { + return el.msMatchesSelector(selector); + } else if (el.webkitMatchesSelector) { + return el.webkitMatchesSelector(selector); + } + } catch(_) { + return false; + } + } + + return false; +} + +function getParentOrHost(el) { + return (el.host && el !== document && el.host.nodeType) + ? el.host + : el.parentNode; +} + +function closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx, includeCTX) { + if (el) { + ctx = ctx || document; + + do { + if ( + selector != null && + ( + selector[0] === '>' ? + el.parentNode === ctx && matches(el, selector) : + matches(el, selector) + ) || + includeCTX && el === ctx + ) { + return el; + } + + if (el === ctx) break; + /* jshint boss:true */ + } while (el = getParentOrHost(el)); + } + + return null; +} + +const R_SPACE = /\s+/g; + +function toggleClass(el, name, state) { + if (el && name) { + if (el.classList) { + el.classList[state ? 'add' : 'remove'](name); + } + else { + let className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); + el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); + } + } +} + + +function css(el, prop, val) { + let style = el && el.style; + + if (style) { + if (val === void 0) { + if (document.defaultView && document.defaultView.getComputedStyle) { + val = document.defaultView.getComputedStyle(el, ''); + } + else if (el.currentStyle) { + val = el.currentStyle; + } + + return prop === void 0 ? val : val[prop]; + } + else { + if (!(prop in style) && prop.indexOf('webkit') === -1) { + prop = '-webkit-' + prop; + } + + style[prop] = val + (typeof val === 'string' ? '' : 'px'); + } + } +} + +function matrix(el, selfOnly) { + let appliedTransforms = ''; + if (typeof(el) === 'string') { + appliedTransforms = el; + } else { + do { + let transform = css(el, 'transform'); + + if (transform && transform !== 'none') { + appliedTransforms = transform + ' ' + appliedTransforms; + } + /* jshint boss:true */ + } while (!selfOnly && (el = el.parentNode)); + } + + const matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix; + /*jshint -W056 */ + return matrixFn && (new matrixFn(appliedTransforms)); +} + + +function find(ctx, tagName, iterator) { + if (ctx) { + let list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; + + if (iterator) { + for (; i < n; i++) { + iterator(list[i], i); + } + } + + return list; + } + + return []; +} + + + +function getWindowScrollingElement() { + let scrollingElement = document.scrollingElement; + + if (scrollingElement) { + return scrollingElement + } else { + return document.documentElement + } +} + + +/** + * Returns the "bounding client rect" of given element + * @param {HTMLElement} el The element whose boundingClientRect is wanted + * @param {[Boolean]} relativeToContainingBlock Whether the rect should be relative to the containing block of (including) the container + * @param {[Boolean]} relativeToNonStaticParent Whether the rect should be relative to the relative parent of (including) the contaienr + * @param {[Boolean]} undoScale Whether the container's scale() should be undone + * @param {[HTMLElement]} container The parent the element will be placed in + * @return {Object} The boundingClientRect of el, with specified adjustments + */ +function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) { + if (!el.getBoundingClientRect && el !== window) return; + + let elRect, + top, + left, + bottom, + right, + height, + width; + + if (el !== window && el.parentNode && el !== getWindowScrollingElement()) { + elRect = el.getBoundingClientRect(); + top = elRect.top; + left = elRect.left; + bottom = elRect.bottom; + right = elRect.right; + height = elRect.height; + width = elRect.width; + } else { + top = 0; + left = 0; + bottom = window.innerHeight; + right = window.innerWidth; + height = window.innerHeight; + width = window.innerWidth; + } + + if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) { + // Adjust for translate() + container = container || el.parentNode; + + // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312) + // Not needed on <= IE11 + if (!IE11OrLess) { + do { + if ( + container && + container.getBoundingClientRect && + ( + css(container, 'transform') !== 'none' || + relativeToNonStaticParent && + css(container, 'position') !== 'static' + ) + ) { + let containerRect = container.getBoundingClientRect(); + + // Set relative to edges of padding box of container + top -= containerRect.top + parseInt(css(container, 'border-top-width')); + left -= containerRect.left + parseInt(css(container, 'border-left-width')); + bottom = top + elRect.height; + right = left + elRect.width; + + break; + } + /* jshint boss:true */ + } while (container = container.parentNode); + } + } + + if (undoScale && el !== window) { + // Adjust for scale() + let elMatrix = matrix(container || el), + scaleX = elMatrix && elMatrix.a, + scaleY = elMatrix && elMatrix.d; + + if (elMatrix) { + top /= scaleY; + left /= scaleX; + + width /= scaleX; + height /= scaleY; + + bottom = top + height; + right = left + width; + } + } + + return { + top: top, + left: left, + bottom: bottom, + right: right, + width: width, + height: height + }; +} + +/** + * Checks if a side of an element is scrolled past a side of its parents + * @param {HTMLElement} el The element who's side being scrolled out of view is in question + * @param {String} elSide Side of the element in question ('top', 'left', 'right', 'bottom') + * @param {String} parentSide Side of the parent in question ('top', 'left', 'right', 'bottom') + * @return {HTMLElement} The parent scroll element that the el's side is scrolled past, or null if there is no such element + */ +function isScrolledPast(el, elSide, parentSide) { + let parent = getParentAutoScrollElement(el, true), + elSideVal = getRect(el)[elSide]; + + /* jshint boss:true */ + while (parent) { + let parentSideVal = getRect(parent)[parentSide], + visible; + + if (parentSide === 'top' || parentSide === 'left') { + visible = elSideVal >= parentSideVal; + } else { + visible = elSideVal <= parentSideVal; + } + + if (!visible) return parent; + + if (parent === getWindowScrollingElement()) break; + + parent = getParentAutoScrollElement(parent, false); + } + + return false; +} + + + +/** + * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) + * and non-draggable elements + * @param {HTMLElement} el The parent element + * @param {Number} childNum The index of the child + * @param {Object} options Parent Sortable's options + * @return {HTMLElement} The child at index childNum, or null if not found + */ +function getChild(el, childNum, options, includeDragEl) { + let currentChild = 0, + i = 0, + children = el.children; + + while (i < children.length) { + if ( + children[i].style.display !== 'none' && + children[i] !== Sortable.ghost && + (includeDragEl || children[i] !== Sortable.dragged) && + closest(children[i], options.draggable, el, false) + ) { + if (currentChild === childNum) { + return children[i]; + } + currentChild++; + } + + i++; + } + return null; +} + +/** + * Gets the last child in the el, ignoring ghostEl or invisible elements (clones) + * @param {HTMLElement} el Parent element + * @param {selector} selector Any other elements that should be ignored + * @return {HTMLElement} The last child, ignoring ghostEl + */ +function lastChild(el, selector) { + let last = el.lastElementChild; + + while ( + last && + ( + last === Sortable.ghost || + css(last, 'display') === 'none' || + selector && !matches(last, selector) + ) + ) { + last = last.previousElementSibling; + } + + return last || null; +} + + +/** + * Returns the index of an element within its parent for a selected set of + * elements + * @param {HTMLElement} el + * @param {selector} selector + * @return {number} + */ +function index(el, selector) { + let index = 0; + + if (!el || !el.parentNode) { + return -1; + } + + /* jshint boss:true */ + while (el = el.previousElementSibling) { + if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && el !== Sortable.clone && (!selector || matches(el, selector))) { + index++; + } + } + + return index; +} + +/** + * Returns the scroll offset of the given element, added with all the scroll offsets of parent elements. + * The value is returned in real pixels. + * @param {HTMLElement} el + * @return {Array} Offsets in the format of [left, top] + */ +function getRelativeScrollOffset(el) { + let offsetLeft = 0, + offsetTop = 0, + winScroller = getWindowScrollingElement(); + + if (el) { + do { + let elMatrix = matrix(el), + scaleX = elMatrix.a, + scaleY = elMatrix.d; + + offsetLeft += el.scrollLeft * scaleX; + offsetTop += el.scrollTop * scaleY; + } while (el !== winScroller && (el = el.parentNode)); + } + + return [offsetLeft, offsetTop]; +} + +/** + * Returns the index of the object within the given array + * @param {Array} arr Array that may or may not hold the object + * @param {Object} obj An object that has a key-value pair unique to and identical to a key-value pair in the object you want to find + * @return {Number} The index of the object in the array, or -1 + */ +function indexOfObject(arr, obj) { + for (let i in arr) { + if (!arr.hasOwnProperty(i)) continue; + for (let key in obj) { + if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i); + } + } + return -1; +} + + +function getParentAutoScrollElement(el, includeSelf) { + // skip to window + if (!el || !el.getBoundingClientRect) return getWindowScrollingElement(); + + let elem = el; + let gotSelf = false; + do { + // we don't need to get elem css if it isn't even overflowing in the first place (performance) + if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { + let elemCSS = css(elem); + if ( + elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || + elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll') + ) { + if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement(); + + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + } + /* jshint boss:true */ + } while (elem = elem.parentNode); + + return getWindowScrollingElement(); +} + +function extend(dst, src) { + if (dst && src) { + for (let key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + } + + return dst; +} + + +function isRectEqual(rect1, rect2) { + return Math.round(rect1.top) === Math.round(rect2.top) && + Math.round(rect1.left) === Math.round(rect2.left) && + Math.round(rect1.height) === Math.round(rect2.height) && + Math.round(rect1.width) === Math.round(rect2.width); +} + + +let _throttleTimeout; +function throttle(callback, ms) { + return function () { + if (!_throttleTimeout) { + let args = arguments, + _this = this; + + if (args.length === 1) { + callback.call(_this, args[0]); + } else { + callback.apply(_this, args); + } + + _throttleTimeout = setTimeout(function () { + _throttleTimeout = void 0; + }, ms); + } + }; +} + + +function cancelThrottle() { + clearTimeout(_throttleTimeout); + _throttleTimeout = void 0; +} + + +function scrollBy(el, x, y) { + el.scrollLeft += x; + el.scrollTop += y; +} + + +function clone(el) { + let Polymer = window.Polymer; + let $ = window.jQuery || window.Zepto; + + if (Polymer && Polymer.dom) { + return Polymer.dom(el).cloneNode(true); + } + else if ($) { + return $(el).clone(true)[0]; + } + else { + return el.cloneNode(true); + } +} + + +function setRect(el, rect) { + css(el, 'position', 'absolute'); + css(el, 'top', rect.top); + css(el, 'left', rect.left); + css(el, 'width', rect.width); + css(el, 'height', rect.height); +} + +function unsetRect(el) { + css(el, 'position', ''); + css(el, 'top', ''); + css(el, 'left', ''); + css(el, 'width', ''); + css(el, 'height', ''); +} + + +const expando = 'Sortable' + (new Date).getTime(); + + +export { + on, + off, + matches, + getParentOrHost, + closest, + toggleClass, + css, + matrix, + find, + getWindowScrollingElement, + getRect, + isScrolledPast, + getChild, + lastChild, + index, + getRelativeScrollOffset, + indexOfObject, + getParentAutoScrollElement, + extend, + isRectEqual, + throttle, + cancelThrottle, + scrollBy, + clone, + setRect, + unsetRect, + expando +}; -- cgit v1.2.3