/* * HTML5Sortable package * https://github.com/lukasoppermann/html5sortable * * Maintained by Lukas Oppermann * * 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} 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} 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 = ''; } 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} 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} 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; }());