1215 lines
49 KiB
JavaScript
Vendored
1215 lines
49 KiB
JavaScript
Vendored
/*
|
|
* HTML5Sortable package
|
|
* https://github.com/lukasoppermann/html5sortable
|
|
*
|
|
* Maintained by Lukas Oppermann <lukas@vea.re>
|
|
*
|
|
* Released under the MIT license.
|
|
*/
|
|
var sortable = (function () {
|
|
'use strict';
|
|
|
|
/**
|
|
* Get or set data on element
|
|
* @param {HTMLElement} element
|
|
* @param {string} key
|
|
* @param {any} value
|
|
* @return {*}
|
|
*/
|
|
function addData(element, key, value) {
|
|
if (value === undefined) {
|
|
return element && element.h5s && element.h5s.data && element.h5s.data[key];
|
|
}
|
|
else {
|
|
element.h5s = element.h5s || {};
|
|
element.h5s.data = element.h5s.data || {};
|
|
element.h5s.data[key] = value;
|
|
}
|
|
}
|
|
/**
|
|
* Remove data from element
|
|
* @param {HTMLElement} element
|
|
*/
|
|
function removeData(element) {
|
|
if (element.h5s) {
|
|
delete element.h5s.data;
|
|
}
|
|
}
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Filter only wanted nodes
|
|
* @param {NodeList|HTMLCollection|Array} nodes
|
|
* @param {String} selector
|
|
* @returns {Array}
|
|
*/
|
|
var _filter = (function (nodes, selector) {
|
|
if (!(nodes instanceof NodeList || nodes instanceof HTMLCollection || nodes instanceof Array)) {
|
|
throw new Error('You must provide a nodeList/HTMLCollection/Array of elements to be filtered.');
|
|
}
|
|
if (typeof selector !== 'string') {
|
|
return Array.from(nodes);
|
|
}
|
|
return Array.from(nodes).filter(function (item) { return item.nodeType === 1 && item.matches(selector); });
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
var stores = new Map();
|
|
/**
|
|
* Stores data & configurations per Sortable
|
|
* @param {Object} config
|
|
*/
|
|
var Store = /** @class */ (function () {
|
|
function Store() {
|
|
this._config = new Map(); // eslint-disable-line no-undef
|
|
this._placeholder = undefined; // eslint-disable-line no-undef
|
|
this._data = new Map(); // eslint-disable-line no-undef
|
|
}
|
|
Object.defineProperty(Store.prototype, "config", {
|
|
/**
|
|
* get the configuration map of a class instance
|
|
* @method config
|
|
* @return {object}
|
|
*/
|
|
get: function () {
|
|
// transform Map to object
|
|
var config = {};
|
|
this._config.forEach(function (value, key) {
|
|
config[key] = value;
|
|
});
|
|
// return object
|
|
return config;
|
|
},
|
|
/**
|
|
* set the configuration of a class instance
|
|
* @method config
|
|
* @param {object} config object of configurations
|
|
*/
|
|
set: function (config) {
|
|
if (typeof config !== 'object') {
|
|
throw new Error('You must provide a valid configuration object to the config setter.');
|
|
}
|
|
// combine config with default
|
|
var mergedConfig = Object.assign({}, config);
|
|
// add config to map
|
|
this._config = new Map(Object.entries(mergedConfig));
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
/**
|
|
* set individual configuration of a class instance
|
|
* @method setConfig
|
|
* @param key valid configuration key
|
|
* @param value any value
|
|
* @return void
|
|
*/
|
|
Store.prototype.setConfig = function (key, value) {
|
|
if (!this._config.has(key)) {
|
|
throw new Error("Trying to set invalid configuration item: " + key);
|
|
}
|
|
// set config
|
|
this._config.set(key, value);
|
|
};
|
|
/**
|
|
* get an individual configuration of a class instance
|
|
* @method getConfig
|
|
* @param key valid configuration key
|
|
* @return any configuration value
|
|
*/
|
|
Store.prototype.getConfig = function (key) {
|
|
if (!this._config.has(key)) {
|
|
throw new Error("Invalid configuration item requested: " + key);
|
|
}
|
|
return this._config.get(key);
|
|
};
|
|
Object.defineProperty(Store.prototype, "placeholder", {
|
|
/**
|
|
* get the placeholder for a class instance
|
|
* @method placeholder
|
|
* @return {HTMLElement|null}
|
|
*/
|
|
get: function () {
|
|
return this._placeholder;
|
|
},
|
|
/**
|
|
* set the placeholder for a class instance
|
|
* @method placeholder
|
|
* @param {HTMLElement} placeholder
|
|
* @return {void}
|
|
*/
|
|
set: function (placeholder) {
|
|
if (!(placeholder instanceof HTMLElement) && placeholder !== null) {
|
|
throw new Error('A placeholder must be an html element or null.');
|
|
}
|
|
this._placeholder = placeholder;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
/**
|
|
* set an data entry
|
|
* @method setData
|
|
* @param {string} key
|
|
* @param {any} value
|
|
* @return {void}
|
|
*/
|
|
Store.prototype.setData = function (key, value) {
|
|
if (typeof key !== 'string') {
|
|
throw new Error("The key must be a string.");
|
|
}
|
|
this._data.set(key, value);
|
|
};
|
|
/**
|
|
* get an data entry
|
|
* @method getData
|
|
* @param {string} key an existing key
|
|
* @return {any}
|
|
*/
|
|
Store.prototype.getData = function (key) {
|
|
if (typeof key !== 'string') {
|
|
throw new Error("The key must be a string.");
|
|
}
|
|
return this._data.get(key);
|
|
};
|
|
/**
|
|
* delete an data entry
|
|
* @method deleteData
|
|
* @param {string} key an existing key
|
|
* @return {boolean}
|
|
*/
|
|
Store.prototype.deleteData = function (key) {
|
|
if (typeof key !== 'string') {
|
|
throw new Error("The key must be a string.");
|
|
}
|
|
return this._data.delete(key);
|
|
};
|
|
return Store;
|
|
}());
|
|
/**
|
|
* @param {HTMLElement} sortableElement
|
|
* @returns {Class: Store}
|
|
*/
|
|
var store = (function (sortableElement) {
|
|
// if sortableElement is wrong type
|
|
if (!(sortableElement instanceof HTMLElement)) {
|
|
throw new Error('Please provide a sortable to the store function.');
|
|
}
|
|
// create new instance if not avilable
|
|
if (!stores.has(sortableElement)) {
|
|
stores.set(sortableElement, new Store());
|
|
}
|
|
// return instance
|
|
return stores.get(sortableElement);
|
|
});
|
|
|
|
/**
|
|
* @param {Array|HTMLElement} element
|
|
* @param {Function} callback
|
|
* @param {string} event
|
|
*/
|
|
function addEventListener(element, eventName, callback) {
|
|
if (element instanceof Array) {
|
|
for (var i = 0; i < element.length; ++i) {
|
|
addEventListener(element[i], eventName, callback);
|
|
}
|
|
return;
|
|
}
|
|
element.addEventListener(eventName, callback);
|
|
store(element).setData("event" + eventName, callback);
|
|
}
|
|
/**
|
|
* @param {Array<HTMLElement>|HTMLElement} element
|
|
* @param {string} eventName
|
|
*/
|
|
function removeEventListener(element, eventName) {
|
|
if (element instanceof Array) {
|
|
for (var i = 0; i < element.length; ++i) {
|
|
removeEventListener(element[i], eventName);
|
|
}
|
|
return;
|
|
}
|
|
element.removeEventListener(eventName, store(element).getData("event" + eventName));
|
|
store(element).deleteData("event" + eventName);
|
|
}
|
|
|
|
/**
|
|
* @param {Array<HTMLElement>|HTMLElement} element
|
|
* @param {string} attribute
|
|
* @param {string} value
|
|
*/
|
|
function addAttribute(element, attribute, value) {
|
|
if (element instanceof Array) {
|
|
for (var i = 0; i < element.length; ++i) {
|
|
addAttribute(element[i], attribute, value);
|
|
}
|
|
return;
|
|
}
|
|
element.setAttribute(attribute, value);
|
|
}
|
|
/**
|
|
* @param {Array|HTMLElement} element
|
|
* @param {string} attribute
|
|
*/
|
|
function removeAttribute(element, attribute) {
|
|
if (element instanceof Array) {
|
|
for (var i = 0; i < element.length; ++i) {
|
|
removeAttribute(element[i], attribute);
|
|
}
|
|
return;
|
|
}
|
|
element.removeAttribute(attribute);
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} element
|
|
* @returns {Object}
|
|
*/
|
|
var _offset = (function (element) {
|
|
if (!element.parentElement || element.getClientRects().length === 0) {
|
|
throw new Error('target element must be part of the dom');
|
|
}
|
|
var rect = element.getClientRects()[0];
|
|
return {
|
|
left: rect.left + window.pageXOffset,
|
|
right: rect.right + window.pageXOffset,
|
|
top: rect.top + window.pageYOffset,
|
|
bottom: rect.bottom + window.pageYOffset
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed
|
|
* @param {Function} func to debounce
|
|
* @param {number} time to wait before calling function with latest arguments, 0 - no debounce
|
|
* @returns {function} - debounced function
|
|
*/
|
|
var _debounce = (function (func, wait) {
|
|
if (wait === void 0) { wait = 0; }
|
|
var timeout;
|
|
return function () {
|
|
var args = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
args[_i] = arguments[_i];
|
|
}
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(function () {
|
|
func.apply(void 0, args);
|
|
}, wait);
|
|
};
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Get position of the element relatively to its sibling elements
|
|
* @param {HTMLElement} element
|
|
* @returns {number}
|
|
*/
|
|
var _index = (function (element, elementList) {
|
|
if (!(element instanceof HTMLElement) || !(elementList instanceof NodeList || elementList instanceof HTMLCollection || elementList instanceof Array)) {
|
|
throw new Error('You must provide an element and a list of elements.');
|
|
}
|
|
return Array.from(elementList).indexOf(element);
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Test whether element is in DOM
|
|
* @param {HTMLElement} element
|
|
* @returns {boolean}
|
|
*/
|
|
var isInDom = (function (element) {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error('Element is not a node element.');
|
|
}
|
|
return element.parentNode !== null;
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Insert node before or after target
|
|
* @param {HTMLElement} referenceNode - reference element
|
|
* @param {HTMLElement} newElement - element to be inserted
|
|
* @param {String} position - insert before or after reference element
|
|
*/
|
|
var insertNode = function (referenceNode, newElement, position) {
|
|
if (!(referenceNode instanceof HTMLElement) || !(referenceNode.parentElement instanceof HTMLElement)) {
|
|
throw new Error('target and element must be a node');
|
|
}
|
|
referenceNode.parentElement.insertBefore(newElement, (position === 'before' ? referenceNode : referenceNode.nextElementSibling));
|
|
};
|
|
/**
|
|
* Insert before target
|
|
* @param {HTMLElement} target
|
|
* @param {HTMLElement} element
|
|
*/
|
|
var insertBefore = function (target, element) { return insertNode(target, element, 'before'); };
|
|
/**
|
|
* Insert after target
|
|
* @param {HTMLElement} target
|
|
* @param {HTMLElement} element
|
|
*/
|
|
var insertAfter = function (target, element) { return insertNode(target, element, 'after'); };
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Filter only wanted nodes
|
|
* @param {HTMLElement} sortableContainer
|
|
* @param {Function} customSerializer
|
|
* @returns {Array}
|
|
*/
|
|
var _serialize = (function (sortableContainer, customItemSerializer, customContainerSerializer) {
|
|
if (customItemSerializer === void 0) { customItemSerializer = function (serializedItem, sortableContainer) { return serializedItem; }; }
|
|
if (customContainerSerializer === void 0) { customContainerSerializer = function (serializedContainer) { return serializedContainer; }; }
|
|
// check for valid sortableContainer
|
|
if (!(sortableContainer instanceof HTMLElement) || !sortableContainer.isSortable === true) {
|
|
throw new Error('You need to provide a sortableContainer to be serialized.');
|
|
}
|
|
// check for valid serializers
|
|
if (typeof customItemSerializer !== 'function' || typeof customContainerSerializer !== 'function') {
|
|
throw new Error('You need to provide a valid serializer for items and the container.');
|
|
}
|
|
// get options
|
|
var options = addData(sortableContainer, 'opts');
|
|
var item = options.items;
|
|
// serialize container
|
|
var items = _filter(sortableContainer.children, item);
|
|
var serializedItems = items.map(function (item) {
|
|
return {
|
|
parent: sortableContainer,
|
|
node: item,
|
|
html: item.outerHTML,
|
|
index: _index(item, items)
|
|
};
|
|
});
|
|
// serialize container
|
|
var container = {
|
|
node: sortableContainer,
|
|
itemCount: serializedItems.length
|
|
};
|
|
return {
|
|
container: customContainerSerializer(container),
|
|
items: serializedItems.map(function (item) { return customItemSerializer(item, sortableContainer); })
|
|
};
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* create a placeholder element
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
* @param {string|undefined} placeholder a string representing an html element
|
|
* @param {string} placeholderClasses a string representing the classes that should be added to the placeholder
|
|
*/
|
|
var _makePlaceholder = (function (sortableElement, placeholder, placeholderClass) {
|
|
var _a;
|
|
if (placeholderClass === void 0) { placeholderClass = 'sortable-placeholder'; }
|
|
if (!(sortableElement instanceof HTMLElement)) {
|
|
throw new Error('You must provide a valid element as a sortable.');
|
|
}
|
|
// if placeholder is not an element
|
|
if (!(placeholder instanceof HTMLElement) && placeholder !== undefined) {
|
|
throw new Error('You must provide a valid element as a placeholder or set ot to undefined.');
|
|
}
|
|
// if no placeholder element is given
|
|
if (placeholder === undefined) {
|
|
if (['UL', 'OL'].includes(sortableElement.tagName)) {
|
|
placeholder = document.createElement('li');
|
|
}
|
|
else if (['TABLE', 'TBODY'].includes(sortableElement.tagName)) {
|
|
placeholder = document.createElement('tr');
|
|
// set colspan to always all rows, otherwise the item can only be dropped in first column
|
|
placeholder.innerHTML = '<td colspan="100"></td>';
|
|
}
|
|
else {
|
|
placeholder = document.createElement('div');
|
|
}
|
|
}
|
|
// add classes to placeholder
|
|
if (typeof placeholderClass === 'string') {
|
|
(_a = placeholder.classList).add.apply(_a, placeholderClass.split(' '));
|
|
}
|
|
return placeholder;
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* Get height of an element including padding
|
|
* @param {HTMLElement} element an dom element
|
|
*/
|
|
var _getElementHeight = (function (element) {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error('You must provide a valid dom element');
|
|
}
|
|
// get calculated style of element
|
|
var style = window.getComputedStyle(element);
|
|
// pick applicable properties, convert to int and reduce by adding
|
|
return ['height', 'padding-top', 'padding-bottom']
|
|
.map(function (key) {
|
|
var int = parseInt(style.getPropertyValue(key), 10);
|
|
return isNaN(int) ? 0 : int;
|
|
})
|
|
.reduce(function (sum, value) { return sum + value; });
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* get handle or return item
|
|
* @param {Array<HTMLElement>} items
|
|
* @param {string} selector
|
|
*/
|
|
var _getHandles = (function (items, selector) {
|
|
if (!(items instanceof Array)) {
|
|
throw new Error('You must provide a Array of HTMLElements to be filtered.');
|
|
}
|
|
if (typeof selector !== 'string') {
|
|
return items;
|
|
}
|
|
return items
|
|
// remove items without handle from array
|
|
.filter(function (item) {
|
|
return item.querySelector(selector) instanceof HTMLElement ||
|
|
(item.shadowRoot && item.shadowRoot.querySelector(selector) instanceof HTMLElement);
|
|
})
|
|
// replace item with handle in array
|
|
.map(function (item) {
|
|
return item.querySelector(selector) || (item.shadowRoot && item.shadowRoot.querySelector(selector));
|
|
});
|
|
});
|
|
|
|
/**
|
|
* @param {Event} event
|
|
* @returns {HTMLElement}
|
|
*/
|
|
var getEventTarget = (function (event) {
|
|
return (event.composedPath && event.composedPath()[0]) || event.target;
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* defaultDragImage returns the current item as dragged image
|
|
* @param {HTMLElement} draggedElement - the item that the user drags
|
|
* @param {object} elementOffset - an object with the offsets top, left, right & bottom
|
|
* @param {Event} event - the original drag event object
|
|
* @return {object} with element, posX and posY properties
|
|
*/
|
|
var defaultDragImage = function (draggedElement, elementOffset, event) {
|
|
return {
|
|
element: draggedElement,
|
|
posX: event.pageX - elementOffset.left,
|
|
posY: event.pageY - elementOffset.top
|
|
};
|
|
};
|
|
/**
|
|
* attaches an element as the drag image to an event
|
|
* @param {Event} event - the original drag event object
|
|
* @param {HTMLElement} draggedElement - the item that the user drags
|
|
* @param {Function} customDragImage - function to create a custom dragImage
|
|
* @return void
|
|
*/
|
|
var setDragImage = (function (event, draggedElement, customDragImage) {
|
|
// check if event is provided
|
|
if (!(event instanceof Event)) {
|
|
throw new Error('setDragImage requires a DragEvent as the first argument.');
|
|
}
|
|
// check if draggedElement is provided
|
|
if (!(draggedElement instanceof HTMLElement)) {
|
|
throw new Error('setDragImage requires the dragged element as the second argument.');
|
|
}
|
|
// set default function of none provided
|
|
if (!customDragImage) {
|
|
customDragImage = defaultDragImage;
|
|
}
|
|
// check if setDragImage method is available
|
|
if (event.dataTransfer && event.dataTransfer.setDragImage) {
|
|
// get the elements offset
|
|
var elementOffset = _offset(draggedElement);
|
|
// get the dragImage
|
|
var dragImage = customDragImage(draggedElement, elementOffset, event);
|
|
// check if custom function returns correct values
|
|
if (!(dragImage.element instanceof HTMLElement) || typeof dragImage.posX !== 'number' || typeof dragImage.posY !== 'number') {
|
|
throw new Error('The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].');
|
|
}
|
|
// needs to be set for HTML5 drag & drop to work
|
|
event.dataTransfer.effectAllowed = 'copyMove';
|
|
// Firefox requires it to use the event target's id for the data
|
|
event.dataTransfer.setData('text/plain', getEventTarget(event).id);
|
|
// set the drag image on the event
|
|
event.dataTransfer.setDragImage(dragImage.element, dragImage.posX, dragImage.posY);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Check if curList accepts items from destList
|
|
* @param {sortable} destination the container an item is move to
|
|
* @param {sortable} origin the container an item comes from
|
|
*/
|
|
var _listsConnected = (function (destination, origin) {
|
|
// check if valid sortable
|
|
if (destination.isSortable === true) {
|
|
var acceptFrom = store(destination).getConfig('acceptFrom');
|
|
// check if acceptFrom is valid
|
|
if (acceptFrom !== null && acceptFrom !== false && typeof acceptFrom !== 'string') {
|
|
throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.');
|
|
}
|
|
if (acceptFrom !== null) {
|
|
return acceptFrom !== false && acceptFrom.split(',').filter(function (sel) {
|
|
return sel.length > 0 && origin.matches(sel);
|
|
}).length > 0;
|
|
}
|
|
// drop in same list
|
|
if (destination === origin) {
|
|
return true;
|
|
}
|
|
// check if lists are connected with connectWith
|
|
if (store(destination).getConfig('connectWith') !== undefined && store(destination).getConfig('connectWith') !== null) {
|
|
return store(destination).getConfig('connectWith') === store(origin).getConfig('connectWith');
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
/**
|
|
* default configurations
|
|
*/
|
|
var defaultConfiguration = {
|
|
items: null,
|
|
// deprecated
|
|
connectWith: null,
|
|
// deprecated
|
|
disableIEFix: null,
|
|
acceptFrom: null,
|
|
copy: false,
|
|
placeholder: null,
|
|
placeholderClass: 'sortable-placeholder',
|
|
draggingClass: 'sortable-dragging',
|
|
hoverClass: false,
|
|
debounce: 0,
|
|
throttleTime: 100,
|
|
maxItems: 0,
|
|
itemSerializer: undefined,
|
|
containerSerializer: undefined,
|
|
customDragImage: null
|
|
};
|
|
|
|
/**
|
|
* make sure a function is only called once within the given amount of time
|
|
* @param {Function} fn the function to throttle
|
|
* @param {number} threshold time limit for throttling
|
|
*/
|
|
// must use function to keep this context
|
|
function _throttle (fn, threshold) {
|
|
var _this = this;
|
|
if (threshold === void 0) { threshold = 250; }
|
|
// check function
|
|
if (typeof fn !== 'function') {
|
|
throw new Error('You must provide a function as the first argument for throttle.');
|
|
}
|
|
// check threshold
|
|
if (typeof threshold !== 'number') {
|
|
throw new Error('You must provide a number as the second argument for throttle.');
|
|
}
|
|
var lastEventTimestamp = null;
|
|
return function () {
|
|
var args = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
args[_i] = arguments[_i];
|
|
}
|
|
var now = Date.now();
|
|
if (lastEventTimestamp === null || now - lastEventTimestamp >= threshold) {
|
|
lastEventTimestamp = now;
|
|
fn.apply(_this, args);
|
|
}
|
|
};
|
|
}
|
|
|
|
/* eslint-env browser */
|
|
/**
|
|
* enable or disable hoverClass on mouseenter/leave if container Items
|
|
* @param {sortable} sortableContainer a valid sortableContainer
|
|
* @param {boolean} enable enable or disable event
|
|
*/
|
|
// export default (sortableContainer: sortable, enable: boolean) => {
|
|
var enableHoverClass = (function (sortableContainer, enable) {
|
|
if (typeof store(sortableContainer).getConfig('hoverClass') === 'string') {
|
|
var hoverClasses_1 = store(sortableContainer).getConfig('hoverClass').split(' ');
|
|
// add class on hover
|
|
if (enable === true) {
|
|
addEventListener(sortableContainer, 'mousemove', _throttle(function (event) {
|
|
// check of no mouse button was pressed when mousemove started == no drag
|
|
if (event.buttons === 0) {
|
|
_filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) {
|
|
var _a, _b;
|
|
if (item !== event.target) {
|
|
(_a = item.classList).remove.apply(_a, hoverClasses_1);
|
|
}
|
|
else {
|
|
(_b = item.classList).add.apply(_b, hoverClasses_1);
|
|
}
|
|
});
|
|
}
|
|
}, store(sortableContainer).getConfig('throttleTime')));
|
|
// remove class on leave
|
|
addEventListener(sortableContainer, 'mouseleave', function () {
|
|
_filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) {
|
|
var _a;
|
|
(_a = item.classList).remove.apply(_a, hoverClasses_1);
|
|
});
|
|
});
|
|
// remove events
|
|
}
|
|
else {
|
|
removeEventListener(sortableContainer, 'mousemove');
|
|
removeEventListener(sortableContainer, 'mouseleave');
|
|
}
|
|
}
|
|
});
|
|
|
|
/* eslint-env browser */
|
|
/*
|
|
* variables global to the plugin
|
|
*/
|
|
var dragging;
|
|
var draggingHeight;
|
|
/*
|
|
* Keeps track of the initialy selected list, where 'dragstart' event was triggered
|
|
* It allows us to move the data in between individual Sortable List instances
|
|
*/
|
|
// Origin List - data from before any item was changed
|
|
var originContainer;
|
|
var originIndex;
|
|
var originElementIndex;
|
|
var originItemsBeforeUpdate;
|
|
// Previous Sortable Container - we dispatch as sortenter event when a
|
|
// dragged item enters a sortableContainer for the first time
|
|
var previousContainer;
|
|
// Destination List - data from before any item was changed
|
|
var destinationItemsBeforeUpdate;
|
|
/**
|
|
* remove event handlers from items
|
|
* @param {Array|NodeList} items
|
|
*/
|
|
var _removeItemEvents = function (items) {
|
|
removeEventListener(items, 'dragstart');
|
|
removeEventListener(items, 'dragend');
|
|
removeEventListener(items, 'dragover');
|
|
removeEventListener(items, 'dragenter');
|
|
removeEventListener(items, 'drop');
|
|
removeEventListener(items, 'mouseenter');
|
|
removeEventListener(items, 'mouseleave');
|
|
};
|
|
/**
|
|
* _getDragging returns the current element to drag or
|
|
* a copy of the element.
|
|
* Is Copy Active for sortable
|
|
* @param {HTMLElement} draggedItem - the item that the user drags
|
|
* @param {HTMLElement} sortable a single sortable
|
|
*/
|
|
var _getDragging = function (draggedItem, sortable) {
|
|
var ditem = draggedItem;
|
|
if (store(sortable).getConfig('copy') === true) {
|
|
ditem = draggedItem.cloneNode(true);
|
|
addAttribute(ditem, 'aria-copied', 'true');
|
|
draggedItem.parentElement.appendChild(ditem);
|
|
ditem.style.display = 'none';
|
|
ditem.oldDisplay = draggedItem.style.display;
|
|
}
|
|
return ditem;
|
|
};
|
|
/**
|
|
* Remove data from sortable
|
|
* @param {HTMLElement} sortable a single sortable
|
|
*/
|
|
var _removeSortableData = function (sortable) {
|
|
removeData(sortable);
|
|
removeAttribute(sortable, 'aria-dropeffect');
|
|
};
|
|
/**
|
|
* Remove data from items
|
|
* @param {Array<HTMLElement>|HTMLElement} items
|
|
*/
|
|
var _removeItemData = function (items) {
|
|
removeAttribute(items, 'aria-grabbed');
|
|
removeAttribute(items, 'aria-copied');
|
|
removeAttribute(items, 'draggable');
|
|
removeAttribute(items, 'role');
|
|
};
|
|
/**
|
|
* find sortable from element. travels up parent element until found or null.
|
|
* @param {HTMLElement} element a single sortable
|
|
* @param {Event} event - the current event. We need to pass it to be able to
|
|
* find Sortable whith shadowRoot (document fragment has no parent)
|
|
*/
|
|
function findSortable(element, event) {
|
|
if (event.composedPath) {
|
|
return event.composedPath().find(function (el) { return el.isSortable; });
|
|
}
|
|
while (element.isSortable !== true) {
|
|
element = element.parentElement;
|
|
}
|
|
return element;
|
|
}
|
|
/**
|
|
* Dragging event is on the sortable element. finds the top child that
|
|
* contains the element.
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
* @param {HTMLElement} element is that being dragged
|
|
*/
|
|
function findDragElement(sortableElement, element) {
|
|
var options = addData(sortableElement, 'opts');
|
|
var items = _filter(sortableElement.children, options.items);
|
|
var itemlist = items.filter(function (ele) {
|
|
return ele.contains(element) || (ele.shadowRoot && ele.shadowRoot.contains(element));
|
|
});
|
|
return itemlist.length > 0 ? itemlist[0] : element;
|
|
}
|
|
/**
|
|
* Destroy the sortable
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
*/
|
|
var _destroySortable = function (sortableElement) {
|
|
var opts = addData(sortableElement, 'opts') || {};
|
|
var items = _filter(sortableElement.children, opts.items);
|
|
var handles = _getHandles(items, opts.handle);
|
|
// remove event handlers & data from sortable
|
|
removeEventListener(sortableElement, 'dragover');
|
|
removeEventListener(sortableElement, 'dragenter');
|
|
removeEventListener(sortableElement, 'drop');
|
|
// remove event data from sortable
|
|
_removeSortableData(sortableElement);
|
|
// remove event handlers & data from items
|
|
removeEventListener(handles, 'mousedown');
|
|
_removeItemEvents(items);
|
|
_removeItemData(items);
|
|
};
|
|
/**
|
|
* Enable the sortable
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
*/
|
|
var _enableSortable = function (sortableElement) {
|
|
var opts = addData(sortableElement, 'opts');
|
|
var items = _filter(sortableElement.children, opts.items);
|
|
var handles = _getHandles(items, opts.handle);
|
|
addAttribute(sortableElement, 'aria-dropeffect', 'move');
|
|
addData(sortableElement, '_disabled', 'false');
|
|
addAttribute(handles, 'draggable', 'true');
|
|
// @todo: remove this fix
|
|
// IE FIX for ghost
|
|
// can be disabled as it has the side effect that other events
|
|
// (e.g. click) will be ignored
|
|
if (opts.disableIEFix === false) {
|
|
var spanEl = (document || window.document).createElement('span');
|
|
if (typeof spanEl.dragDrop === 'function') {
|
|
addEventListener(handles, 'mousedown', function () {
|
|
if (items.indexOf(this) !== -1) {
|
|
this.dragDrop();
|
|
}
|
|
else {
|
|
var parent = this.parentElement;
|
|
while (items.indexOf(parent) === -1) {
|
|
parent = parent.parentElement;
|
|
}
|
|
parent.dragDrop();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Disable the sortable
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
*/
|
|
var _disableSortable = function (sortableElement) {
|
|
var opts = addData(sortableElement, 'opts');
|
|
var items = _filter(sortableElement.children, opts.items);
|
|
var handles = _getHandles(items, opts.handle);
|
|
addAttribute(sortableElement, 'aria-dropeffect', 'none');
|
|
addData(sortableElement, '_disabled', 'true');
|
|
addAttribute(handles, 'draggable', 'false');
|
|
removeEventListener(handles, 'mousedown');
|
|
};
|
|
/**
|
|
* Reload the sortable
|
|
* @param {HTMLElement} sortableElement a single sortable
|
|
* @description events need to be removed to not be double bound
|
|
*/
|
|
var _reloadSortable = function (sortableElement) {
|
|
var opts = addData(sortableElement, 'opts');
|
|
var items = _filter(sortableElement.children, opts.items);
|
|
var handles = _getHandles(items, opts.handle);
|
|
addData(sortableElement, '_disabled', 'false');
|
|
// remove event handlers from items
|
|
_removeItemEvents(items);
|
|
removeEventListener(handles, 'mousedown');
|
|
// remove event handlers from sortable
|
|
removeEventListener(sortableElement, 'dragover');
|
|
removeEventListener(sortableElement, 'dragenter');
|
|
removeEventListener(sortableElement, 'drop');
|
|
};
|
|
/**
|
|
* Public sortable object
|
|
* @param {Array|NodeList} sortableElements
|
|
* @param {object|string} options|method
|
|
*/
|
|
function sortable(sortableElements, options) {
|
|
// get method string to see if a method is called
|
|
var method = String(options);
|
|
options = options || {};
|
|
// check if the user provided a selector instead of an element
|
|
if (typeof sortableElements === 'string') {
|
|
sortableElements = document.querySelectorAll(sortableElements);
|
|
}
|
|
// if the user provided an element, return it in an array to keep the return value consistant
|
|
if (sortableElements instanceof HTMLElement) {
|
|
sortableElements = [sortableElements];
|
|
}
|
|
sortableElements = Array.prototype.slice.call(sortableElements);
|
|
if (/serialize/.test(method)) {
|
|
return sortableElements.map(function (sortableContainer) {
|
|
var opts = addData(sortableContainer, 'opts');
|
|
return _serialize(sortableContainer, opts.itemSerializer, opts.containerSerializer);
|
|
});
|
|
}
|
|
sortableElements.forEach(function (sortableElement) {
|
|
if (/enable|disable|destroy/.test(method)) {
|
|
return sortable[method](sortableElement);
|
|
}
|
|
// log deprecation
|
|
['connectWith', 'disableIEFix'].forEach(function (configKey) {
|
|
if (options.hasOwnProperty(configKey) && options[configKey] !== null) {
|
|
console.warn("HTML5Sortable: You are using the deprecated configuration \"" + configKey + "\". This will be removed in an upcoming version, make sure to migrate to the new options when updating.");
|
|
}
|
|
});
|
|
// merge options with default options
|
|
options = Object.assign({}, defaultConfiguration, store(sortableElement).config, options);
|
|
// init data store for sortable
|
|
store(sortableElement).config = options;
|
|
// set options on sortable
|
|
addData(sortableElement, 'opts', options);
|
|
// property to define as sortable
|
|
sortableElement.isSortable = true;
|
|
// reset sortable
|
|
_reloadSortable(sortableElement);
|
|
// initialize
|
|
var listItems = _filter(sortableElement.children, options.items);
|
|
// create element if user defined a placeholder element as a string
|
|
var customPlaceholder;
|
|
if (options.placeholder !== null && options.placeholder !== undefined) {
|
|
var tempContainer = document.createElement(sortableElement.tagName);
|
|
if (options.placeholder instanceof HTMLElement) {
|
|
tempContainer.appendChild(options.placeholder);
|
|
}
|
|
else {
|
|
tempContainer.innerHTML = options.placeholder;
|
|
}
|
|
customPlaceholder = tempContainer.children[0];
|
|
}
|
|
// add placeholder
|
|
store(sortableElement).placeholder = _makePlaceholder(sortableElement, customPlaceholder, options.placeholderClass);
|
|
addData(sortableElement, 'items', options.items);
|
|
if (options.acceptFrom) {
|
|
addData(sortableElement, 'acceptFrom', options.acceptFrom);
|
|
}
|
|
else if (options.connectWith) {
|
|
addData(sortableElement, 'connectWith', options.connectWith);
|
|
}
|
|
_enableSortable(sortableElement);
|
|
addAttribute(listItems, 'role', 'option');
|
|
addAttribute(listItems, 'aria-grabbed', 'false');
|
|
// enable hover class
|
|
enableHoverClass(sortableElement, true);
|
|
/*
|
|
Handle drag events on draggable items
|
|
Handle is set at the sortableElement level as it will bubble up
|
|
from the item
|
|
*/
|
|
addEventListener(sortableElement, 'dragstart', function (e) {
|
|
// ignore dragstart events
|
|
var target = getEventTarget(e);
|
|
if (target.isSortable === true) {
|
|
return;
|
|
}
|
|
e.stopImmediatePropagation();
|
|
if ((options.handle && !target.matches(options.handle)) || target.getAttribute('draggable') === 'false') {
|
|
return;
|
|
}
|
|
var sortableContainer = findSortable(target, e);
|
|
var dragItem = findDragElement(sortableContainer, target);
|
|
// grab values
|
|
originItemsBeforeUpdate = _filter(sortableContainer.children, options.items);
|
|
originIndex = originItemsBeforeUpdate.indexOf(dragItem);
|
|
originElementIndex = _index(dragItem, sortableContainer.children);
|
|
originContainer = sortableContainer;
|
|
// add transparent clone or other ghost to cursor
|
|
setDragImage(e, dragItem, options.customDragImage);
|
|
// cache selsection & add attr for dragging
|
|
draggingHeight = _getElementHeight(dragItem);
|
|
dragItem.classList.add(options.draggingClass);
|
|
dragging = _getDragging(dragItem, sortableContainer);
|
|
addAttribute(dragging, 'aria-grabbed', 'true');
|
|
// dispatch sortstart event on each element in group
|
|
sortableContainer.dispatchEvent(new CustomEvent('sortstart', {
|
|
detail: {
|
|
origin: {
|
|
elementIndex: originElementIndex,
|
|
index: originIndex,
|
|
container: originContainer
|
|
},
|
|
item: dragging,
|
|
originalTarget: target
|
|
}
|
|
}));
|
|
});
|
|
/*
|
|
We are capturing targetSortable before modifications with 'dragenter' event
|
|
*/
|
|
addEventListener(sortableElement, 'dragenter', function (e) {
|
|
var target = getEventTarget(e);
|
|
var sortableContainer = findSortable(target, e);
|
|
if (sortableContainer && sortableContainer !== previousContainer) {
|
|
destinationItemsBeforeUpdate = _filter(sortableContainer.children, addData(sortableContainer, 'items'))
|
|
.filter(function (item) { return item !== store(sortableElement).placeholder; });
|
|
sortableContainer.dispatchEvent(new CustomEvent('sortenter', {
|
|
detail: {
|
|
origin: {
|
|
elementIndex: originElementIndex,
|
|
index: originIndex,
|
|
container: originContainer
|
|
},
|
|
destination: {
|
|
container: sortableContainer,
|
|
itemsBeforeUpdate: destinationItemsBeforeUpdate
|
|
},
|
|
item: dragging,
|
|
originalTarget: target
|
|
}
|
|
}));
|
|
}
|
|
previousContainer = sortableContainer;
|
|
});
|
|
/*
|
|
* Dragend Event - https://developer.mozilla.org/en-US/docs/Web/Events/dragend
|
|
* Fires each time dragEvent end, or ESC pressed
|
|
* We are using it to clean up any draggable elements and placeholders
|
|
*/
|
|
addEventListener(sortableElement, 'dragend', function (e) {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
dragging.classList.remove(options.draggingClass);
|
|
addAttribute(dragging, 'aria-grabbed', 'false');
|
|
if (dragging.getAttribute('aria-copied') === 'true' && addData(dragging, 'dropped') !== 'true') {
|
|
dragging.remove();
|
|
}
|
|
dragging.style.display = dragging.oldDisplay;
|
|
delete dragging.oldDisplay;
|
|
var visiblePlaceholder = Array.from(stores.values()).map(function (data) { return data.placeholder; })
|
|
.filter(function (placeholder) { return placeholder instanceof HTMLElement; })
|
|
.filter(isInDom)[0];
|
|
if (visiblePlaceholder) {
|
|
visiblePlaceholder.remove();
|
|
}
|
|
// dispatch sortstart event on each element in group
|
|
sortableElement.dispatchEvent(new CustomEvent('sortstop', {
|
|
detail: {
|
|
origin: {
|
|
elementIndex: originElementIndex,
|
|
index: originIndex,
|
|
container: originContainer
|
|
},
|
|
item: dragging
|
|
}
|
|
}));
|
|
previousContainer = null;
|
|
dragging = null;
|
|
draggingHeight = null;
|
|
});
|
|
/*
|
|
* Drop Event - https://developer.mozilla.org/en-US/docs/Web/Events/drop
|
|
* Fires when valid drop target area is hit
|
|
*/
|
|
addEventListener(sortableElement, 'drop', function (e) {
|
|
if (!_listsConnected(sortableElement, dragging.parentElement)) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
addData(dragging, 'dropped', 'true');
|
|
// get the one placeholder that is currently visible
|
|
var visiblePlaceholder = Array.from(stores.values()).map(function (data) {
|
|
return data.placeholder;
|
|
})
|
|
// filter only HTMLElements
|
|
.filter(function (placeholder) { return placeholder instanceof HTMLElement; })
|
|
// filter only elements in DOM
|
|
.filter(isInDom)[0];
|
|
// attach element after placeholder
|
|
insertAfter(visiblePlaceholder, dragging);
|
|
// remove placeholder from dom
|
|
visiblePlaceholder.remove();
|
|
/*
|
|
* Fires Custom Event - 'sortstop'
|
|
*/
|
|
sortableElement.dispatchEvent(new CustomEvent('sortstop', {
|
|
detail: {
|
|
origin: {
|
|
elementIndex: originElementIndex,
|
|
index: originIndex,
|
|
container: originContainer
|
|
},
|
|
item: dragging
|
|
}
|
|
}));
|
|
var placeholder = store(sortableElement).placeholder;
|
|
var originItems = _filter(originContainer.children, options.items)
|
|
.filter(function (item) { return item !== placeholder; });
|
|
var destinationContainer = this.isSortable === true ? this : this.parentElement;
|
|
var destinationItems = _filter(destinationContainer.children, addData(destinationContainer, 'items'))
|
|
.filter(function (item) { return item !== placeholder; });
|
|
var destinationElementIndex = _index(dragging, Array.from(dragging.parentElement.children)
|
|
.filter(function (item) { return item !== placeholder; }));
|
|
var destinationIndex = _index(dragging, destinationItems);
|
|
/*
|
|
* When a list item changed container lists or index within a list
|
|
* Fires Custom Event - 'sortupdate'
|
|
*/
|
|
if (originElementIndex !== destinationElementIndex || originContainer !== destinationContainer) {
|
|
sortableElement.dispatchEvent(new CustomEvent('sortupdate', {
|
|
detail: {
|
|
origin: {
|
|
elementIndex: originElementIndex,
|
|
index: originIndex,
|
|
container: originContainer,
|
|
itemsBeforeUpdate: originItemsBeforeUpdate,
|
|
items: originItems
|
|
},
|
|
destination: {
|
|
index: destinationIndex,
|
|
elementIndex: destinationElementIndex,
|
|
container: destinationContainer,
|
|
itemsBeforeUpdate: destinationItemsBeforeUpdate,
|
|
items: destinationItems
|
|
},
|
|
item: dragging
|
|
}
|
|
}));
|
|
}
|
|
});
|
|
var debouncedDragOverEnter = _debounce(function (sortableElement, element, pageY) {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
// set placeholder height if forcePlaceholderSize option is set
|
|
if (options.forcePlaceholderSize) {
|
|
store(sortableElement).placeholder.style.height = draggingHeight + 'px';
|
|
}
|
|
// if element the draggedItem is dragged onto is within the array of all elements in list
|
|
// (not only items, but also disabled, etc.)
|
|
if (Array.from(sortableElement.children).indexOf(element) > -1) {
|
|
var thisHeight = _getElementHeight(element);
|
|
var placeholderIndex = _index(store(sortableElement).placeholder, element.parentElement.children);
|
|
var thisIndex = _index(element, element.parentElement.children);
|
|
// Check if `element` is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering
|
|
if (thisHeight > draggingHeight) {
|
|
// Dead zone?
|
|
var deadZone = thisHeight - draggingHeight;
|
|
var offsetTop = _offset(element).top;
|
|
if (placeholderIndex < thisIndex && pageY < offsetTop) {
|
|
return;
|
|
}
|
|
if (placeholderIndex > thisIndex &&
|
|
pageY > offsetTop + thisHeight - deadZone) {
|
|
return;
|
|
}
|
|
}
|
|
if (dragging.oldDisplay === undefined) {
|
|
dragging.oldDisplay = dragging.style.display;
|
|
}
|
|
if (dragging.style.display !== 'none') {
|
|
dragging.style.display = 'none';
|
|
}
|
|
// To avoid flicker, determine where to position the placeholder
|
|
// based on where the mouse pointer is relative to the elements
|
|
// vertical center.
|
|
var placeAfter = false;
|
|
try {
|
|
var elementMiddle = _offset(element).top + element.offsetHeight / 2;
|
|
placeAfter = pageY >= elementMiddle;
|
|
}
|
|
catch (e) {
|
|
placeAfter = placeholderIndex < thisIndex;
|
|
}
|
|
if (placeAfter) {
|
|
insertAfter(element, store(sortableElement).placeholder);
|
|
}
|
|
else {
|
|
insertBefore(element, store(sortableElement).placeholder);
|
|
}
|
|
// get placeholders from all stores & remove all but current one
|
|
Array.from(stores.values())
|
|
// remove empty values
|
|
.filter(function (data) { return data.placeholder !== undefined; })
|
|
// foreach placeholder in array if outside of current sorableContainer -> remove from DOM
|
|
.forEach(function (data) {
|
|
if (data.placeholder !== store(sortableElement).placeholder) {
|
|
data.placeholder.remove();
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
// get all placeholders from store
|
|
var placeholders = Array.from(stores.values())
|
|
.filter(function (data) { return data.placeholder !== undefined; })
|
|
.map(function (data) {
|
|
return data.placeholder;
|
|
});
|
|
// check if element is not in placeholders
|
|
if (placeholders.indexOf(element) === -1 && sortableElement === element && !_filter(element.children, options.items).length) {
|
|
placeholders.forEach(function (element) { return element.remove(); });
|
|
element.appendChild(store(sortableElement).placeholder);
|
|
}
|
|
}
|
|
}, options.debounce);
|
|
// Handle dragover and dragenter events on draggable items
|
|
var onDragOverEnter = function (e) {
|
|
var element = e.target;
|
|
var sortableElement = element.isSortable === true ? element : findSortable(element, e);
|
|
element = findDragElement(sortableElement, element);
|
|
if (!dragging || !_listsConnected(sortableElement, dragging.parentElement) || addData(sortableElement, '_disabled') === 'true') {
|
|
return;
|
|
}
|
|
var options = addData(sortableElement, 'opts');
|
|
if (parseInt(options.maxItems) && _filter(sortableElement.children, addData(sortableElement, 'items')).length >= parseInt(options.maxItems) && dragging.parentElement !== sortableElement) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.dataTransfer.dropEffect = store(sortableElement).getConfig('copy') === true ? 'copy' : 'move';
|
|
debouncedDragOverEnter(sortableElement, element, e.pageY);
|
|
};
|
|
addEventListener(listItems.concat(sortableElement), 'dragover', onDragOverEnter);
|
|
addEventListener(listItems.concat(sortableElement), 'dragenter', onDragOverEnter);
|
|
});
|
|
return sortableElements;
|
|
}
|
|
sortable.destroy = function (sortableElement) {
|
|
_destroySortable(sortableElement);
|
|
};
|
|
sortable.enable = function (sortableElement) {
|
|
_enableSortable(sortableElement);
|
|
};
|
|
sortable.disable = function (sortableElement) {
|
|
_disableSortable(sortableElement);
|
|
};
|
|
/* START.TESTS_ONLY */
|
|
sortable.__testing = {
|
|
// add internal methods here for testing purposes
|
|
_data: addData,
|
|
_removeItemEvents: _removeItemEvents,
|
|
_removeItemData: _removeItemData,
|
|
_removeSortableData: _removeSortableData
|
|
};
|
|
|
|
return sortable;
|
|
|
|
}());
|