sortable-dnd
Version:
JS library for drag-and-drop lists, supports sortable and draggable
1,288 lines (1,239 loc) • 46.6 kB
JavaScript
/*!
* sortable-dnd v0.6.22
* open source under the MIT license
* https://github.com/mfuu/sortable-dnd#readme
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Sortable = factory());
})(this, (function () { 'use strict';
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _extends() {
_extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
var captureMode = {
capture: false,
passive: false
};
var R_SPACE = /\s+/g;
function userAgent(pattern) {
if (typeof window !== 'undefined' && window.navigator) {
return !!( /*@__PURE__*/navigator.userAgent.match(pattern));
}
}
var IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i);
var Edge = userAgent(/Edge/i);
var Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i);
/**
* detect passive event support
*/
var supportPassive = function () {
var supportPassive = false;
document.addEventListener('checkIfSupportPassive', null, {
get passive() {
supportPassive = true;
return true;
}
});
return supportPassive;
}();
(function () {
if (typeof window === 'undefined' || typeof document === 'undefined') {
// Server environment
return '';
}
// window.getComputedStyle() returns null inside an iframe with `display: none`
// in this case return an array with a fake mozilla style in it.
var styles = window.getComputedStyle(document.documentElement, '') || ['-moz-hidden-iframe'];
var pre = (Array.prototype.slice.call(styles).join('').match(/-(moz|webkit|ms)-/) || styles.OLink === '' && ['', 'o'])[1];
return pre ? "-".concat(pre, "-") : '';
})();
/**
* add specified event listener
*/
function on(el, event, fn) {
if (window.addEventListener) {
el.addEventListener(event, fn, supportPassive || !IE11OrLess ? captureMode : false);
} else if (window.attachEvent) {
el.attachEvent('on' + event, fn);
} else {
el['on' + event] = fn;
}
}
/**
* remove specified event listener
*/
function off(el, event, fn) {
if (window.removeEventListener) {
el.removeEventListener(event, fn, supportPassive || !IE11OrLess ? captureMode : false);
} else if (window.detachEvent) {
el.detachEvent('on' + event, fn);
} else {
el['on' + event] = null;
}
}
/**
* get scrolling element
*/
function getAutoScrollElement(el, includeSelf) {
// skip to window
if (!el || !el.getBoundingClientRect) {
return getWindowScrollingElement();
}
var elem = el;
var 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) {
var 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;
}
}
} while (elem = elem.parentNode);
return getWindowScrollingElement();
}
function getWindowScrollingElement() {
return document.scrollingElement || 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 {HTMLElement} container The parent the element will be placed in
* @return {Object} The boundingClientRect of el, with specified adjustments
*/
function getRect(el, relativeToContainingBlock, container) {
if (!el.getBoundingClientRect && el !== window) return;
var 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 && el !== window) {
container = container || el.parentNode;
do {
if (container && container.getBoundingClientRect) {
var 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);
}
return {
top: top,
left: left,
bottom: bottom,
right: right,
width: width,
height: height
};
}
function closest(el, selector, ctx, includeCTX) {
if (!el) return;
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;
} while (el = el.parentNode);
return null;
}
/**
* Check if child element is contained in parent element
*/
function containes(el, parent) {
if (!el || !parent) return false;
if (parent.compareDocumentPosition) {
return !!(parent.compareDocumentPosition(el) & 16);
}
if (parent.contains && el.nodeType === 1) {
return parent.contains(el) && parent !== el;
}
while (el = el.parentNode) if (el === parent) return true;
return false;
}
/**
* Gets the last child in the el, ignoring ghostEl and invisible elements
*/
function lastChild(el, selector) {
var 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
*/
function index(el, selector) {
if (!el || !el.parentNode) {
return -1;
}
var index = 0;
while (el = el.previousElementSibling) {
if (el.nodeName.toUpperCase() !== 'TEMPLATE' && (!selector || matches(el, selector)) && css(el, 'display') !== 'none') {
index++;
}
}
return index;
}
/**
* Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) and non-draggable elements
*/
function getChild(el, childNum, selector, includeDragEl) {
var i = 0,
currentChild = 0,
children = el.children;
while (i < children.length) {
if (children[i] !== Sortable.ghost && css(children[i], 'display') !== 'none' && closest(children[i], selector, el, false) && (includeDragEl || children[i] !== Sortable.dragged)) {
if (currentChild === childNum) {
return children[i];
}
currentChild++;
}
i++;
}
return null;
}
function detectDirection(el, selector) {
var elCSS = css(el),
elWidth = parseInt(elCSS.width) - parseInt(elCSS.paddingLeft) - parseInt(elCSS.paddingRight) - parseInt(elCSS.borderLeftWidth) - parseInt(elCSS.borderRightWidth),
child1 = getChild(el, 0, selector),
child2 = getChild(el, 1, selector),
child1CSS = child1 && css(child1),
child2CSS = child2 && css(child2),
child1Width = child1CSS && parseInt(child1CSS.marginLeft) + parseInt(child1CSS.marginRight) + getRect(child1).width,
child2Width = child2CSS && parseInt(child2CSS.marginLeft) + parseInt(child2CSS.marginRight) + getRect(child2).width,
CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float';
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 && child1CSS["float"] && child1CSS["float"] !== 'none') {
var touchingSideChild2 = child1CSS["float"] === 'left' ? 'left' : 'right';
return child2 && (child2CSS.clear === 'both' || child2CSS.clear === touchingSideChild2) ? 'vertical' : 'horizontal';
}
return child1 && (child1CSS.display === 'block' || child1CSS.display === 'flex' || child1CSS.display === 'table' || child1CSS.display === 'grid' || child1Width >= elWidth && elCSS[CSSFloatProperty] === 'none' || child2 && elCSS[CSSFloatProperty] === 'none' && child1Width + child2Width > elWidth) ? 'vertical' : 'horizontal';
}
/**
* add or remove element's class
*/
function toggleClass(el, name, state) {
if (el && name) {
if (el.classList) {
el.classList[state ? 'add' : 'remove'](name);
} else {
var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' ');
el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' ');
}
}
}
/**
* Check if a DOM element matches a given selector
*/
function matches(el, 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 (error) {
return false;
}
}
return false;
}
/**
* get or set css property
*/
function css(el, prop, val) {
var 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 repaint(el) {
return el.offsetWidth;
}
/**
* Reports the position of its argument node relative to the node on which it is called.
*/
function comparePosition(a, b) {
return a.compareDocumentPosition ? a.compareDocumentPosition(b) : a.contains ? (a != b && a.contains(b) && 16) + (a != b && b.contains(a) && 8) + (a.sourceIndex >= 0 && b.sourceIndex >= 0 ? (a.sourceIndex < b.sourceIndex && 4) + (a.sourceIndex > b.sourceIndex && 2) : 1) : 0;
}
/**
* Sorts the sequence of two elements.
*/
function sort(before, after) {
var compareValue = comparePosition(before, after);
return compareValue === 2 ? 1 : compareValue === 4 ? -1 : 0;
}
function preventDefault(evt) {
evt.preventDefault !== void 0 && evt.cancelable && evt.preventDefault();
}
function dispatchEvent(_ref) {
var sortable = _ref.sortable,
name = _ref.name,
evt = _ref.evt;
var callback = sortable.options[name];
if (typeof callback === 'function') {
callback(_extends({}, evt));
}
}
var expando = 'Sortable' + Date.now();
function AutoScroll(options) {
this.options = options;
this.scrollEl = null;
this.autoScrollInterval = null;
}
AutoScroll.prototype = {
nulling: function nulling() {
if (this.autoScrollInterval) {
clearInterval(this.autoScrollInterval);
this.autoScrollInterval = null;
}
},
onStarted: function onStarted() {
var _this = this;
this.nulling();
this.autoScrollInterval = setInterval(function () {
_this.autoScroll();
});
},
onMove: function onMove(scrollEl, moveEvent, options) {
this.options = options;
this.scrollEl = scrollEl;
this.moveEvent = moveEvent;
},
autoScroll: function autoScroll() {
var event = this.moveEvent;
var scrollEl = this.scrollEl;
if (!scrollEl || event.clientX === void 0 || event.clientY === void 0) return;
var rect = getRect(scrollEl);
if (!rect) return;
var clientX = event.clientX,
clientY = event.clientY;
var top = rect.top,
right = rect.right,
bottom = rect.bottom,
left = rect.left,
height = rect.height,
width = rect.width;
// execute only inside scrolling elements
if (clientY < top || clientX > right || clientY > bottom || clientX < left) return;
var _this$options = this.options,
scrollThreshold = _this$options.scrollThreshold,
scrollSpeed = _this$options.scrollSpeed;
var scrollTop = scrollEl.scrollTop,
scrollLeft = scrollEl.scrollLeft,
scrollHeight = scrollEl.scrollHeight,
scrollWidth = scrollEl.scrollWidth;
// check direction
var toTop = scrollTop > 0 && clientY >= top && clientY <= top + scrollThreshold,
toLeft = scrollLeft > 0 && clientX >= left && clientX <= left + scrollThreshold,
toRight = scrollLeft + width < scrollWidth && clientX <= right && clientX >= right - scrollThreshold,
toBottom = scrollTop + height < scrollHeight && clientY <= bottom && clientY >= bottom - scrollThreshold;
if (toLeft) {
scrollEl.scrollLeft += Math.floor(Math.max(-1, (clientX - left) / scrollThreshold - 1) * scrollSpeed.x);
}
if (toRight) {
scrollEl.scrollLeft += Math.ceil(Math.min(1, (clientX - right) / scrollThreshold + 1) * scrollSpeed.x);
}
if (toTop) {
scrollEl.scrollTop += Math.floor(Math.max(-1, (clientY - top) / scrollThreshold - 1) * scrollSpeed.y);
}
if (toBottom) {
scrollEl.scrollTop += Math.ceil(Math.min(1, (clientY - bottom) / scrollThreshold + 1) * scrollSpeed.y);
}
}
};
function Animation(options) {
this.options = options;
this.stack = [];
}
Animation.prototype = {
collect: function collect(parentEl) {
if (!parentEl) return;
var parentRect = getRect(parentEl),
docWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
docHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
maxWidth = Math.min(parentRect.right, docWidth),
maxHeight = Math.min(parentRect.bottom, docHeight),
children = Array.prototype.slice.call(parentEl.children),
animations = [];
for (var i = 0, len = children.length; i <= len; i++) {
var el = children[i];
if (!el || el === Sortable.ghost || css(el, 'display') === 'none') continue;
var rect = getRect(el);
if (rect.bottom < 0 || rect.right < 0) continue;
// Animate only elements within the visible area
if (rect.top - rect.height > maxHeight || rect.left - rect.width > maxWidth) break;
animations.push({
el: el,
rect: rect
});
}
this.stack.push(animations);
},
animate: function animate() {
var animations = this.stack.pop();
if (!animations || !this.options.animation) return;
for (var i = 0, len = animations.length; i < len; i++) {
var _animations$i = animations[i],
el = _animations$i.el,
rect = _animations$i.rect;
this.execute(el, rect);
}
},
execute: function execute(el, fromRect) {
var toRect = getRect(el);
if (toRect.top === fromRect.top && toRect.left === fromRect.left) return;
var dx = fromRect.left - toRect.left;
var dy = fromRect.top - toRect.top;
css(el, 'transition', '');
css(el, 'transform', "translate3d(".concat(dx, "px, ").concat(dy, "px, 0)"));
this.repaintDummy = repaint(el);
var _this$options = this.options,
animation = _this$options.animation,
easing = _this$options.easing;
css(el, 'transition', "transform ".concat(animation, "ms ").concat(easing ? ' ' + easing : ''));
css(el, 'transform', 'translate3d(0px, 0px, 0px)');
typeof el.animated === 'number' && clearTimeout(el.animated);
el.animated = setTimeout(function () {
css(el, 'transition', '');
css(el, 'transform', '');
el.animated = null;
}, animation);
}
};
var dragElements, cloneElements, _useSelectHandle;
function Multiple(options) {
this.options = options || {};
this.selects = [];
}
Multiple.prototype = {
eventProperties: function eventProperties() {
return {
nodes: dragElements || [],
clones: cloneElements || []
};
},
isActive: function isActive() {
return !!dragElements;
},
nulling: function nulling() {
dragElements = cloneElements = _useSelectHandle = null;
},
select: function select(element) {
toggleClass(element, this.options.selectedClass, true);
this.selects.push(element);
this.selects.sort(function (a, b) {
return sort(a, b);
});
},
deselect: function deselect(element) {
var index = this.selects.indexOf(element);
if (index > -1) {
toggleClass(element, this.options.selectedClass, false);
this.selects.splice(index, 1);
}
},
useSelectHandle: function useSelectHandle(event, target) {
var selectHandle = this.options.selectHandle;
_useSelectHandle = typeof selectHandle === 'function' && selectHandle(event) || typeof selectHandle === 'string' && matches(target, selectHandle);
return !!_useSelectHandle;
},
onChoose: function onChoose() {
if (!this.options.multiple || this.selects.length === 0 || this.selects.indexOf(Sortable.dragged) < 0) {
return;
}
this.selects.sort(function (a, b) {
return sort(a, b);
});
dragElements = this.selects;
this.toggleChosenClass(true);
},
onDrop: function onDrop(from, to, isClone) {
if (!dragElements) return;
var dragEl = Sortable.dragged,
cloneEl = Sortable.clone,
dragIndex = dragElements.indexOf(dragEl);
if (from !== to && isClone) {
css(cloneEl, 'display', 'none');
this.toggleVisible(true);
cloneElements = dragElements.map(function (el) {
return el.cloneNode(true);
});
this.sortElements(cloneElements, dragIndex, cloneEl);
} else {
this.sortElements(dragElements, dragIndex, cloneEl);
}
// Recalculate selected elements
if (from !== to) {
to[expando].multiplayer.toggleSelected(cloneElements || dragElements, 'add');
!isClone && from[expando].multiplayer.toggleSelected(dragElements, 'remove');
}
},
onSelect: function onSelect(event, dragEl, startEl, sortable) {
var _this$options = this.options,
multiple = _this$options.multiple,
selectHandle = _this$options.selectHandle;
if (!(multiple && (selectHandle && _useSelectHandle || !selectHandle && !startEl))) {
return;
}
var dragIndex = this.selects.indexOf(dragEl);
toggleClass(dragEl, this.options.selectedClass, dragIndex < 0);
var evt = {
from: sortable.el,
event: event,
node: dragEl,
index: index(dragEl)
};
if (dragIndex < 0) {
this.selects.push(dragEl);
dispatchEvent({
sortable: sortable,
name: 'onSelect',
evt: evt
});
} else {
this.selects.splice(dragIndex, 1);
dispatchEvent({
sortable: sortable,
name: 'onDeselect',
evt: evt
});
}
this.selects.sort(function (a, b) {
return sort(a, b);
});
},
toggleChosenClass: function toggleChosenClass(state) {
if (!dragElements) return;
for (var i = 0, len = dragElements.length; i < len; i++) {
toggleClass(dragElements[i], this.options.chosenClass, state);
}
},
toggleVisible: function toggleVisible(visible) {
if (!dragElements) return;
for (var i = 0, len = dragElements.length; i < len; i++) {
if (dragElements[i] == Sortable.dragged) continue;
css(dragElements[i], 'display', visible ? '' : 'none');
}
},
toggleSelected: function toggleSelected(elements, status) {
var _this = this;
if (status === 'add') {
elements.forEach(function (el) {
return _this.selects.push(el);
});
} else {
this.selects = this.selects.filter(function (el) {
return elements.indexOf(el) < 0;
});
}
},
sortElements: function sortElements(elements, index, target) {
for (var i = 0, len = elements.length; i < len; i++) {
css(elements[i], 'display', '');
if (i < index) {
target.parentNode.insertBefore(elements[i], target);
} else {
var dropEl = i > 0 ? elements[i - 1] : target;
target.parentNode.insertBefore(elements[i], dropEl.nextSibling);
}
}
}
};
var sortables = [];
var to, from, dragEl, dropEl, nextEl, cloneEl, ghostEl, startEl, targetEl, parentEl, pullMode, oldIndex, newIndex, startIndex, dragEvent, moveEvent, lastDropEl, cloneEvent, cloneTarget, listenerNode, lastHoverArea, dragStartTimer;
function _prepareGroup(options) {
var _originalGroup$pull, _originalGroup$put, _originalGroup$revert;
var group = {};
var originalGroup = options.group;
if (!originalGroup || _typeof(originalGroup) !== 'object') {
originalGroup = {
name: originalGroup,
pull: true,
put: true,
revertDrag: true
};
}
group.name = originalGroup.name;
group.pull = (_originalGroup$pull = originalGroup.pull) !== null && _originalGroup$pull !== void 0 ? _originalGroup$pull : true;
group.put = (_originalGroup$put = originalGroup.put) !== null && _originalGroup$put !== void 0 ? _originalGroup$put : true;
group.revertDrag = (_originalGroup$revert = originalGroup.revertDrag) !== null && _originalGroup$revert !== void 0 ? _originalGroup$revert : true;
options.group = group;
}
/**
* Detects nearest empty sortable to X and Y position using emptyInsertThreshold.
*/
function _detectNearestSortable(x, y) {
var nearestRect;
return sortables.reduce(function (result, element) {
var threshold = element[expando].options.emptyInsertThreshold;
if (threshold == void 0) return;
var rect = getRect(element),
insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold,
insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold;
if (insideHorizontally && insideVertically && (!nearestRect || nearestRect && rect.left >= nearestRect.left && rect.right <= nearestRect.right && rect.top >= nearestRect.top && rect.bottom <= nearestRect.bottom)) {
result = element;
nearestRect = rect;
}
return result;
}, null);
}
function _positionChanged(evt) {
var lastEvent = moveEvent || dragEvent;
return !(evt.clientX !== void 0 && evt.clientY !== void 0 && Math.abs(evt.clientX - lastEvent.clientX) <= 0 && Math.abs(evt.clientY - lastEvent.clientY) <= 0);
}
/**
* @class Sortable
*/
function Sortable(el, options) {
if (!(el && el.nodeType && el.nodeType === 1)) {
throw "Sortable-dnd: `el` must be an HTMLElement, not ".concat({}.toString.call(el));
}
el[expando] = this;
this.el = el;
this.options = options = _extends({}, options);
var defaults = {
store: null,
group: '',
handle: null,
sortable: true,
disabled: false,
multiple: false,
lockAxis: '',
direction: '',
animation: 150,
easing: '',
draggable: '>*',
selectHandle: null,
customGhost: null,
autoScroll: true,
scrollThreshold: 55,
scrollSpeed: {
x: 10,
y: 10
},
delay: 0,
delayOnTouchOnly: false,
touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1,
ghostClass: '',
ghostStyle: {},
chosenClass: '',
selectedClass: '',
placeholderClass: '',
swapOnDrop: true,
removeCloneOnDrop: true,
fallbackOnBody: false,
supportTouch: 'ontouchstart' in window,
emptyInsertThreshold: -1
};
// Set default options
for (var name in defaults) {
!(name in this.options) && (this.options[name] = defaults[name]);
}
_prepareGroup(options);
// Bind all private methods
for (var fn in this) {
if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
this[fn] = this[fn].bind(this);
}
}
on(el, this.options.supportTouch ? 'touchstart' : 'mousedown', this._onDrag);
this.autoScroller = new AutoScroll(this.options);
this.multiplayer = new Multiple(this.options);
this.animator = new Animation(this.options);
sortables.push(el);
}
Sortable.prototype = {
constructor: Sortable,
_onDrag: function _onDrag(event) {
var _this = this;
// Don't trigger start event when an element is been dragged
if (dragEl || this.options.disabled || !this.options.group.pull) return;
// only left button and enabled
if (/mousedown|pointerdown/.test(event.type) && event.button !== 0) return;
var touch = event.touches && event.touches[0],
target = (touch || event).target;
// Safari ignores further event handling after mousedown
if (Safari && target && target.tagName.toUpperCase() === 'SELECT') return;
var element = closest(target, this.options.draggable, this.el);
// No dragging is allowed when there is no dragging element
if (!element || element.animated) return;
dragEvent = {
event: event,
clientX: (touch || event).clientX,
clientY: (touch || event).clientY
};
dragEl = element;
listenerNode = touch ? dragEl : document;
on(listenerNode, 'mouseup', this._onDrop);
on(listenerNode, 'touchend', this._onDrop);
on(listenerNode, 'touchcancel', this._onDrop);
// use multi-select-handle
if (this.multiplayer.useSelectHandle(event, target)) return;
var handle = this.options.handle;
if (typeof handle === 'function' && !handle(event)) return;
if (typeof handle === 'string' && !matches(target, handle)) return;
var _this$options = this.options,
delay = _this$options.delay,
delayOnTouchOnly = _this$options.delayOnTouchOnly;
// Delay is impossible for native DnD in Edge or IE
if (delay && (!delayOnTouchOnly || touch) && !(Edge || IE11OrLess)) {
on(this.el.ownerDocument, 'touchmove', this._delayMoveHandler);
on(this.el.ownerDocument, 'mousemove', this._delayMoveHandler);
on(this.el.ownerDocument, 'mouseup', this._cancelStart);
on(this.el.ownerDocument, 'touchend', this._cancelStart);
on(this.el.ownerDocument, 'touchcancel', this._cancelStart);
dragStartTimer = setTimeout(function () {
return _this._onStart(touch, event);
}, delay);
} else {
this._onStart(touch, event);
}
// Do not allow text to be selected when draggable
on(document, 'selectstart', preventDefault);
Safari && css(document.body, 'user-select', 'none');
},
_delayMoveHandler: function _delayMoveHandler(event) {
var evt = event.touches ? event.touches[0] : event;
if (Math.max(Math.abs(evt.clientX - dragEvent.clientX), Math.abs(evt.clientY - dragEvent.clientY)) >= Math.floor(this.options.touchStartThreshold / (window.devicePixelRatio || 1))) {
this._cancelStart();
}
},
_cancelStart: function _cancelStart() {
clearTimeout(dragStartTimer);
off(this.el.ownerDocument, 'touchmove', this._delayMoveHandler);
off(this.el.ownerDocument, 'mousemove', this._delayMoveHandler);
off(this.el.ownerDocument, 'mouseup', this._cancelStart);
off(this.el.ownerDocument, 'touchend', this._cancelStart);
off(this.el.ownerDocument, 'touchcancel', this._cancelStart);
off(document, 'selectstart', preventDefault);
Safari && css(document.body, 'user-select', '');
},
_onStart: function _onStart(touch, event) {
preventDefault(event);
var i = index(dragEl);
to = this.el;
from = this.el;
targetEl = dragEl;
oldIndex = i;
newIndex = i;
startIndex = i;
cloneEvent = {
to: this.el,
target: dragEl,
newIndex: i,
relative: 0
};
cloneTarget = dragEl;
startEl = this.el;
cloneEl = dragEl.cloneNode(true);
parentEl = dragEl.parentNode;
pullMode = this.options.group.pull;
Sortable.clone = cloneEl;
Sortable.active = this;
Sortable.dragged = dragEl;
this.multiplayer.onChoose();
toggleClass(dragEl, this.options.chosenClass, true);
dispatchEvent({
sortable: this,
name: 'onChoose',
evt: this._getEventProperties(event)
});
on(listenerNode, touch ? 'touchmove' : 'mousemove', this._nearestSortable);
// clear selection
try {
if (document.selection) {
// Timeout neccessary for IE9
setTimeout(function () {
return document.selection.empty();
}, 0);
} else {
window.getSelection().removeAllRanges();
}
} catch (error) {}
},
_onStarted: function _onStarted() {
this.animator.collect(parentEl);
toggleClass(cloneEl, this.options.chosenClass, true);
toggleClass(cloneEl, this.options.placeholderClass, true);
this._appendGhost();
this.multiplayer.toggleVisible(false);
css(dragEl, 'display', 'none');
dragEl.parentNode.insertBefore(cloneEl, dragEl);
dispatchEvent({
sortable: this,
name: 'onDrag',
evt: this._getEventProperties(dragEvent.event)
});
this.animator.animate();
this.autoScroller.onStarted();
},
_getGhostElement: function _getGhostElement() {
var customGhost = this.options.customGhost;
if (typeof customGhost === 'function') {
var selects = this.multiplayer.selects;
return customGhost(this.multiplayer.isActive() ? selects : [dragEl]);
}
return dragEl;
},
_appendGhost: function _appendGhost() {
if (ghostEl) return;
var container = this.options.fallbackOnBody ? document.body : this.el;
var element = this._getGhostElement();
ghostEl = element.cloneNode(true);
toggleClass(ghostEl, this.options.ghostClass, true);
var rect = getRect(dragEl);
var style = _extends({
position: 'fixed',
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
zIndex: '100000',
opacity: '0.8',
overflow: 'hidden',
boxSizing: 'border-box',
transform: '',
transition: '',
pointerEvents: 'none'
}, this.options.ghostStyle);
for (var key in style) {
css(ghostEl, key, style[key]);
}
Sortable.ghost = ghostEl;
container.appendChild(ghostEl);
var ox = (dragEvent.clientX - rect.left) / parseInt(ghostEl.style.width) * 100;
var oy = (dragEvent.clientY - rect.top) / parseInt(ghostEl.style.height) * 100;
css(ghostEl, 'transform-origin', "".concat(ox, "% ").concat(oy, "%"));
css(ghostEl, 'will-change', 'transform');
},
_nearestSortable: function _nearestSortable(event) {
preventDefault(event);
var touch = event.touches && event.touches[0],
evt = touch || event;
if (!dragEl || !_positionChanged(evt)) return;
// init in the move event to prevent conflict with the click event
!moveEvent && this._onStarted();
var lockAxis = this.options.lockAxis,
clientX = lockAxis === 'x' ? dragEvent.clientX : evt.clientX,
clientY = lockAxis === 'y' ? dragEvent.clientY : evt.clientY,
target = document.elementFromPoint(clientX, clientY),
dx = clientX - dragEvent.clientX,
dy = clientY - dragEvent.clientY;
moveEvent = {
event: event,
clientX: clientX,
clientY: clientY
};
css(ghostEl, 'transform', "translate3d(".concat(dx, "px, ").concat(dy, "px, 0)"));
var nearest = _detectNearestSortable(clientX, clientY);
nearest && nearest[expando]._onMove(event, target);
var options = nearest ? nearest[expando].options : null;
var scrollEl = null;
if ((!nearest || options.autoScroll) && dragEvent && moveEvent) {
scrollEl = getAutoScrollElement(target, true);
}
this.autoScroller.onMove(scrollEl, moveEvent, options || this.options);
},
_allowPut: function _allowPut() {
if (startEl === this.el) {
return true;
}
if (!this.options.group.put) {
return false;
}
var _this$options$group = this.options.group,
name = _this$options$group.name,
put = _this$options$group.put;
var fromGroup = startEl[expando].options.group;
return put.join && put.indexOf(fromGroup.name) > -1 || fromGroup.name && name && fromGroup.name === name;
},
_getDirection: function _getDirection() {
var _this$options2 = this.options,
draggable = _this$options2.draggable,
direction = _this$options2.direction;
return direction ? typeof direction === 'function' ? direction.call(moveEvent.event, cloneEl, this) : direction : detectDirection(parentEl, draggable);
},
_allowSwap: function _allowSwap() {
var rect = getRect(dropEl),
vertical = this._getDirection() === 'vertical',
front = vertical ? 'top' : 'left',
behind = vertical ? 'bottom' : 'right',
dropElSize = dropEl[vertical ? 'offsetHeight' : 'offsetWidth'],
mouseAxis = vertical ? moveEvent.clientY : moveEvent.clientX,
hoverArea = mouseAxis >= rect[front] && mouseAxis < rect[behind] - dropElSize / 2 ? -1 : 1,
childf = getChild(parentEl, 0, this.options.draggable),
childl = lastChild(parentEl),
childfRect = getRect(childf),
childlRect = getRect(childl);
if (dropEl === parentEl || containes(parentEl, dropEl)) {
// The dragged element is the first child of its parent
if (cloneEl === childf && mouseAxis < childfRect[front]) {
nextEl = dropEl;
return true;
}
// Dragged element is the last child of its parent
if (cloneEl === childl && mouseAxis > childlRect[behind]) {
nextEl = dropEl.nextSibling;
return true;
}
return false;
}
var order = sort(cloneEl, dropEl);
nextEl = order < 0 ? dropEl.nextSibling : dropEl;
if (lastDropEl !== dropEl) {
lastHoverArea = hoverArea;
return true;
}
if (lastHoverArea !== hoverArea) {
lastHoverArea = hoverArea;
return hoverArea < 0 ? order > 0 : order < 0;
}
return false;
},
_onMove: function _onMove(event, target) {
if (this.options.disabled || !this._allowPut()) return;
dropEl = closest(target, this.options.draggable, this.el);
dispatchEvent({
sortable: this,
name: 'onMove',
evt: this._getEventProperties(event, {
target: dropEl
})
});
// dragEl is allowed to return to the original list in `sortable: false`
if (!this.options.sortable && this.el === startEl) {
if (from !== startEl) {
dropEl = lastDropEl = dragEl;
lastHoverArea = 0;
this._onInsert(event);
}
return;
}
// insert to last
if (this.el !== from && (target === this.el || !lastChild(this.el))) {
dropEl = lastDropEl = null;
this._onInsert(event);
return;
}
if (!dropEl || dropEl.animated || containes(dropEl, cloneEl) || !this._allowSwap()) return;
if (dropEl === cloneEl || nextEl === cloneEl) {
lastDropEl = dropEl;
return;
}
if (this.el !== from) {
this._onInsert(event);
} else if (dropEl !== dragEl) {
this._onChange(event);
}
lastDropEl = dropEl;
},
_onInsert: function _onInsert(event) {
var target = dropEl || cloneEl,
cloneTo = pullMode === 'clone' && this.el !== startEl && from === startEl,
cloneBack = pullMode === 'clone' && this.el === startEl && from !== startEl,
dropExist = containes(dropEl, document),
dragRemoved = dropEl === dragEl && !dropExist,
fromSortable = from[expando],
startSortable = startEl[expando];
to = this.el;
oldIndex = index(cloneEl);
targetEl = target;
parentEl = dropExist ? dropEl.parentNode : this.el;
fromSortable.animator.collect(cloneEl.parentNode);
this.animator.collect(parentEl);
// show dragEl before clone to another list
if (cloneTo) {
cloneEvent.target = cloneTarget;
cloneEvent.newIndex = oldIndex;
cloneEvent.relative = cloneTarget === dragEl ? 0 : sort(cloneEl, cloneTarget);
css(dragEl, 'display', '');
startSortable.multiplayer.toggleVisible(true);
if (!startSortable.options.group.revertDrag) {
cloneEl.parentNode.insertBefore(dragEl, cloneEl);
}
}
// hide dragEl when returning to the original list
if (cloneBack) {
oldIndex = index(dragEl);
css(dragEl, 'display', 'none');
this.multiplayer.toggleVisible(false);
}
css(cloneEl, 'display', dragRemoved ? 'none' : '');
if (dropEl && dropExist) {
parentEl.insertBefore(cloneEl, lastHoverArea < 0 ? dropEl : dropEl.nextSibling);
} else {
parentEl.appendChild(cloneEl);
}
newIndex = dragRemoved ? startIndex : index(cloneEl);
if (cloneTo && startSortable.options.group.revertDrag) {
cloneEvent.target = dragEl;
cloneEvent.newIndex = startIndex;
cloneEvent.relative = 0;
dispatchEvent({
sortable: startSortable,
name: 'onChange',
evt: this._getEventProperties(event, {
to: startEl,
target: dragEl,
newIndex: startIndex,
revertDrag: true
})
});
}
if (!cloneTo) {
dispatchEvent({
sortable: fromSortable,
name: 'onRemove',
evt: this._getEventProperties(event, {
newIndex: -1
})
});
}
if (cloneBack && target !== dragEl) {
cloneTarget = target;
dispatchEvent({
sortable: this,
name: 'onChange',
evt: this._getEventProperties(event, {
from: startEl,
backToOrigin: true
})
});
}
if (!cloneBack) {
dispatchEvent({
sortable: this,
name: 'onAdd',
evt: this._getEventProperties(event, {
oldIndex: -1
})
});
}
fromSortable.animator.animate();
this.animator.animate();
from = this.el;
},
_onChange: function _onChange(event) {
this.animator.collect(parentEl);
oldIndex = index(cloneEl);
parentEl = dropEl.parentNode;
targetEl = dropEl;
if (this.el === startEl) {
cloneTarget = dropEl;
}
parentEl.insertBefore(cloneEl, nextEl);
newIndex = index(cloneEl);
dispatchEvent({
sortable: this,
name: 'onChange',
evt: this._getEventProperties(event)
});
this.animator.animate();
from = this.el;
},
_onDrop: function _onDrop(event) {
this._cancelStart();
off(listenerNode, 'touchmove', this._nearestSortable);
off(listenerNode, 'mousemove', this._nearestSortable);
off(listenerNode, 'mouseup', this._onDrop);
off(listenerNode, 'touchend', this._onDrop);
off(listenerNode, 'touchcancel', this._onDrop);
if (startEl) {
from = startEl;
oldIndex = startIndex;
if (targetEl === cloneEl) {
targetEl = dragEl;
}
this.animator.collect(parentEl);
this.multiplayer.toggleChosenClass(false);
toggleClass(dragEl, this.options.chosenClass, false);
dispatchEvent({
sortable: this,
name: 'onUnchoose',
evt: this._getEventProperties(event)
});
moveEvent && this._onEnd(event);
!moveEvent && this.animator.animate();
}
// check whether the event is a click event
var evt = event.changedTouches ? event.changedTouches[0] : event;
!_positionChanged(evt) && this.multiplayer.onSelect(event, dragEl, startEl, this);
if (ghostEl && ghostEl.parentNode) {
ghostEl.parentNode.removeChild(ghostEl);
}
this._nulling();
},
_onEnd: function _onEnd(event) {
toggleClass(cloneEl, this.options.chosenClass, false);
toggleClass(cloneEl, this.options.placeholderClass, false);
var isClone = pullMode === 'clone';
this.multiplayer.onDrop(from, to, isClone);
var evt = this._getEventProperties(event);
// swap real drag element to the current drop position
var _this$options3 = this.options,
swapOnDrop = _this$options3.swapOnDrop,
removeCloneOnDrop = _this$options3.removeCloneOnDrop;
if ((!isClone || from === to) && (typeof swapOnDrop === 'function' ? swapOnDrop(evt) : swapOnDrop)) {
parentEl.insertBefore(dragEl, cloneEl);
}
if ((!isClone || from === to || this.multiplayer.isActive()) && (typeof removeCloneOnDrop === 'function' ? removeCloneOnDrop(evt) : removeCloneOnDrop)) {
cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl);
}
css(dragEl, 'display', '');
this.animator.animate();
if (from !== to) {
dispatchEvent({
sortable: from[expando],
name: 'onDrop',
evt: _extends({}, evt, isClone ? cloneEvent : {
newIndex: -1
})
});
}
dispatchEvent({
sortable: to[expando],
name: 'onDrop',
evt: _extends({}, evt, from === to ? {} : {
oldIndex: -1
})
});
},
_getEventProperties: function _getEventProperties(originalEvent) {
var extra = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var evt = {};
evt.event = originalEvent;
evt.to = to;
evt.from = from;
evt.node = dragEl;
evt.clone = cloneEl;
evt.target = targetEl;
evt.oldIndex = oldIndex;
evt.newIndex = newIndex;
evt.pullMode = pullMode;
_extends(evt, this.multiplayer.eventProperties(), extra);
evt.relative = targetEl === dragEl ? 0 : sort(cloneEl, targetEl);
return evt;
},
_nulling: function _nulling() {
to = from = dragEl = dropEl = nextEl = cloneEl = ghostEl = startEl = targetEl = parentEl = pullMode = oldIndex = newIndex = startIndex = dragEvent = moveEvent = lastDropEl = cloneEvent = cloneTarget = listenerNode = lastHoverArea = dragStartTimer = Sortable.clone = Sortable.ghost = Sortable.active = Sortable.dragged = null;
this.multiplayer.nulling();
this.autoScroller.nulling();
},
destroy: function destroy() {
this._cancelStart();
this._nulling();
off(this.el, 'touchstart', this._onDrag);
off(this.el, 'mousedown', this._onDrag);
var index = sortables.indexOf(this.el);
index > -1 && sortables.splice(index, 1);
this.el[expando] = this.animator = this.multiplayer = this.autoScroller = null;
},
option: function option(key, value) {
if (value === void 0) {
return this.options[key];
}
// set option
this.options[key] = value;
this.animator.options[key] = value;
this.multiplayer.options[key] = value;
this.autoScroller.options[key] = value;
if (key === 'group') {
_prepareGroup(this.options);
}
},
select: function select(element) {
this.multiplayer.select(element);
},
deselect: function deselect(element) {
this.multiplayer.deselect(element);
},
getSelectedElements: function getSelectedElements() {
return this.multiplayer.selects;
}
};
Sortable.utils = {
on: on,
off: off,
css: css,
index: index,
closest: closest,
getRect: getRect,
toggleClass: toggleClass,
detectDirection: detectDirection
};
Sortable.get = function (element) {
return element[expando];
};
Sortable.create = function (el, options) {
return new Sortable(el, options);
};
return Sortable;
}));