@formkit/drag-and-drop
Version:
Drag and drop package.
1 lines • 172 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/plugins/animations/index.ts","../src/plugins/insert/index.ts","../src/plugins/drop-or-swap/index.ts"],"sourcesContent":["import type {\n DNDPlugin,\n DragAndDrop,\n DragState,\n DragStateProps,\n Node,\n NodeData,\n NodeDragEventData,\n NodeEventData,\n NodePointerEventData,\n NodeRecord,\n NodeTargetData,\n NodesData,\n ParentConfig,\n ParentData,\n ParentEventData,\n ParentTargetData,\n ParentsData,\n PointeroverNodeEvent,\n PointeroverParentEvent,\n SetupNodeData,\n TearDownNodeData,\n BaseDragState,\n SynthDragState,\n ParentKeydownEventData,\n SynthDragStateProps,\n ParentRecord,\n EventHandlers,\n NodeFromPoint,\n ParentFromPoint,\n Coordinates,\n ParentDragEventData,\n} from \"./types\";\n\nexport * from \"./types\";\nexport { animations } from \"./plugins/animations\";\nexport { insert } from \"./plugins/insert\";\nexport { dropOrSwap } from \"./plugins/drop-or-swap\";\n\n// Function to detect if the device supports touch\nfunction checkTouchSupport() {\n if (!isBrowser) return false;\n\n return \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n}\n\n/**\n * Check to see if code is running in a browser.\n *\n * @internal\n */\nexport const isBrowser = typeof window !== \"undefined\";\n\nlet dropped = false;\n\n/**\n * Abort controller for the document.\n */\nlet documentController: AbortController | undefined;\n\nlet windowController: AbortController | undefined;\n\nlet touchDevice: boolean = false;\n\nexport const nodes: NodesData<any> = new WeakMap<Node, NodeData<unknown>>();\n\nexport const parents: ParentsData<any> = new WeakMap<\n HTMLElement,\n ParentData<unknown>\n>();\n\nexport const treeAncestors: Record<string, HTMLElement> = {};\n\nlet synthNodePointerDown = false;\n\nexport function createEmitter() {\n const callbacks = new Map<string, CallableFunction[]>();\n\n const emit = function (eventName: string, data: any) {\n if (!callbacks.get(eventName)) return;\n callbacks.get(eventName)!.forEach((cb) => {\n cb(data);\n });\n };\n\n const on = function (eventName: string, callback: any) {\n const cbs = callbacks.get(eventName) ?? [];\n\n cbs.push(callback);\n\n callbacks.set(eventName, cbs);\n };\n\n return [emit, on];\n}\n\nexport const [emit, on] = createEmitter();\n\nconst baseDragState = {\n activeDescendant: undefined,\n affectedNodes: [],\n currentTargetValue: undefined,\n on,\n emit,\n newActiveDescendant: undefined,\n originalZIndex: undefined,\n pointerSelection: false,\n preventEnter: false,\n longPress: false,\n longPressTimeout: 0,\n remapJustFinished: false,\n selectednodes: [],\n selectedParent: undefined,\n preventSynthDrag: false,\n};\n\n/**\n * The state of the drag and drop.\n */\nexport let state: BaseDragState<unknown> = baseDragState;\n\nexport function resetState() {\n const baseDragState = {\n activeDescendant: undefined,\n affectedNodes: [],\n on,\n emit,\n currentTargetValue: undefined,\n originalZIndex: undefined,\n pointerId: undefined,\n preventEnter: false,\n remapJustFinished: false,\n selectednodes: [],\n preventSynthDrag: false,\n selectedParent: undefined,\n pointerSelection: false,\n synthScrollDirection: undefined,\n draggedNodeDisplay: undefined,\n synthDragScrolling: false,\n longPress: false,\n longPressTimeout: 0,\n };\n\n state = { ...baseDragState } as BaseDragState<unknown>;\n}\n\n/**\n * @param {DragStateProps} dragStateProps - Attributes to update state with.\n *\n * @mutation - Updates state with node values.\n *\n * @returns void\n */\nexport function setDragState<T>(\n dragStateProps: (SynthDragStateProps & DragStateProps<T>) | DragStateProps<T>\n): DragState<T> | SynthDragState<T> {\n Object.assign(state, dragStateProps);\n\n dragStateProps.initialParent.data.emit(\"dragStarted\", state);\n\n dropped = false;\n\n state.emit(\"dragStarted\", state);\n\n return state as DragState<T> | SynthDragState<T>;\n}\n\n/**\n *\n */\nfunction handleRootPointerdown(_e: PointerEvent) {\n if (state.activeState) setActive(state.activeState.parent, undefined, state);\n\n if (state.selectedState)\n deselect(state.selectedState.nodes, state.selectedState.parent, state);\n\n state.selectedState = state.activeState = undefined;\n}\n\nfunction handleRootPointerup(_e: PointerEvent) {\n if (!isSynthDragState(state)) return;\n\n const config = state.currentParent.data.config;\n\n if (isSynthDragState(state)) config.handleEnd(state);\n}\n\n/**\n * Handles the keydown event on the root element.\n *\n * @param {KeyboardEvent} e - The keyboard event.\n */\nfunction handleRootKeydown(e: KeyboardEvent) {\n if (e.key === \"Escape\") {\n if (state.selectedState)\n deselect(state.selectedState.nodes, state.selectedState.parent, state);\n\n if (state.activeState)\n setActive(state.activeState.parent, undefined, state);\n\n state.selectedState = state.activeState = undefined;\n }\n}\n\nfunction handleRootDrop(_e: DragEvent) {}\n\n/**\n * If we are currently dragging, then let's prevent default on dragover to avoid\n * the default behavior of the browser on drop.\n */\nfunction handleRootDragover(e: DragEvent) {\n if (!isDragState(state)) return;\n\n e.preventDefault();\n}\n\n/**\n * Initializes the drag and drop functionality for a given parent.\n *\n * @param {DragAndDrop} dragAndDrop - The drag and drop configuration.\n *\n * @returns void\n */\nexport function dragAndDrop<T>({\n parent,\n getValues,\n setValues,\n config = {},\n}: DragAndDrop<T>): void {\n if (!isBrowser) return;\n\n touchDevice = checkTouchSupport();\n\n if (!documentController)\n documentController = addEvents(document, {\n dragover: handleRootDragover,\n pointerdown: handleRootPointerdown,\n pointerup: handleRootPointerup,\n keydown: handleRootKeydown,\n drop: handleRootDrop,\n });\n\n if (!windowController)\n windowController = addEvents(window, {\n resize: () => {\n touchDevice = checkTouchSupport();\n },\n });\n\n tearDown(parent);\n\n const [emit, on] = createEmitter();\n\n const parentData: ParentData<T> = {\n getValues,\n setValues,\n config: {\n dragDropEffect: config.dragDropEffect ?? \"move\",\n dragEffectAllowed: config.dragEffectAllowed ?? \"move\",\n draggedNodes,\n dragstartClasses,\n deepCopyStyles: config.deepCopyStyles ?? false,\n handleNodeKeydown,\n handleParentKeydown,\n handleDragstart,\n handleNodeDragover,\n handleParentDragover,\n handleNodeDrop,\n handlePointercancel,\n handleEnd,\n handleDragend,\n handleParentBlur,\n handleParentFocus,\n handleNodePointerup,\n handleNodePointerover,\n handleParentPointerover,\n handleParentScroll,\n handleNodePointerdown,\n handleNodePointermove,\n handleNodeDragenter,\n handleNodeDragleave,\n handleParentDrop,\n multiDrag: config.multiDrag ?? false,\n nativeDrag: config.nativeDrag ?? true,\n performSort,\n performTransfer,\n root: config.root ?? document,\n setupNode,\n setupNodeRemap,\n reapplyDragClasses,\n tearDownNode,\n tearDownNodeRemap,\n remapFinished,\n scrollBehavior: {\n x: 0.95,\n y: 0.95,\n },\n threshold: {\n horizontal: 0,\n vertical: 0,\n },\n ...config,\n },\n enabledNodes: [],\n abortControllers: {},\n privateClasses: [],\n on,\n emit,\n };\n\n const nodesObserver = new MutationObserver(nodesMutated);\n\n nodesObserver.observe(parent, { childList: true });\n\n parents.set(parent, parentData);\n\n if (config.treeAncestor && config.treeGroup)\n treeAncestors[config.treeGroup] = parent;\n\n config.plugins?.forEach((plugin) => {\n plugin(parent)?.tearDown?.();\n });\n\n config.plugins?.forEach((plugin) => {\n plugin(parent)?.tearDown?.();\n });\n\n config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(parent)?.setup?.();\n });\n\n setup(parent, parentData);\n\n remapNodes(parent, true);\n}\n\nexport function dragStateProps<T>(\n data: NodeDragEventData<T> | NodePointerEventData<T>,\n draggedNodes: Array<NodeRecord<T>>\n): DragStateProps<T> {\n const { x, y } = eventCoordinates(data.e);\n\n const rect = data.targetData.node.el.getBoundingClientRect();\n\n return {\n affectedNodes: [],\n ascendingDirection: false,\n clonedDraggedEls: [],\n dynamicValues: [],\n coordinates: {\n x,\n y,\n },\n draggedNode: {\n el: data.targetData.node.el,\n data: data.targetData.node.data,\n },\n draggedNodes,\n incomingDirection: undefined,\n initialIndex: data.targetData.node.data.index,\n initialParent: {\n el: data.targetData.parent.el,\n data: data.targetData.parent.data,\n },\n currentParent: {\n el: data.targetData.parent.el,\n data: data.targetData.parent.data,\n },\n longPress: data.targetData.parent.data.config.longPress ?? false,\n longPressTimeout: 0,\n currentTargetValue: data.targetData.node.data.value,\n scrollEls: [],\n startLeft: x - rect.left,\n startTop: y - rect.top,\n targetIndex: data.targetData.node.data.index,\n transferred: false,\n };\n}\n\nexport function performSort<T>({\n parent,\n draggedNodes,\n targetNode,\n}: {\n parent: ParentRecord<T>;\n draggedNodes: Array<NodeRecord<T>>;\n targetNode: NodeRecord<T>;\n}) {\n const draggedValues = draggedNodes.map((x) => x.data.value);\n\n const targetParentValues = parentValues(parent.el, parent.data);\n\n const originalIndex = draggedNodes[0].data.index;\n\n const enabledNodes = [...parent.data.enabledNodes];\n\n const newParentValues = [\n ...targetParentValues.filter((x) => !draggedValues.includes(x)),\n ];\n\n newParentValues.splice(targetNode.data.index, 0, ...draggedValues);\n\n if (\"draggedNode\" in state) state.currentTargetValue = targetNode.data.value;\n\n setParentValues(parent.el, parent.data, [...newParentValues]);\n\n if (parent.data.config.onSort)\n parent.data.config.onSort({\n parent: {\n el: parent.el,\n data: parent.data,\n },\n previousValues: [...targetParentValues],\n previousNodes: [...enabledNodes],\n nodes: [...parent.data.enabledNodes],\n values: [...newParentValues],\n draggedNode: draggedNodes[0],\n previousPosition: originalIndex,\n position: targetNode.data.index,\n });\n}\n\n/**\n * This function sets the active node as well as removing any classes or\n * attribute set.\n *\n * @param {ParentEventData} data - The parent event data.\n * @param {NodeRecord} newActiveNode - The new active node.\n * @param {BaseDragState} state - The current drag state.\n */\nfunction setActive<T>(\n parent: ParentRecord<T>,\n newActiveNode: NodeRecord<T> | undefined,\n state: BaseDragState<T>\n) {\n const activeDescendantClass = parent.data.config.activeDescendantClass;\n\n if (state.activeState) {\n {\n removeClass([state.activeState.node.el], activeDescendantClass);\n\n if (state.activeState.parent.el !== parent.el)\n state.activeState.parent.el.setAttribute(\"aria-activedescendant\", \"\");\n }\n }\n\n if (!newActiveNode) {\n state.activeState?.parent.el.setAttribute(\"aria-activedescendant\", \"\");\n\n state.activeState = undefined;\n\n return;\n }\n\n state.activeState = {\n node: newActiveNode,\n parent,\n };\n\n addNodeClass([newActiveNode.el], activeDescendantClass);\n\n state.activeState.parent.el.setAttribute(\n \"aria-activedescendant\",\n state.activeState.node.el.id\n );\n}\n\nfunction deselect<T>(\n nodes: Array<NodeRecord<T>>,\n parent: ParentRecord<T>,\n state: BaseDragState<T>\n) {\n const selectedClass = parent.data.config.selectedClass;\n\n if (!state.selectedState) return;\n\n const iterativeNodes = Array.from(nodes);\n\n removeClass(\n nodes.map((x) => x.el),\n selectedClass\n );\n\n for (const node of iterativeNodes) {\n node.el.setAttribute(\"aria-selected\", \"false\");\n\n const index = state.selectedState.nodes.findIndex((x) => x.el === node.el);\n\n if (index === -1) continue;\n\n state.selectedState.nodes.splice(index, 1);\n }\n\n clearLiveRegion(parent);\n}\n\n/**\n * This function sets the selected nodes. This will clean the prior selected state\n * as well as removing any classes or attributes set.\n */\nfunction setSelected<T>(\n parent: ParentRecord<T>,\n selectedNodes: Array<NodeRecord<T>>,\n newActiveNode: NodeRecord<T> | undefined,\n state: BaseDragState<T>,\n pointerdown = false\n) {\n state.pointerSelection = pointerdown;\n\n for (const node of selectedNodes) {\n node.el.setAttribute(\"aria-selected\", \"true\");\n\n addNodeClass([node.el], parent.data.config.selectedClass, true);\n }\n\n state.selectedState = {\n nodes: selectedNodes,\n parent,\n };\n\n const selectedItems = selectedNodes.map((x) =>\n x.el.getAttribute(\"aria-label\")\n );\n\n if (selectedItems.length === 0) {\n state.selectedState = undefined;\n\n clearLiveRegion(parent);\n\n return;\n }\n\n setActive(parent, newActiveNode, state);\n\n updateLiveRegion(\n parent,\n `${selectedItems.join(\n \", \"\n )} ready for dragging. Use arrow keys to navigate. Press enter to drop ${selectedItems.join(\n \", \"\n )}.`\n );\n}\n\nfunction updateLiveRegion<T>(parent: ParentRecord<T>, message: string) {\n const parentId = parent.el.id;\n\n const liveRegion = document.getElementById(parentId + \"-live-region\");\n\n if (!liveRegion) return;\n\n liveRegion.textContent = message;\n}\n\nfunction clearLiveRegion<T>(parent: ParentRecord<T>) {\n const liveRegion = document.getElementById(parent.el.id + \"-live-region\");\n\n if (!liveRegion) return;\n\n liveRegion.textContent = \"\";\n}\n\nexport function handleParentBlur<T>(\n _data: ParentEventData<T>,\n _state: BaseDragState<T> | DragState<T> | SynthDragState<T>\n) {}\n\nexport function handleParentFocus<T>(\n data: ParentEventData<T>,\n state: BaseDragState<T> | DragState<T> | SynthDragState<T>\n) {\n const firstEnabledNode = data.targetData.parent.data.enabledNodes[0];\n\n if (!firstEnabledNode) return;\n\n if (\n state.selectedState &&\n state.selectedState.parent.el !== data.targetData.parent.el\n ) {\n setActive(data.targetData.parent, firstEnabledNode, state);\n } else if (!state.selectedState) {\n setActive(data.targetData.parent, firstEnabledNode, state);\n }\n}\n\nexport function performTransfer<T>({\n currentParent,\n targetParent,\n initialParent,\n draggedNodes,\n initialIndex,\n targetNode,\n state,\n}: {\n currentParent: ParentRecord<T>;\n targetParent: ParentRecord<T>;\n initialParent: ParentRecord<T>;\n draggedNodes: Array<NodeRecord<T>>;\n initialIndex: number;\n state: BaseDragState<T> | DragState<T> | SynthDragState<T>;\n targetNode?: NodeRecord<T>;\n}) {\n const draggedValues = draggedNodes.map((x) => x.data.value);\n\n const currentParentValues = parentValues(\n currentParent.el,\n currentParent.data\n ).filter((x: any) => !draggedValues.includes(x));\n\n const targetParentValues = parentValues(targetParent.el, targetParent.data);\n\n const reset =\n initialParent.el === targetParent.el &&\n targetParent.data.config.sortable === false;\n\n let targetIndex: number;\n\n if (targetNode) {\n if (reset) {\n targetIndex = initialIndex;\n } else if (targetParent.data.config.sortable === false) {\n targetIndex = targetParent.data.enabledNodes.length;\n } else {\n targetIndex = targetNode.data.index;\n }\n\n targetParentValues.splice(targetIndex, 0, ...draggedValues);\n } else {\n targetIndex = reset ? initialIndex : targetParent.data.enabledNodes.length;\n\n targetParentValues.splice(targetIndex, 0, ...draggedValues);\n }\n\n setParentValues(currentParent.el, currentParent.data, currentParentValues);\n\n setParentValues(targetParent.el, targetParent.data, targetParentValues);\n\n if (targetParent.data.config.onTransfer) {\n targetParent.data.config.onTransfer({\n sourceParent: currentParent,\n targetParent,\n initialParent,\n draggedNodes,\n targetIndex,\n state,\n });\n }\n\n if (currentParent.data.config.onTransfer) {\n currentParent.data.config.onTransfer({\n sourceParent: currentParent,\n targetParent,\n initialParent,\n draggedNodes,\n targetIndex,\n state,\n });\n }\n}\n\nexport function parentValues<T>(\n parent: HTMLElement,\n parentData: ParentData<T>\n): Array<T> {\n return [...parentData.getValues(parent)];\n}\n\nfunction findArrayCoordinates(\n obj: any,\n targetArray: Array<any>,\n path: Array<any> = []\n) {\n let result: Array<any> = [];\n\n if (obj === targetArray) result.push(path);\n\n if (Array.isArray(obj)) {\n const index = obj.findIndex((el) => el === targetArray);\n\n if (index !== -1) {\n result.push([...path, index]);\n } else {\n for (let i = 0; i < obj.length; i++) {\n result = result.concat(\n findArrayCoordinates(obj[i], targetArray, [...path, i])\n );\n }\n }\n } else if (typeof obj === \"object\" && obj !== null) {\n for (const key in obj) {\n result = result.concat(\n findArrayCoordinates(obj[key], targetArray, [...path, key])\n );\n }\n }\n\n return result;\n}\n\nfunction setValueAtCoordinatesUsingFindIndex(\n obj: Array<any>,\n targetArray: Array<any>,\n newArray: Array<any>\n) {\n const coordinates = findArrayCoordinates(obj, targetArray);\n\n let newValues;\n\n coordinates.forEach((coords) => {\n let current = obj;\n\n for (let i = 0; i < coords.length - 1; i++) {\n const index = coords[i];\n\n current = current[index];\n }\n\n const lastIndex = coords[coords.length - 1];\n\n current[lastIndex] = newArray;\n\n // We want to access getter of object we are setting to set the new values\n // of the nested parent element (should be a part of the original structure of\n // ancestor values).\n newValues = current[lastIndex];\n });\n\n return newValues;\n}\n\nexport function setParentValues<T>(\n parent: HTMLElement,\n parentData: ParentData<T>,\n values: Array<any>\n): void {\n const treeGroup = parentData.config.treeGroup;\n\n if (treeGroup) {\n const ancestorEl = treeAncestors[treeGroup];\n\n const ancestorData = parents.get(ancestorEl);\n\n if (!ancestorData) return;\n\n const ancestorValues = ancestorData.getValues(ancestorEl);\n\n const initialParentValues = parentData.getValues(parent);\n\n const updatedValues = setValueAtCoordinatesUsingFindIndex(\n ancestorValues,\n initialParentValues,\n values\n );\n if (!updatedValues) {\n console.warn(\"No updated value found\");\n\n return;\n }\n\n parentData.setValues(updatedValues, parent);\n\n return;\n }\n\n parentData.setValues(values, parent);\n}\n\nexport function dragValues<T>(state: DragState<T>): Array<T> {\n return [...state.draggedNodes.map((x) => x.data.value)];\n}\n\n/**\n * Utility function to update parent config.\n */\nexport function updateConfig<T>(\n parent: HTMLElement,\n config: Partial<ParentConfig<T>>\n) {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n parents.set(parent, {\n ...parentData,\n config: { ...parentData.config, ...config },\n });\n\n dragAndDrop({\n parent,\n getValues: parentData.getValues,\n setValues: parentData.setValues,\n config,\n });\n}\n\nexport function handleParentDrop<T>(\n data: ParentEventData<T>,\n state: DragState<T>\n) {\n data.e.stopPropagation();\n\n dropped = true;\n\n const config = data.targetData.parent.data.config;\n\n config.handleEnd(state);\n}\n\nexport function tearDown(parent: HTMLElement) {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n if (parentData.abortControllers.mainParent)\n parentData.abortControllers.mainParent.abort();\n}\n\nexport function isDragState<T>(\n state: BaseDragState<T>\n): state is DragState<T> | SynthDragState<T> {\n return \"draggedNode\" in state && !!state.draggedNode;\n}\n\nexport function isSynthDragState<T>(\n state: BaseDragState<T>\n): state is SynthDragState<T> {\n return \"pointerId\" in state && !!state.pointerId;\n}\n\nfunction setup<T>(parent: HTMLElement, parentData: ParentData<T>): void {\n parentData.abortControllers.mainParent = addEvents(parent, {\n keydown: parentEventData(parentData.config.handleParentKeydown),\n dragover: parentEventData(parentData.config.handleParentDragover),\n handleParentPointerover: parentData.config.handleParentPointerover,\n drop: parentEventData(parentData.config.handleParentDrop),\n hasNestedParent: (e: CustomEvent) => {\n const parent = parents.get(e.target as HTMLElement);\n\n if (!parent) return;\n\n parent.nestedParent = e.detail.parent;\n },\n blur: parentEventData(parentData.config.handleParentBlur),\n focus: parentEventData(parentData.config.handleParentFocus),\n });\n\n setAttrs(parent, {\n role: \"listbox\",\n tabindex: \"0\",\n \"aria-multiselectable\": parentData.config.multiDrag ? \"true\" : \"false\",\n \"aria-activedescendant\": \"\",\n \"aria-describedby\": parent.id + \"-live-region\",\n });\n\n const liveRegion = document.createElement(\"div\");\n\n setAttrs(liveRegion, {\n \"aria-live\": \"polite\",\n \"aria-atomic\": \"true\",\n \"data-drag-and-drop-live-region\": \"true\",\n id: parent.id.toString() + \"-live-region\",\n });\n\n Object.assign(liveRegion.style, {\n position: \"absolute\",\n width: \"1px\",\n height: \"1px\",\n padding: \"0\",\n overflow: \"hidden\",\n clip: \"rect(0, 0, 0, 0)\",\n whiteSpace: \"nowrap\",\n border: \"0\",\n });\n\n document.body.appendChild(liveRegion);\n}\n\nexport function setAttrs(el: HTMLElement, attrs: Record<string, string>) {\n for (const key in attrs) el.setAttribute(key, attrs[key]);\n}\n\nexport function setupNode<T>(data: SetupNodeData<T>) {\n const config = data.parent.data.config;\n\n data.node.el.draggable = true;\n\n data.node.data.abortControllers.mainNode = addEvents(data.node.el, {\n keydown: nodeEventData(config.handleNodeKeydown),\n dragstart: nodeEventData(config.handleDragstart),\n dragover: nodeEventData(config.handleNodeDragover),\n dragenter: nodeEventData(config.handleNodeDragenter),\n dragleave: nodeEventData(config.handleNodeDragleave),\n dragend: nodeEventData(config.handleDragend),\n drop: nodeEventData(config.handleNodeDrop),\n pointercancel: nodeEventData(config.handlePointercancel),\n pointerdown: nodeEventData(config.handleNodePointerdown),\n pointerup: nodeEventData(config.handleNodePointerup),\n pointermove: nodeEventData(config.handleNodePointermove),\n handleNodePointerover: config.handleNodePointerover,\n contextmenu: (e: Event) => {\n if (touchDevice) e.preventDefault();\n },\n });\n\n data.node.el.setAttribute(\"role\", \"option\");\n\n data.node.el.setAttribute(\"aria-selected\", \"false\");\n\n data.node.el.style.touchAction = \"none\";\n\n config.reapplyDragClasses(data.node.el, data.parent.data);\n\n data.parent.data.config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(data.parent.el)?.setupNode?.(data);\n });\n}\n\nexport function setupNodeRemap<T>(data: SetupNodeData<T>) {\n nodes.set(data.node.el, data.node.data);\n\n data.parent.data.config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(data.parent.el)?.setupNodeRemap?.(data);\n });\n}\n\nfunction reapplyDragClasses<T>(node: Node, parentData: ParentData<T>) {\n if (!isDragState(state)) return;\n\n const dropZoneClass = isSynthDragState(state)\n ? parentData.config.synthDropZoneClass\n : parentData.config.dropZoneClass;\n\n if (state.draggedNode.el !== node) return;\n\n addNodeClass([node], dropZoneClass, true);\n}\n\nexport function tearDownNodeRemap<T>(data: TearDownNodeData<T>) {\n data.parent.data.config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(data.parent.el)?.tearDownNodeRemap?.(data);\n });\n}\n\nexport function tearDownNode<T>(data: TearDownNodeData<T>) {\n data.parent.data.config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(data.parent.el)?.tearDownNode?.(data);\n });\n\n data.node.el.draggable = false;\n\n if (data.node.data?.abortControllers?.mainNode)\n data.node.data?.abortControllers?.mainNode.abort();\n}\n\n/**\n * Called when the nodes of a given parent element are mutated.\n *\n * @param mutationList - The list of mutations.\n *\n * @returns void\n *\n * @internal\n */\nfunction nodesMutated(mutationList: MutationRecord[]) {\n const parentEl = mutationList[0].target;\n\n if (!(parentEl instanceof HTMLElement)) return;\n\n const allSelectedAndActiveNodes = document.querySelectorAll(\n `[aria-selected=\"true\"]`\n );\n\n const parentData = parents.get(parentEl);\n\n if (!parentData) return;\n\n for (let x = 0; x < allSelectedAndActiveNodes.length; x++) {\n const node = allSelectedAndActiveNodes[x];\n\n node.setAttribute(\"aria-selected\", \"false\");\n\n removeClass([node], parentData.config.selectedClass);\n }\n\n remapNodes(parentEl);\n}\n\n/**\n * Remaps the data of the parent element's children.\n *\n * @param parent - The parent element.\n *\n * @returns void\n *\n * @internal\n */\nexport function remapNodes<T>(parent: HTMLElement, force?: boolean) {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n parentData.privateClasses = Array.from(parent.classList);\n\n const enabledNodes: Array<Node> = [];\n\n const config = parentData.config;\n\n for (let x = 0; x < parent.children.length; x++) {\n const node = parent.children[x];\n\n if (!isNode(node)) continue;\n\n const nodeData = nodes.get(node);\n\n // Only tear down the node if we have explicitly called dragAndDrop\n if (force || !nodeData)\n config.tearDownNode({\n parent: {\n el: parent,\n data: parentData,\n },\n node: {\n el: node,\n data: nodeData,\n },\n });\n\n if (config.disabled) continue;\n\n if (!config.draggable || (config.draggable && config.draggable(node))) {\n enabledNodes.push(node);\n }\n }\n\n if (\n enabledNodes.length !== parentData.getValues(parent).length &&\n !config.disabled\n ) {\n console.warn(\n \"The number of draggable items defined in the parent element does not match the number of values. This may cause unexpected behavior.\"\n );\n\n return;\n }\n\n if (parentData.config.treeGroup && !parentData.config.treeAncestor) {\n let nextAncestorEl = parent.parentElement;\n\n let eventDispatched = false;\n\n while (nextAncestorEl) {\n if (!parents.has(nextAncestorEl as HTMLElement)) {\n nextAncestorEl = nextAncestorEl.parentElement;\n\n continue;\n }\n\n nextAncestorEl.dispatchEvent(\n new CustomEvent(\"hasNestedParent\", {\n detail: {\n parent: { data: parentData, el: parent },\n },\n })\n );\n\n eventDispatched = true;\n\n nextAncestorEl = null;\n }\n\n if (!eventDispatched) console.warn(\"No ancestor found for tree group\");\n }\n\n const values = parentData.getValues(parent);\n\n const enabledNodeRecords: Array<NodeRecord<T>> = [];\n\n for (let x = 0; x < enabledNodes.length; x++) {\n const node = enabledNodes[x];\n\n const prevNodeData = nodes.get(node);\n\n const nodeData = Object.assign(\n prevNodeData ?? {\n privateClasses: [],\n abortControllers: {},\n },\n {\n value: values[x],\n index: x,\n }\n );\n\n if (\n !isDragState(state) &&\n state.newActiveDescendant &&\n state.newActiveDescendant.data.value === nodeData.value\n ) {\n setActive(\n {\n data: parentData,\n el: parent,\n },\n {\n el: node,\n data: nodeData,\n },\n state\n );\n }\n\n if (\n !isDragState(state) &&\n state.activeState &&\n state.activeState.node.data.value === nodeData.value\n ) {\n setActive(\n {\n data: parentData,\n el: parent,\n },\n {\n el: node,\n data: nodeData,\n },\n state\n );\n }\n\n if (isDragState(state) && nodeData.value === state.draggedNode.data.value) {\n state.draggedNode.data = nodeData;\n\n state.draggedNode.el = node;\n\n const draggedNode = state.draggedNodes.find(\n (x) => x.data.value === nodeData.value\n );\n\n if (draggedNode) draggedNode.el = node;\n\n if (isSynthDragState(state)) {\n state.draggedNode.el.setPointerCapture(state.pointerId);\n }\n }\n\n enabledNodeRecords.push({\n el: node,\n data: nodeData,\n });\n\n if (force || !prevNodeData)\n config.setupNode({\n parent: {\n el: parent,\n data: parentData,\n },\n node: {\n el: node,\n data: nodeData,\n },\n });\n\n setupNodeRemap({\n parent: {\n el: parent,\n data: parentData,\n },\n node: {\n el: node,\n data: nodeData,\n },\n });\n }\n\n parents.set(parent, { ...parentData, enabledNodes: enabledNodeRecords });\n\n config.remapFinished(parentData);\n\n parentData.config.plugins?.forEach((plugin: DNDPlugin) => {\n plugin(parent)?.remapFinished?.();\n });\n}\n\nexport function remapFinished() {\n state.remapJustFinished = true;\n\n if (\"draggedNode\" in state) state.affectedNodes = [];\n}\n\nexport function validateDragstart(data: NodeEventData<any>): boolean {\n return !!data.targetData.parent.data.config.nativeDrag;\n}\n\nfunction draggedNodes<T>(data: NodeEventData<T>): Array<NodeRecord<T>> {\n if (!data.targetData.parent.data.config.multiDrag) {\n return [data.targetData.node];\n } else if (state.selectedState) {\n return [\n data.targetData.node,\n ...(state.selectedState?.nodes.filter(\n (x) => x.el !== data.targetData.node.el\n ) as Array<NodeRecord<T>>),\n ];\n }\n\n return [];\n}\n\nlet scrollTimeout: number | undefined;\n\nfunction handleParentScroll<T>(_data: ParentEventData<T>) {\n if (!isDragState(state)) return;\n\n if (isSynthDragState(state)) return;\n\n state.preventEnter = true;\n\n if (scrollTimeout) clearTimeout(scrollTimeout);\n\n scrollTimeout = setTimeout(() => {\n state.preventEnter = false;\n }, 100);\n}\n\n/**\n * Responsible for assigning dragstart classes to the dragged nodes.\n */\nexport function handleDragstart<T>(\n data: NodeDragEventData<T>,\n state: BaseDragState<T>\n) {\n const config = data.targetData.parent.data.config;\n\n if (\n touchDevice ||\n !validateDragstart(data) ||\n !validateDragHandle({\n x: data.e.clientX,\n y: data.e.clientY,\n node: data.targetData.node,\n config,\n })\n ) {\n data.e.preventDefault();\n\n return;\n }\n\n const nodes = config.draggedNodes(data);\n\n config.dragstartClasses(data.targetData.node, nodes, config);\n\n const dragState = initDrag(data, nodes);\n\n if (config.onDragstart)\n config.onDragstart(\n {\n parent: data.targetData.parent,\n values: parentValues(\n data.targetData.parent.el,\n data.targetData.parent.data\n ),\n draggedNode: dragState.draggedNode,\n draggedNodes: dragState.draggedNodes,\n position: dragState.initialIndex,\n },\n state as DragState<T>\n );\n}\n\nexport function handleNodePointerdown<T>(\n data: NodePointerEventData<T>,\n state: BaseDragState<T>\n) {\n if (\n !validateDragHandle({\n x: data.e.clientX,\n y: data.e.clientY,\n node: data.targetData.node,\n config: data.targetData.parent.data.config,\n })\n )\n return;\n\n data.e.stopPropagation();\n\n synthNodePointerDown = true;\n\n handleLongPress(data, state, data.targetData.node);\n\n const parentData = data.targetData.parent.data;\n\n let selectedNodes = [data.targetData.node];\n\n const commandKey = data.e.ctrlKey || data.e.metaKey;\n const shiftKey = data.e.shiftKey;\n\n const targetNode = data.targetData.node;\n\n if (commandKey && parentData.config.multiDrag) {\n if (state.selectedState) {\n const idx = state.selectedState.nodes.findIndex(\n (x) => x.el === targetNode.el\n );\n\n if (idx === -1) {\n selectedNodes = [...state.selectedState.nodes, targetNode];\n } else {\n selectedNodes = state.selectedState.nodes.filter(\n (x) => x.el !== targetNode.el\n );\n }\n } else {\n selectedNodes = [targetNode];\n }\n\n setSelected(\n data.targetData.parent,\n selectedNodes,\n data.targetData.node,\n state,\n true\n );\n\n return;\n }\n\n if (shiftKey && parentData.config.multiDrag) {\n const nodes = data.targetData.parent.data.enabledNodes;\n if (state.selectedState && state.activeState) {\n if (state.selectedState.parent.el !== data.targetData.parent.el) {\n deselect(state.selectedState.nodes, state.selectedState.parent, state);\n state.selectedState = undefined;\n for (let x = 0; x <= targetNode.data.index; x++)\n selectedNodes.push(nodes[x]);\n } else {\n const [minIndex, maxIndex] =\n state.activeState.node.data.index < data.targetData.node.data.index\n ? [\n state.activeState.node.data.index,\n data.targetData.node.data.index,\n ]\n : [\n data.targetData.node.data.index,\n state.activeState.node.data.index,\n ];\n selectedNodes = nodes.slice(minIndex, maxIndex + 1);\n }\n } else {\n for (let x = 0; x <= targetNode.data.index; x++)\n selectedNodes.push(nodes[x]);\n }\n setSelected(\n data.targetData.parent,\n selectedNodes,\n data.targetData.node,\n state,\n true\n );\n\n return;\n }\n\n if (state.selectedState?.nodes?.length) {\n const idx = state.selectedState.nodes.findIndex(\n (x) => x.el === data.targetData.node.el\n );\n\n if (idx === -1) {\n if (state.selectedState.parent.el !== data.targetData.parent.el) {\n deselect(state.selectedState.nodes, data.targetData.parent, state);\n } else if (parentData.config.multiDrag && touchDevice) {\n selectedNodes.push(...state.selectedState.nodes);\n } else {\n deselect(state.selectedState.nodes, data.targetData.parent, state);\n }\n setSelected(\n data.targetData.parent,\n selectedNodes,\n data.targetData.node,\n state,\n true\n );\n }\n } else {\n setSelected(\n data.targetData.parent,\n [data.targetData.node],\n data.targetData.node,\n state,\n true\n );\n }\n}\n\nexport function dragstartClasses<T>(\n _node: NodeRecord<T>,\n nodes: Array<NodeRecord<T>>,\n config: ParentConfig<T>,\n isSynth = false\n) {\n addNodeClass(\n nodes.map((x) => x.el),\n isSynth ? config.synthDraggingClass : config.draggingClass\n );\n\n setTimeout(() => {\n removeClass(\n nodes.map((x) => x.el),\n isSynth ? config.synthDraggingClass : config.draggingClass\n );\n\n addNodeClass(\n nodes.map((x) => x.el),\n isSynth ? config.synthDragPlaceholderClass : config.dragPlaceholderClass\n );\n\n addNodeClass(\n nodes.map((x) => x.el),\n isSynth ? config.synthDropZoneClass : config.dropZoneClass\n );\n\n removeClass(\n nodes.map((x) => x.el),\n config.activeDescendantClass\n );\n\n removeClass(\n nodes.map((x) => x.el),\n config.selectedClass\n );\n });\n}\n\nexport function initDrag<T>(\n data: NodeDragEventData<T>,\n draggedNodes: Array<NodeRecord<T>>\n): DragState<T> {\n data.e.stopPropagation();\n\n const dragState = setDragState(dragStateProps(data, draggedNodes));\n\n if (data.e.dataTransfer) {\n const config = data.targetData.parent.data.config;\n\n data.e.dataTransfer.dropEffect = config.dragDropEffect;\n\n data.e.dataTransfer.effectAllowed = config.dragEffectAllowed;\n\n let dragImage: HTMLElement | undefined;\n\n if (config.dragImage) {\n dragImage = config.dragImage(data, draggedNodes);\n } else {\n if (!config.multiDrag) {\n data.e.dataTransfer.setDragImage(\n data.targetData.node.el,\n data.e.offsetX,\n data.e.offsetY\n );\n\n const originalZIndex = data.targetData.node.el.style.zIndex;\n\n dragState.originalZIndex = originalZIndex;\n\n data.targetData.node.el.style.zIndex = \"9999\";\n\n return dragState;\n } else {\n const wrapper = document.createElement(\"div\");\n\n for (const node of draggedNodes) {\n const clonedNode = node.el.cloneNode(true) as HTMLElement;\n\n clonedNode.style.pointerEvents = \"none\";\n\n clonedNode.id = node.el.id + \"-clone\";\n\n copyNodeStyle(node.el, clonedNode, true);\n\n wrapper.append(clonedNode);\n }\n\n const { width } = draggedNodes[0].el.getBoundingClientRect();\n\n Object.assign(wrapper.style, {\n display: \"flex\",\n flexDirection: \"column\",\n width: `${width}px`,\n position: \"absolute\",\n pointerEvents: \"none\",\n zIndex: \"9999\",\n left: \"-9999px\",\n });\n\n dragImage = wrapper;\n }\n\n document.body.appendChild(dragImage);\n }\n\n data.e.dataTransfer.setDragImage(dragImage, data.e.offsetX, data.e.offsetY);\n\n setTimeout(() => {\n dragImage?.remove();\n });\n }\n\n return dragState;\n}\n\nexport function validateDragHandle<T>({\n x,\n y,\n node,\n config,\n}: {\n x: number;\n y: number;\n node: NodeRecord<T>;\n config: ParentConfig<T>;\n}): boolean {\n if (!config.dragHandle) return true;\n\n const dragHandles = node.el.querySelectorAll(config.dragHandle);\n\n if (!dragHandles) return false;\n\n const elFromPoint = config.root.elementFromPoint(x, y);\n\n if (!elFromPoint) return false;\n\n for (const handle of Array.from(dragHandles))\n if (elFromPoint === handle || handle.contains(elFromPoint)) return true;\n\n return false;\n}\n\nexport function handleClickNode<T>(_data: NodeEventData<T>) {}\n\nexport function handleClickParent<T>(_data: ParentEventData<T>) {}\n\nexport function handleNodeKeydown<T>(_data: NodeEventData<T>) {}\n\nexport function handleParentKeydown<T>(\n data: ParentKeydownEventData<T>,\n state: BaseDragState<T>\n) {\n const activeDescendant = state.activeState?.node;\n\n if (!activeDescendant) return;\n\n const parentData = data.targetData.parent.data;\n\n const enabledNodes = parentData.enabledNodes;\n\n const index = enabledNodes.findIndex((x) => x.el === activeDescendant.el);\n\n if (index === -1) return;\n\n if (\n [\"ArrowDown\", \"ArrowUp\", \"ArrowRight\", \"ArrowLeft\"].includes(data.e.key)\n ) {\n data.e.preventDefault();\n\n const nextIndex =\n data.e.key === \"ArrowDown\" || data.e.key === \"ArrowRight\"\n ? index + 1\n : index - 1;\n\n if (nextIndex < 0 || nextIndex >= enabledNodes.length) return;\n\n const nextNode = enabledNodes[nextIndex];\n\n setActive(data.targetData.parent, nextNode, state);\n } else if (data.e.key === \" \") {\n data.e.preventDefault();\n\n state.selectedState && state.selectedState.nodes.includes(activeDescendant)\n ? setSelected(\n data.targetData.parent,\n state.selectedState.nodes.filter((x) => x.el !== activeDescendant.el),\n activeDescendant,\n state\n )\n : setSelected(\n data.targetData.parent,\n [activeDescendant],\n activeDescendant,\n state\n );\n\n //if (!state.selectedState)\n // updateLiveRegion(data.targetData.parent, \"\", true);\n } else if (data.e.key === \"Enter\" && state.selectedState) {\n if (\n state.selectedState.parent.el === data.targetData.parent.el &&\n state.activeState\n ) {\n if (state.selectedState.nodes[0].el === state.activeState.node.el) {\n updateLiveRegion(data.targetData.parent, \"Cannot drop item on itself\");\n\n return;\n }\n\n state.newActiveDescendant = state.selectedState.nodes[0];\n\n parentData.config.performSort({\n parent: data.targetData.parent,\n draggedNodes: state.selectedState.nodes,\n targetNode: state.activeState.node,\n });\n\n deselect([], data.targetData.parent, state);\n\n updateLiveRegion(data.targetData.parent, \"Drop successful\");\n } else if (\n state.activeState &&\n state.selectedState.parent.el !== data.targetData.parent.el &&\n validateTransfer({\n currentParent: data.targetData.parent,\n targetParent: state.selectedState.parent,\n initialParent: state.selectedState.parent,\n draggedNodes: state.selectedState.nodes,\n state,\n })\n ) {\n parentData.config.performTransfer({\n currentParent: state.selectedState.parent,\n targetParent: data.targetData.parent,\n initialParent: state.selectedState.parent,\n draggedNodes: state.selectedState.nodes,\n initialIndex: state.selectedState.nodes[0].data.index,\n state,\n targetNode: state.activeState.node,\n });\n\n state.newActiveDescendant = state.selectedState.nodes[0];\n\n setSelected(data.targetData.parent, [], undefined, state);\n\n updateLiveRegion(data.targetData.parent, \"Drop successful\");\n }\n }\n}\n\nexport function preventSortOnScroll() {\n let scrollTimeout: ReturnType<typeof setTimeout>;\n\n return () => {\n clearTimeout(scrollTimeout);\n\n if (state) state.preventEnter = true;\n\n scrollTimeout = setTimeout(() => {\n if (state) state.preventEnter = false;\n }, 100);\n };\n}\n\nexport function handleNodePointerover<T>(e: PointeroverNodeEvent<T>) {\n if (e.detail.targetData.parent.el === e.detail.state.currentParent.el)\n sort(e.detail, e.detail.state);\n else transfer(e.detail, e.detail.state);\n}\n\nexport function handleNodeDrop<T>(\n data: NodeDragEventData<T>,\n state: DragState<T> | SynthDragState<T>\n) {\n data.e.stopPropagation();\n\n dropped = true;\n\n const config = data.targetData.parent.data.config;\n\n config.handleEnd(state);\n}\n\nexport function handleDragend<T>(\n data: NodeDragEventData<T>,\n state: DragState<T>\n) {\n data.e.preventDefault();\n\n data.e.stopPropagation();\n\n if (dropped) {\n dropped = false;\n\n return;\n }\n\n const config = data.targetData.parent.data.config;\n\n config.handleEnd(state);\n}\n\nexport function handlePointercancel<T>(\n data: NodeEventData<T>,\n state: DragState<T> | SynthDragState<T> | BaseDragState<T>\n) {\n if (!isSynthDragState(state)) return;\n\n data.e.preventDefault();\n\n if (dropped) {\n dropped = false;\n\n return;\n }\n\n const config = parents.get(state.initialParent.el)?.config;\n\n if (config?.onDragend) {\n config.onDragend({\n parent: state.currentParent,\n values: parentValues(state.currentParent.el, state.currentParent.data),\n draggedNode: state.draggedNode,\n draggedNodes: state.draggedNodes,\n });\n }\n\n config?.handleEnd(state);\n}\n\nexport function handleEnd<T>(state: DragState<T> | SynthDragState<T>) {\n if (isSynthDragState(state)) cancelSynthScroll(state);\n\n if (\"longPressTimeout\" in state && state.longPressTimeout)\n clearTimeout(state.longPressTimeout);\n\n const config = parents.get(state.initialParent.el)?.config;\n\n const isSynth = isSynthDragState(state);\n\n const dropZoneClass = isSynth\n ? config?.synthDropZoneClass\n : config?.dropZoneClass;\n\n if (state.originalZIndex !== undefined)\n state.draggedNode.el.style.zIndex = state.originalZIndex;\n\n removeClass(\n state.draggedNodes.map((x) => x.el),\n dropZoneClass\n );\n\n removeClass(\n state.draggedNodes.map((x) => x.el),\n state.initialParent.data?.config?.longPressClass\n );\n\n removeClass(\n state.draggedNodes.map((x) => x.el),\n isSynth\n ? state.initialParent.data.config.synthDragPlaceholderClass\n : state.initialParent.data?.config?.dragPlaceholderClass\n );\n\n if (isSynth) state.clonedDraggedNode.remove();\n\n deselect(state.draggedNodes, state.currentParent, state);\n\n setActive(state.currentParent, undefined, state);\n\n resetState();\n\n state.selectedState = undefined;\n\n synthNodePointerDown = false;\n\n config?.onDragend?.({\n parent: state.currentParent,\n values: parentValues(state.currentParent.el, state.currentParent.data),\n draggedNode: state.draggedNode,\n draggedNodes: state.draggedNodes,\n });\n\n state.emit(\"dragEnded\", state);\n}\n\nexport function handleNodePointerup<T>(\n data: NodePointerEventData<T>,\n state: DragState<T> | SynthDragState<T> | BaseDragState<T>\n) {\n state.preventSynthDrag = false;\n\n if (!state.pointerSelection && state.selectedState)\n deselect(state.selectedState.nodes, data.targetData.parent, state);\n\n const config = data.targetData.parent.data.config;\n\n state.pointerSelection = false;\n\n synthNodePointerDown = false;\n\n if (\"longPressTimeout\" in state && state.longPressTimeout)\n clearTimeout(state.longPressTimeout);\n\n removeClass(\n data.targetData.parent.data.enabledNodes.map((x) => x.el),\n config.longPressClass\n );\n\n if (!isDragState(state)) return;\n\n config.handleEnd(state as DragState<T> | SynthDragState<T>);\n}\n\nexport function handleNodePointermove<T>(\n data: NodePointerEventData<T>,\n state: SynthDragState<T> | BaseDragState<T>\n) {\n if (data.targetData.parent.data.config.nativeDrag && !touchDevice) return;\n\n if (state.preventSynthDrag) return;\n\n const { x, y } = eventCoordinates(data.e as PointerEvent);\n\n if (\n !synthNodePointerDown ||\n (!isSynthDragState(state) &&\n !validateDragHandle({\n x,\n y,\n node: data.targetData.node,\n config: data.targetData.parent.data.config,\n }))\n )\n return;\n\n if (!isSynthDragState(state)) {\n const config = data.targetData.parent.data.config;\n\n if (config.longPress && !state.longPress) {\n clearTimeout(state.longPressTimeout);\n\n state.longPress = false;\n\n return;\n }\n\n const nodes = config.draggedNodes(data);\n\n config.dragstartClasses(data.targetData.node, nodes, config, true);\n\n const synthDragState = initSynthDrag(data, state, nodes);\n\n synthDragState.clonedDraggedNode.style.display =\n synthDragState.draggedNodeDisplay || \"\";\n\n synthMove(data, synthDragState);\n\n if (config.onDragstart)\n config.onDragstart(\n {\n parent: data.targetData.parent,\n values: parentValues(\n data.targetData.parent.el,\n data.targetData.parent.data\n ),\n draggedNode: synthDragState.draggedNode,\n draggedNodes: synthDragState.draggedNodes,\n position: synthDragState.initialIndex,\n },\n synthDragState\n );\n\n synthDragState.draggedNode.el.setPointerCapture(data.e.pointerId);\n\n synthDragState.pointerId = data.e.pointerId;\n\n return;\n }\n\n synthMove(data, state as SynthDragState<T>);\n}\n\nfunction initSynthDrag<T>(\n data: NodePointerEventData<T>,\n _state: BaseDragState<T>,\n draggedNodes: Array<NodeRecord<T>>\n): SynthDragState<T> {\n const config = data.targetData.parent.data.config;\n\n let dragImage: HTMLElement | undefined;\n\n if (config.synthDragImage) {\n dragImage = config.synthDragImage(data, draggedNodes);\n } else {\n if (!config.multiDrag || draggedNodes.length === 1) {\n dragImage = data.targetData.node.el.cloneNode(true) as HTMLElement;\n\n dragImage.id = data.targetData.node.el.id + \"-clone\";\n\n copyNodeStyle(data.targetData.node.el, dragImage);\n\n Object.assign(dragImage.style, {\n width: data.targetData.node.el.getBoundingClientRect().width,\n zIndex: 9999,\n pointerEvents: \"none\",\n });\n\n document.body.appendChild(dragImage);\n } else {\n const wrapper = document.createElement(\"div\");\n\n for (const node of draggedNodes) {\n const clonedNode = node.el.cloneNode(true) as HTMLElement;\n\n copyNodeStyle(node.el, clonedNode);\n\n clonedNode.style.pointerEvents = \"none\";\n\n clonedNode.id = node.el.id + \"-clone\";\n\n wrapper.append(clonedNode);\n }\n\n const { width } = draggedNodes[0].el.getBoundingClientRect();\n\n Object.assign(wrapper.style, {\n display: \"flex\",\n flexDirection: \"column\",\n width: `${width}px`,\n position: \"fixed\",\n pointerEvents: \"none\",\n zIndex: \"9999\",\n left: \"-9999px\",\n });\n\n dragImage = wrapper;\n }\n }\n\n const display = dragImage.style.display;\n\n dragImage.style.display = \"none\";\n\n document.body.append(dragImage);\n\n dragImage.style.position = \"absolute\";\n\n const synthDragStateProps = {\n clonedDraggedEls: [],\n clonedDraggedNode: dragImage,\n draggedNodeDisplay: display,\n synthDragScrolling: false,\n };\n\n const synthDragState = setDragState({\n ...dragS