@formkit/drag-and-drop
Version:
Drag and drop package.
1 lines • 192 kB
Source Map (JSON)
{"version":3,"sources":["../src/utils.ts","../src/plugins/animations/index.ts","../src/plugins/insert/index.ts","../src/plugins/drop-or-swap/index.ts","../src/index.ts"],"sourcesContent":["/**\n * Function to prevent default behavior of an event.\n *\n * @param {Event} e - The event to prevent default behavior of.\n */\nexport function pd(e: Event) {\n e.preventDefault();\n}\n\n/**\n * Function to stop propagation of an event.\n *\n * @param {Event} e - The event to stop propagation of.\n */\nexport function sp(e: Event) {\n e.stopPropagation();\n}\n\n/**\n * Function to get the bounding client rect of an element.\n *\n * @param {HTMLElement} el - The element to get the bounding client rect of.\n *\n * @returns {ClientRect} The bounding client rect of the element.\n */\nexport function rect(el: HTMLElement): ClientRect {\n return el.getBoundingClientRect();\n}\n\n/**\n * Function to create an emitter.\n *\n * @returns {[Function, Function]} A tuple containing emit and on functions\n */\nexport function createEmitter<T>() {\n const callbacks = new Map<string, Array<(data: T) => void>>();\n\n const emit = function (eventName: string, data: T) {\n if (!callbacks.get(eventName)) return;\n\n callbacks.get(eventName)!.forEach((cb) => {\n cb(data);\n });\n };\n\n const on = function (eventName: string, callback: (data: T) => void) {\n const cbs = callbacks.get(eventName) ?? [];\n\n cbs.push(callback);\n\n callbacks.set(eventName, cbs);\n };\n\n return [emit, on] as const;\n}\n\n/**\n * The emit and on functions for drag and drop.\n *\n * @type {[Function, Function]}\n */\nexport const [emit, on] = createEmitter();\n\n/**\n * A regular expression to test for a valid date string.\n *\n * @param x - A RegExp to compare.\n * @param y - A RegExp to compare.\n * @public\n */\nexport function eqRegExp(x: RegExp, y: RegExp): boolean {\n return (\n x.source === y.source &&\n x.flags.split(\"\").sort().join(\"\") === y.flags.split(\"\").sort().join(\"\")\n );\n}\n\n/**\n * Compare two values for equality, optionally at depth.\n *\n * @param valA - First value.\n * @param valB - Second value.\n * @param deep - If it will compare deeply if it's an object.\n * @param explicit - An array of keys to explicity check.\n *\n * @returns `boolean`\n *\n * @public\n */\nexport function eq(\n valA: unknown,\n valB: unknown,\n deep = true,\n explicit: string[] = [\"__key\"]\n): boolean {\n if (valA === valB) return true;\n\n if (\n typeof valB === \"object\" &&\n typeof valA === \"object\" &&\n valA !== null &&\n valB !== null\n ) {\n if (valA instanceof Map) return false;\n if (valA instanceof Set) return false;\n if (valA instanceof Date && valB instanceof Date)\n return valA.getTime() === valB.getTime();\n if (valA instanceof RegExp && valB instanceof RegExp)\n return eqRegExp(valA, valB);\n if (valA === null || valB === null) return false;\n\n const objA = valA as Record<string, unknown>;\n const objB = valB as Record<string, unknown>;\n\n if (Object.keys(objA).length !== Object.keys(objB).length) return false;\n\n for (const k of explicit) {\n if ((k in objA || k in objB) && objA[k] !== objB[k]) return false;\n }\n\n for (const key in objA) {\n if (!(key in objB)) return false;\n if (objA[key] !== objB[key] && !deep) return false;\n if (deep && !eq(objA[key], objB[key], deep, explicit)) return false;\n }\n return true;\n }\n return false;\n}\n\n/**\n * Split a class name into an array of class names.\n *\n * @param className - The class name to split.\n *\n * @returns An array of class names.\n */\nexport function splitClass(className: string): Array<string> {\n return className.split(\" \").filter((x) => x);\n}\n\nexport function getRealCoords(el: HTMLElement): {\n top: number;\n bottom: number;\n left: number;\n right: number;\n height: number;\n width: number;\n} {\n const { top, bottom, left, right, height, width } =\n el.getBoundingClientRect();\n\n const scrollLeft = window.scrollX || document.documentElement.scrollLeft;\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n\n return {\n top: top + scrollTop,\n bottom: bottom + scrollTop,\n left: left + scrollLeft,\n right: right + scrollLeft,\n height,\n width,\n };\n}\n\nexport function eventCoordinates(data: DragEvent | PointerEvent) {\n return { x: data.clientX, y: data.clientY };\n}\n","import type { SetupNodeData, Node } from \"../../types\";\nimport type { AnimationsConfig } from \"./types\";\nimport { state, parents, isDragState } from \"../../index\";\n\nexport function animations(animationsConfig: Partial<AnimationsConfig> = {}) {\n const slideUp = [\n {\n transform: `translateY(${animationsConfig.yScale || 50}%)`,\n },\n {\n transform: `translateY(${animationsConfig.yScale || 0}%)`,\n },\n ];\n\n const slideDown = [\n {\n transform: `translateY(-${animationsConfig.yScale || 50}%)`,\n },\n {\n transform: `translateY(${animationsConfig.yScale || 0}%)`,\n },\n ];\n\n const slideLeft = [\n {\n transform: `translateX(${animationsConfig.xScale || 50}%)`,\n },\n {\n transform: `translateX(${animationsConfig.xScale || 0}%)`,\n },\n ];\n\n const slideRight = [\n {\n transform: `translateX(-${animationsConfig.xScale || 50}%)`,\n },\n {\n transform: `translateX(${animationsConfig.xScale || 0}%)`,\n },\n ];\n return (parent: HTMLElement) => {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n return {\n setup() {\n if (document.head.querySelector(\"[data-drag-and-drop]\")) return;\n },\n\n setupNodeRemap<T>(data: SetupNodeData<T>) {\n if (!isDragState(state)) return;\n\n const duration = animationsConfig.duration || 150;\n\n if (data.node.data.value === state.draggedNode.data.value) {\n switch (state.incomingDirection) {\n case \"below\":\n animate(data.node.el, slideUp, duration);\n\n break;\n case \"above\":\n animate(data.node.el, slideDown, duration);\n\n break;\n case \"left\":\n animate(data.node.el, slideRight, duration);\n\n break;\n case \"right\":\n animate(data.node.el, slideLeft, duration);\n\n break;\n }\n\n return;\n }\n\n if (\n !state.affectedNodes\n .map((x) => x.data.value)\n .includes(data.node.data.value)\n )\n return;\n\n const nodeRect = data.node.el.getBoundingClientRect();\n\n const nodeIndex = state.affectedNodes.findIndex(\n (x) => x.data.value === data.node.data.value\n );\n\n const draggedNodeIndex = state.draggedNode.data.index;\n\n const ascendingDirection = draggedNodeIndex >= state.targetIndex;\n\n let adjacentNode;\n\n if (ascendingDirection) {\n adjacentNode = state.affectedNodes[nodeIndex + 1]\n ? state.affectedNodes[nodeIndex + 1]\n : state.affectedNodes[nodeIndex - 1];\n } else {\n adjacentNode = state.affectedNodes[nodeIndex - 1]\n ? state.affectedNodes[nodeIndex - 1]\n : state.affectedNodes[nodeIndex + 1];\n }\n\n if (adjacentNode) {\n const xDiff = Math.abs(\n nodeRect.x - adjacentNode.el.getBoundingClientRect().x\n );\n\n const yDiff = Math.abs(\n nodeRect.y - adjacentNode.el.getBoundingClientRect().y\n );\n\n if (xDiff > yDiff && ascendingDirection) {\n animate(data.node.el, slideRight, duration);\n } else if (xDiff > yDiff && !ascendingDirection) {\n animate(data.node.el, slideLeft, duration);\n }\n } else {\n switch (state.incomingDirection) {\n case \"below\":\n animate(data.node.el, slideDown, duration);\n\n break;\n case \"above\":\n animate(data.node.el, slideUp, duration);\n\n break;\n case \"left\":\n animate(data.node.el, slideLeft, duration);\n\n break;\n case \"right\":\n animate(data.node.el, slideRight, duration);\n\n break;\n }\n }\n },\n };\n };\n}\n\nfunction animate(\n node: Node,\n animation: Keyframe[] | PropertyIndexedKeyframes,\n duration: number\n) {\n if (!state) return;\n\n state.preventEnter = true;\n\n node.animate(animation, {\n duration: duration,\n easing: \"ease-in-out\",\n });\n\n setTimeout(() => {\n if (!state) return;\n\n state.preventEnter = false;\n }, duration);\n}\n","import type { InsertConfig } from \"../../types\";\nimport type {\n DragState,\n NodeDragEventData,\n NodeRecord,\n ParentEventData,\n PointeroverParentEvent,\n ParentRecord,\n SynthDragState,\n InsertEvent,\n BaseDragState,\n InsertState,\n Coordinates,\n Node,\n} from \"../../types\";\n\nimport {\n parents,\n parentValues,\n setParentValues,\n state,\n addParentClass,\n isDragState,\n isSynthDragState,\n removeClass,\n addEvents,\n remapNodes,\n nodes,\n} from \"../../index\";\n\nimport { eq, pd, eventCoordinates } from \"../../utils\";\n\nexport const insertState: InsertState<unknown> = {\n draggedOverNodes: [],\n draggedOverParent: null,\n targetIndex: 0,\n ascending: false,\n insertPoint: null,\n dragging: false,\n};\n\nlet documentController: AbortController | undefined;\n\n// WIP: This is a work in progress and not yet fully functional\nexport function insert<T>(insertConfig: InsertConfig<T>) {\n return (parent: HTMLElement) => {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n const insertParentConfig = {\n ...parentData.config,\n insertConfig,\n };\n\n return {\n teardown() {\n if (parentData.abortControllers.root) {\n parentData.abortControllers.root.abort();\n }\n },\n setup() {\n insertParentConfig.handleNodeDragover =\n insertConfig.handleNodeDragover || handleNodeDragover;\n\n insertParentConfig.handleParentPointerover =\n insertConfig.handleParentPointerover || handleParentPointerover;\n\n insertParentConfig.handleNodePointerover =\n insertConfig.handleNodePointerover || handleParentPointerover;\n\n insertParentConfig.handleParentDragover =\n insertConfig.handleParentDragover || handleParentDragover;\n\n const originalHandleend = insertParentConfig.handleEnd;\n\n insertParentConfig.handleEnd = (\n state: DragState<T> | SynthDragState<T>\n ) => {\n handleEnd(state);\n\n originalHandleend(state);\n };\n\n parentData.on(\"dragStarted\", () => {\n documentController = addEvents(document, {\n dragover: checkPosition,\n pointermove: checkPosition,\n });\n });\n\n parentData.on(\"dragEnded\", () => {\n documentController?.abort();\n });\n\n parentData.config = insertParentConfig;\n\n state.on(\"dragStarted\", () => {\n defineRanges(parent);\n });\n\n state.on(\"scrollStarted\", () => {\n if (insertState.insertPoint)\n insertState.insertPoint.el.style.display = \"none\";\n });\n\n state.on(\"scrollEnded\", () => {\n defineRanges(parent);\n });\n\n const firstScrollableParent = findFirstOverflowingParent(parent);\n\n if (firstScrollableParent) {\n firstScrollableParent.addEventListener(\n \"scroll\",\n defineRanges.bind(null, parent)\n );\n }\n\n window.addEventListener(\"resize\", defineRanges.bind(null, parent));\n },\n };\n };\n}\n\nfunction findFirstOverflowingParent(element: HTMLElement): HTMLElement | null {\n let parent = element.parentElement;\n\n while (parent) {\n const { overflow, overflowY, overflowX } = getComputedStyle(parent);\n\n // Check if the overflow property is set to scroll, auto, or hidden (anything other than visible)\n const isOverflowSet =\n overflow !== \"visible\" ||\n overflowY !== \"visible\" ||\n overflowX !== \"visible\";\n\n // Check if there is actual overflow (scrolling)\n const isOverflowing =\n parent.scrollHeight > parent.clientHeight ||\n parent.scrollWidth > parent.clientWidth;\n const hasScrollPosition = parent.scrollTop > 0 || parent.scrollLeft > 0;\n\n if (isOverflowSet && (isOverflowing || hasScrollPosition)) {\n return parent;\n }\n\n parent = parent.parentElement;\n }\n\n return null; // No overflowing parent found\n}\n\nfunction checkPosition(e: DragEvent | PointerEvent) {\n if (!isDragState(state)) return;\n\n const el = document.elementFromPoint(e.clientX, e.clientY);\n\n // 1. If the element is not an HTMLElement or is the insert point itself, do nothing.\n if (!(el instanceof HTMLElement) || el === insertState.insertPoint?.el) {\n return;\n }\n\n // 2. Traverse up the DOM from the element under the cursor\n // to see if any ancestor is a registered parent.\n let isWithinAParent = false;\n let current: HTMLElement | null = el;\n while (current) {\n if (nodes.has(current as Node) || parents.has(current)) {\n isWithinAParent = true;\n break; // Found a registered parent ancestor\n }\n if (current === document.body) break; // Stop if we reach the body\n current = current.parentElement;\n }\n\n // 3. If the cursor is NOT within any registered parent...\n if (!isWithinAParent) {\n // Hide the insert point if it exists\n if (insertState.insertPoint) {\n insertState.insertPoint.el.style.display = \"none\";\n }\n\n // Remove drop zone class if a parent was previously being dragged over\n if (insertState.draggedOverParent) {\n removeClass(\n [insertState.draggedOverParent.el],\n insertState.draggedOverParent.data.config.dropZoneClass\n );\n }\n\n // Reset insert state related to dragged over elements\n insertState.draggedOverNodes = [];\n insertState.draggedOverParent = null;\n\n // Reset current parent in the main state back to the initial one\n state.currentParent = state.initialParent;\n }\n // 4. If the cursor IS within a registered parent, do nothing in this function.\n // Other event handlers will manage the insertion point positioning.\n}\n\ninterface Range {\n x: [number, number];\n y: [number, number];\n vertical: boolean;\n}\n\nfunction createVerticalRange(\n nodeCoords: Coordinates,\n otherCoords: Coordinates | undefined,\n isAscending: boolean\n): Range {\n const center = nodeCoords.top + nodeCoords.height / 2;\n\n if (!otherCoords) {\n const offset = nodeCoords.height / 2 + 10;\n return {\n y: isAscending ? [center, center + offset] : [center - offset, center],\n x: [nodeCoords.left, nodeCoords.right],\n vertical: true,\n };\n }\n\n const otherEdge = isAscending ? otherCoords.top : otherCoords.bottom;\n const nodeEdge = isAscending ? nodeCoords.bottom : nodeCoords.top;\n\n let midpoint: number;\n let range: [number, number];\n\n if (isAscending) {\n // Midpoint between current node's bottom and next node's top\n midpoint = nodeEdge + (otherEdge - nodeEdge) / 2; // nodeCoords.bottom + (otherCoords.top - nodeCoords.bottom) / 2\n range = [center, midpoint]; // Range from node center down to midpoint\n } else {\n // Midpoint between previous node's bottom and current node's top\n midpoint = otherEdge + (nodeEdge - otherEdge) / 2; // otherCoords.bottom + (nodeCoords.top - otherCoords.bottom) / 2\n range = [midpoint, center]; // Range from midpoint down to node center\n }\n\n return {\n y: range,\n x: [nodeCoords.left, nodeCoords.right],\n vertical: true,\n };\n}\n\nfunction createHorizontalRange(\n nodeCoords: Coordinates,\n otherCoords: Coordinates | undefined,\n isAscending: boolean,\n lastInRow = false\n): Range {\n const center = nodeCoords.left + nodeCoords.width / 2;\n\n if (!otherCoords) {\n if (isAscending) {\n return {\n x: [center, center + nodeCoords.width],\n y: [nodeCoords.top, nodeCoords.bottom],\n vertical: false,\n };\n } else {\n return {\n x: [nodeCoords.left - 10, center],\n y: [nodeCoords.top, nodeCoords.bottom],\n vertical: false,\n };\n }\n }\n\n if (isAscending && lastInRow) {\n return {\n x: [center, nodeCoords.right + 10],\n y: [nodeCoords.top, nodeCoords.bottom],\n vertical: false,\n };\n }\n\n if (isAscending) {\n const nextNodeCenter = otherCoords.left + otherCoords.width / 2;\n return {\n x: [center, center + Math.abs(center - nextNodeCenter) / 2],\n y: [nodeCoords.top, nodeCoords.bottom],\n vertical: false,\n };\n } else {\n return {\n x: [\n otherCoords.right + Math.abs(otherCoords.right - nodeCoords.left) / 2,\n center,\n ],\n y: [nodeCoords.top, nodeCoords.bottom],\n vertical: false,\n };\n }\n}\n\nfunction getRealCoords(el: HTMLElement): Coordinates {\n const { top, bottom, left, right, height, width } =\n el.getBoundingClientRect();\n\n const scrollLeft = window.scrollX || document.documentElement.scrollLeft;\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n\n return {\n top: top + scrollTop,\n bottom: bottom + scrollTop,\n left: left + scrollLeft,\n right: right + scrollLeft,\n height,\n width,\n };\n}\n\nfunction defineRanges(parent: HTMLElement) {\n if (!isDragState(state) && !isSynthDragState(state)) return;\n\n const parentData = parents.get(parent);\n if (!parentData) return;\n\n const enabledNodes = parentData.enabledNodes;\n\n enabledNodes.forEach((node, index) => {\n node.data.range = {};\n\n const prevNode = enabledNodes[index - 1];\n const nextNode = enabledNodes[index + 1];\n const nodeCoords = getRealCoords(node.el);\n const prevNodeCoords = prevNode ? getRealCoords(prevNode.el) : undefined;\n const nextNodeCoords = nextNode ? getRealCoords(nextNode.el) : undefined;\n\n const aboveOrBelowPrevious =\n prevNodeCoords &&\n (nodeCoords.top > prevNodeCoords.bottom ||\n nodeCoords.bottom < prevNodeCoords.top);\n\n const aboveOrBelowAfter =\n nextNodeCoords &&\n (nodeCoords.top > nextNodeCoords.bottom ||\n nodeCoords.bottom < nextNodeCoords.top);\n\n const fullishWidth =\n parent.getBoundingClientRect().width * 0.8 < nodeCoords.width;\n\n if (fullishWidth) {\n node.data.range.ascending = createVerticalRange(\n nodeCoords,\n nextNodeCoords,\n true\n );\n node.data.range.descending = createVerticalRange(\n nodeCoords,\n prevNodeCoords,\n false\n );\n } else if (aboveOrBelowAfter && !aboveOrBelowPrevious) {\n node.data.range.ascending = createHorizontalRange(\n nodeCoords,\n nextNodeCoords,\n true,\n true\n );\n node.data.range.descending = createHorizontalRange(\n nodeCoords,\n prevNodeCoords,\n false\n );\n } else if (!aboveOrBelowPrevious && !aboveOrBelowAfter) {\n node.data.range.ascending = createHorizontalRange(\n nodeCoords,\n nextNodeCoords,\n true\n );\n node.data.range.descending = createHorizontalRange(\n nodeCoords,\n prevNodeCoords,\n false\n );\n } else if (aboveOrBelowPrevious && !nextNodeCoords) {\n node.data.range.ascending = createHorizontalRange(\n nodeCoords,\n undefined,\n true\n );\n } else if (aboveOrBelowPrevious && !aboveOrBelowAfter) {\n node.data.range.ascending = createHorizontalRange(\n nodeCoords,\n nextNodeCoords,\n true\n );\n node.data.range.descending = createHorizontalRange(\n nodeCoords,\n undefined,\n false\n );\n }\n });\n}\n\nexport function handleNodeDragover<T>(data: NodeDragEventData<T>) {\n const config = data.targetData.parent.data.config;\n\n if (!config.nativeDrag) return;\n\n data.e.preventDefault();\n}\n\nfunction processParentDragEvent<T>(\n e: DragEvent | PointerEvent,\n targetData: ParentEventData<T>[\"targetData\"],\n state: DragState<T>,\n nativeDrag = false\n) {\n pd(e);\n\n if (nativeDrag && e instanceof PointerEvent) return;\n\n const { x, y } = eventCoordinates(e);\n\n // Calculate global coordinates\n const clientX = x;\n const clientY = y;\n\n const scrollLeft = window.scrollX || document.documentElement.scrollLeft;\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n\n state.coordinates.x = clientX + scrollLeft;\n state.coordinates.y = clientY + scrollTop;\n\n const nestedParent = targetData.parent.data.nestedParent;\n\n let realTargetParent = targetData.parent;\n\n if (nestedParent) {\n const rect = nestedParent.el.getBoundingClientRect();\n\n if (state.coordinates.y > rect.top && state.coordinates.y < rect.bottom)\n realTargetParent = nestedParent;\n }\n\n if (realTargetParent.el === state.currentParent?.el) {\n moveBetween(realTargetParent, state);\n } else {\n moveOutside(realTargetParent, state);\n }\n\n state.currentParent = realTargetParent;\n}\n\nexport function handleParentDragover<T>(\n data: ParentEventData<T>,\n state: DragState<T>\n) {\n processParentDragEvent(data.e as DragEvent, data.targetData, state, true);\n}\n\nexport function handleParentPointerover<T>(data: PointeroverParentEvent<T>) {\n const { detail } = data;\n\n const { state, targetData } = detail;\n\n if (state.scrolling) return;\n\n processParentDragEvent(detail.e, targetData, state);\n}\n\nexport function moveBetween<T>(data: ParentRecord<T>, state: DragState<T>) {\n if (data.data.config.sortable === false) return;\n\n if (\n data.el === insertState.draggedOverParent?.el &&\n insertState.draggedOverParent.data.getValues(data.el).length === 0\n ) {\n return;\n } else if (insertState.draggedOverParent?.el) {\n removeClass(\n [insertState.draggedOverParent.el],\n insertState.draggedOverParent.data.config.dropZoneClass\n );\n insertState.draggedOverParent = null;\n }\n\n const foundRange = findClosest(data.data.enabledNodes, state);\n\n if (!foundRange) return;\n\n const key = foundRange[1] as \"ascending\" | \"descending\";\n\n if (foundRange) {\n const position = foundRange[0].data.range\n ? foundRange[0].data.range[key]\n : undefined;\n\n if (position)\n positionInsertPoint(\n data,\n position,\n foundRange[1] === \"ascending\",\n foundRange[0],\n insertState as InsertState<T>\n );\n }\n}\n\nfunction moveOutside<T>(data: ParentRecord<T>, state: DragState<T>) {\n if (data.el === state.currentParent.el) return false;\n\n const targetConfig = data.data.config;\n\n if (state.draggedNode.el.contains(data.el)) return false;\n\n if (targetConfig.dropZone === false) return;\n\n const initialParentConfig = state.initialParent.data.config;\n\n if (targetConfig.accepts) {\n return targetConfig.accepts(\n data,\n state.initialParent,\n state.currentParent,\n state\n );\n } else if (\n !targetConfig.group ||\n targetConfig.group !== initialParentConfig.group\n ) {\n return false;\n }\n\n const values = data.data.getValues(data.el);\n\n if (!values.length) {\n addParentClass([data.el], targetConfig.dropZoneClass);\n\n insertState.draggedOverParent = data as ParentRecord<unknown>;\n\n const insertPoint = insertState.insertPoint;\n\n if (insertPoint) insertPoint.el.style.display = \"none\";\n } else {\n removeClass([state.currentParent.el], targetConfig.dropZoneClass);\n\n const enabledNodes = data.data.enabledNodes;\n\n const foundRange = findClosest(enabledNodes, state);\n\n if (!foundRange) return;\n\n const key = foundRange[1] as \"ascending\" | \"descending\";\n\n if (foundRange) {\n const position = foundRange[0].data.range\n ? foundRange[0].data.range[key]\n : undefined;\n\n if (position)\n positionInsertPoint(\n data,\n position,\n foundRange[1] === \"ascending\",\n foundRange[0],\n insertState as InsertState<T>\n );\n }\n }\n}\n\nfunction findClosest<T>(enabledNodes: NodeRecord<T>[], state: DragState<T>) {\n let foundRange: [NodeRecord<T>, string] | null = null;\n\n for (let x = 0; x < enabledNodes.length; x++) {\n if (!state || !enabledNodes[x].data.range) continue;\n\n if (enabledNodes[x].data.range!.ascending) {\n if (\n state.coordinates.y > enabledNodes[x].data.range!.ascending!.y[0] &&\n state.coordinates.y < enabledNodes[x].data.range!.ascending!.y[1] &&\n state.coordinates.x > enabledNodes[x].data.range!.ascending!.x[0] &&\n state.coordinates.x < enabledNodes[x].data.range!.ascending!.x[1]\n ) {\n foundRange = [enabledNodes[x], \"ascending\"];\n\n return foundRange;\n }\n }\n\n if (enabledNodes[x].data.range!.descending) {\n if (\n state.coordinates.y > enabledNodes[x].data.range!.descending!.y[0] &&\n state.coordinates.y < enabledNodes[x].data.range!.descending!.y[1] &&\n state.coordinates.x > enabledNodes[x].data.range!.descending!.x[0] &&\n state.coordinates.x < enabledNodes[x].data.range!.descending!.x[1]\n ) {\n foundRange = [enabledNodes[x], \"descending\"];\n\n return foundRange;\n }\n }\n }\n}\n\nfunction createInsertPoint<T>(\n parent: ParentRecord<T>,\n insertState: InsertState<T>\n) {\n const insertPoint = parent.data.config.insertConfig?.insertPoint({\n el: parent.el,\n data: parent.data,\n });\n\n if (!insertPoint)\n throw new Error(\"Insert point not found\", { cause: parent });\n\n insertState.insertPoint = {\n parent,\n el: insertPoint,\n };\n\n document.body.appendChild(insertPoint);\n\n Object.assign(insertPoint.style, {\n position: \"absolute\",\n display: \"none\",\n });\n}\n\nfunction removeInsertPoint<T>(insertState: InsertState<T>) {\n if (insertState.insertPoint?.el) insertState.insertPoint.el.remove();\n\n insertState.insertPoint = null;\n}\n\nfunction positionInsertPoint<T>(\n parent: ParentRecord<T>,\n position: { x: number[]; y: number[]; vertical: boolean },\n ascending: boolean,\n node: NodeRecord<T>,\n insertState: InsertState<T>\n) {\n if (insertState.insertPoint?.el !== parent.el) {\n removeInsertPoint(insertState);\n\n createInsertPoint(parent, insertState);\n }\n\n insertState.draggedOverNodes = [node];\n\n if (!insertState.insertPoint) return;\n\n insertState.insertPoint.el.style.display = \"block\";\n\n if (position.vertical) {\n const insertPointHeight =\n insertState.insertPoint.el.getBoundingClientRect().height;\n const targetY = position.y[ascending ? 1 : 0];\n const topPosition = targetY - insertPointHeight / 2;\n\n Object.assign(insertState.insertPoint.el.style, {\n top: `${topPosition}px`,\n left: `${position.x[0]}px`,\n right: `${position.x[1]}px`,\n width: `${position.x[1] - position.x[0]}px`,\n });\n } else {\n const leftPosition =\n position.x[ascending ? 1 : 0] -\n insertState.insertPoint.el.getBoundingClientRect().width / 2;\n insertState.insertPoint.el.style.left = `${leftPosition}px`;\n\n Object.assign(insertState.insertPoint.el.style, {\n top: `${position.y[0]}px`,\n bottom: `${position.y[1]}px`,\n height: `${position.y[1] - position.y[0]}px`,\n });\n }\n\n insertState.targetIndex = node.data.index;\n\n insertState.ascending = ascending;\n}\n\nexport function handleParentDrop<T>(_data: NodeDragEventData<T>) {}\n\n/**\n * Performs the actual insertion of the dragged nodes into the target parent.\n *\n * @param state - The current drag state.\n */\nexport function handleEnd<T>(\n state: DragState<T> | SynthDragState<T> | BaseDragState<T>\n) {\n if (!isDragState(state) && !isSynthDragState(state)) return;\n\n const insertPoint = insertState.insertPoint;\n\n if (!insertState.draggedOverParent) {\n const draggedParentValues = parentValues(\n state.initialParent.el,\n state.initialParent.data\n );\n\n const transferred = state.initialParent.el !== state.currentParent.el;\n\n remapNodes(state.initialParent.el);\n\n const draggedValues = state.draggedNodes.map((node) => node.data.value);\n\n const enabledNodes = [...state.initialParent.data.enabledNodes];\n\n const originalIndex = state.draggedNodes[0].data.index;\n\n const targetIndex = insertState.targetIndex;\n\n if (\n !transferred &&\n insertState.draggedOverNodes[0] &&\n insertState.draggedOverNodes[0].el !== state.draggedNodes[0].el\n ) {\n const newParentValues = [\n ...draggedParentValues.filter(\n (x) => !draggedValues.some((y) => eq(x, y))\n ),\n ];\n\n let index = insertState.draggedOverNodes[0].data.index;\n\n if (\n insertState.targetIndex > state.draggedNodes[0].data.index &&\n !insertState.ascending\n ) {\n index--;\n } else if (\n insertState.targetIndex < state.draggedNodes[0].data.index &&\n insertState.ascending\n ) {\n index++;\n }\n\n newParentValues.splice(index, 0, ...draggedValues);\n\n setParentValues(state.initialParent.el, state.initialParent.data, [\n ...newParentValues,\n ]);\n\n if (state.initialParent.data.config.onSort) {\n const sortEventData = {\n parent: {\n el: state.initialParent.el,\n data: state.initialParent.data,\n } as ParentRecord<unknown>,\n previousValues: [...draggedParentValues],\n previousNodes: [...enabledNodes],\n nodes: [...state.initialParent.data.enabledNodes],\n values: [...newParentValues],\n draggedNodes: state.draggedNodes,\n targetNodes: insertState.draggedOverNodes,\n previousPosition: originalIndex,\n position: index,\n state: state as DragState<unknown>,\n };\n\n state.initialParent.data.config.onSort(sortEventData);\n }\n } else if (transferred && insertState.draggedOverNodes.length) {\n const draggedParentValues = parentValues(\n state.initialParent.el,\n state.initialParent.data\n );\n\n // For the time being, we will not be remoing the value of the original dragged parent.\n let index = insertState.draggedOverNodes[0].data.index || 0;\n\n if (insertState.ascending) index++;\n\n const insertValues = state.initialParent.data.config.insertConfig\n ?.dynamicValues\n ? state.initialParent.data.config.insertConfig.dynamicValues({\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n draggedNodes: state.draggedNodes,\n targetNodes: insertState.draggedOverNodes as NodeRecord<T>[],\n targetIndex: index,\n })\n : draggedValues;\n\n const newParentValues = [\n ...draggedParentValues.filter(\n (x) => !draggedValues.some((y) => eq(x, y))\n ),\n ];\n\n if (state.currentParent.el.contains(state.initialParent.el)) {\n // Update initial parent values first\n setParentValues(state.initialParent.el, state.initialParent.data, [\n ...newParentValues,\n ]);\n\n // Now get the target parent values.\n const targetParentValues = parentValues(\n state.currentParent.el,\n state.currentParent.data\n );\n\n targetParentValues.splice(index, 0, ...insertValues);\n\n setParentValues(state.currentParent.el, state.currentParent.data, [\n ...targetParentValues,\n ]);\n } else {\n setParentValues(state.initialParent.el, state.initialParent.data, [\n ...newParentValues,\n ]);\n\n const targetParentValues = parentValues(\n state.currentParent.el,\n state.currentParent.data\n );\n\n targetParentValues.splice(index, 0, ...insertValues);\n\n setParentValues(state.currentParent.el, state.currentParent.data, [\n ...targetParentValues,\n ]);\n }\n\n const data = {\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n initialParent: state.initialParent,\n draggedNodes: state.draggedNodes,\n targetIndex,\n targetNodes: insertState.draggedOverNodes as NodeRecord<T>[],\n state,\n };\n\n if (state.initialParent.data.config.onTransfer)\n state.initialParent.data.config.onTransfer(data);\n if (state.currentParent.data.config.onTransfer)\n state.currentParent.data.config.onTransfer(data);\n }\n } else if (insertState.draggedOverParent) {\n if (state.currentParent.el.contains(state.initialParent.el)) {\n const draggedParentValues = parentValues(\n state.initialParent.el,\n state.initialParent.data\n );\n\n const newParentValues = [\n ...draggedParentValues.filter(\n (x) => !draggedValues.some((y) => eq(x, y))\n ),\n ];\n\n setParentValues(state.initialParent.el, state.initialParent.data, [\n ...newParentValues,\n ]);\n\n const draggedOverParentValues = parentValues(\n insertState.draggedOverParent.el,\n insertState.draggedOverParent.data\n );\n\n const draggedValues = state.draggedNodes.map((node) => node.data.value);\n\n const insertValues = state.initialParent.data.config.insertConfig\n ?.dynamicValues\n ? state.initialParent.data.config.insertConfig.dynamicValues({\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n draggedNodes: state.draggedNodes,\n targetNodes: insertState.draggedOverNodes as NodeRecord<T>[],\n })\n : draggedValues;\n\n draggedOverParentValues.push(...insertValues);\n\n setParentValues(\n insertState.draggedOverParent.el,\n insertState.draggedOverParent.data,\n [...draggedOverParentValues]\n );\n } else {\n const draggedValues = state.draggedNodes.map((node) => node.data.value);\n\n const draggedOverParentValues = parentValues(\n insertState.draggedOverParent.el,\n insertState.draggedOverParent.data\n );\n\n const insertValues = state.initialParent.data.config.insertConfig\n ?.dynamicValues\n ? state.initialParent.data.config.insertConfig.dynamicValues({\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n draggedNodes: state.draggedNodes,\n targetNodes: insertState.draggedOverNodes as NodeRecord<T>[],\n })\n : draggedValues;\n\n draggedOverParentValues.push(...insertValues);\n\n setParentValues(\n insertState.draggedOverParent.el,\n insertState.draggedOverParent.data,\n [...draggedOverParentValues]\n );\n\n const draggedParentValues = parentValues(\n state.initialParent.el,\n state.initialParent.data\n );\n\n const newParentValues = [\n ...draggedParentValues.filter(\n (x) => !draggedValues.some((y) => eq(x, y))\n ),\n ];\n\n setParentValues(state.initialParent.el, state.initialParent.data, [\n ...newParentValues,\n ]);\n }\n\n const data: InsertEvent<T> = {\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n draggedNodes: state.draggedNodes,\n targetNodes: insertState.draggedOverNodes as NodeRecord<T>[],\n state,\n };\n\n if (state.initialParent.data.config.insertConfig?.insertEvent)\n state.initialParent.data.config.insertConfig.insertEvent(data);\n if (state.currentParent.data.config.insertConfig?.insertEvent)\n state.currentParent.data.config.insertConfig.insertEvent(data);\n\n removeClass(\n [insertState.draggedOverParent.el],\n insertState.draggedOverParent.data.config.dropZoneClass\n );\n }\n\n if (insertPoint) insertPoint.el.style.display = \"none\";\n\n const dropZoneClass = isSynthDragState(state)\n ? state.initialParent.data.config.synthDropZoneClass\n : state.initialParent.data.config.dropZoneClass;\n\n removeClass(\n insertState.draggedOverNodes.map((node) => node.el),\n dropZoneClass\n );\n\n const dragPlaceholderClass =\n state.initialParent.data.config.dragPlaceholderClass;\n\n removeClass(\n state.draggedNodes.map((node) => node.el),\n dragPlaceholderClass\n );\n\n insertState.draggedOverNodes = [];\n\n insertState.draggedOverParent = null;\n}\n","import type {\n DropSwapConfig,\n NodeDragEventData,\n DragState,\n SynthDragState,\n NodeRecord,\n PointeroverNodeEvent,\n ParentDragEventData,\n PointeroverParentEvent,\n DropSwapState,\n ParentRecord,\n BaseDragState,\n} from \"../../types\";\nimport {\n parents,\n parentValues,\n setParentValues,\n addNodeClass,\n isSynthDragState,\n removeClass,\n addClass,\n state,\n addEvents,\n isDragState,\n} from \"../../index\";\n\nexport const dropSwapState: DropSwapState<unknown> = {\n draggedOverNodes: Array<NodeRecord<unknown>>(),\n initialDraggedIndex: undefined,\n transferred: false,\n dragging: false,\n};\n\n/**\n * Abort controller for the document.\n */\nlet documentController: AbortController | undefined;\n\nexport function dropOrSwap<T>(dropSwapConfig: DropSwapConfig<T> = {}) {\n return (parent: HTMLElement) => {\n const parentData = parents.get(parent);\n\n if (!parentData) return;\n\n const dropSwapParentConfig = {\n ...parentData.config,\n dropSwapConfig,\n };\n\n return {\n setup() {\n dropSwapParentConfig.handleNodeDragover =\n dropSwapConfig.handleNodeDragover || handleNodeDragover;\n\n dropSwapParentConfig.handleParentDragover =\n dropSwapConfig.handleParentDragover || handleParentDragover;\n\n dropSwapParentConfig.handleNodePointerover =\n dropSwapConfig.handleNodePointerover || handleNodePointerover;\n\n dropSwapParentConfig.handleParentPointerover =\n dropSwapConfig.handleParentPointerover || handeParentPointerover;\n\n const originalHandleend = dropSwapParentConfig.handleEnd;\n\n dropSwapParentConfig.handleEnd = (\n state: DragState<T> | SynthDragState<T>\n ) => {\n handleEnd(state);\n\n originalHandleend(state);\n };\n\n parentData.on(\"dragStarted\", () => {\n documentController = addEvents(document, {\n dragover: rootDragover,\n handleRootPointerover: rootPointerover,\n });\n });\n\n parentData.on(\"dragEnded\", () => {\n documentController?.abort();\n });\n\n parentData.config = dropSwapParentConfig;\n },\n };\n };\n}\n\nfunction rootDragover(_e: DragEvent) {\n if (!isDragState(state)) return;\n\n removeClass(\n [state.currentParent.el],\n state.currentParent.data.config.dropZoneParentClass\n );\n\n state.currentParent = state.initialParent;\n}\n\nfunction rootPointerover(_e: CustomEvent) {\n if (!isSynthDragState(state)) return;\n\n removeClass(\n [state.currentParent.el],\n state.currentParent.data.config.synthDropZoneParentClass\n );\n\n state.currentParent = state.initialParent;\n}\n\nfunction updateDraggedOverNodes<T>(\n data: PointeroverNodeEvent<T> | NodeDragEventData<T>,\n state: DragState<T> | SynthDragState<T>\n) {\n const targetData =\n \"detail\" in data ? data.detail.targetData : data.targetData;\n\n const config = targetData.parent.data.config;\n\n const dropZoneClass = isSynthDragState(state)\n ? config.synthDropZoneClass\n : config.dropZoneClass;\n\n removeClass(\n dropSwapState.draggedOverNodes.map((node) => node.el),\n dropZoneClass\n );\n\n const enabledNodes = targetData.parent.data.enabledNodes;\n\n if (!enabledNodes) return;\n\n dropSwapState.draggedOverNodes = enabledNodes.slice(\n targetData.node.data.index,\n targetData.node.data.index + state.draggedNodes.length\n );\n\n addNodeClass(\n dropSwapState.draggedOverNodes.map((node) => node.el),\n dropZoneClass,\n true\n );\n\n state.currentTargetValue = targetData.node.data.value;\n\n state.currentParent = targetData.parent;\n\n addClass(\n state.currentParent.el,\n isSynthDragState(state)\n ? config.synthDropZoneParentClass\n : config.dropZoneParentClass,\n state.currentParent.data,\n true\n );\n}\n\nfunction handleNodeDragover<T>(\n data: NodeDragEventData<T>,\n state: DragState<T>\n) {\n data.e.preventDefault();\n\n data.e.stopPropagation();\n\n updateDraggedOverNodes(data, state);\n}\n\nexport function handleParentDragover<T>(\n data: ParentDragEventData<T>,\n state: DragState<T>\n) {\n data.e.preventDefault();\n\n data.e.stopPropagation();\n\n const currentConfig = state.currentParent.data.config;\n\n removeClass(\n dropSwapState.draggedOverNodes.map((node) => node.el),\n currentConfig.dropZoneClass\n );\n\n removeClass([state.currentParent.el], currentConfig.dropZoneParentClass);\n\n const config = data.targetData.parent.data.config;\n\n addClass(\n data.targetData.parent.el,\n config.dropZoneParentClass,\n data.targetData.parent.data,\n true\n );\n\n dropSwapState.draggedOverNodes = [];\n\n state.currentParent = data.targetData.parent;\n}\n\nexport function handeParentPointerover<T>(data: PointeroverParentEvent<T>) {\n const currentConfig = data.detail.state.currentParent.data.config;\n\n removeClass(\n dropSwapState.draggedOverNodes.map((node) => node.el),\n currentConfig.synthDropZoneClass\n );\n\n removeClass(\n [data.detail.state.currentParent.el],\n currentConfig.synthDropZoneParentClass\n );\n\n const config = data.detail.targetData.parent.data.config;\n\n addClass(\n data.detail.targetData.parent.el,\n config.synthDropZoneParentClass,\n data.detail.targetData.parent.data,\n true\n );\n\n dropSwapState.draggedOverNodes = [];\n\n data.detail.state.currentParent = data.detail.targetData.parent;\n}\n\nfunction handleNodePointerover<T>(data: PointeroverNodeEvent<T>) {\n if (!isSynthDragState(data.detail.state)) return;\n\n updateDraggedOverNodes(data, data.detail.state);\n}\n\nfunction swapElements<T>(\n arr1: T[],\n arr2: T[] | null,\n index1: number | number[],\n index2: number\n): T[] | [T[], T[]] {\n const indices1 = Array.isArray(index1) ? index1 : [index1];\n\n if (arr2 === null) {\n const elementsFromArr1 = indices1.map((i) => arr1[i]);\n\n const elementFromArr2 = arr1[index2];\n\n arr1.splice(index2, 1, ...elementsFromArr1);\n\n indices1.forEach((i, idx) => {\n arr1[i] = idx === 0 ? elementFromArr2 : (undefined as unknown as T);\n });\n\n return arr1.filter((el) => el !== undefined);\n } else {\n const elementsFromArr1 = indices1.map((i) => arr1[i]);\n\n const elementFromArr2 = arr2[index2];\n\n arr2.splice(index2, 1, ...elementsFromArr1);\n\n indices1.forEach((i, idx) => {\n arr1[i] = idx === 0 ? elementFromArr2 : (undefined as unknown as T);\n });\n\n return [arr1.filter((el) => el !== undefined), arr2];\n }\n}\n\nfunction handleEnd<T>(state: DragState<T> | SynthDragState<T>) {\n const isSynth = isSynthDragState(state);\n\n removeClass(\n [state.currentParent.el],\n isSynth\n ? state.currentParent.data.config.synthDropZoneParentClass\n : state.currentParent.data.config.dropZoneParentClass\n );\n\n removeClass(\n dropSwapState.draggedOverNodes.map((node) => node.el),\n isSynth\n ? state.currentParent.data.config.synthDropZoneClass\n : state.currentParent.data.config.dropZoneClass\n );\n\n const values = parentValues(state.currentParent.el, state.currentParent.data);\n\n const draggedValues = state.draggedNodes.map((node) => node.data.value);\n\n const newValues = values.filter((x) => !draggedValues.includes(x));\n\n const targetIndex = dropSwapState.draggedOverNodes[0]?.data.index;\n\n const draggedIndex = state.draggedNodes[0].data.index;\n\n const initialParentValues = parentValues(\n state.initialParent.el,\n state.initialParent.data\n );\n\n if (targetIndex === undefined) {\n if (state.initialParent.el === state.currentParent.el) return;\n\n const newInitialValues = initialParentValues.filter(\n (x) => !draggedValues.includes(x)\n );\n\n setParentValues(\n state.initialParent.el,\n state.initialParent.data,\n newInitialValues\n );\n\n setParentValues(\n state.currentParent.el,\n state.currentParent.data,\n values.concat(draggedValues)\n );\n\n return;\n }\n\n let swap = false;\n\n const shouldSwap = state.initialParent.data.config.dropSwapConfig?.shouldSwap;\n\n if (shouldSwap)\n swap = shouldSwap({\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n draggedNodes: state.draggedNodes,\n targetNodes: dropSwapState.draggedOverNodes as NodeRecord<T>[],\n state,\n });\n\n if (state.initialParent.el === state.currentParent.el) {\n newValues.splice(targetIndex, 0, ...draggedValues);\n\n setParentValues(\n state.currentParent.el,\n state.currentParent.data,\n swap ? swapElements(values, null, draggedIndex, targetIndex) : newValues\n );\n\n if (state.initialParent.data.config.onSort) {\n state.initialParent.data.config.onSort({\n parent: {\n el: state.initialParent.el,\n data: state.initialParent.data,\n } as ParentRecord<unknown>,\n previousValues: [...initialParentValues],\n previousNodes: [...state.initialParent.data.enabledNodes],\n nodes: [...state.initialParent.data.enabledNodes],\n values: [...newValues],\n draggedNodes: state.draggedNodes,\n previousPosition: draggedIndex,\n position: targetIndex,\n targetNodes: dropSwapState.draggedOverNodes as NodeRecord<T>[],\n state: state as BaseDragState<unknown>,\n });\n }\n } else {\n if (swap) {\n const res = swapElements(\n initialParentValues,\n newValues,\n state.initialIndex,\n targetIndex\n );\n\n setParentValues(\n state.initialParent.el,\n state.initialParent.data,\n res[0] as T[]\n );\n\n setParentValues(\n state.currentParent.el,\n state.currentParent.data,\n res[1] as T[]\n );\n } else {\n const newInitialValues = initialParentValues.filter(\n (x) => !draggedValues.includes(x)\n );\n\n setParentValues(\n state.initialParent.el,\n state.initialParent.data,\n newInitialValues\n );\n\n newValues.splice(targetIndex, 0, ...draggedValues);\n\n setParentValues(\n state.currentParent.el,\n state.currentParent.data,\n newValues\n );\n }\n }\n\n if (state.currentParent.data.config.onTransfer) {\n state.currentParent.data.config.onTransfer({\n sourceParent: state.currentParent,\n targetParent: state.initialParent,\n initialParent: state.initialParent,\n draggedNodes: state.draggedNodes,\n targetIndex,\n state,\n targetNodes: dropSwapState.draggedOverNodes as NodeRecord<T>[],\n });\n }\n\n if (state.initialParent.data.config.onTransfer) {\n state.initialParent.data.config.onTransfer({\n sourceParent: state.initialParent,\n targetParent: state.currentParent,\n initialParent: state.initialParent,\n draggedNodes: state.draggedNodes,\n targetIndex,\n state,\n targetNodes: dropSwapState.draggedOverNodes as NodeRecord<T>[],\n });\n }\n}\n","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 SynthDragStateProps,\n ParentRecord,\n EventHandlers,\n NodeFromPoint,\n ParentFromPoint,\n ParentDragEventData,\n DragstartEventData,\n} from \"./types\";\n\nimport {\n pd,\n sp,\n on,\n emit,\n createEmitter,\n eq,\n splitClass,\n eventCoordinates,\n} from \"./utils\";\n\nexport * from \"./types\";\nexport { animations } from \"./plugins/animations\";\nexport { insert } from \"./plugins/insert\";\nexport { dropOrSwap } from \"./plugins/drop-or-swap\";\n\n/**\n * Check to see if code is running in a browser.\n */\nexport const isBrowser = typeof window !== \"undefined\";\n\n/**\n * Parents or \"lists\" are the containers that nodes or \"draggable items\" can be\n * dragged from and into.\n *\n * @type {WeakMap<HTMLElement, ParentData<unknown>>}\n */\nexport const parents: ParentsData<any> = new WeakMap<\n HTMLElement,\n ParentData<unknown>\n>();\n\n/**\n * Nodes are the draggable items and the direct descendants of the parents.\n *\n * @type {WeakMap<Node, NodeData<unknown>>}\n */\nexport const nodes: NodesData<any> = new WeakMap<Node, NodeData<