import { toggleClass, getRect, index, closest, on, off, clone, css, setRect, unsetRect, matrix, expando } from '../../src/utils.js'; import dispatchEvent from '../../src/EventDispatcher.js'; let multiDragElements = [], multiDragClones = [], lastMultiDragSelect, // for selection with modifier key down (SHIFT) multiDragSortable, initialFolding = false, // Initial multi-drag fold when drag started folding = false, // Folding any other time dragStarted = false, dragEl, clonesFromRect, clonesHidden; function MultiDragPlugin() { function MultiDrag(sortable) { // Bind all private methods for (let fn in this) { if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { this[fn] = this[fn].bind(this); } } if (sortable.options.supportPointer) { on(document, 'pointerup', this._deselectMultiDrag); } else { on(document, 'mouseup', this._deselectMultiDrag); on(document, 'touchend', this._deselectMultiDrag); } on(document, 'keydown', this._checkKeyDown); on(document, 'keyup', this._checkKeyUp); this.defaults = { selectedClass: 'sortable-selected', multiDragKey: null, setData(dataTransfer, dragEl) { let data = ''; if (multiDragElements.length && multiDragSortable === sortable) { multiDragElements.forEach((multiDragElement, i) => { data += (!i ? '' : ', ') + multiDragElement.textContent; }); } else { data = dragEl.textContent; } dataTransfer.setData('Text', data); } }; } MultiDrag.prototype = { multiDragKeyDown: false, isMultiDrag: false, delayStartGlobal({ dragEl: dragged }) { dragEl = dragged; }, delayEnded() { this.isMultiDrag = ~multiDragElements.indexOf(dragEl); }, setupClone({ sortable, cancel }) { if (!this.isMultiDrag) return; for (let i = 0; i < multiDragElements.length; i++) { multiDragClones.push(clone(multiDragElements[i])); multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex; multiDragClones[i].draggable = false; multiDragClones[i].style['will-change'] = ''; toggleClass(multiDragClones[i], this.options.selectedClass, false); multiDragElements[i] === dragEl && toggleClass(multiDragClones[i], this.options.chosenClass, false); } sortable._hideClone(); cancel(); }, clone({ sortable, rootEl, dispatchSortableEvent, cancel }) { if (!this.isMultiDrag) return; if (!this.options.removeCloneOnHide) { if (multiDragElements.length && multiDragSortable === sortable) { insertMultiDragClones(true, rootEl); dispatchSortableEvent('clone'); cancel(); } } }, showClone({ cloneNowShown, rootEl, cancel }) { if (!this.isMultiDrag) return; insertMultiDragClones(false, rootEl); multiDragClones.forEach(clone => { css(clone, 'display', ''); }); cloneNowShown(); clonesHidden = false; cancel(); }, hideClone({ sortable, cloneNowHidden, cancel }) { if (!this.isMultiDrag) return; multiDragClones.forEach(clone => { css(clone, 'display', 'none'); if (this.options.removeCloneOnHide && clone.parentNode) { clone.parentNode.removeChild(clone); } }); cloneNowHidden(); clonesHidden = true; cancel(); }, dragStartGlobal({ sortable }) { if (!this.isMultiDrag && multiDragSortable) { multiDragSortable.multiDrag._deselectMultiDrag(); } multiDragElements.forEach(multiDragElement => { multiDragElement.sortableIndex = index(multiDragElement); }); // Sort multi-drag elements multiDragElements = multiDragElements.sort(function(a, b) { return a.sortableIndex - b.sortableIndex; }); dragStarted = true; }, dragStarted({ sortable }) { if (!this.isMultiDrag) return; if (this.options.sort) { // Capture rects, // hide multi drag elements (by positioning them absolute), // set multi drag elements rects to dragRect, // show multi drag elements, // animate to rects, // unset rects & remove from DOM sortable.captureAnimationState(); if (this.options.animation) { multiDragElements.forEach(multiDragElement => { if (multiDragElement === dragEl) return; css(multiDragElement, 'position', 'absolute'); }); let dragRect = getRect(dragEl, false, true, true); multiDragElements.forEach(multiDragElement => { if (multiDragElement === dragEl) return; setRect(multiDragElement, dragRect); }); folding = true; initialFolding = true; } } sortable.animateAll(() => { folding = false; initialFolding = false; if (this.options.animation) { multiDragElements.forEach(multiDragElement => { unsetRect(multiDragElement); }); } // Remove all auxiliary multidrag items from el, if sorting enabled if (this.options.sort) { removeMultiDragElements(); } }); }, dragOver({ target, completed, cancel }) { if (folding && ~multiDragElements.indexOf(target)) { completed(false); cancel(); } }, revert({ fromSortable, rootEl, sortable, dragRect }) { if (multiDragElements.length > 1) { // Setup unfold animation multiDragElements.forEach(multiDragElement => { sortable.addAnimationState({ target: multiDragElement, rect: folding ? getRect(multiDragElement) : dragRect }); unsetRect(multiDragElement); multiDragElement.fromRect = dragRect; fromSortable.removeAnimationState(multiDragElement); }); folding = false; insertMultiDragElements(!this.options.removeCloneOnHide, rootEl); } }, dragOverCompleted({ sortable, isOwner, insertion, activeSortable, parentEl, putSortable }) { let options = this.options; if (insertion) { // Clones must be hidden before folding animation to capture dragRectAbsolute properly if (isOwner) { activeSortable._hideClone(); } initialFolding = false; // If leaving sort:false root, or already folding - Fold to new location if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) { // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible let dragRectAbsolute = getRect(dragEl, false, true, true); multiDragElements.forEach(multiDragElement => { if (multiDragElement === dragEl) return; setRect(multiDragElement, dragRectAbsolute); // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted // while folding, and so that we can capture them again because old sortable will no longer be fromSortable parentEl.appendChild(multiDragElement); }); folding = true; } // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out if (!isOwner) { // Only remove if not folding (folding will remove them anyways) if (!folding) { removeMultiDragElements(); } if (multiDragElements.length > 1) { let clonesHiddenBefore = clonesHidden; activeSortable._showClone(sortable); // Unfold animation for clones if showing from hidden if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) { multiDragClones.forEach(clone => { activeSortable.addAnimationState({ target: clone, rect: clonesFromRect }); clone.fromRect = clonesFromRect; clone.thisAnimationDuration = null; }); } } else { activeSortable._showClone(sortable); } } } }, dragOverAnimationCapture({ dragRect, isOwner, activeSortable }) { multiDragElements.forEach(multiDragElement => { multiDragElement.thisAnimationDuration = null; }); if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) { clonesFromRect = Object.assign({}, dragRect); let dragMatrix = matrix(dragEl, true); clonesFromRect.top -= dragMatrix.f; clonesFromRect.left -= dragMatrix.e; } }, dragOverAnimationComplete() { if (folding) { folding = false; removeMultiDragElements(); } }, drop({ originalEvent: evt, rootEl, parentEl, sortable, dispatchSortableEvent, oldIndex, putSortable }) { let toSortable = (putSortable || this.sortable); if (!evt) return; let options = this.options, children = parentEl.children; // Multi-drag selection if (!dragStarted) { if (options.multiDragKey && !this.multiDragKeyDown) { this._deselectMultiDrag(); } toggleClass(dragEl, options.selectedClass, !~multiDragElements.indexOf(dragEl)); if (!~multiDragElements.indexOf(dragEl)) { multiDragElements.push(dragEl); dispatchEvent({ sortable, rootEl, name: 'select', targetEl: dragEl, originalEvt: evt }); // Modifier activated, select from last to dragEl if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) { let lastIndex = index(lastMultiDragSelect), currentIndex = index(dragEl); if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) { // Must include lastMultiDragSelect (select it), in case modified selection from no selection // (but previous selection existed) let n, i; if (currentIndex > lastIndex) { i = lastIndex; n = currentIndex; } else { i = currentIndex; n = lastIndex + 1; } for (; i < n; i++) { if (~multiDragElements.indexOf(children[i])) continue; toggleClass(children[i], options.selectedClass, true); multiDragElements.push(children[i]); dispatchEvent({ sortable, rootEl, name: 'select', targetEl: children[i], originalEvt: evt }); } } } else { lastMultiDragSelect = dragEl; } multiDragSortable = toSortable; } else { multiDragElements.splice(multiDragElements.indexOf(dragEl), 1); lastMultiDragSelect = null; dispatchEvent({ sortable, rootEl, name: 'deselect', targetEl: dragEl, originalEvt: evt }); } } // Multi-drag drop if (dragStarted && this.isMultiDrag) { folding = false; // Do not "unfold" after around dragEl if reverted if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) { let dragRect = getRect(dragEl), multiDragIndex = index(dragEl, ':not(.' + this.options.selectedClass + ')'); if (!initialFolding && options.animation) dragEl.thisAnimationDuration = null; toSortable.captureAnimationState(); if (!initialFolding) { if (options.animation) { dragEl.fromRect = dragRect; multiDragElements.forEach(multiDragElement => { multiDragElement.thisAnimationDuration = null; if (multiDragElement !== dragEl) { let rect = folding ? getRect(multiDragElement) : dragRect; multiDragElement.fromRect = rect; // Prepare unfold animation toSortable.addAnimationState({ target: multiDragElement, rect: rect }); } }); } // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert // properly they must all be removed removeMultiDragElements(); multiDragElements.forEach(multiDragElement => { if (children[multiDragIndex]) { parentEl.insertBefore(multiDragElement, children[multiDragIndex]); } else { parentEl.appendChild(multiDragElement); } multiDragIndex++; }); // If initial folding is done, the elements may have changed position because they are now // unfolding around dragEl, even though dragEl may not have his index changed, so update event // must be fired here as Sortable will not. if (oldIndex === index(dragEl)) { let update = false; multiDragElements.forEach(multiDragElement => { if (multiDragElement.sortableIndex !== index(multiDragElement)) { update = true; return; } }); if (update) { dispatchSortableEvent('update'); } } } // Must be done after capturing individual rects (scroll bar) multiDragElements.forEach(multiDragElement => { unsetRect(multiDragElement); }); toSortable.animateAll(); } multiDragSortable = toSortable; } // Remove clones if necessary if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) { multiDragClones.forEach(clone => { clone.parentNode && clone.parentNode.removeChild(clone); }); } }, nullingGlobal() { this.isMultiDrag = dragStarted = false; multiDragClones.length = 0; }, destroyGlobal() { this._deselectMultiDrag(); off(document, 'pointerup', this._deselectMultiDrag); off(document, 'mouseup', this._deselectMultiDrag); off(document, 'touchend', this._deselectMultiDrag); off(document, 'keydown', this._checkKeyDown); off(document, 'keyup', this._checkKeyUp); }, _deselectMultiDrag(evt) { if (typeof dragStarted !== "undefined" && dragStarted) return; // Only deselect if selection is in this sortable if (multiDragSortable !== this.sortable) return; // Only deselect if target is not item in this sortable if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return; // Only deselect if left click if (evt && evt.button !== 0) return; while (multiDragElements.length) { let el = multiDragElements[0]; toggleClass(el, this.options.selectedClass, false); multiDragElements.shift(); dispatchEvent({ sortable: this.sortable, rootEl: this.sortable.el, name: 'deselect', targetEl: el, originalEvt: evt }); } }, _checkKeyDown(evt) { if (evt.key === this.options.multiDragKey) { this.multiDragKeyDown = true; } }, _checkKeyUp(evt) { if (evt.key === this.options.multiDragKey) { this.multiDragKeyDown = false; } } }; return Object.assign(MultiDrag, { // Static methods & properties pluginName: 'multiDrag', utils: { /** * Selects the provided multi-drag item * @param {HTMLElement} el The element to be selected */ select(el) { let sortable = el.parentNode[expando]; if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return; if (multiDragSortable && multiDragSortable !== sortable) { multiDragSortable.multiDrag._deselectMultiDrag(); multiDragSortable = sortable; } toggleClass(el, sortable.options.selectedClass, true); multiDragElements.push(el); }, /** * Deselects the provided multi-drag item * @param {HTMLElement} el The element to be deselected */ deselect(el) { let sortable = el.parentNode[expando], index = multiDragElements.indexOf(el); if (!sortable || !sortable.options.multiDrag || !~index) return; toggleClass(el, sortable.options.selectedClass, false); multiDragElements.splice(index, 1); } }, eventProperties() { const oldIndicies = [], newIndicies = []; multiDragElements.forEach(multiDragElement => { oldIndicies.push({ multiDragElement, index: multiDragElement.sortableIndex }); // multiDragElements will already be sorted if folding let newIndex; if (folding && multiDragElement !== dragEl) { newIndex = -1; } else if (folding) { newIndex = index(multiDragElement, ':not(.' + this.options.selectedClass + ')'); } else { newIndex = index(multiDragElement); } newIndicies.push({ multiDragElement, index: newIndex }); }); return { items: [...multiDragElements], clones: [...multiDragClones], oldIndicies, newIndicies }; }, optionListeners: { multiDragKey(key) { key = key.toLowerCase(); if (key === 'ctrl') { key = 'Control'; } else if (key.length > 1) { key = key.charAt(0).toUpperCase() + key.substr(1); } return key; } } }); } function insertMultiDragElements(clonesInserted, rootEl) { multiDragElements.forEach((multiDragElement, i) => { let target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)]; if (target) { rootEl.insertBefore(multiDragElement, target); } else { rootEl.appendChild(multiDragElement); } }); } /** * Insert multi-drag clones * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted * @param {HTMLElement} rootEl */ function insertMultiDragClones(elementsInserted, rootEl) { multiDragClones.forEach((clone, i) => { let target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)]; if (target) { rootEl.insertBefore(clone, target); } else { rootEl.appendChild(clone); } }); } function removeMultiDragElements() { multiDragElements.forEach(multiDragElement => { if (multiDragElement === dragEl) return; multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement); }); } export default MultiDragPlugin;