dnd-manager 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/drag-drop-manager.ts","../src/drag-preview-controller.ts"],"sourcesContent":["/**\n * Framework-agnostic Drag and Drop Manager\n *\n * A data-driven drag-and-drop utility that works with React and vanilla JS.\n * Uses data attributes and direct DOM manipulation for optimal performance.\n *\n * @template TItem - The type of data associated with draggable items\n * @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)\n */\n\nexport type DragDropConfig = {\n /**\n * Minimum distance in pixels the pointer must move before a drag operation starts.\n * Prevents accidental drags from small movements and allows normal scrolling on mobile.\n * Checked during `pointermove` before drag has started.\n * @default 10\n * @example\n * dragThreshold: 15 // Harder to start drag (better for mobile scroll)\n */\n dragThreshold?: number\n\n /**\n * Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.\n * Used to distinguish intentional clicks from cancelled drag attempts.\n * Only checked on `pointerup` if drag never started.\n * @default 10\n * @example\n * clickThreshold: 5 // Stricter click detection\n * clickThreshold: 15 // More forgiving for mobile taps\n */\n clickThreshold?: number\n\n /**\n * Distance in pixels from viewport edge that triggers auto-scrolling during drag.\n * When dragging near screen edges, the window automatically scrolls.\n * @default 100\n * @example\n * scrollThreshold: 50 // Start scrolling closer to edges\n * scrollThreshold: 150 // Larger safe zone before scrolling\n */\n scrollThreshold?: number\n\n /**\n * Scroll speed in pixels per animation frame during auto-scroll.\n * Higher values = faster scrolling when dragging near viewport edges.\n * @default 10\n * @example\n * scrollSpeed: 5 // Slower, smoother scroll\n * scrollSpeed: 20 // Faster scroll\n */\n scrollSpeed?: number\n\n /**\n * Cancel the current drag operation when Escape is pressed.\n * Useful for explicit keyboard cancellation.\n * @default true\n */\n cancelOnEscape?: boolean\n\n /**\n * Cancel the current drag operation when the pointer leaves the browser window.\n * Prevents \"stuck dragging\" when pointerup happens outside the app/window.\n * @default true\n */\n cancelOnPointerLeave?: boolean\n\n /**\n * The `data-kind` value(s) for draggable elements.\n * Can be a single kind or multiple kinds.\n * Each draggable element should still expose a single `data-kind` value.\n * @example\n * draggableKind: 'cell' // <div data-kind=\"cell\">\n * draggableKind: ['quest', 'bounty'] // <div data-kind=\"quest\">\n * draggableKind: 'card' // <div data-kind=\"card\">\n * draggableKind: 'list-item' // <div data-kind=\"list-item\">\n */\n draggableKind: string | string[]\n\n /**\n * The `data-kind` value(s) for droppable target elements.\n * Can be a single kind or multiple kinds.\n * Elements with matching kinds can receive dropped items.\n * Each droppable element should still expose a single `data-kind` value.\n * Often the same as `draggableKind` for reorderable lists.\n * @example\n * droppableKind: 'cell' // Can drop on cells\n * droppableKind: ['quest', 'bounty'] // Can drop on either kind\n * droppableKind: 'zone' // Can drop on zones\n */\n droppableKind: string | string[]\n}\n\nexport type PointerPosition = {\n x: number\n y: number\n}\n\nexport type DragDropCallbacks<TItem, TPosition> = {\n /** Called when determining if an element can be dragged */\n canDrag?: (element: HTMLElement, position: TPosition) => boolean\n /** Called when drag starts (after threshold is met) */\n onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void\n /** Called continuously during drag */\n onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void\n /** Called when item is dropped on a valid target */\n onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void\n /** Called when drag ends (regardless of success) */\n onDragEnd?: () => void\n /** Called on simple click/tap (no drag) */\n onClick?: (element: HTMLElement, position: TPosition) => void\n /** Called to extract item data from an element */\n getItemData?: (element: HTMLElement, position: TPosition) => TItem | null\n /** Called to get the custom position/coordinates of an element */\n getItemPosition: (element: HTMLElement, kind: string) => TPosition | null\n}\n\ntype DragState<TItem, TPosition> = {\n startPosition: PointerPosition | null\n sourceElement: HTMLElement | null\n sourcePosition: TPosition | null\n sourceItem: TItem | null\n activePointerId: number | null\n isDragging: boolean\n lastHoveredElement: HTMLElement | null\n}\n\nexport class DragDropManager<TItem = unknown, TPosition = unknown> {\n private container: HTMLElement\n private config: Required<DragDropConfig>\n private draggableKinds: string[]\n private droppableKinds: string[]\n private callbacks: DragDropCallbacks<TItem, TPosition>\n private state: DragState<TItem, TPosition> = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n activePointerId: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n private rafId: number | null = null\n private lastPointerPos: PointerPosition | null = null\n private boundHandlers: {\n pointerDown: (e: PointerEvent) => void\n pointerMove: (e: PointerEvent) => void\n pointerUp: (e: PointerEvent) => void\n pointerCancel: (e: PointerEvent) => void\n keyDown: (e: KeyboardEvent) => void\n mouseOut: (e: MouseEvent) => void\n }\n\n constructor(\n containerRef: { current: HTMLElement | null } | string | HTMLElement,\n config: DragDropConfig,\n callbacks: DragDropCallbacks<TItem, TPosition>,\n ) {\n // Resolve container element\n if (typeof containerRef === 'string') {\n const element = document.querySelector<HTMLElement>(containerRef)\n if (!element) {\n throw new Error(`DragDropManager: Could not find element with selector \"${containerRef}\"`)\n }\n this.container = element\n } else if (typeof containerRef === 'object' && 'current' in containerRef) {\n if (!containerRef.current) {\n throw new Error('DragDropManager: Ref.current is null')\n }\n this.container = containerRef.current\n } else {\n this.container = containerRef as HTMLElement\n }\n\n this.config = {\n dragThreshold: config.dragThreshold ?? 10,\n clickThreshold: config.clickThreshold ?? 10,\n scrollThreshold: config.scrollThreshold ?? 100,\n scrollSpeed: config.scrollSpeed ?? 10,\n cancelOnEscape: config.cancelOnEscape ?? true,\n cancelOnPointerLeave: config.cancelOnPointerLeave ?? true,\n draggableKind: config.draggableKind,\n droppableKind: config.droppableKind,\n }\n this.draggableKinds = this.normalizeKindList(this.config.draggableKind)\n this.droppableKinds = this.normalizeKindList(this.config.droppableKind)\n\n this.callbacks = callbacks\n\n // Bind handlers to preserve context\n this.boundHandlers = {\n pointerDown: this.handlePointerDown.bind(this),\n pointerMove: this.handlePointerMove.bind(this),\n pointerUp: this.handlePointerUp.bind(this),\n pointerCancel: this.handlePointerCancel.bind(this),\n keyDown: this.handleKeyDown.bind(this),\n mouseOut: this.handleMouseOut.bind(this),\n }\n\n this.attachListeners()\n }\n\n private attachListeners(): void {\n this.container.addEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.addEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.addEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('pointerup', this.boundHandlers.pointerUp)\n window.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('keydown', this.boundHandlers.keyDown)\n window.addEventListener('mouseout', this.boundHandlers.mouseOut)\n }\n\n private handlePointerDown(e: PointerEvent): void {\n if (this.state.startPosition) return\n\n const target = e.target as HTMLElement\n const draggableMatch = this.findElementByKinds(target, this.draggableKinds)\n\n if (!draggableMatch) return\n\n const { element: draggableElement, kind: sourceKind } = draggableMatch\n\n const position = this.callbacks.getItemPosition(draggableElement, sourceKind)\n if (!position) return\n\n // Check if element is empty (convention: data-empty=\"any-value\")\n const isEmpty = draggableElement.dataset.empty !== undefined\n if (isEmpty) return\n\n // Check canDrag callback\n if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) {\n return\n }\n\n // Get item data\n const item = this.callbacks.getItemData?.(draggableElement, position) ?? null\n if (!item) return\n\n // Prevent text selection during potential drag\n e.preventDefault()\n\n // Store start position and element info\n this.state.startPosition = { x: e.clientX, y: e.clientY }\n this.state.sourceElement = draggableElement\n this.state.sourcePosition = position\n this.state.sourceItem = item\n this.state.activePointerId = this.getPointerId(e)\n\n // Capture pointer events outside container boundaries when supported.\n if (\n this.state.activePointerId !== null &&\n typeof draggableElement.setPointerCapture === 'function'\n ) {\n try {\n draggableElement.setPointerCapture(this.state.activePointerId)\n } catch {\n // Ignore unsupported/inactive-pointer capture errors.\n }\n }\n }\n\n private handlePointerMove(e: PointerEvent): void {\n if (!this.isActivePointer(e)) return\n\n const currentPos: PointerPosition = { x: e.clientX, y: e.clientY }\n\n // Check if we should start dragging (threshold not yet met)\n if (this.state.startPosition && !this.state.isDragging) {\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n if (distance >= this.config.dragThreshold) {\n // Start drag\n this.state.isDragging = true\n\n if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {\n // Set data-dragging attribute on source\n this.state.sourceElement.setAttribute('data-dragging', 'true')\n\n // Notify drag start\n this.callbacks.onDragStart?.(\n this.state.sourceElement,\n this.state.sourcePosition,\n this.state.sourceItem,\n )\n }\n }\n return\n }\n\n if (!this.state.isDragging) return\n\n // Store latest position for RAF\n this.lastPointerPos = currentPos\n\n // Throttle updates to 60fps using RAF\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n if (!this.lastPointerPos) return\n\n const pos = this.lastPointerPos\n\n // Find hovered droppable element\n const elementUnderCursor = document.elementFromPoint(pos.x, pos.y) as HTMLElement\n const droppableMatch = this.findElementByKinds(elementUnderCursor, this.droppableKinds)\n const droppableElement = droppableMatch?.element ?? null\n\n // Update hover feedback\n if (droppableElement !== this.state.lastHoveredElement) {\n // Remove hover from previous element\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Add hover to current element (including the source element).\n // This enables \"drag over itself\" states (e.g. combined data-dragging + data-hovered styling).\n if (droppableElement) {\n droppableElement.setAttribute('data-hovered', 'true')\n this.state.lastHoveredElement = droppableElement\n } else {\n this.state.lastHoveredElement = null\n }\n }\n\n // Auto-scroll\n this.handleAutoScroll(pos)\n\n // Notify drag move\n this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement)\n })\n }\n }\n\n private handlePointerUp(e: PointerEvent): void {\n if (!this.isActivePointer(e)) return\n\n // Handle click/tap if we never started dragging\n if (this.state.startPosition && !this.state.isDragging) {\n const currentPos = { x: e.clientX, y: e.clientY }\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n // If distance is very small, treat as click\n if (\n distance < this.config.clickThreshold &&\n this.state.sourceElement &&\n this.state.sourcePosition\n ) {\n this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition)\n }\n\n this.cleanup()\n return\n }\n\n if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {\n this.cleanup()\n return\n }\n\n // Find drop target\n const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement\n const droppableMatch = this.findElementByKinds(target, this.droppableKinds)\n const droppableElement = droppableMatch?.element ?? null\n\n if (droppableElement && droppableMatch) {\n const targetPosition = this.callbacks.getItemPosition(droppableElement, droppableMatch.kind)\n\n if (targetPosition) {\n // Notify drop\n this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem)\n }\n }\n\n this.cleanup()\n }\n\n private handlePointerCancel(): void {\n this.cleanup()\n }\n\n private handleKeyDown(e: KeyboardEvent): void {\n if (!this.config.cancelOnEscape) return\n if (e.key !== 'Escape') return\n if (!this.state.startPosition) return\n this.cleanup()\n }\n\n private handleMouseOut(e: MouseEvent): void {\n if (!this.config.cancelOnPointerLeave) return\n if (!this.state.startPosition) return\n if (e.relatedTarget !== null) return\n this.cleanup()\n }\n\n private handleAutoScroll(pos: PointerPosition): void {\n const { scrollThreshold, scrollSpeed } = this.config\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n let scrollX = 0\n let scrollY = 0\n\n // Check horizontal scroll\n if (pos.x < scrollThreshold) {\n scrollX = -scrollSpeed\n } else if (pos.x > viewportWidth - scrollThreshold) {\n scrollX = scrollSpeed\n }\n\n // Check vertical scroll\n if (pos.y < scrollThreshold) {\n scrollY = -scrollSpeed\n } else if (pos.y > viewportHeight - scrollThreshold) {\n scrollY = scrollSpeed\n }\n\n if (scrollX !== 0 || scrollY !== 0) {\n window.scrollBy(scrollX, scrollY)\n }\n }\n\n private cleanup(): void {\n // Release pointer capture if we own it.\n if (\n this.state.sourceElement &&\n this.state.activePointerId !== null &&\n typeof this.state.sourceElement.hasPointerCapture === 'function' &&\n typeof this.state.sourceElement.releasePointerCapture === 'function' &&\n this.state.sourceElement.hasPointerCapture(this.state.activePointerId)\n ) {\n try {\n this.state.sourceElement.releasePointerCapture(this.state.activePointerId)\n } catch {\n // Ignore release errors for inactive pointer.\n }\n }\n\n // Remove data-dragging attribute\n if (this.state.sourceElement) {\n this.state.sourceElement.removeAttribute('data-dragging')\n }\n\n // Remove hover attribute\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Cancel RAF\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n // Notify drag end\n if (this.state.isDragging) {\n this.callbacks.onDragEnd?.()\n }\n\n // Reset state\n this.state = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n activePointerId: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n this.lastPointerPos = null\n }\n\n private getPointerId(e: PointerEvent): number | null {\n return typeof e.pointerId === 'number' ? e.pointerId : null\n }\n\n private isActivePointer(e: PointerEvent): boolean {\n if (this.state.activePointerId === null) {\n return true\n }\n const pointerId = this.getPointerId(e)\n return pointerId === null || pointerId === this.state.activePointerId\n }\n\n private normalizeKindList(kinds: string | string[]): string[] {\n const values = Array.isArray(kinds) ? kinds : [kinds]\n return [...new Set(values.map((value) => value.trim()).filter(Boolean))]\n }\n\n /**\n * Find closest ancestor element with a `data-kind` matching any expected kind.\n */\n private findElementByKinds(\n element: HTMLElement | null,\n kinds: string[],\n ): { element: HTMLElement; kind: string } | null {\n let current = element\n while (current && current !== this.container) {\n const elementKind = current.dataset.kind\n if (elementKind && kinds.includes(elementKind)) {\n return { element: current, kind: elementKind }\n }\n current = current.parentElement\n }\n return null\n }\n\n /**\n * Calculate distance between two points\n */\n private getDistance(p1: PointerPosition, p2: PointerPosition): number {\n return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))\n }\n\n /**\n * Get current drag state (read-only)\n */\n public getState(): Readonly<DragState<TItem, TPosition>> {\n return { ...this.state }\n }\n\n /**\n * Check if currently dragging\n */\n public isDragging(): boolean {\n return this.state.isDragging\n }\n\n /**\n * Cleanup and remove event listeners\n */\n public destroy(): void {\n this.cleanup()\n this.container.removeEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.removeEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n window.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.removeEventListener('keydown', this.boundHandlers.keyDown)\n window.removeEventListener('mouseout', this.boundHandlers.mouseOut)\n }\n}\n","import type { PointerPosition } from './drag-drop-manager'\n\nexport type DragPreviewControllerOptions = {\n zIndex?: number\n opacity?: number\n centerOnCursor?: boolean\n className?: string | string[]\n copyComputedStyles?: string[]\n appendTo?: HTMLElement\n}\n\nconst OFFSCREEN_TRANSFORM = 'translate(-9999px, -9999px)'\n\nfunction normalizeClassNames(className: string | string[] | undefined): string[] {\n if (!className) return []\n return Array.isArray(className) ? className : [className]\n}\n\nexport class DragPreviewController {\n private options: Required<\n Pick<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n > &\n Omit<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n private previewElement: HTMLElement | null = null\n\n constructor(options: DragPreviewControllerOptions = {}) {\n this.options = {\n zIndex: options.zIndex ?? 9999,\n opacity: options.opacity ?? 0.95,\n centerOnCursor: options.centerOnCursor ?? true,\n className: options.className,\n copyComputedStyles: options.copyComputedStyles,\n appendTo: options.appendTo,\n }\n }\n\n public startFromElement(element: HTMLElement): void {\n this.stop()\n const rect = element.getBoundingClientRect()\n const clone = element.cloneNode(true) as HTMLElement\n const computed = window.getComputedStyle(element)\n const appendTo = this.options.appendTo ?? document.body\n\n clone.setAttribute('aria-hidden', 'true')\n clone.style.position = 'fixed'\n clone.style.left = '0'\n clone.style.top = '0'\n clone.style.width = `${rect.width}px`\n clone.style.height = `${rect.height}px`\n clone.style.pointerEvents = 'none'\n clone.style.zIndex = String(this.options.zIndex)\n clone.style.opacity = String(this.options.opacity)\n clone.style.transform = OFFSCREEN_TRANSFORM\n clone.style.transition = 'none'\n clone.style.cursor = 'grabbing'\n clone.style.margin = '0'\n clone.style.boxSizing = 'border-box'\n clone.style.background = computed.backgroundColor\n\n for (const classToken of normalizeClassNames(this.options.className)) {\n clone.classList.add(classToken)\n }\n\n if (this.options.copyComputedStyles && this.options.copyComputedStyles.length > 0) {\n for (const propertyName of this.options.copyComputedStyles) {\n clone.style.setProperty(propertyName, computed.getPropertyValue(propertyName))\n }\n }\n\n appendTo.appendChild(clone)\n this.previewElement = clone\n }\n\n public moveToPointer(pos: PointerPosition): void {\n if (!this.previewElement) return\n if (this.options.centerOnCursor) {\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px) translate(-50%, -50%)`\n return\n }\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px)`\n }\n\n public stop(): void {\n this.previewElement?.remove()\n this.previewElement = null\n }\n\n public destroy(): void {\n this.stop()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/auto-scroll.ts","../src/drag-drop-manager.ts","../src/drag-preview-controller.ts"],"sourcesContent":["import type { PointerPosition } from './drag-drop-manager'\n\nexport type AutoScrollOptions = {\n /** Px from an edge where scrolling starts (a scroll container's zone is at most a quarter of it). */\n threshold: number\n /** Px per frame. */\n speed: number\n}\n\nconst SCROLLABLE_OVERFLOW = /^(auto|scroll|overlay)$/\n\n/** Whether `element` may scroll on an axis: its overflow allows it and its content overflows. */\nfunction scrollsOn(element: Element, style: CSSStyleDeclaration, axis: 'x' | 'y'): boolean {\n return axis === 'y'\n ? SCROLLABLE_OVERFLOW.test(style.overflowY) && element.scrollHeight > element.clientHeight\n : SCROLLABLE_OVERFLOW.test(style.overflowX) && element.scrollWidth > element.clientWidth\n}\n\n/** The step on one axis: toward the edge the pointer is near, if the scroller can still go there. */\nfunction stepOn(\n pointer: number,\n start: number,\n end: number,\n zone: number,\n speed: number,\n canGoBack: boolean,\n canGoForward: boolean,\n): number {\n if (pointer < start + zone && canGoBack) return -speed\n if (pointer > end - zone && canGoForward) return speed\n return 0\n}\n\n/**\n * Scrolls one step toward the edge the pointer is near: the closest scroll container around\n * `hit` (the element under the pointer) that can still scroll that way, or else the window.\n * Returns whether anything scrolled, so the caller keeps going while the pointer rests there.\n */\nexport function autoScrollStep(\n pos: PointerPosition,\n hit: Element | null,\n { threshold, speed }: AutoScrollOptions,\n): boolean {\n if (speed <= 0 || threshold <= 0) return false\n\n for (let element = hit; element; element = element.parentElement) {\n // The document's own scrolling is the window's, below.\n if (element === document.body || element === document.documentElement) break\n const style = getComputedStyle(element)\n const scrollsX = scrollsOn(element, style, 'x')\n const scrollsY = scrollsOn(element, style, 'y')\n if (!scrollsX && !scrollsY) continue\n\n const rect = element.getBoundingClientRect()\n const left = scrollsX\n ? stepOn(\n pos.x,\n rect.left,\n rect.right,\n Math.min(threshold, rect.width / 4),\n speed,\n element.scrollLeft > 0,\n element.scrollLeft + element.clientWidth < element.scrollWidth - 1,\n )\n : 0\n const top = scrollsY\n ? stepOn(\n pos.y,\n rect.top,\n rect.bottom,\n Math.min(threshold, rect.height / 4),\n speed,\n element.scrollTop > 0,\n element.scrollTop + element.clientHeight < element.scrollHeight - 1,\n )\n : 0\n if (left !== 0 || top !== 0) {\n // `instant` so a `scroll-behavior: smooth` scroller does not restart an animation per frame.\n element.scrollBy({ left, top, behavior: 'instant' })\n return true\n }\n }\n\n const left = stepOn(pos.x, 0, window.innerWidth, threshold, speed, true, true)\n const top = stepOn(pos.y, 0, window.innerHeight, threshold, speed, true, true)\n if (left === 0 && top === 0) return false\n const before = [window.scrollX, window.scrollY]\n window.scrollBy({ left, top, behavior: 'instant' })\n return window.scrollX !== before[0] || window.scrollY !== before[1]\n}\n","/**\n * Framework-agnostic Drag and Drop Manager\n *\n * A data-driven drag-and-drop utility that works with React and vanilla JS.\n * Uses data attributes and direct DOM manipulation for optimal performance.\n *\n * @template TItem - The type of data associated with draggable items\n * @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)\n */\n\nimport { autoScrollStep } from './auto-scroll'\n\n/**\n * Presses a manager took: with nested containers the inner manager hears the `pointerdown` first\n * and takes it, and the outer ones leave it alone.\n */\nconst claimedPresses = new WeakSet<Event>()\n\n/** The pointer events of a press that a detached source still receives (see handlePointerDown). */\nconst SOURCE_POINTER_EVENTS = ['pointermove', 'pointerup', 'pointercancel'] as const\n\nexport type DragDropConfig = {\n /**\n * Minimum distance in pixels the pointer must move before a drag operation starts.\n * Prevents accidental drags from small movements and allows normal scrolling on mobile.\n * Checked during `pointermove` before drag has started.\n * @default 10\n * @example\n * dragThreshold: 15 // Harder to start drag (better for mobile scroll)\n */\n dragThreshold?: number\n\n /**\n * Milliseconds a press must be held, moving at most `dragDelayTolerance`, before it becomes a\n * drag: a long press, so a touch list still scrolls under a swiping finger. A number applies to\n * every pointer type; an object sets it per type (`{ touch: 250 }`: a mouse still drags at once).\n * Moving further during the delay releases the press and leaves the gesture to the browser. Once\n * the delay passes the drag starts at once, without waiting for a move.\n * @default 0\n */\n dragDelay?: number | { mouse?: number; pen?: number; touch?: number }\n\n /**\n * Pixels a press may move during `dragDelay` before it is released.\n * @default 5\n */\n dragDelayTolerance?: number\n\n /**\n * Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.\n * Used to distinguish intentional clicks from cancelled drag attempts.\n * Only checked on `pointerup` if drag never started.\n * @default 10\n * @example\n * clickThreshold: 5 // Stricter click detection\n * clickThreshold: 15 // More forgiving for mobile taps\n */\n clickThreshold?: number\n\n /**\n * Distance in pixels from an edge that triggers auto-scrolling during drag.\n * Near an edge of the closest scroll container under the pointer that can still scroll that way\n * (an `overflow: auto | scroll` element), that container scrolls; its zone is at most a quarter\n * of its size. Otherwise the window scrolls near the viewport's edges. Scrolling continues while\n * the pointer rests in the zone.\n * @default 100\n * @example\n * scrollThreshold: 50 // Start scrolling closer to edges\n * scrollThreshold: 150 // Larger safe zone before scrolling\n */\n scrollThreshold?: number\n\n /**\n * Scroll speed in pixels per animation frame during auto-scroll (`0` disables it).\n * Higher values = faster scrolling when dragging near edges.\n * @default 10\n * @example\n * scrollSpeed: 5 // Slower, smoother scroll\n * scrollSpeed: 20 // Faster scroll\n */\n scrollSpeed?: number\n\n /**\n * Cancel the current drag operation when Escape is pressed.\n * Useful for explicit keyboard cancellation.\n * @default true\n */\n cancelOnEscape?: boolean\n\n /**\n * Cancel the current drag operation when the pointer leaves the browser window.\n * Prevents \"stuck dragging\" when pointerup happens outside the app/window.\n * @default true\n */\n cancelOnPointerLeave?: boolean\n\n /**\n * The `data-kind` value(s) for draggable elements.\n * Can be a single kind or multiple kinds.\n * Each draggable element should still expose a single `data-kind` value.\n * @example\n * draggableKind: 'cell' // <div data-kind=\"cell\">\n * draggableKind: ['quest', 'bounty'] // <div data-kind=\"quest\">\n * draggableKind: 'card' // <div data-kind=\"card\">\n * draggableKind: 'list-item' // <div data-kind=\"list-item\">\n */\n draggableKind: string | string[]\n\n /**\n * The `data-kind` value(s) for droppable target elements.\n * Can be a single kind or multiple kinds.\n * Elements with matching kinds can receive dropped items.\n * Each droppable element should still expose a single `data-kind` value.\n * Often the same as `draggableKind` for reorderable lists.\n * @example\n * droppableKind: 'cell' // Can drop on cells\n * droppableKind: ['quest', 'bounty'] // Can drop on either kind\n * droppableKind: 'zone' // Can drop on zones\n */\n droppableKind: string | string[]\n\n /**\n * Where droppables are looked for under the pointer.\n * - `'document'`: anywhere in the page, so items can move to another manager's container (a box\n * and a party panel each with its own manager).\n * - `'container'`: only inside this manager's container, for independent widgets on one page\n * that share kinds.\n * @default 'document'\n */\n dropScope?: 'document' | 'container'\n\n /**\n * Keyboard pick-and-place. Space or Enter on a focused draggable (make it focusable: a\n * `<button>`, or `tabindex=\"0\"`) picks it up; the arrow keys move the target to the nearest\n * droppable in that direction (scrolled into view); Space or Enter drops there (ignored on a\n * target `canDrop` rejects); Escape cancels (whatever `cancelOnEscape` says), and so do Tab and\n * focus leaving the source. The callbacks fire as for a pointer drag, `onDragMove` with the\n * target's center. Announce the moves from them in an `aria-live` region.\n * @default false\n */\n keyboard?: boolean\n\n /**\n * Let a press focus the draggable, as a click would. By default the manager cancels the\n * `pointerdown`, which keeps the browser from selecting text or dragging an image natively, but\n * also from moving focus. With `true` it cancels `selectstart` and `dragstart` during the press\n * instead: the source is focused (no focus ring for a mouse, per `:focus-visible`), so focus\n * styles, keyboard follow-ups and a virtual list that keeps its focused row rendered all work.\n * @default false\n */\n focusOnPress?: boolean\n}\n\nexport type PointerPosition = {\n x: number\n y: number\n}\n\n/** Result passed to onDragEnd when the item was dropped on a valid target; null when drag was cancelled or dropped on invalid target. */\nexport type DragEndResult<TItem, TPosition> = {\n sourcePosition: TPosition\n targetPosition: TPosition\n sourceItem: TItem\n}\n\nexport type DragDropCallbacks<TItem, TPosition> = {\n /** Called when determining if an element can be dragged */\n canDrag?: (element: HTMLElement, position: TPosition) => boolean\n /**\n * Called for the droppable under the pointer on every drag frame, and again at the drop: return\n * `false` to reject it. A rejected droppable gets `data-drop-invalid` instead of `data-hovered`,\n * and a drop on it ends the drag with `onDragEnd(null)`. Keep it cheap.\n */\n canDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => boolean\n /** Called when drag starts (after threshold is met) */\n onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void\n /** Called continuously during drag, with the droppable that would receive a drop (or null) */\n onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void\n /** Called when item is dropped on a valid target */\n onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void\n /** Called when drag ends (success or cancel). Receives drop result when dropped on valid target, null otherwise. Use for reordering DOM/state and cleanup. */\n onDragEnd?: (result: DragEndResult<TItem, TPosition> | null) => void\n /** Called on simple click/tap (no drag) */\n onClick?: (element: HTMLElement, position: TPosition) => void\n /** Called to extract item data from an element */\n getItemData?: (element: HTMLElement, position: TPosition) => TItem | null\n /** Called to get the custom position/coordinates of an element */\n getItemPosition: (element: HTMLElement, kind: string) => TPosition | null\n}\n\ntype DragState<TItem, TPosition> = {\n startPosition: PointerPosition | null\n sourceElement: HTMLElement | null\n sourcePosition: TPosition | null\n sourceItem: TItem | null\n activePointerId: number | null\n /** What drives the current press or drag. */\n input: 'pointer' | 'keyboard' | null\n isDragging: boolean\n lastHoveredElement: HTMLElement | null\n /** The droppable under the pointer that `canDrop` rejected (marked `data-drop-invalid`). */\n lastInvalidElement: HTMLElement | null\n /** Set when drop on valid target; passed to onDragEnd in cleanup */\n lastDropResult: DragEndResult<TItem, TPosition> | null\n}\n\n/** Moves the boolean attribute `name` from `from` to `to`, and returns `to`. */\nfunction moveMark(\n name: string,\n from: HTMLElement | null,\n to: HTMLElement | null,\n): HTMLElement | null {\n if (from === to) return to\n from?.removeAttribute(name)\n to?.setAttribute(name, 'true')\n return to\n}\n\nfunction idleState<TItem, TPosition>(): DragState<TItem, TPosition> {\n return {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n activePointerId: null,\n input: null,\n isDragging: false,\n lastHoveredElement: null,\n lastInvalidElement: null,\n lastDropResult: null,\n }\n}\n\ntype KindMatch = { element: HTMLElement; kind: string }\n\nconst centerOf = (rect: DOMRect): PointerPosition => ({\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height / 2,\n})\n\nconst ARROWS: Record<string, { axis: 'x' | 'y'; sign: 1 | -1 }> = {\n ArrowLeft: { axis: 'x', sign: -1 },\n ArrowRight: { axis: 'x', sign: 1 },\n ArrowUp: { axis: 'y', sign: -1 },\n ArrowDown: { axis: 'y', sign: 1 },\n}\n\nconst isActionKey = (e: KeyboardEvent): boolean =>\n (e.key === ' ' || e.key === 'Enter') && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey\n\nexport class DragDropManager<TItem = unknown, TPosition = unknown> {\n private container: HTMLElement\n private config: Required<DragDropConfig>\n private draggableKinds: string[]\n private droppableKinds: string[]\n private callbacks: DragDropCallbacks<TItem, TPosition>\n private state: DragState<TItem, TPosition> = idleState()\n private rafId: number | null = null\n private lastPointerPos: PointerPosition | null = null\n /**\n * Set while a drag holds the pointer down: the browser follows the release with a native\n * `click` (on the source element under pointer capture), which must not activate it.\n */\n private suppressClickOnRelease = false\n private releaseClickBlocker: (() => void) | null = null\n /** The pending `dragDelay` of the current press. */\n private delayTimer: ReturnType<typeof setTimeout> | null = null\n private activePointerType = ''\n /** The droppable a keyboard drag points at. */\n private keyboardTarget: KindMatch | null = null\n /**\n * The key that picked up or dropped: its `keyup` would activate a focused button (Space clicks\n * on keyup), so it is swallowed.\n */\n private swallowKeyUp: string | null = null\n private boundHandlers: {\n pointerDown: (e: PointerEvent) => void\n pointerMove: (e: PointerEvent) => void\n pointerUp: (e: PointerEvent) => void\n pointerCancel: (e: PointerEvent) => void\n keyDown: (e: KeyboardEvent) => void\n mouseOut: (e: MouseEvent) => void\n touchMove: (e: TouchEvent) => void\n detachedSourcePointer: (e: PointerEvent) => void\n contextMenu: (e: MouseEvent) => void\n preventDefault: (e: Event) => void\n pickUpKey: (e: KeyboardEvent) => void\n keyboardDragKey: (e: KeyboardEvent) => void\n keyUp: (e: KeyboardEvent) => void\n focusOut: (e: FocusEvent) => void\n }\n\n constructor(\n containerRef: { current: HTMLElement | null } | string | HTMLElement,\n config: DragDropConfig,\n callbacks: DragDropCallbacks<TItem, TPosition>,\n ) {\n // Resolve container element\n if (typeof containerRef === 'string') {\n const element = document.querySelector<HTMLElement>(containerRef)\n if (!element) {\n throw new Error(`DragDropManager: Could not find element with selector \"${containerRef}\"`)\n }\n this.container = element\n } else if (typeof containerRef === 'object' && 'current' in containerRef) {\n if (!containerRef.current) {\n throw new Error('DragDropManager: Ref.current is null')\n }\n this.container = containerRef.current\n } else {\n this.container = containerRef as HTMLElement\n }\n\n this.config = {\n dragThreshold: config.dragThreshold ?? 10,\n dragDelay: config.dragDelay ?? 0,\n dragDelayTolerance: config.dragDelayTolerance ?? 5,\n clickThreshold: config.clickThreshold ?? 10,\n scrollThreshold: config.scrollThreshold ?? 100,\n scrollSpeed: config.scrollSpeed ?? 10,\n cancelOnEscape: config.cancelOnEscape ?? true,\n cancelOnPointerLeave: config.cancelOnPointerLeave ?? true,\n draggableKind: config.draggableKind,\n droppableKind: config.droppableKind,\n dropScope: config.dropScope ?? 'document',\n keyboard: config.keyboard ?? false,\n focusOnPress: config.focusOnPress ?? false,\n }\n this.draggableKinds = this.normalizeKindList(this.config.draggableKind)\n this.droppableKinds = this.normalizeKindList(this.config.droppableKind)\n\n this.callbacks = callbacks\n\n // Bind handlers to preserve context\n this.boundHandlers = {\n pointerDown: this.handlePointerDown.bind(this),\n pointerMove: this.handlePointerMove.bind(this),\n pointerUp: this.handlePointerUp.bind(this),\n pointerCancel: this.handlePointerCancel.bind(this),\n keyDown: this.handleKeyDown.bind(this),\n mouseOut: this.handleMouseOut.bind(this),\n touchMove: this.handleTouchMove.bind(this),\n detachedSourcePointer: this.handleDetachedSourcePointer.bind(this),\n contextMenu: this.handleContextMenu.bind(this),\n preventDefault: (e: Event) => e.preventDefault(),\n pickUpKey: this.handlePickUpKey.bind(this),\n keyboardDragKey: this.handleKeyboardDragKey.bind(this),\n keyUp: this.handleKeyUp.bind(this),\n focusOut: this.handleFocusOut.bind(this),\n }\n\n this.attachListeners()\n }\n\n private attachListeners(): void {\n this.container.addEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.addEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('pointerup', this.boundHandlers.pointerUp)\n window.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('keydown', this.boundHandlers.keyDown)\n window.addEventListener('mouseout', this.boundHandlers.mouseOut)\n // Non-passive and there from the start: iOS Safari only lets a touch sequence's `touchmove`\n // be cancelled (handleTouchMove) when a non-passive listener was in place at `touchstart`.\n // A listener added at the press is ignored there, so the browser pans and cancels the drag.\n this.container.addEventListener('touchmove', this.boundHandlers.touchMove, { passive: false })\n if (this.config.keyboard) {\n this.container.addEventListener('keydown', this.boundHandlers.pickUpKey)\n // Capture phase: the drag's keys come first, before a roving-focus grid or a dialog.\n window.addEventListener('keydown', this.boundHandlers.keyboardDragKey, true)\n window.addEventListener('keyup', this.boundHandlers.keyUp, true)\n window.addEventListener('focusout', this.boundHandlers.focusOut, true)\n }\n }\n\n /**\n * The source position and item of the draggable `match`, if it may be dragged: it has a\n * position, is not `data-empty`, `canDrag` allows it and it has item data.\n */\n private resolveSource(match: KindMatch): { position: TPosition; item: TItem } | null {\n const position = this.callbacks.getItemPosition(match.element, match.kind)\n if (!position) return null\n\n // Check if element is empty (convention: data-empty=\"any-value\")\n if (match.element.dataset.empty !== undefined) return null\n\n // Check canDrag callback\n if (this.callbacks.canDrag && !this.callbacks.canDrag(match.element, position)) return null\n\n // Get item data\n const item = this.callbacks.getItemData?.(match.element, position) ?? null\n if (!item) return null\n return { position, item }\n }\n\n private handlePointerDown(e: PointerEvent): void {\n if (this.state.startPosition) return\n // Only the main button (or a touch/pen contact) drags: a right-click opens a context menu and a\n // middle-click auto-scrolls. An extra finger is part of a pinch, not a drag. (Only touch has\n // non-primary pointers; a constructed PointerEvent defaults to `isPrimary: false`.)\n if (e.button !== 0 || (e.pointerType === 'touch' && !e.isPrimary)) return\n if (claimedPresses.has(e)) return\n\n const draggableMatch = this.findElementByKinds(e.target as HTMLElement, this.draggableKinds)\n if (!draggableMatch) return\n const source = this.resolveSource(draggableMatch)\n if (!source) return\n const draggableElement = draggableMatch.element\n\n claimedPresses.add(e)\n\n // Prevent text selection and native image/link drags during potential drag: by cancelling\n // the press (no focus either), or, with focusOnPress, what the press would start.\n if (!this.config.focusOnPress) e.preventDefault()\n window.addEventListener('selectstart', this.boundHandlers.preventDefault, true)\n window.addEventListener('dragstart', this.boundHandlers.preventDefault, true)\n\n // Store start position and element info\n this.state.startPosition = { x: e.clientX, y: e.clientY }\n this.state.sourceElement = draggableElement\n this.state.sourcePosition = source.position\n this.state.sourceItem = source.item\n this.state.input = 'pointer'\n this.state.activePointerId = this.getPointerId(e)\n this.activePointerType = e.pointerType ?? ''\n this.lastPointerPos = this.state.startPosition\n\n // Capture pointer events outside container boundaries when supported.\n if (\n this.state.activePointerId !== null &&\n typeof draggableElement.setPointerCapture === 'function'\n ) {\n try {\n draggableElement.setPointerCapture(this.state.activePointerId)\n } catch {\n // Ignore unsupported/inactive-pointer capture errors.\n }\n }\n\n // Follow the pointer anywhere in the page: capture ends when the source element leaves the DOM\n // (a virtualized list unmounting it, a framework moving it), and moves outside the container\n // would otherwise go unseen.\n window.addEventListener('pointermove', this.boundHandlers.pointerMove)\n // A touch's events keep their first target: once the source leaves the DOM (a virtualized list\n // unmounting the row), its touch and pointer events reach no container or window listener,\n // only the detached source's own. Without these the drag would never end.\n draggableElement.addEventListener('touchmove', this.boundHandlers.touchMove, { passive: false })\n for (const type of SOURCE_POINTER_EVENTS) {\n draggableElement.addEventListener(type, this.boundHandlers.detachedSourcePointer)\n }\n window.addEventListener('contextmenu', this.boundHandlers.contextMenu)\n\n const delay = this.delayFor(this.activePointerType)\n if (delay > 0) {\n this.delayTimer = setTimeout(() => {\n this.delayTimer = null\n this.startDrag()\n this.scheduleFrame()\n }, delay)\n }\n }\n\n private handlePointerMove(e: PointerEvent): void {\n if (!this.isActivePointer(e)) return\n\n const currentPos: PointerPosition = { x: e.clientX, y: e.clientY }\n\n // Check if we should start dragging (threshold not yet met)\n if (this.state.startPosition && !this.state.isDragging) {\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n if (this.delayTimer !== null) {\n // A long press stays put: moving away is a scroll or a swipe, which the browser handles.\n if (distance > this.config.dragDelayTolerance) this.cleanup()\n else this.lastPointerPos = currentPos\n return\n }\n\n if (distance >= this.config.dragThreshold) this.startDrag()\n return\n }\n\n if (!this.state.isDragging) return\n\n // Store latest position for RAF\n this.lastPointerPos = currentPos\n\n // Throttle updates to 60fps using RAF\n this.scheduleFrame()\n }\n\n private startDrag(): void {\n this.state.isDragging = true\n // Only a pointer release is followed by a click.\n this.suppressClickOnRelease = this.state.input === 'pointer'\n\n if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {\n // Set data-dragging attribute on source\n this.state.sourceElement.setAttribute('data-dragging', 'true')\n\n // Notify drag start\n this.callbacks.onDragStart?.(\n this.state.sourceElement,\n this.state.sourcePosition,\n this.state.sourceItem,\n )\n }\n }\n\n /** The `dragDelay` for a pointer type (`''` from a constructed event counts as a mouse). */\n private delayFor(pointerType: string): number {\n const { dragDelay } = this.config\n if (typeof dragDelay === 'number') return dragDelay\n return dragDelay[pointerType === 'touch' || pointerType === 'pen' ? pointerType : 'mouse'] ?? 0\n }\n\n /**\n * Once a touch press is a drag, the finger moves the item, not the page: a browser that starts\n * panning would cancel the pointer. Before that, touches stay the browser's.\n */\n private handleTouchMove(e: TouchEvent): void {\n if (this.state.isDragging && e.cancelable) e.preventDefault()\n }\n\n /**\n * The press's pointer events, heard on the source itself. While it is in the DOM they also reach\n * the window's listeners, which handle them; once it is detached only these hear them.\n */\n private handleDetachedSourcePointer(e: PointerEvent): void {\n if (this.state.sourceElement?.isConnected !== false) return\n if (e.type === 'pointermove') this.handlePointerMove(e)\n else if (e.type === 'pointerup') this.handlePointerUp(e)\n else this.handlePointerCancel()\n }\n\n /** A long press opens no context menu (Android, iOS) while it is a drag or about to be one. */\n private handleContextMenu(e: MouseEvent): void {\n const pendingTouch = this.delayTimer !== null && this.activePointerType === 'touch'\n if (this.state.isDragging || pendingTouch) e.preventDefault()\n }\n\n private scheduleFrame(): void {\n if (this.rafId !== null) return\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n this.runFrame()\n })\n }\n\n /** One drag frame: hover feedback, auto-scroll and `onDragMove` at the latest pointer position. */\n private runFrame(): void {\n const pos = this.lastPointerPos\n if (!pos || !this.state.isDragging) return\n\n // Find hovered droppable element\n const elementUnderCursor = document.elementFromPoint(pos.x, pos.y) as HTMLElement | null\n this.markTarget(this.findDroppable(elementUnderCursor))\n\n const scrolled = autoScrollStep(pos, elementUnderCursor, {\n threshold: this.config.scrollThreshold,\n speed: this.config.scrollSpeed,\n })\n\n // Notify drag move\n this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement)\n\n // Keep scrolling while the pointer rests in an edge zone: the next frame also hit-tests again,\n // since the content moved under the pointer.\n if (scrolled) this.scheduleFrame()\n }\n\n private handlePointerUp(e: PointerEvent): void {\n // A stray release does not drop a keyboard drag where the mouse happens to be.\n if (this.state.input === 'keyboard') return\n if (!this.isActivePointer(e)) return\n\n // Also after a drag that Escape cancelled while the pointer stayed down.\n if (this.suppressClickOnRelease) {\n this.suppressClickOnRelease = false\n this.blockNextClick()\n }\n\n // Handle click/tap if we never started dragging\n if (this.state.startPosition && !this.state.isDragging) {\n const currentPos = { x: e.clientX, y: e.clientY }\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n // If distance is very small, treat as click\n if (\n distance < this.config.clickThreshold &&\n this.state.sourceElement &&\n this.state.sourcePosition\n ) {\n this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition)\n }\n\n this.cleanup()\n return\n }\n\n if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {\n this.cleanup()\n return\n }\n\n // Find drop target\n const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement\n const droppableMatch = this.findDroppable(target)\n const droppableElement = droppableMatch?.element ?? null\n\n if (droppableElement && droppableMatch) {\n const targetPosition = this.acceptedPosition(droppableMatch)\n\n if (targetPosition) {\n const result: DragEndResult<TItem, TPosition> = {\n sourcePosition: this.state.sourcePosition,\n targetPosition,\n sourceItem: this.state.sourceItem,\n }\n this.state.lastDropResult = result\n this.callbacks.onDrop?.(result.sourcePosition, result.targetPosition, result.sourceItem)\n }\n }\n\n this.cleanup()\n }\n\n private handlePointerCancel(): void {\n if (this.state.input === 'keyboard') return\n // No click follows a cancelled pointer.\n this.suppressClickOnRelease = false\n this.cleanup()\n }\n\n private handleKeyDown(e: KeyboardEvent): void {\n if (!this.config.cancelOnEscape) return\n if (e.key !== 'Escape') return\n if (!this.state.startPosition) return\n this.cleanup()\n }\n\n private handleMouseOut(e: MouseEvent): void {\n if (e.relatedTarget !== null) return\n // A release outside the window clicks nothing in the page.\n this.suppressClickOnRelease = false\n if (!this.config.cancelOnPointerLeave) return\n if (this.state.input !== 'pointer') return\n this.cleanup()\n }\n\n /** Space or Enter on a focused draggable picks it up (`keyboard: true`). */\n private handlePickUpKey(e: KeyboardEvent): void {\n if (this.state.startPosition || e.repeat || !isActionKey(e) || claimedPresses.has(e)) return\n const match = this.findElementByKinds(e.target as HTMLElement, this.draggableKinds)\n if (!match) return\n const source = this.resolveSource(match)\n if (!source) return\n\n claimedPresses.add(e)\n // No button activation, no page scroll on Space.\n e.preventDefault()\n this.swallowKeyUp = e.key\n\n this.state.startPosition = centerOf(match.element.getBoundingClientRect())\n this.state.sourceElement = match.element\n this.state.sourcePosition = source.position\n this.state.sourceItem = source.item\n this.state.input = 'keyboard'\n this.startDrag()\n // The first target is the source itself when it is a droppable, as a pointer drag hovers it.\n this.pointKeyboardAt(this.findDroppable(match.element))\n }\n\n /** The keys of a keyboard drag, ahead of anything else on the page. */\n private handleKeyboardDragKey(e: KeyboardEvent): void {\n if (this.state.input !== 'keyboard') {\n // A new key press: the keyup of an earlier pick-up or drop is no longer coming.\n if (!e.repeat) this.swallowKeyUp = null\n return\n }\n const arrow = ARROWS[e.key]\n if (arrow) {\n this.pointKeyboardAt(this.nearestDroppable(arrow.axis, arrow.sign) ?? this.keyboardTarget)\n } else if (isActionKey(e)) {\n // Swallowed whether or not it drops: its keyup must not click the focused source either.\n this.swallowKeyUp = e.key\n if (!e.repeat) this.dropWithKeyboard()\n } else if (e.key === 'Escape') {\n this.cleanup()\n } else {\n // Tab moves focus away (focusout cancels); other keys are the page's.\n return\n }\n e.preventDefault()\n e.stopPropagation()\n }\n\n private handleKeyUp(e: KeyboardEvent): void {\n if (this.swallowKeyUp === null || e.key !== this.swallowKeyUp) return\n this.swallowKeyUp = null\n e.preventDefault()\n e.stopPropagation()\n }\n\n /** Focus leaving the source (Tab, a click elsewhere) cancels a keyboard drag. */\n private handleFocusOut(e: FocusEvent): void {\n const source = this.state.sourceElement\n if (this.state.input !== 'keyboard' || !source) return\n if (!source.contains(e.target as Node)) return\n if (source.contains(e.relatedTarget as Node | null)) return\n this.cleanup()\n }\n\n /** Makes `match` the keyboard drag's target: marked, scrolled into view, sent to `onDragMove`. */\n private pointKeyboardAt(match: KindMatch | null): void {\n this.keyboardTarget = match\n this.markTarget(match)\n if (!match) return\n match.element.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n this.callbacks.onDragMove?.(\n centerOf(match.element.getBoundingClientRect()),\n this.state.lastHoveredElement,\n )\n }\n\n /**\n * The droppable nearest the current target (or the source) in a direction: its center lies that\n * way, and it is closest along the axis, with distance across the axis counting double, so the\n * next cell in the same row or column wins over a diagonal one.\n */\n private nearestDroppable(axis: 'x' | 'y', sign: 1 | -1): KindMatch | null {\n const from = this.keyboardTarget?.element ?? this.state.sourceElement\n if (!from) return null\n const origin = centerOf(from.getBoundingClientRect())\n const root = this.config.dropScope === 'container' ? this.container : document\n let best: KindMatch | null = null\n let bestScore = Infinity\n for (const element of root.querySelectorAll<HTMLElement>('[data-kind]')) {\n const kind = element.dataset.kind\n if (!kind || element === from || !this.droppableKinds.includes(kind)) continue\n const rect = element.getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) continue\n const center = centerOf(rect)\n const along = (axis === 'x' ? center.x - origin.x : center.y - origin.y) * sign\n const across = Math.abs(axis === 'x' ? center.y - origin.y : center.x - origin.x)\n if (along < 1) continue\n const score = along + across * 2\n if (score < bestScore) {\n best = { element, kind }\n bestScore = score\n }\n }\n return best\n }\n\n /** Drops a keyboard drag on its target, unless there is none or `canDrop` rejects it. */\n private dropWithKeyboard(): void {\n const target = this.keyboardTarget\n const targetPosition = target && this.acceptedPosition(target)\n const { sourcePosition, sourceItem } = this.state\n if (!targetPosition || sourcePosition === null || sourceItem === null) return\n this.state.lastDropResult = { sourcePosition, targetPosition, sourceItem }\n this.callbacks.onDrop?.(sourcePosition, targetPosition, sourceItem)\n this.cleanup()\n }\n\n /**\n * Swallows the `click` the browser dispatches right after the release that ended a drag, in the\n * same task; a timeout lets the blocker go when no click comes.\n */\n private blockNextClick(): void {\n this.releaseClickBlocker?.()\n const block = (event: MouseEvent): void => {\n event.preventDefault()\n event.stopImmediatePropagation()\n release()\n }\n const release = (): void => {\n window.removeEventListener('click', block, true)\n clearTimeout(timer)\n if (this.releaseClickBlocker === release) this.releaseClickBlocker = null\n }\n window.addEventListener('click', block, true)\n const timer = setTimeout(release, 0)\n this.releaseClickBlocker = release\n }\n\n private cleanup(): void {\n window.removeEventListener('pointermove', this.boundHandlers.pointerMove)\n this.state.sourceElement?.removeEventListener('touchmove', this.boundHandlers.touchMove)\n for (const type of SOURCE_POINTER_EVENTS) {\n this.state.sourceElement?.removeEventListener(type, this.boundHandlers.detachedSourcePointer)\n }\n window.removeEventListener('contextmenu', this.boundHandlers.contextMenu)\n window.removeEventListener('selectstart', this.boundHandlers.preventDefault, true)\n window.removeEventListener('dragstart', this.boundHandlers.preventDefault, true)\n if (this.delayTimer !== null) {\n clearTimeout(this.delayTimer)\n this.delayTimer = null\n }\n this.activePointerType = ''\n\n // Release pointer capture if we own it.\n if (\n this.state.sourceElement &&\n this.state.activePointerId !== null &&\n typeof this.state.sourceElement.hasPointerCapture === 'function' &&\n typeof this.state.sourceElement.releasePointerCapture === 'function' &&\n this.state.sourceElement.hasPointerCapture(this.state.activePointerId)\n ) {\n try {\n this.state.sourceElement.releasePointerCapture(this.state.activePointerId)\n } catch {\n // Ignore release errors for inactive pointer.\n }\n }\n\n // Remove data-dragging attribute\n if (this.state.sourceElement) {\n this.state.sourceElement.removeAttribute('data-dragging')\n }\n\n // Remove hover attributes\n this.markTarget(null)\n\n // Cancel RAF\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n // Notify drag end (pass drop result when drop was valid, null when cancelled or invalid)\n if (this.state.isDragging) {\n this.callbacks.onDragEnd?.(this.state.lastDropResult)\n }\n\n // Reset state\n this.state = idleState()\n this.lastPointerPos = null\n this.keyboardTarget = null\n }\n\n private getPointerId(e: PointerEvent): number | null {\n return typeof e.pointerId === 'number' ? e.pointerId : null\n }\n\n private isActivePointer(e: PointerEvent): boolean {\n if (this.state.activePointerId === null) {\n return true\n }\n const pointerId = this.getPointerId(e)\n return pointerId === null || pointerId === this.state.activePointerId\n }\n\n private normalizeKindList(kinds: string | string[]): string[] {\n const values = Array.isArray(kinds) ? kinds : [kinds]\n return [...new Set(values.map((value) => value.trim()).filter(Boolean))]\n }\n\n /** The target position of the droppable `match` if the current source may drop there. */\n private acceptedPosition(match: { element: HTMLElement; kind: string }): TPosition | null {\n const { sourcePosition, sourceItem } = this.state\n const targetPosition = this.callbacks.getItemPosition(match.element, match.kind)\n if (targetPosition === null || sourcePosition === null || sourceItem === null) return null\n if (\n this.callbacks.canDrop &&\n !this.callbacks.canDrop(sourcePosition, targetPosition, sourceItem)\n ) {\n return null\n }\n return targetPosition\n }\n\n /**\n * Marks the droppable under the pointer: `data-hovered` when it would receive a drop (also the\n * source itself, for \"drag over itself\" styling), `data-drop-invalid` when `canDrop` rejects it.\n * Without `canDrop` every droppable is marked hovered.\n */\n private markTarget(match: { element: HTMLElement; kind: string } | null): void {\n const accepted =\n match && (!this.callbacks.canDrop || this.acceptedPosition(match) !== null)\n ? match.element\n : null\n const rejected = match && !accepted ? match.element : null\n this.state.lastHoveredElement = moveMark(\n 'data-hovered',\n this.state.lastHoveredElement,\n accepted,\n )\n this.state.lastInvalidElement = moveMark(\n 'data-drop-invalid',\n this.state.lastInvalidElement,\n rejected,\n )\n }\n\n /** The droppable around `element` (the one under the pointer), within `dropScope`. */\n private findDroppable(\n element: HTMLElement | null,\n ): { element: HTMLElement; kind: string } | null {\n if (this.config.dropScope === 'container' && !(element && this.container.contains(element))) {\n return null\n }\n return this.findElementByKinds(element, this.droppableKinds)\n }\n\n /**\n * Find closest ancestor element with a `data-kind` matching any expected kind.\n */\n private findElementByKinds(\n element: HTMLElement | null,\n kinds: string[],\n ): { element: HTMLElement; kind: string } | null {\n let current = element\n while (current && current !== this.container) {\n const elementKind = current.dataset.kind\n if (elementKind && kinds.includes(elementKind)) {\n return { element: current, kind: elementKind }\n }\n current = current.parentElement\n }\n return null\n }\n\n /**\n * Calculate distance between two points\n */\n private getDistance(p1: PointerPosition, p2: PointerPosition): number {\n return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))\n }\n\n /**\n * Get current drag state (read-only)\n */\n public getState(): Readonly<DragState<TItem, TPosition>> {\n return { ...this.state }\n }\n\n /**\n * Check if currently dragging\n */\n public isDragging(): boolean {\n return this.state.isDragging\n }\n\n /**\n * Cleanup and remove event listeners\n */\n public destroy(): void {\n this.cleanup()\n this.suppressClickOnRelease = false\n this.releaseClickBlocker?.()\n this.container.removeEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n this.container.removeEventListener('touchmove', this.boundHandlers.touchMove)\n window.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n window.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.removeEventListener('keydown', this.boundHandlers.keyDown)\n window.removeEventListener('mouseout', this.boundHandlers.mouseOut)\n if (this.config.keyboard) {\n this.container.removeEventListener('keydown', this.boundHandlers.pickUpKey)\n window.removeEventListener('keydown', this.boundHandlers.keyboardDragKey, true)\n window.removeEventListener('keyup', this.boundHandlers.keyUp, true)\n window.removeEventListener('focusout', this.boundHandlers.focusOut, true)\n }\n }\n}\n","import type { PointerPosition } from './drag-drop-manager'\n\nexport type DragPreviewControllerOptions = {\n zIndex?: number\n opacity?: number\n centerOnCursor?: boolean\n className?: string | string[]\n copyComputedStyles?: string[]\n appendTo?: HTMLElement\n}\n\nconst OFFSCREEN_TRANSFORM = 'translate(-9999px, -9999px)'\n\n/**\n * Attributes of the source (and its descendants) that belong to its place in the page or to the\n * drag, not to its look: a clone keeping them would duplicate ids, join radio groups, count as a\n * draggable or droppable, and match `[data-dragging]` placeholder styles.\n */\nconst SOURCE_ONLY_ATTRIBUTES = [\n 'id',\n 'name',\n 'data-kind',\n 'data-dragging',\n 'data-hovered',\n 'data-drop-invalid',\n]\n\nfunction stripSourceAttributes(clone: HTMLElement): void {\n for (const element of [clone, ...clone.querySelectorAll('*')]) {\n for (const name of SOURCE_ONLY_ATTRIBUTES) element.removeAttribute(name)\n }\n}\n\nfunction normalizeClassNames(className: string | string[] | undefined): string[] {\n if (!className) return []\n return Array.isArray(className) ? className : [className]\n}\n\nexport class DragPreviewController {\n private options: Required<\n Pick<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n > &\n Omit<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n private previewElement: HTMLElement | null = null\n\n constructor(options: DragPreviewControllerOptions = {}) {\n this.options = {\n zIndex: options.zIndex ?? 9999,\n opacity: options.opacity ?? 0.95,\n centerOnCursor: options.centerOnCursor ?? true,\n className: options.className,\n copyComputedStyles: options.copyComputedStyles,\n appendTo: options.appendTo,\n }\n }\n\n public startFromElement(element: HTMLElement): void {\n this.stop()\n const rect = element.getBoundingClientRect()\n const clone = element.cloneNode(true) as HTMLElement\n const computed = window.getComputedStyle(element)\n const appendTo = this.options.appendTo ?? document.body\n\n stripSourceAttributes(clone)\n // A picture of the source: not focusable (a tabindex is copied), not announced.\n clone.setAttribute('inert', '')\n clone.setAttribute('aria-hidden', 'true')\n clone.setAttribute('data-drag-preview', '')\n clone.style.position = 'fixed'\n clone.style.left = '0'\n clone.style.top = '0'\n clone.style.width = `${rect.width}px`\n clone.style.height = `${rect.height}px`\n clone.style.pointerEvents = 'none'\n clone.style.zIndex = String(this.options.zIndex)\n clone.style.opacity = String(this.options.opacity)\n clone.style.transform = OFFSCREEN_TRANSFORM\n clone.style.transition = 'none'\n clone.style.cursor = 'grabbing'\n clone.style.margin = '0'\n clone.style.boxSizing = 'border-box'\n clone.style.background = computed.backgroundColor\n\n for (const classToken of normalizeClassNames(this.options.className)) {\n clone.classList.add(classToken)\n }\n\n if (this.options.copyComputedStyles && this.options.copyComputedStyles.length > 0) {\n for (const propertyName of this.options.copyComputedStyles) {\n clone.style.setProperty(propertyName, computed.getPropertyValue(propertyName))\n }\n }\n\n appendTo.appendChild(clone)\n this.previewElement = clone\n }\n\n public moveToPointer(pos: PointerPosition): void {\n if (!this.previewElement) return\n if (this.options.centerOnCursor) {\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px) translate(-50%, -50%)`\n return\n }\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px)`\n }\n\n public stop(): void {\n this.previewElement?.remove()\n this.previewElement = null\n }\n\n public destroy(): void {\n this.stop()\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "dnd-manager",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "homepage": "https://itsjavi.com/dnd-manager",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/itsjavi/dnd-manager.git"
8
+ "url": "git+https://github.com/dendrell/dnd-manager.git"
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
12
  "README.md",
13
+ "CHANGELOG.md",
13
14
  "LICENSE"
14
15
  ],
15
16
  "type": "module",
@@ -22,16 +23,6 @@
22
23
  "require": "./dist/index.mjs"
23
24
  }
24
25
  },
25
- "devDependencies": {
26
- "@vitest/browser-playwright": "^4.0.18",
27
- "@vitest/coverage-v8": "^4.0.18",
28
- "jsdom": "^28.1.0",
29
- "oxfmt": "^0.35.0",
30
- "publint": "^0.3.17",
31
- "tsdown": "^0.20.3",
32
- "typescript": "^5.9.3",
33
- "vitest": "^4.0.18"
34
- },
35
26
  "scripts": {
36
27
  "dev": "pnpm --filter dnd-manager-demo dev",
37
28
  "build": "tsdown src/index.ts --dts --sourcemap --format esm --clean",
@@ -40,11 +31,23 @@
40
31
  "build:all": "pnpm build && pnpm build:demo",
41
32
  "format": "oxfmt --write .",
42
33
  "format:check": "oxfmt --check .",
34
+ "prepublishOnly": "pnpm typecheck && pnpm build && publint",
35
+ "release:version": "node scripts/release.ts",
43
36
  "test": "vitest --run",
44
37
  "test:coverage": "pnpm build && vitest --run --coverage",
45
38
  "test:e2e": "pnpm build && vitest --project e2e",
46
39
  "test:unit": "vitest --project unit",
47
40
  "test:watch": "vitest --watch",
48
41
  "typecheck": "tsc --noEmit"
42
+ },
43
+ "devDependencies": {
44
+ "@vitest/browser-playwright": "^4.1.2",
45
+ "@vitest/coverage-v8": "^4.1.2",
46
+ "jsdom": "^29.0.1",
47
+ "oxfmt": "^0.43.0",
48
+ "publint": "^0.3.18",
49
+ "tsdown": "^0.21.7",
50
+ "typescript": "^6.0.2",
51
+ "vitest": "^4.1.2"
49
52
  }
50
- }
53
+ }