UNPKG

svelte-dnd-action

Version:

*An awesome drag and drop library for Svelte 3 and 4 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *

680 lines (636 loc) 29.5 kB
import { decrementActiveDropZoneCount, incrementActiveDropZoneCount, ITEM_ID_KEY, printDebug, SHADOW_ELEMENT_ATTRIBUTE_NAME, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID, SOURCES, TRIGGERS } from "./constants"; import {observe, unobserve} from "./helpers/observer"; import {createMultiScroller} from "./helpers/multiScroller"; import { createDraggedElementFrom, decorateShadowEl, hideElement, morphDraggedElementToBeLike, moveDraggedElementToWasDroppedState, preventShrinking, styleActiveDropZones, styleDraggable, styleInactiveDropZones, unDecorateShadowElement } from "./helpers/styler"; import { dispatchConsiderEvent, dispatchFinalizeEvent, DRAGGED_ENTERED_EVENT_NAME, DRAGGED_LEFT_DOCUMENT_EVENT_NAME, DRAGGED_LEFT_EVENT_NAME, DRAGGED_LEFT_TYPES, DRAGGED_OVER_INDEX_EVENT_NAME } from "./helpers/dispatcher"; import {areArraysShallowEqualSameOrder, areObjectsShallowEqual, toString} from "./helpers/util"; import {getBoundingRectNoTransforms, findCenterOfElement} from "./helpers/intersection"; const DEFAULT_DROP_ZONE_TYPE = "--any--"; const MIN_OBSERVATION_INTERVAL_MS = 100; const DISABLED_OBSERVATION_INTERVAL_MS = 20; const MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3; const DEFAULT_TOUCH_DELAY_MS = 80; const DEFAULT_DROP_TARGET_STYLE = { outline: "rgba(255, 255, 102, 0.7) solid 2px" }; const ORIGINAL_DRAGGED_ITEM_MARKER_ATTRIBUTE = "data-is-dnd-original-dragged-item"; let originalDragTarget; let draggedEl; let draggedElData; let draggedElType; let originDropZone; let originIndex; let shadowElData; let shadowElDropZone; let dragStartMousePosition; let currentMousePosition; let isWorkingOnPreviousDrag = false; let finalizingPreviousDrag = false; let unlockOriginDzMinDimensions; let isDraggedOutsideOfAnyDz = false; let scheduledForRemovalAfterDrop = []; let multiScroller; let touchDragHoldTimer; let touchHoldElapsed = false; let useCursorForDetectionActive = false; // a map from type to a set of drop-zones const typeToDropZones = new Map(); // important - this is needed because otherwise the config that would be used for everyone is the config of the element that created the event listeners const dzToConfig = new Map(); // this is needed in order to be able to cleanup old listeners and avoid stale closures issues (as the listener is defined within each zone) const elToMouseDownListener = new WeakMap(); /* drop-zones registration management */ function registerDropZone(dropZoneEl, type) { printDebug(() => "registering drop-zone if absent"); if (!typeToDropZones.has(type)) { typeToDropZones.set(type, new Set()); } if (!typeToDropZones.get(type).has(dropZoneEl)) { typeToDropZones.get(type).add(dropZoneEl); incrementActiveDropZoneCount(); } } function unregisterDropZone(dropZoneEl, type) { typeToDropZones.get(type).delete(dropZoneEl); decrementActiveDropZoneCount(); if (typeToDropZones.get(type).size === 0) { typeToDropZones.delete(type); } } /* functions to manage observing the dragged element and trigger custom drag-events */ function watchDraggedElement() { printDebug(() => "watching dragged element"); const dropZones = typeToDropZones.get(draggedElType); for (const dz of dropZones) { dz.addEventListener(DRAGGED_ENTERED_EVENT_NAME, handleDraggedEntered); dz.addEventListener(DRAGGED_LEFT_EVENT_NAME, handleDraggedLeft); dz.addEventListener(DRAGGED_OVER_INDEX_EVENT_NAME, handleDraggedIsOverIndex); } window.addEventListener(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, handleDrop); // it is important that we don't have an interval that is faster than the flip duration because it can cause elements to jump bach and forth const setIntervalMs = Math.max(...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs)); const observationIntervalMs = setIntervalMs === 0 ? DISABLED_OBSERVATION_INTERVAL_MS : Math.max(setIntervalMs, MIN_OBSERVATION_INTERVAL_MS); // if setIntervalMs is 0 it goes to 20, otherwise it is max between it and min observation. multiScroller = createMultiScroller(dropZones, () => currentMousePosition); // Returns reference point in document coordinates - either cursor position or element center const getReferencePoint = useCursorForDetectionActive ? () => ({ x: currentMousePosition.x + window.scrollX, y: currentMousePosition.y + window.scrollY }) : () => findCenterOfElement(draggedEl); observe(draggedEl, dropZones, observationIntervalMs * 1.07, multiScroller, getReferencePoint); } function unWatchDraggedElement() { printDebug(() => "unwatching dragged element"); const dropZones = typeToDropZones.get(draggedElType); for (const dz of dropZones) { dz.removeEventListener(DRAGGED_ENTERED_EVENT_NAME, handleDraggedEntered); dz.removeEventListener(DRAGGED_LEFT_EVENT_NAME, handleDraggedLeft); dz.removeEventListener(DRAGGED_OVER_INDEX_EVENT_NAME, handleDraggedIsOverIndex); } window.removeEventListener(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, handleDrop); // ensuring multiScroller is not already destroyed before destroying if (multiScroller) { multiScroller.destroy(); multiScroller = undefined; } unobserve(); } function findShadowElementIdx(items) { return items.findIndex(item => !!item[SHADOW_ITEM_MARKER_PROPERTY_NAME]); } function createShadowElData(draggedElData) { return {...draggedElData, [SHADOW_ITEM_MARKER_PROPERTY_NAME]: true, [ITEM_ID_KEY]: SHADOW_PLACEHOLDER_ITEM_ID}; } /* custom drag-events handlers */ function handleDraggedEntered(e) { printDebug(() => ["dragged entered", e.currentTarget, e.detail]); let {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { printDebug(() => "ignoring dragged entered because drop is currently disabled"); return; } isDraggedOutsideOfAnyDz = false; // this deals with another race condition. on some occasions (super rapid operations) the list hasn't updated yet items = items.filter(item => item[ITEM_ID_KEY] !== shadowElData[ITEM_ID_KEY] && item[ITEM_ID_KEY] !== SHADOW_PLACEHOLDER_ITEM_ID); printDebug(() => `dragged entered items ${toString(items)}`); if (originDropZone !== e.currentTarget) { const originZoneItems = dzToConfig.get(originDropZone).items; const newOriginZoneItems = originZoneItems.filter(item => !item[SHADOW_ITEM_MARKER_PROPERTY_NAME]); dispatchConsiderEvent(originDropZone, newOriginZoneItems, { trigger: TRIGGERS.DRAGGED_ENTERED_ANOTHER, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER }); } const {index: shadowElIdx} = e.detail.indexObj; shadowElDropZone = e.currentTarget; items.splice(shadowElIdx, 0, shadowElData); dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_ENTERED, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); } function handleDraggedLeft(e) { // dealing with a rare race condition on extremely rapid clicking and dropping if (!isWorkingOnPreviousDrag) return; printDebug(() => ["dragged left", e.currentTarget, e.detail]); const {items: originalItems, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); if (dropFromOthersDisabled && e.currentTarget !== originDropZone && e.currentTarget !== shadowElDropZone) { printDebug(() => "drop is currently disabled"); return; } const items = [...originalItems]; const shadowElIdx = findShadowElementIdx(items); if (shadowElIdx !== -1) { items.splice(shadowElIdx, 1); } const origShadowDz = shadowElDropZone; shadowElDropZone = undefined; const {type, theOtherDz} = e.detail; if ( type === DRAGGED_LEFT_TYPES.OUTSIDE_OF_ANY || (type === DRAGGED_LEFT_TYPES.LEFT_FOR_ANOTHER && theOtherDz !== originDropZone && dzToConfig.get(theOtherDz).dropFromOthersDisabled) ) { printDebug(() => "dragged left all, putting shadow element back in the origin dz"); isDraggedOutsideOfAnyDz = true; shadowElDropZone = originDropZone; // if the last zone it left is the origin dz, we will put it back into items (which we just removed it from) const originZoneItems = origShadowDz === originDropZone ? items : [...dzToConfig.get(originDropZone).items]; originZoneItems.splice(originIndex, 0, shadowElData); dispatchConsiderEvent(originDropZone, originZoneItems, { trigger: TRIGGERS.DRAGGED_LEFT_ALL, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER }); } // for the origin dz, when the dragged is outside of any, this will be fired in addition to the previous. this is for simplicity dispatchConsiderEvent(e.currentTarget, items, { trigger: TRIGGERS.DRAGGED_LEFT, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER }); } function handleDraggedIsOverIndex(e) { printDebug(() => ["dragged is over index", e.currentTarget, e.detail]); const {items: originalItems, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { printDebug(() => "drop is currently disabled"); return; } const items = [...originalItems]; isDraggedOutsideOfAnyDz = false; const {index} = e.detail.indexObj; const shadowElIdx = findShadowElementIdx(items); if (shadowElIdx !== -1) { items.splice(shadowElIdx, 1); } items.splice(index, 0, shadowElData); dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_OVER_INDEX, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); } // Global mouse/touch-events handlers function handleMouseMove(e) { e.preventDefault(); const c = e.touches ? e.touches[0] : e; currentMousePosition = {x: c.clientX, y: c.clientY}; draggedEl.style.transform = `translate3d(${currentMousePosition.x - dragStartMousePosition.x}px, ${ currentMousePosition.y - dragStartMousePosition.y }px, 0)`; } function handleDrop() { printDebug(() => "dropped"); finalizingPreviousDrag = true; // cleanup window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("touchmove", handleMouseMove); window.removeEventListener("mouseup", handleDrop); window.removeEventListener("touchend", handleDrop); unWatchDraggedElement(); moveDraggedElementToWasDroppedState(draggedEl); if (!shadowElDropZone) { printDebug(() => "element was dropped right after it left origin but before entering somewhere else"); shadowElDropZone = originDropZone; } printDebug(() => ["dropped in dz", shadowElDropZone]); let {items, type} = dzToConfig.get(shadowElDropZone); styleInactiveDropZones( typeToDropZones.get(type), dz => dzToConfig.get(dz).dropTargetStyle, dz => dzToConfig.get(dz).dropTargetClasses ); let shadowElIdx = findShadowElementIdx(items); // the handler might remove the shadow element, ex: dragula like copy on drag if (shadowElIdx === -1) { if (shadowElDropZone === originDropZone) { shadowElIdx = originIndex; } } items = items.map(item => (item[SHADOW_ITEM_MARKER_PROPERTY_NAME] ? draggedElData : item)); function finalizeWithinZone() { unlockOriginDzMinDimensions(); dispatchFinalizeEvent(shadowElDropZone, items, { trigger: isDraggedOutsideOfAnyDz ? TRIGGERS.DROPPED_OUTSIDE_OF_ANY : TRIGGERS.DROPPED_INTO_ZONE, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER }); if (shadowElDropZone !== originDropZone) { // letting the origin drop zone know the element was permanently taken away dispatchFinalizeEvent(originDropZone, dzToConfig.get(originDropZone).items, { trigger: TRIGGERS.DROPPED_INTO_ANOTHER, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER }); } // In edge cases the dom might have not been updated yet so we can't rely on data list index const domShadowEl = Array.from(shadowElDropZone.children).find(c => c.getAttribute(SHADOW_ELEMENT_ATTRIBUTE_NAME)); if (domShadowEl) unDecorateShadowElement(domShadowEl); cleanupPostDrop(); } if (dzToConfig.get(shadowElDropZone).dropAnimationDisabled) { finalizeWithinZone(); } else { animateDraggedToFinalPosition(shadowElIdx, finalizeWithinZone); } } // helper function for handleDrop function animateDraggedToFinalPosition(shadowElIdx, callback) { const shadowElRect = shadowElIdx > -1 ? getBoundingRectNoTransforms(shadowElDropZone.children[shadowElIdx], false) : getBoundingRectNoTransforms(shadowElDropZone, false); const newTransform = { x: shadowElRect.left - parseFloat(draggedEl.style.left), y: shadowElRect.top - parseFloat(draggedEl.style.top) }; const {dropAnimationDurationMs} = dzToConfig.get(shadowElDropZone); const transition = `transform ${dropAnimationDurationMs}ms ease`; draggedEl.style.transition = draggedEl.style.transition ? draggedEl.style.transition + "," + transition : transition; draggedEl.style.transform = `translate3d(${newTransform.x}px, ${newTransform.y}px, 0)`; window.setTimeout(callback, dropAnimationDurationMs); } function scheduleDZForRemovalAfterDrop(dz, destroy) { scheduledForRemovalAfterDrop.push({dz, destroy}); window.requestAnimationFrame(() => { hideElement(dz); document.body.appendChild(dz); }); } /* cleanup */ function cleanupPostDrop() { // Remove the temporary elements that were kept in the DOM during the drag if (draggedEl && draggedEl.remove) { draggedEl.remove(); } if (originalDragTarget && originalDragTarget.remove) { originalDragTarget.remove(); } draggedEl = undefined; originalDragTarget = undefined; draggedElData = undefined; draggedElType = undefined; originDropZone = undefined; originIndex = undefined; shadowElData = undefined; shadowElDropZone = undefined; dragStartMousePosition = undefined; currentMousePosition = undefined; isWorkingOnPreviousDrag = false; finalizingPreviousDrag = false; unlockOriginDzMinDimensions = undefined; isDraggedOutsideOfAnyDz = false; if (touchDragHoldTimer) { clearTimeout(touchDragHoldTimer); } touchDragHoldTimer = undefined; touchHoldElapsed = false; useCursorForDetectionActive = false; if (scheduledForRemovalAfterDrop.length) { printDebug(() => ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop]); scheduledForRemovalAfterDrop.forEach(({dz, destroy}) => { destroy(); dz.remove(); }); scheduledForRemovalAfterDrop = []; } } export function dndzone(node, options) { let initialized = false; const config = { items: undefined, type: undefined, flipDurationMs: 0, dragDisabled: false, morphDisabled: false, dropFromOthersDisabled: false, dropTargetStyle: DEFAULT_DROP_TARGET_STYLE, dropTargetClasses: [], transformDraggedElement: () => {}, centreDraggedOnCursor: false, useCursorForDetection: false, dropAnimationDisabled: false, delayTouchStartMs: 0 }; printDebug(() => [`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}]); let elToIdx = new Map(); function addMaybeListeners() { window.addEventListener("mousemove", handleMouseMoveMaybeDragStart, {passive: false}); window.addEventListener("touchmove", handleMouseMoveMaybeDragStart, {passive: false, capture: false}); window.addEventListener("mouseup", handleFalseAlarm, {passive: false}); window.addEventListener("touchend", handleFalseAlarm, {passive: false}); } function removeMaybeListeners() { window.removeEventListener("mousemove", handleMouseMoveMaybeDragStart); window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart); window.removeEventListener("mouseup", handleFalseAlarm); window.removeEventListener("touchend", handleFalseAlarm); if (touchDragHoldTimer) { clearTimeout(touchDragHoldTimer); touchDragHoldTimer = undefined; touchHoldElapsed = false; } } function handleFalseAlarm(e) { removeMaybeListeners(); originalDragTarget = undefined; dragStartMousePosition = undefined; currentMousePosition = undefined; // dragging initiated by touch events prevents onclick from initially firing if (e.type === "touchend") { const clickEvent = new Event("click", { bubbles: true, cancelable: true }); // doing it this way instead of calling .click() because that doesn't work for SVG elements e.target.dispatchEvent(clickEvent); } } function handleMouseMoveMaybeDragStart(e) { const isTouch = !!e.touches; const c = isTouch ? e.touches[0] : e; // If touch drag delay is configured and not elapsed yet, allow scrolling until either // the delay elapses (timer will call handleDragStart) or the user moves significantly, // in which case we cancel the potential drag and let the interaction be a scroll. if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) { currentMousePosition = {x: c.clientX, y: c.clientY}; if ( Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX || Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ) { // User started scrolling, cancel drag attempt. if (touchDragHoldTimer) { clearTimeout(touchDragHoldTimer); touchDragHoldTimer = undefined; } handleFalseAlarm(e); } return; // Do not preventDefault so scrolling works. } // legacy / post-delay path – block scrolling and maybe start drag e.preventDefault(); currentMousePosition = {x: c.clientX, y: c.clientY}; if ( Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX || Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ) { removeMaybeListeners(); handleDragStart(); } } function handleMouseDown(e) { // on safari clicking on a select element doesn't fire mouseup at the end of the click and in general this makes more sense if (e.target !== e.currentTarget && (e.target.value !== undefined || e.target.isContentEditable)) { printDebug(() => "won't initiate drag on a nested input element"); return; } // prevents responding to any button but left click which equals 0 (which is falsy) if (e.button) { printDebug(() => `ignoring none left click button: ${e.button}`); return; } if (isWorkingOnPreviousDrag) { printDebug(() => "cannot start a new drag before finalizing previous one"); return; } const isTouchStart = !!e.touches; const useDelay = isTouchStart && config.delayTouchStartMs > 0; if (!useDelay) { e.preventDefault(); } e.stopPropagation(); const c = isTouchStart ? e.touches[0] : e; dragStartMousePosition = {x: c.clientX, y: c.clientY}; currentMousePosition = {...dragStartMousePosition}; originalDragTarget = e.currentTarget; if (useDelay) { touchHoldElapsed = false; touchDragHoldTimer = window.setTimeout(() => { // If the finger is still down and no false-alarm happened if (!originalDragTarget) return; touchHoldElapsed = true; removeMaybeListeners(); handleDragStart(); }, config.delayTouchStartMs); } addMaybeListeners(); } function handleDragStart() { printDebug(() => [`drag start config: ${toString(config)}`, originalDragTarget]); isWorkingOnPreviousDrag = true; // initialising globals const currentIdx = elToIdx.get(originalDragTarget); originIndex = currentIdx; originDropZone = originalDragTarget.parentElement; /** @type {ShadowRoot | HTMLDocument | Element } */ const rootNode = originDropZone.closest("dialog") || originDropZone.closest("[popover]") || originDropZone.getRootNode(); const originDropZoneRoot = rootNode.body || rootNode; const {items: originalItems, type, centreDraggedOnCursor, useCursorForDetection} = config; const items = [...originalItems]; draggedElData = items[currentIdx]; draggedElType = type; shadowElData = createShadowElData(draggedElData); useCursorForDetectionActive = useCursorForDetection; // creating the draggable element draggedEl = createDraggedElementFrom(originalDragTarget, centreDraggedOnCursor && currentMousePosition); originDropZoneRoot.appendChild(draggedEl); // We will keep the original dom node in the dom because touch events keep firing on it, we want to re-add it after the framework removes it function keepOriginalElementInDom() { if (!originalDragTarget.parentElement) { originalDragTarget.setAttribute(ORIGINAL_DRAGGED_ITEM_MARKER_ATTRIBUTE, true); originDropZoneRoot.appendChild(originalDragTarget); // have to watch before we hide, otherwise Svelte 5 $state gets confused watchDraggedElement(); hideElement(originalDragTarget); // after the removal of the original element we can give the shadow element the original item id so that the host zone can find it and render it correctly if it does lookups by id shadowElData[ITEM_ID_KEY] = draggedElData[ITEM_ID_KEY]; // to prevent the outline from disappearing draggedEl.focus(); } else { window.requestAnimationFrame(keepOriginalElementInDom); } } window.requestAnimationFrame(keepOriginalElementInDom); styleActiveDropZones( Array.from(typeToDropZones.get(config.type)).filter(dz => dz === originDropZone || !dzToConfig.get(dz).dropFromOthersDisabled), dz => dzToConfig.get(dz).dropTargetStyle, dz => dzToConfig.get(dz).dropTargetClasses ); // removing the original element by removing its data entry items.splice(currentIdx, 1, shadowElData); unlockOriginDzMinDimensions = preventShrinking(originDropZone); dispatchConsiderEvent(originDropZone, items, {trigger: TRIGGERS.DRAG_STARTED, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); // handing over to global handlers - starting to watch the element window.addEventListener("mousemove", handleMouseMove, {passive: false}); window.addEventListener("touchmove", handleMouseMove, {passive: false, capture: false}); window.addEventListener("mouseup", handleDrop, {passive: false}); window.addEventListener("touchend", handleDrop, {passive: false}); } function configure({ items = undefined, flipDurationMs: dropAnimationDurationMs = 0, type: newType = DEFAULT_DROP_ZONE_TYPE, dragDisabled = false, morphDisabled = false, dropFromOthersDisabled = false, dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, dropTargetClasses = [], transformDraggedElement = () => {}, centreDraggedOnCursor = false, useCursorForDetection = false, dropAnimationDisabled = false, delayTouchStart: delayTouchStartOpt = false }) { config.dropAnimationDurationMs = dropAnimationDurationMs; let effectiveDelayMs = 0; if (delayTouchStartOpt === true) { effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS; } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) { effectiveDelayMs = delayTouchStartOpt; } config.delayTouchStartMs = effectiveDelayMs; if (config.type && newType !== config.type) { unregisterDropZone(node, config.type); } config.type = newType; config.items = [...items]; config.dragDisabled = dragDisabled; config.morphDisabled = morphDisabled; config.transformDraggedElement = transformDraggedElement; config.centreDraggedOnCursor = centreDraggedOnCursor; config.useCursorForDetection = useCursorForDetection; config.dropAnimationDisabled = dropAnimationDisabled; // realtime update for dropTargetStyle if ( initialized && isWorkingOnPreviousDrag && !finalizingPreviousDrag && (!areObjectsShallowEqual(dropTargetStyle, config.dropTargetStyle) || !areArraysShallowEqualSameOrder(dropTargetClasses, config.dropTargetClasses)) ) { styleInactiveDropZones( [node], () => config.dropTargetStyle, () => dropTargetClasses ); styleActiveDropZones( [node], () => dropTargetStyle, () => dropTargetClasses ); } config.dropTargetStyle = dropTargetStyle; config.dropTargetClasses = [...dropTargetClasses]; // realtime update for dropFromOthersDisabled function getConfigProp(dz, propName) { return dzToConfig.get(dz) ? dzToConfig.get(dz)[propName] : config[propName]; } if (initialized && isWorkingOnPreviousDrag && config.dropFromOthersDisabled !== dropFromOthersDisabled) { if (dropFromOthersDisabled) { styleInactiveDropZones( [node], dz => getConfigProp(dz, "dropTargetStyle"), dz => getConfigProp(dz, "dropTargetClasses") ); } else { styleActiveDropZones( [node], dz => getConfigProp(dz, "dropTargetStyle"), dz => getConfigProp(dz, "dropTargetClasses") ); } } config.dropFromOthersDisabled = dropFromOthersDisabled; dzToConfig.set(node, config); registerDropZone(node, newType); const shadowElIdx = isWorkingOnPreviousDrag ? findShadowElementIdx(config.items) : -1; for (let idx = 0; idx < node.children.length; idx++) { const draggableEl = node.children[idx]; styleDraggable(draggableEl, dragDisabled); if (idx === shadowElIdx) { if (!morphDisabled) { morphDraggedElementToBeLike(draggedEl, draggableEl, currentMousePosition.x, currentMousePosition.y); } config.transformDraggedElement(draggedEl, draggedElData, idx); decorateShadowEl(draggableEl); continue; } draggableEl.removeEventListener("mousedown", elToMouseDownListener.get(draggableEl)); draggableEl.removeEventListener("touchstart", elToMouseDownListener.get(draggableEl)); if (!dragDisabled) { draggableEl.addEventListener("mousedown", handleMouseDown); draggableEl.addEventListener("touchstart", handleMouseDown); elToMouseDownListener.set(draggableEl, handleMouseDown); } // updating the idx elToIdx.set(draggableEl, idx); if (!initialized) { initialized = true; } } } configure(options); return { update: newOptions => { printDebug(() => `pointer dndzone will update newOptions: ${toString(newOptions)}`); configure(newOptions); }, destroy: () => { function destroyDz() { printDebug(() => "pointer dndzone will destroy"); unregisterDropZone(node, dzToConfig.get(node).type); dzToConfig.delete(node); } if (isWorkingOnPreviousDrag && !node.closest(`[${ORIGINAL_DRAGGED_ITEM_MARKER_ATTRIBUTE}]`)) { printDebug(() => "pointer dndzone will be scheduled for destruction"); scheduleDZForRemovalAfterDrop(node, destroyDz); } else { destroyDz(); } } }; }