@vielzeug/dnd 1.2.2 → 1.2.3
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.
- package/dist/_dev.cjs +1 -1
- package/dist/_dev.cjs.map +1 -1
- package/dist/_dev.js +2 -5
- package/dist/_dev.js.map +1 -1
- package/dist/dnd.cjs +1 -1
- package/dist/dnd.cjs.map +1 -1
- package/dist/dnd.iife.js +1 -1
- package/dist/dnd.iife.js.map +1 -1
- package/dist/dnd.js +1 -1
- package/dist/dnd.js.map +1 -1
- package/dist/drop-zone.cjs +1 -1
- package/dist/drop-zone.cjs.map +1 -1
- package/dist/drop-zone.js +4 -4
- package/dist/drop-zone.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/sortable.cjs +1 -1
- package/dist/sortable.cjs.map +1 -1
- package/dist/sortable.js +92 -92
- package/dist/sortable.js.map +1 -1
- package/dist/touch.cjs +1 -1
- package/dist/touch.cjs.map +1 -1
- package/dist/touch.js +53 -53
- package/dist/touch.js.map +1 -1
- package/package.json +3 -4
package/dist/dnd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dnd.js","names":[],"sources":["../src/_shared.ts","../src/drop-zone.ts","../src/errors.ts","../src/_dev.ts","../src/sortable.ts","../src/touch.ts"],"sourcesContent":["import { type Disposable } from './types';\n\n// ─── Disposable ──────────────────────────────────────────────────────────────\n\n/**\n * Create a `Disposable` backed by an `AbortController`.\n * `dispose()` is idempotent — the signal aborts and `cleanup` runs at most once.\n * Pass the returned `disposalSignal` to `addEventListener` calls so removal happens\n * automatically on dispose, instead of pairing manual `removeEventListener` calls.\n */\nexport function createDisposable(cleanup?: () => void): Disposable {\n let disposed = false;\n const abortController = new AbortController();\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n abortController.abort();\n cleanup?.();\n };\n\n return {\n get disposalSignal() {\n return abortController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n [Symbol.dispose]: dispose,\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nexport function resolveDisabled(disabled: boolean | undefined): boolean {\n return disabled === true;\n}\n","import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n","/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n","const isDev = !(globalThis as { __DND_PROD__?: boolean }).__DND_PROD__;\n\n/** @internal @security Messages may include developer-supplied config values. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/dnd] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n","import { warn } from './_dev';\nimport { createDisposable, resolveDisabled } from './_shared';\nimport { DndScopeError } from './errors';\nimport { type Disposable } from './types';\n\n// ─── Branded scope ────────────────────────────────────────────────────────────\n\nconst SCOPE_BRAND = Symbol('SortableScope');\n\nexport interface SortableScope extends Disposable {\n /** `true` while any sortable in this scope is actively dragging. */\n readonly isDragging: boolean;\n readonly [SCOPE_BRAND]: true;\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AutoScrollOptions {\n /** Distance in pixels from an edge that triggers auto-scroll. @default 32 */\n edgeThreshold?: number;\n /** Pixels scrolled per dragover frame while near an edge. @default 18 */\n speed?: number;\n /** Scroll the sortable container while dragging near its edges. @default true */\n container?: boolean;\n /** Scroll the viewport while dragging near the window edges. @default false */\n viewport?: boolean;\n}\n\n/**\n * Passed to `onReorder` after every successful reorder (drag or keyboard).\n */\nexport interface ReorderEvent {\n /** The new ordered list of item keys after the reorder. */\n ids: string[];\n /**\n * Register a revert function that will be called when `sortable.revert()` is invoked.\n * Useful for rolling back optimistic UI updates on server error.\n * Only the most recent `setRevert` registration is retained — a new reorder overwrites it.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n setRevert(fn: () => void): void;\n}\n\nexport interface SortableOptions {\n /** Container element whose direct-child items are sortable. */\n element: HTMLElement;\n /** Shared scope for connected sortable containers. Containers only exchange items within the same scope. */\n scope?: SortableScope;\n /**\n * Selector for the drag handle inside each item.\n * When omitted the whole item is the handle.\n */\n handle?: string;\n /**\n * Enables keyboard-based reordering using arrow keys plus Home/End.\n * @default true\n */\n keyboard?: boolean;\n /**\n * Returns the identity key for a given item element.\n * This separates the \"what is this item?\" concern (yours) from the \"which children\n * are sortable?\" concern (ours — marked with `data-dnd-item`).\n *\n * @example\n * ```ts\n * getKey: (el) => el.dataset.taskId!\n * ```\n */\n getKey: (element: HTMLElement) => string;\n /** Sorting axis used to compute insertion position. @default 'vertical' */\n axis?: 'vertical' | 'horizontal';\n /** Auto-scrolls the container (and viewport) near edges while dragging. @default true */\n autoScroll?: boolean | AutoScrollOptions;\n /** Optional custom drag preview element. */\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n /** CSS class applied to the placeholder element. @default 'dnd-placeholder' */\n placeholderClass?: string;\n /**\n * Called with a {@link ReorderEvent} after a successful reorder, only when the order changed.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n onReorder?: (event: ReorderEvent) => void;\n /**\n * Called just before a successful drag commit with the before and after order snapshots.\n * Use this hook to set up FLIP animations — the source items are still in their\n * pre-commit positions at the time of the call.\n *\n * @example\n * ```ts\n * onBeforeReorder: (from, to) => {\n * // record element positions here, then animate after the next microtask\n * }\n * ```\n */\n onBeforeReorder?: (from: string[], to: string[]) => void;\n /**\n * When `true`, drag interactions are ignored.\n *\n * Note: if `disabled` transitions to `true` while a drag is in progress the\n * drag is treated as a cancellation — the item snaps back to its original\n * position rather than committing the last placeholder location.\n */\n disabled?: boolean;\n /** Called when the user starts dragging an item. */\n onDragStart?: (id: string, event: DragEvent) => void;\n /** Called when a drag ends (whether dropped or cancelled). */\n onDragEnd?: (id: string, event: DragEvent) => void;\n /**\n * Hotspot offset `[x, y]` passed to `setDragImage`.\n * Controls which point of the preview image follows the cursor.\n * @default [0, 0]\n */\n dragImageOffset?: [number, number];\n}\n\nexport interface Sortable extends Disposable {\n readonly isDragging: boolean;\n /**\n * Calls the revert function registered via `setRevert` in the last `onReorder` invocation (if any) and clears it.\n * A no-op when no revert function was registered or has already been consumed.\n *\n * Works for both drag-based and keyboard-based reorders.\n * Note: only the most recent reorder can be reverted; a new reorder overwrites the stored function.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * // later, on server error:\n * sortable.revert();\n * ```\n */\n revert(): void;\n /**\n * Re-reads the container's children and reapplies `draggable`, ARIA roles,\n * and handle attributes. Call this after programmatically adding, removing,\n * or replacing items — e.g. after a framework render that replaces DOM nodes.\n *\n * Not needed when items are only reordered via drag or keyboard.\n */\n sync(): void;\n}\n\n// ─── Internal types ───────────────────────────────────────────────────────────\n\ninterface ResolvedAutoScrollOptions {\n container: boolean;\n edgeThreshold: number;\n speed: number;\n viewport: boolean;\n}\n\n/** Per-container closure passed into shared session functions. */\ninterface ContainerHandle {\n getOrderedIds: () => string[];\n isDisabled: () => boolean;\n commitReorder: (orderedIds: string[]) => void;\n notifyBeforeReorder: (from: string[], to: string[]) => void;\n notifyDragEnd: (id: string, event: DragEvent) => void;\n notifyDragStart: (id: string, event: DragEvent) => void;\n}\n\n/**\n * Encapsulates all mutable state for a single in-progress drag operation.\n * Created at dragstart, destroyed on dragend/cancel.\n */\ninterface DragSession {\n draggedEl: HTMLElement;\n draggedId: string;\n /**\n * Order snapshots captured lazily: snapshot is taken the first time a handle\n * becomes the active target (and always for the source at drag start).\n */\n initialOrders: Map<ContainerHandle, string[]>;\n originalDisplay: string;\n originalNextSibling: ChildNode | null;\n originalParent: HTMLElement;\n placeholder: HTMLElement;\n source: ContainerHandle;\n target: ContainerHandle | null;\n /** rAF handle for the deferred element-hide; null once fired or cancelled. */\n hideFrame: number | null;\n}\n\ninterface SortableScopeState {\n active: DragSession | null;\n handles: Set<ContainerHandle>;\n /** All sortables registered in this scope, kept for scope.dispose(). */\n disposables: Set<() => void>;\n}\n\n// ─── Internal constants ───────────────────────────────────────────────────────\n\nconst HANDLE_ATTR = 'data-dnd-handle';\nconst ITEM_ATTR = 'data-dnd-item';\nconst sortableScopeStates = new WeakMap<SortableScope, SortableScopeState>();\nconst DEFAULT_AUTO_SCROLL: ResolvedAutoScrollOptions = {\n container: true,\n edgeThreshold: 32,\n speed: 18,\n viewport: false,\n};\n\n// ─── Scope helpers ────────────────────────────────────────────────────────────\n\nfunction getSortableScopeState(scope: SortableScope): SortableScopeState {\n const state = sortableScopeStates.get(scope);\n\n if (!state) throw new DndScopeError();\n\n return state;\n}\n\n// ─── Option resolution ────────────────────────────────────────────────────────\n\nfunction resolveAutoScrollOptions(\n autoScroll: boolean | AutoScrollOptions | undefined,\n): ResolvedAutoScrollOptions | null {\n if (autoScroll === false) return null;\n\n if (autoScroll === true || autoScroll === undefined) return DEFAULT_AUTO_SCROLL;\n\n return {\n container: autoScroll.container ?? true,\n edgeThreshold: autoScroll.edgeThreshold ?? 32,\n speed: autoScroll.speed ?? 18,\n viewport: autoScroll.viewport ?? false,\n };\n}\n\n// ─── DOM helpers ──────────────────────────────────────────────────────────────\n\nfunction hasOrderChanged(before: string[], after: string[]): boolean {\n return after.length !== before.length || after.some((id, index) => id !== before[index]);\n}\n\nfunction clearHandleAttributes(element: HTMLElement): void {\n element.querySelectorAll<HTMLElement>(`[${HANDLE_ATTR}]`).forEach((handleEl) => {\n handleEl.removeAttribute(HANDLE_ATTR);\n handleEl.removeAttribute('draggable');\n handleEl.style.touchAction = '';\n });\n}\n\n// ─── Drag session helpers ─────────────────────────────────────────────────────\n\nfunction scheduleHide(session: DragSession): void {\n if (typeof requestAnimationFrame !== 'function') return;\n\n session.hideFrame = requestAnimationFrame(() => {\n session.hideFrame = null;\n session.draggedEl.style.display = 'none';\n });\n}\n\nfunction cancelHide(session: DragSession): void {\n if (session.hideFrame !== null) {\n cancelAnimationFrame(session.hideFrame);\n session.hideFrame = null;\n }\n}\n\nfunction restoreSessionElement(session: DragSession): void {\n cancelHide(session);\n session.draggedEl.style.display = session.originalDisplay;\n session.draggedEl.removeAttribute('data-dragging');\n}\n\n/**\n * Snapshot a handle's current order if not already recorded.\n * Called the first time a handle becomes an active target so we only capture\n * lists that actually participate in the drag.\n */\nfunction snapshotOrder(session: DragSession, handle: ContainerHandle): void {\n if (!session.initialOrders.has(handle)) {\n session.initialOrders.set(handle, handle.getOrderedIds());\n }\n}\n\n// ─── Session commit / cancel ──────────────────────────────────────────────────\n\nfunction cancelSession(scopeState: SortableScopeState, event: DragEvent): void {\n const session = scopeState.active;\n\n if (!session) return;\n\n restoreSessionElement(session);\n session.originalParent.insertBefore(session.draggedEl, session.originalNextSibling);\n session.placeholder.remove();\n scopeState.active = null;\n\n session.source.notifyDragEnd(session.draggedId, event);\n}\n\nfunction commitSession(scopeState: SortableScopeState, event: DragEvent): void {\n const session = scopeState.active;\n\n if (!session) return;\n\n const targetHandle = session.target;\n const targetElement = session.placeholder.parentElement;\n\n restoreSessionElement(session);\n\n if (targetHandle && scopeState.handles.has(targetHandle) && targetElement) {\n targetElement.insertBefore(session.draggedEl, session.placeholder);\n } else {\n session.originalParent.insertBefore(session.draggedEl, session.originalNextSibling);\n }\n\n session.placeholder.remove();\n scopeState.active = null;\n\n session.source.notifyDragEnd(session.draggedId, event);\n\n for (const [handle, beforeOrder] of session.initialOrders) {\n if (!scopeState.handles.has(handle)) continue;\n\n const afterOrder = handle.getOrderedIds();\n\n if (!hasOrderChanged(beforeOrder, afterOrder)) continue;\n\n handle.notifyBeforeReorder(beforeOrder, afterOrder);\n handle.commitReorder(afterOrder);\n }\n}\n\nfunction finishSession(scopeState: SortableScopeState, event: DragEvent, forceCancel: boolean): void {\n if (!scopeState.active) return;\n\n const { active } = scopeState;\n\n // If source or target became disabled during the drag, treat as cancellation.\n if (\n forceCancel ||\n !event.dataTransfer ||\n event.dataTransfer.dropEffect === 'none' ||\n !active.target ||\n !scopeState.handles.has(active.target) ||\n active.source.isDisabled() ||\n active.target.isDisabled()\n ) {\n cancelSession(scopeState, event);\n } else {\n commitSession(scopeState, event);\n }\n}\n\nfunction maybeAutoScroll(\n event: DragEvent,\n container: HTMLElement,\n axis: 'vertical' | 'horizontal',\n options: ResolvedAutoScrollOptions | null,\n): void {\n if (!options) return;\n\n const { container: scrollContainer, edgeThreshold: threshold, speed, viewport } = options;\n const rect = container.getBoundingClientRect();\n\n if (axis === 'vertical') {\n if (scrollContainer && event.clientY < rect.top + threshold) {\n container.scrollTop -= speed;\n } else if (scrollContainer && event.clientY > rect.bottom - threshold) {\n container.scrollTop += speed;\n }\n\n if (viewport && event.clientY < threshold) {\n window.scrollBy({ left: 0, top: -speed });\n } else if (viewport && event.clientY > window.innerHeight - threshold) {\n window.scrollBy({ left: 0, top: speed });\n }\n\n return;\n }\n\n if (scrollContainer && event.clientX < rect.left + threshold) {\n container.scrollLeft -= speed;\n } else if (scrollContainer && event.clientX > rect.right - threshold) {\n container.scrollLeft += speed;\n }\n\n if (viewport && event.clientX < threshold) {\n window.scrollBy({ left: -speed, top: 0 });\n } else if (viewport && event.clientX > window.innerWidth - threshold) {\n window.scrollBy({ left: speed, top: 0 });\n }\n}\n\n// ─── Keyboard reorder ─────────────────────────────────────────────────────────\n\nfunction applyKeyboardReorder(\n item: HTMLElement,\n element: HTMLElement,\n getItems: () => HTMLElement[],\n getOrderedIds: () => string[],\n key: string,\n axis: 'vertical' | 'horizontal',\n): string[] | null {\n const items = getItems();\n const currentIndex = items.indexOf(item);\n\n if (currentIndex < 0) return null;\n\n const isForward = axis === 'vertical' ? key === 'ArrowDown' : key === 'ArrowRight';\n const isBackward = axis === 'vertical' ? key === 'ArrowUp' : key === 'ArrowLeft';\n let targetIndex: number;\n\n if (isForward) {\n targetIndex = Math.min(items.length - 1, currentIndex + 1);\n } else if (isBackward) {\n targetIndex = Math.max(0, currentIndex - 1);\n } else if (key === 'Home') {\n targetIndex = 0;\n } else if (key === 'End') {\n targetIndex = items.length - 1;\n } else {\n return null;\n }\n\n // Already at the boundary — return null so the caller does not call preventDefault\n // and the browser can handle the key (e.g. scrolling the page).\n if (targetIndex === currentIndex) return null;\n\n const targetItem = items[targetIndex];\n\n if (!targetItem) return null;\n\n element.insertBefore(item, targetIndex > currentIndex ? targetItem.nextSibling : targetItem);\n item.focus();\n\n return getOrderedIds();\n}\n\n// ─── createSortableScope ──────────────────────────────────────────────────────\n\n/**\n * Create a shared scope for connected sortable containers.\n *\n * Items can be dragged between all sortables that share the same scope.\n * The scope exposes `isDragging` and `dispose()` — calling `dispose()` tears down\n * all member sortables at once.\n *\n * @example\n * ```ts\n * const scope = createSortableScope();\n * const left = createSortable({ element: leftEl, scope, getKey, onReorder });\n * const right = createSortable({ element: rightEl, scope, getKey, onReorder });\n *\n * // Tear down everything at once:\n * scope.dispose();\n * ```\n */\nexport function createSortableScope(): SortableScope {\n const state: SortableScopeState = { active: null, disposables: new Set(), handles: new Set() };\n const disposable = createDisposable(() => {\n // Dispose all registered sortables (each dispose() call is idempotent)\n for (const disposeFn of state.disposables) {\n disposeFn();\n }\n });\n\n const scope = {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return state.active !== null;\n },\n [SCOPE_BRAND]: true as const,\n [Symbol.dispose]: disposable[Symbol.dispose],\n } as SortableScope;\n\n sortableScopeStates.set(scope, state);\n\n return scope;\n}\n\n// ─── createSortable ───────────────────────────────────────────────────────────\n\n/**\n * Makes a list of items sortable via native HTML drag interactions.\n *\n * Provide `getKey` to map each item element to a stable string identity.\n * `createSortable` marks qualifying children with `data-dnd-item` and sets\n * `draggable=\"true\"` automatically.\n *\n * Style the drop indicator by targeting `.dnd-placeholder` in your CSS.\n * Supports keyboard reordering (arrow keys, Home/End) and auto-scrolling near edges.\n *\n * @example\n * ```ts\n * import { createSortable } from '@vielzeug/dnd';\n *\n * using sortable = createSortable({\n * element: listEl,\n * getKey: (el) => el.dataset.id!,\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * });\n * ```\n */\nexport function createSortable(options: SortableOptions): Sortable {\n const {\n autoScroll = true,\n axis = 'vertical',\n element,\n getKey,\n handle,\n keyboard = true,\n placeholderClass = 'dnd-placeholder',\n scope = createSortableScope(),\n } = options;\n const autoScrollOptions = resolveAutoScrollOptions(autoScroll);\n const scopeState = getSortableScopeState(scope);\n\n if (handle !== undefined && handle.trim() === '') {\n warn(\n 'handle option is an empty string — no handle elements will be found. Provide a valid CSS selector or omit the option.',\n );\n }\n\n const getItems = (): HTMLElement[] =>\n Array.from(element.children).filter((c) => (c as HTMLElement).hasAttribute(ITEM_ATTR)) as HTMLElement[];\n\n const getOrderedIds = (): string[] => getItems().map((el) => getKey(el));\n\n const syncItems = (): void => {\n clearHandleAttributes(element);\n\n getItems().forEach((el) => {\n el.setAttribute('role', 'listitem');\n el.tabIndex = 0;\n\n if (handle) {\n el.removeAttribute('draggable');\n el.style.touchAction = '';\n el.querySelectorAll<HTMLElement>(handle).forEach((handleEl) => {\n handleEl.setAttribute(HANDLE_ATTR, '');\n handleEl.setAttribute('draggable', 'true');\n // See the matching comment above `cleanupItems()` for why this matters at all.\n handleEl.style.touchAction = 'none';\n });\n } else {\n el.setAttribute('draggable', 'true');\n // A native mouse drag has no competing gesture to arbitrate; touch does. Without this,\n // a mobile browser can decide the very first bit of finger movement is a page\n // scroll/pan — a decision it makes independently of, and before, this library's own\n // touch-shim threshold/`preventDefault()` logic ever runs — and hand the rest of the\n // gesture to native scrolling. Once that happens the item never receives the\n // `dragover` sequence needed to update the drop target, so the session ends up\n // committing back to wherever it started: indistinguishable from the drop \"reverting\".\n // `touch-action: none` opts the element out of every default touch gesture from\n // `touchstart` onward, leaving the whole interaction to this library's own JS.\n el.style.touchAction = 'none';\n }\n });\n };\n\n const markItems = (): void => {\n const seenKeys = new Set<string>();\n\n // Mark all children that have a key as sortable items\n Array.from(element.children).forEach((child) => {\n const el = child as HTMLElement;\n\n try {\n const key = getKey(el);\n\n if (key) {\n if (seenKeys.has(key)) {\n warn(\n `getKey returned the duplicate key \"${key}\" for two sibling items — onReorder's ids and applyReorder may become inconsistent. Ensure getKey returns a unique value per item.`,\n );\n } else {\n seenKeys.add(key);\n }\n\n el.setAttribute(ITEM_ATTR, '');\n }\n } catch (err) {\n warn(\n `getKey threw for a child element — the item will not be sortable. Check your getKey implementation. ${String(err)}`,\n );\n }\n });\n\n syncItems();\n };\n\n const cleanupItems = (): void => {\n clearHandleAttributes(element);\n\n element.querySelectorAll<HTMLElement>(`[${ITEM_ATTR}]`).forEach((item) => {\n item.removeAttribute(ITEM_ATTR);\n item.removeAttribute('draggable');\n item.removeAttribute('role');\n item.removeAttribute('tabindex');\n item.style.touchAction = '';\n });\n };\n\n const createPlaceholder = (source: HTMLElement): HTMLElement => {\n const p = document.createElement('div');\n\n p.className = placeholderClass;\n p.setAttribute('aria-hidden', 'true');\n\n if (axis === 'horizontal') {\n p.style.width = `${source.offsetWidth}px`;\n } else {\n p.style.height = `${source.offsetHeight}px`;\n }\n\n return p;\n };\n\n let lastRevert: (() => void) | null = null;\n\n const handle_: ContainerHandle = {\n commitReorder: (orderedIds) => {\n if (!options.onReorder) return;\n\n const event: ReorderEvent = {\n ids: orderedIds,\n setRevert(fn) {\n lastRevert = fn;\n },\n };\n\n options.onReorder(event);\n },\n getOrderedIds,\n isDisabled: () => resolveDisabled(options.disabled),\n notifyBeforeReorder: (from, to) => options.onBeforeReorder?.(from, to),\n notifyDragEnd: (id, event) => options.onDragEnd?.(id, event),\n notifyDragStart: (id, event) => options.onDragStart?.(id, event),\n };\n\n scopeState.handles.add(handle_);\n\n const handleDragStart = (e: DragEvent): void => {\n if (scopeState.active) return;\n\n if (handle_.isDisabled()) return;\n\n const target = e.target as HTMLElement;\n const item = target.closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item) return;\n\n if (handle && !target.closest(handle)) return;\n\n const originalParent = item.parentElement;\n\n if (!originalParent) return;\n\n const placeholder = createPlaceholder(item);\n const originalNextSibling = item.nextSibling;\n const activeId = getKey(item);\n\n // Snapshot only the source handle at drag start; targets are snapshotted lazily.\n const initialOrders = new Map<ContainerHandle, string[]>();\n\n initialOrders.set(handle_, handle_.getOrderedIds());\n item.setAttribute('data-dragging', '');\n originalParent.insertBefore(placeholder, originalNextSibling);\n\n const session: DragSession = {\n draggedEl: item,\n draggedId: activeId,\n hideFrame: null,\n initialOrders,\n originalDisplay: item.style.display,\n originalNextSibling,\n originalParent,\n placeholder,\n source: handle_,\n target: handle_,\n };\n\n scheduleHide(session);\n scopeState.active = session;\n\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', activeId);\n\n if (options.dragImage) {\n const preview =\n typeof options.dragImage === 'function' ? options.dragImage(activeId, item, e) : options.dragImage;\n const [offsetX, offsetY] = options.dragImageOffset ?? [0, 0];\n\n if (preview) e.dataTransfer.setDragImage(preview, offsetX, offsetY);\n }\n }\n\n handle_.notifyDragStart(session.draggedId, e);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n maybeAutoScroll(e, element, axis, autoScrollOptions);\n\n // Lazily snapshot this handle's order the first time it becomes a target.\n snapshotOrder(session, handle_);\n\n const { draggedEl, placeholder } = session;\n const target = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!target) {\n // Only append placeholder when it isn't already inside this container.\n // Moving it to the end on every over-empty-space event causes the\n // placeholder to oscillate between positions as the cursor moves.\n if (placeholder.parentElement !== element) {\n element.appendChild(placeholder);\n }\n\n session.target = handle_;\n\n return;\n }\n\n if (target === draggedEl || target === placeholder) return;\n\n const rect = target.getBoundingClientRect();\n const insertAfter =\n axis === 'vertical' ? e.clientY >= rect.top + rect.height / 2 : e.clientX >= rect.left + rect.width / 2;\n\n element.insertBefore(placeholder, insertAfter ? target.nextSibling : target);\n session.target = handle_;\n };\n\n const handleDrop = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n // Record the drop target; the actual commit happens in handleDragEnd where\n // dataTransfer.dropEffect tells us whether the browser accepted the operation.\n session.target = handle_;\n };\n\n const handleDragEnd = (e: DragEvent): void => {\n if (scopeState.active?.source !== handle_) return;\n\n finishSession(scopeState, e, false);\n };\n\n const handleKeydown = (e: KeyboardEvent): void => {\n if (!keyboard || handle_.isDisabled()) return;\n\n const tagName = (e.target as HTMLElement | null)?.tagName;\n\n if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return;\n\n const item = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item || !element.contains(item)) return;\n\n const prevOrder = getOrderedIds();\n const newOrder = applyKeyboardReorder(item, element, getItems, getOrderedIds, e.key, axis);\n\n // null means unrecognized key or boundary — let the browser handle it (e.g. page scroll)\n if (newOrder === null) return;\n\n e.preventDefault();\n handle_.notifyBeforeReorder(prevOrder, newOrder);\n handle_.commitReorder(newOrder);\n };\n\n markItems();\n\n const disposable = createDisposable(() => {\n scopeState.disposables.delete(disposable.dispose);\n\n if (scopeState.active && (scopeState.active.source === handle_ || scopeState.active.target === handle_)) {\n finishSession(scopeState, new Event('dragend') as DragEvent, true);\n }\n\n scopeState.handles.delete(handle_);\n element.removeAttribute('role');\n cleanupItems();\n });\n\n element.setAttribute('role', 'list');\n element.addEventListener('dragstart', handleDragStart, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n element.addEventListener('dragend', handleDragEnd, { signal: disposable.disposalSignal });\n element.addEventListener('keydown', handleKeydown, { signal: disposable.disposalSignal });\n\n // Register with scope so scope.dispose() can tear this down\n scopeState.disposables.add(disposable.dispose);\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return scopeState.active?.source === handle_;\n },\n revert: () => {\n lastRevert?.();\n lastRevert = null;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n sync: () => {\n markItems();\n },\n };\n}\n\n// ─── applyReorder ─────────────────────────────────────────────────────────────\n\n/**\n * Applies a sorted key array to a backing data array.\n * Unknown keys are ignored; items not present in `ids` are appended in original order.\n */\nexport function applyReorder<T>(items: T[], ids: string[], getKey: (item: T) => string): T[] {\n const byId = new Map(items.map((item) => [getKey(item), item] as const));\n const ordered: T[] = [];\n\n for (const id of ids) {\n if (!byId.has(id)) continue;\n\n const item = byId.get(id) as T;\n\n ordered.push(item);\n byId.delete(id);\n }\n\n for (const item of byId.values()) ordered.push(item);\n\n return ordered;\n}\n","import { warn } from './_dev';\nimport { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\nexport interface TouchDragOptions {\n /**\n * Set to `true` (or mutate this same options object's field later) to pause the shim without\n * disposing it — matches `createSortable`'s/`createDropZone`'s own `disabled` option.\n */\n disabled?: boolean;\n /**\n * CSS selector identifying draggable elements. Defaults to `[draggable=\"true\"]` — the exact\n * attribute `createSortable`/`createDropZone`-managed elements already carry, so the default\n * needs no configuration for the common case of bridging touch to an existing sortable/drop zone.\n */\n draggableSelector?: string;\n /**\n * Renders a floating clone of the dragged element that follows the touch point for the\n * duration of the drag. @default true\n *\n * A native mouse-driven HTML5 drag gets this for free: the browser snapshots the dragged\n * element into its own drag image the moment `dragstart` fires, then keeps that image under\n * the cursor itself for the whole gesture — `createSortable`'s `scheduleHide()` hides the real\n * element a frame later assuming that snapshot already exists. This shim's `dragstart` is a\n * synthetic `Event`, not a real drag, so the browser never creates that image; without this\n * preview, `scheduleHide()` still hides the real element on schedule and touch users are left\n * with *no* visual feedback for the whole gesture — only an empty placeholder box moving\n * between positions — making any drop feel arbitrary regardless of where it actually lands.\n * Set to `false` to render fully custom feedback instead (e.g. toggling a class from your own\n * `dragstart`/`dragend` listeners).\n */\n showDragPreview?: boolean;\n}\n\nconst PREVIEW_Z_INDEX = 2147483647;\n\n/**\n * Floating visual stand-in for the (real, but hidden) dragged element — see `showDragPreview`'s\n * doc comment for why touch needs one at all. `cloneNode(true)` only clones light-DOM content;\n * a draggable item that renders its visible content inside a shadow root (e.g. a shadow-DOM\n * custom element) will clone as an empty shell. This mirrors the same light-DOM-only limitation\n * every `cloneNode`-based drag preview (most JS sortable libraries included) accepts in exchange\n * for never having to special-case every possible element type.\n */\nfunction createPreviewElement(source: HTMLElement): HTMLElement {\n const rect = source.getBoundingClientRect();\n const clone = source.cloneNode(true) as HTMLElement;\n\n clone.removeAttribute('id');\n clone.setAttribute('aria-hidden', 'true');\n clone.setAttribute('inert', '');\n clone.setAttribute('data-dnd-touch-preview', '');\n Object.assign(clone.style, {\n height: `${rect.height}px`,\n left: `${rect.left}px`,\n margin: '0',\n opacity: '0.85',\n pointerEvents: 'none',\n position: 'fixed',\n top: `${rect.top}px`,\n transition: 'none',\n width: `${rect.width}px`,\n zIndex: String(PREVIEW_Z_INDEX),\n });\n\n document.body.appendChild(clone);\n\n return clone;\n}\n\n// Deliberately a plain object, never a real `new DataTransfer()`. Verified directly against a\n// real Chromium build (not just jsdom): a genuine `DataTransfer` instance created outside an\n// active *native* drag operation is permanently in the spec's \"disabled mode\" — every property\n// write, `dropEffect` included, is silently ignored, and every read of it keeps returning the\n// type's default (`'none'`), no matter what's assigned. `createSortable`/`createDropZone`'s own\n// `finishSession()` reads `dropEffect === 'none'` as an explicit cancellation signal, so with a\n// real `DataTransfer` backing this shim's synthetic events, *every* commit — same-column reorder\n// or cross-column move alike — would always revert, regardless of where it was actually dropped.\n// A plain object has no such \"mode\" concept: its `dropEffect` is an ordinary, freely-settable JS\n// property, which is all `createSortable`/`createDropZone`'s handlers ever read or write on the\n// events this shim dispatches. This is also why the previous Safari-<14 fallback (which already\n// used a plain object) never exhibited this bug — it was the correct approach the whole time.\nfunction makeDataTransfer(): DataTransfer {\n return {\n dropEffect: 'move',\n effectAllowed: 'move',\n getData: () => '',\n setData() {},\n setDragImage() {},\n } as unknown as DataTransfer;\n}\n\n/**\n * Bridges touch gestures to the synthetic `DragEvent` sequence `createSortable()`/\n * `createDropZone()` already listen for. HTML5 drag-and-drop has no native touch story —\n * this translates `touchstart`/`touchmove`/`touchend`/`touchcancel` into\n * `dragstart`/`dragover`/`drop`/`dragend` on the same document, so touch devices get\n * sortable/drop-zone behavior for free with no extra wiring at each call site.\n *\n * Listens at the `document` level (matching how `createSortable`/`createDropZone` themselves\n * scope drag detection) — one instance covers the whole page. Dispose to remove all listeners.\n *\n * @example\n * ```ts\n * using touchDrag = createTouchDragShim();\n * // ...later, or via the `using` declaration above:\n * touchDrag.dispose();\n * ```\n */\nexport function createTouchDragShim(options: TouchDragOptions = {}): Disposable {\n const dt = makeDataTransfer();\n const DRAG_START_DISTANCE_PX = 6;\n\n let pendingDraggable: HTMLElement | null = null;\n let pendingStartPoint: { clientX: number; clientY: number } | null = null;\n let dragging: HTMLElement | null = null;\n let lastTarget: Element | null = null;\n\n // The preview follows the touch point via a `translate3d` delta from wherever the touch was\n // when the preview was created (`previewOrigin`), not from the element's own rect — so it never\n // \"jumps\" at drag start (the delta starts at zero regardless of exactly where inside the\n // element the user first touched).\n let previewEl: HTMLElement | null = null;\n let previewOrigin: { clientX: number; clientY: number } | null = null;\n\n function removePreview(): void {\n previewEl?.remove();\n previewEl = null;\n previewOrigin = null;\n }\n\n // Hides both the real dragged element AND the floating preview for the duration of the\n // hit-test, then restores them. Hiding `dragging` alone isn't enough: the preview sits on top\n // of everything (by design, to follow the touch point) and, since `createPreviewElement()`\n // clones the dragged element's subtree, it can carry cloned custom elements (e.g. shadow-DOM\n // components) that re-run their own setup on the clone and may set `pointer-events` on their\n // *internal* shadow content — which a `pointer-events: none` set only on the preview's light-DOM\n // root does not reliably override. Hiding it outright (rather than relying on `pointer-events`)\n // sidesteps that entirely: a hidden element is never returned by `elementFromPoint`, regardless\n // of what any of its descendants — shadow DOM included — set.\n function elementBelow(clientX: number, clientY: number): Element | null {\n const prevDraggingDisplay = dragging?.style.display ?? '';\n const prevPreviewDisplay = previewEl?.style.display ?? '';\n\n if (dragging) dragging.style.display = 'none';\n\n if (previewEl) previewEl.style.display = 'none';\n\n const below = document.elementFromPoint(clientX, clientY);\n\n if (dragging) dragging.style.display = prevDraggingDisplay;\n\n if (previewEl) previewEl.style.display = prevPreviewDisplay;\n\n return below;\n }\n\n const disposable = createDisposable(() => {\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n });\n\n // Built on a plain `Event` rather than `new DragEvent(...)`: jsdom (and potentially other\n // non-browser DOM implementations) doesn't expose a `DragEvent` constructor at all, and\n // `createSortable`/`createDropZone`'s own handlers only ever read `type`/`clientX`/`clientY`/\n // `dataTransfer` off the event object — they never check `instanceof DragEvent` — so a patched\n // plain `Event` is indistinguishable to them and works everywhere a real `DragEvent` would.\n function dispatch(el: Element, type: string, clientX: number, clientY: number): void {\n const event = new Event(type, { bubbles: true, cancelable: true });\n\n Object.defineProperty(event, 'clientX', { configurable: true, value: clientX });\n Object.defineProperty(event, 'clientY', { configurable: true, value: clientY });\n Object.defineProperty(event, 'dataTransfer', { configurable: true, value: dt });\n el.dispatchEvent(event);\n }\n\n document.addEventListener(\n 'touchstart',\n (e: TouchEvent) => {\n if (resolveDisabled(options.disabled)) return;\n\n const touch = e.touches[0];\n\n if (!touch) return;\n\n const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;\n const draggable = target?.closest<HTMLElement>(options.draggableSelector ?? '[draggable=\"true\"]');\n\n if (!draggable) return;\n\n pendingDraggable = draggable;\n pendingStartPoint = { clientX: touch.clientX, clientY: touch.clientY };\n },\n { passive: false, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchmove',\n (e: TouchEvent) => {\n const touch = e.touches[0];\n\n if (!touch) return;\n\n if (!dragging) {\n if (!pendingDraggable || !pendingStartPoint) return;\n\n const dx = touch.clientX - pendingStartPoint.clientX;\n const dy = touch.clientY - pendingStartPoint.clientY;\n const distance = Math.hypot(dx, dy);\n\n if (distance < DRAG_START_DISTANCE_PX) return;\n\n dragging = pendingDraggable;\n lastTarget = pendingDraggable;\n\n // Captured before dispatching 'dragstart': createSortable's own scheduleHide() hides\n // `dragging` a frame after that event fires, and by then its layout position may already\n // reflect the placeholder having been inserted. Reading the rect now, while it's still\n // exactly where the user picked it up, is what createPreviewElement() sizes/positions the\n // clone from.\n //\n // Wrapped in try/catch: `createPreviewElement()` clones the dragged element's whole\n // subtree, which — for a draggable item backed by custom elements — re-instantiates live\n // copies that re-run their own `connectedCallback`/setup once appended to `document.body`.\n // A best-effort *visual* feature must never be able to take down the *functional* one:\n // if any of that throws (a component assuming a page-unique id, a framework-internal\n // invariant, anything), the drag must still start normally with no preview, not silently\n // never start at all.\n if (options.showDragPreview !== false) {\n try {\n previewEl = createPreviewElement(dragging);\n previewOrigin = { clientX: touch.clientX, clientY: touch.clientY };\n } catch (err) {\n warn(`drag preview failed to render, continuing without one: ${String(err)}`);\n removePreview();\n }\n }\n\n dispatch(dragging, 'dragstart', touch.clientX, touch.clientY);\n }\n\n if (previewEl && previewOrigin) {\n const dx = touch.clientX - previewOrigin.clientX;\n const dy = touch.clientY - previewOrigin.clientY;\n\n previewEl.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;\n }\n\n const below = elementBelow(touch.clientX, touch.clientY);\n\n if (below && below !== lastTarget) {\n if (lastTarget) dispatch(lastTarget, 'dragleave', touch.clientX, touch.clientY);\n\n lastTarget = below;\n }\n\n if (below) dispatch(below, 'dragover', touch.clientX, touch.clientY);\n\n pendingDraggable = null;\n pendingStartPoint = null;\n e.preventDefault();\n },\n { passive: false, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchend',\n (e: TouchEvent) => {\n if (!dragging) {\n pendingDraggable = null;\n pendingStartPoint = null;\n\n return;\n }\n\n const touch = e.changedTouches[0];\n\n if (!touch) return;\n\n const below = elementBelow(touch.clientX, touch.clientY);\n\n if (below) dispatch(below, 'drop', touch.clientX, touch.clientY);\n\n dispatch(dragging, 'dragend', touch.clientX, touch.clientY);\n\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n },\n { passive: true, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchcancel',\n (e: TouchEvent) => {\n if (!dragging) {\n pendingDraggable = null;\n pendingStartPoint = null;\n\n return;\n }\n\n const touch = e.changedTouches[0];\n\n if (!touch) return;\n\n dispatch(dragging, 'dragend', touch.clientX, touch.clientY);\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n },\n { passive: true, signal: disposable.disposalSignal },\n );\n\n return disposable;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dnd.js","names":[],"sources":["../src/_shared.ts","../src/drop-zone.ts","../src/errors.ts","../src/_dev.ts","../src/sortable.ts","../src/touch.ts"],"sourcesContent":["import { type Disposable } from './types';\n\n// ─── Disposable ──────────────────────────────────────────────────────────────\n\n/**\n * Create a `Disposable` backed by an `AbortController`.\n * `dispose()` is idempotent — the signal aborts and `cleanup` runs at most once.\n * Pass the returned `disposalSignal` to `addEventListener` calls so removal happens\n * automatically on dispose, instead of pairing manual `removeEventListener` calls.\n */\nexport function createDisposable(cleanup?: () => void): Disposable {\n let disposed = false;\n const abortController = new AbortController();\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n abortController.abort();\n cleanup?.();\n };\n\n return {\n get disposalSignal() {\n return abortController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n [Symbol.dispose]: dispose,\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nexport function resolveDisabled(disabled: boolean | undefined): boolean {\n return disabled === true;\n}\n","import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n","/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n","const isDev = !(globalThis as { __DND_PROD__?: boolean }).__DND_PROD__;\n\n/** @internal @security Messages may include developer-supplied config values. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/dnd] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n","import { warn } from './_dev';\nimport { createDisposable, resolveDisabled } from './_shared';\nimport { DndScopeError } from './errors';\nimport { type Disposable } from './types';\n\n// ─── Branded scope ────────────────────────────────────────────────────────────\n\nconst SCOPE_BRAND = Symbol('SortableScope');\n\nexport interface SortableScope extends Disposable {\n /** `true` while any sortable in this scope is actively dragging. */\n readonly isDragging: boolean;\n readonly [SCOPE_BRAND]: true;\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AutoScrollOptions {\n /** Distance in pixels from an edge that triggers auto-scroll. @default 32 */\n edgeThreshold?: number;\n /** Pixels scrolled per dragover frame while near an edge. @default 18 */\n speed?: number;\n /** Scroll the sortable container while dragging near its edges. @default true */\n container?: boolean;\n /** Scroll the viewport while dragging near the window edges. @default false */\n viewport?: boolean;\n}\n\n/**\n * Passed to `onReorder` after every successful reorder (drag or keyboard).\n */\nexport interface ReorderEvent {\n /** The new ordered list of item keys after the reorder. */\n ids: string[];\n /**\n * Register a revert function that will be called when `sortable.revert()` is invoked.\n * Useful for rolling back optimistic UI updates on server error.\n * Only the most recent `setRevert` registration is retained — a new reorder overwrites it.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n setRevert(fn: () => void): void;\n}\n\nexport interface SortableOptions {\n /** Container element whose direct-child items are sortable. */\n element: HTMLElement;\n /** Shared scope for connected sortable containers. Containers only exchange items within the same scope. */\n scope?: SortableScope;\n /**\n * Selector for the drag handle inside each item.\n * When omitted the whole item is the handle.\n */\n handle?: string;\n /**\n * Enables keyboard-based reordering using arrow keys plus Home/End.\n * @default true\n */\n keyboard?: boolean;\n /**\n * Returns the identity key for a given item element.\n * This separates the \"what is this item?\" concern (yours) from the \"which children\n * are sortable?\" concern (ours — marked with `data-dnd-item`).\n *\n * @example\n * ```ts\n * getKey: (el) => el.dataset.taskId!\n * ```\n */\n getKey: (element: HTMLElement) => string;\n /** Sorting axis used to compute insertion position. @default 'vertical' */\n axis?: 'vertical' | 'horizontal';\n /** Auto-scrolls the container (and viewport) near edges while dragging. @default true */\n autoScroll?: boolean | AutoScrollOptions;\n /** Optional custom drag preview element. */\n dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);\n /** CSS class applied to the placeholder element. @default 'dnd-placeholder' */\n placeholderClass?: string;\n /**\n * Called with a {@link ReorderEvent} after a successful reorder, only when the order changed.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * ```\n */\n onReorder?: (event: ReorderEvent) => void;\n /**\n * Called just before a successful drag commit with the before and after order snapshots.\n * Use this hook to set up FLIP animations — the source items are still in their\n * pre-commit positions at the time of the call.\n *\n * @example\n * ```ts\n * onBeforeReorder: (from, to) => {\n * // record element positions here, then animate after the next microtask\n * }\n * ```\n */\n onBeforeReorder?: (from: string[], to: string[]) => void;\n /**\n * When `true`, drag interactions are ignored.\n *\n * Note: if `disabled` transitions to `true` while a drag is in progress the\n * drag is treated as a cancellation — the item snaps back to its original\n * position rather than committing the last placeholder location.\n */\n disabled?: boolean;\n /** Called when the user starts dragging an item. */\n onDragStart?: (id: string, event: DragEvent) => void;\n /** Called when a drag ends (whether dropped or cancelled). */\n onDragEnd?: (id: string, event: DragEvent) => void;\n /**\n * Hotspot offset `[x, y]` passed to `setDragImage`.\n * Controls which point of the preview image follows the cursor.\n * @default [0, 0]\n */\n dragImageOffset?: [number, number];\n}\n\nexport interface Sortable extends Disposable {\n readonly isDragging: boolean;\n /**\n * Calls the revert function registered via `setRevert` in the last `onReorder` invocation (if any) and clears it.\n * A no-op when no revert function was registered or has already been consumed.\n *\n * Works for both drag-based and keyboard-based reorders.\n * Note: only the most recent reorder can be reverted; a new reorder overwrites the stored function.\n *\n * @example\n * ```ts\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * // later, on server error:\n * sortable.revert();\n * ```\n */\n revert(): void;\n /**\n * Re-reads the container's children and reapplies `draggable`, ARIA roles,\n * and handle attributes. Call this after programmatically adding, removing,\n * or replacing items — e.g. after a framework render that replaces DOM nodes.\n *\n * Not needed when items are only reordered via drag or keyboard.\n */\n sync(): void;\n}\n\n// ─── Internal types ───────────────────────────────────────────────────────────\n\ninterface ResolvedAutoScrollOptions {\n container: boolean;\n edgeThreshold: number;\n speed: number;\n viewport: boolean;\n}\n\n/** Per-container closure passed into shared session functions. */\ninterface ContainerHandle {\n getOrderedIds: () => string[];\n isDisabled: () => boolean;\n commitReorder: (orderedIds: string[]) => void;\n notifyBeforeReorder: (from: string[], to: string[]) => void;\n notifyDragEnd: (id: string, event: DragEvent) => void;\n notifyDragStart: (id: string, event: DragEvent) => void;\n}\n\n/**\n * Encapsulates all mutable state for a single in-progress drag operation.\n * Created at dragstart, destroyed on dragend/cancel.\n */\ninterface DragSession {\n draggedEl: HTMLElement;\n draggedId: string;\n /**\n * Order snapshots captured lazily: snapshot is taken the first time a handle\n * becomes the active target (and always for the source at drag start).\n */\n initialOrders: Map<ContainerHandle, string[]>;\n originalDisplay: string;\n originalNextSibling: ChildNode | null;\n originalParent: HTMLElement;\n placeholder: HTMLElement;\n source: ContainerHandle;\n target: ContainerHandle | null;\n /** rAF handle for the deferred element-hide; null once fired or cancelled. */\n hideFrame: number | null;\n}\n\ninterface SortableScopeState {\n active: DragSession | null;\n handles: Set<ContainerHandle>;\n /** All sortables registered in this scope, kept for scope.dispose(). */\n disposables: Set<() => void>;\n}\n\n// ─── Internal constants ───────────────────────────────────────────────────────\n\nconst HANDLE_ATTR = 'data-dnd-handle';\nconst ITEM_ATTR = 'data-dnd-item';\nconst sortableScopeStates = new WeakMap<SortableScope, SortableScopeState>();\nconst DEFAULT_AUTO_SCROLL: ResolvedAutoScrollOptions = {\n container: true,\n edgeThreshold: 32,\n speed: 18,\n viewport: false,\n};\n\n// ─── Scope helpers ────────────────────────────────────────────────────────────\n\nfunction getSortableScopeState(scope: SortableScope): SortableScopeState {\n const state = sortableScopeStates.get(scope);\n\n if (!state) throw new DndScopeError();\n\n return state;\n}\n\n// ─── Option resolution ────────────────────────────────────────────────────────\n\nfunction resolveAutoScrollOptions(\n autoScroll: boolean | AutoScrollOptions | undefined,\n): ResolvedAutoScrollOptions | null {\n if (autoScroll === false) return null;\n\n if (autoScroll === true || autoScroll === undefined) return DEFAULT_AUTO_SCROLL;\n\n return {\n container: autoScroll.container ?? true,\n edgeThreshold: autoScroll.edgeThreshold ?? 32,\n speed: autoScroll.speed ?? 18,\n viewport: autoScroll.viewport ?? false,\n };\n}\n\n// ─── DOM helpers ──────────────────────────────────────────────────────────────\n\nfunction hasOrderChanged(before: string[], after: string[]): boolean {\n return after.length !== before.length || after.some((id, index) => id !== before[index]);\n}\n\nfunction clearHandleAttributes(element: HTMLElement): void {\n element.querySelectorAll<HTMLElement>(`[${HANDLE_ATTR}]`).forEach((handleEl) => {\n handleEl.removeAttribute(HANDLE_ATTR);\n handleEl.removeAttribute('draggable');\n handleEl.style.touchAction = '';\n });\n}\n\n// ─── Drag session helpers ─────────────────────────────────────────────────────\n\nfunction scheduleHide(session: DragSession): void {\n if (typeof requestAnimationFrame !== 'function') return;\n\n session.hideFrame = requestAnimationFrame(() => {\n session.hideFrame = null;\n session.draggedEl.style.display = 'none';\n });\n}\n\nfunction cancelHide(session: DragSession): void {\n if (session.hideFrame !== null) {\n cancelAnimationFrame(session.hideFrame);\n session.hideFrame = null;\n }\n}\n\nfunction restoreSessionElement(session: DragSession): void {\n cancelHide(session);\n session.draggedEl.style.display = session.originalDisplay;\n session.draggedEl.removeAttribute('data-dragging');\n}\n\n/**\n * Snapshot a handle's current order if not already recorded.\n * Called the first time a handle becomes an active target so we only capture\n * lists that actually participate in the drag.\n */\nfunction snapshotOrder(session: DragSession, handle: ContainerHandle): void {\n if (!session.initialOrders.has(handle)) {\n session.initialOrders.set(handle, handle.getOrderedIds());\n }\n}\n\n// ─── Session commit / cancel ──────────────────────────────────────────────────\n\nfunction cancelSession(scopeState: SortableScopeState, event: DragEvent): void {\n const session = scopeState.active;\n\n if (!session) return;\n\n restoreSessionElement(session);\n session.originalParent.insertBefore(session.draggedEl, session.originalNextSibling);\n session.placeholder.remove();\n scopeState.active = null;\n\n session.source.notifyDragEnd(session.draggedId, event);\n}\n\nfunction commitSession(scopeState: SortableScopeState, event: DragEvent): void {\n const session = scopeState.active;\n\n if (!session) return;\n\n const targetHandle = session.target;\n const targetElement = session.placeholder.parentElement;\n\n restoreSessionElement(session);\n\n if (targetHandle && scopeState.handles.has(targetHandle) && targetElement) {\n targetElement.insertBefore(session.draggedEl, session.placeholder);\n } else {\n session.originalParent.insertBefore(session.draggedEl, session.originalNextSibling);\n }\n\n session.placeholder.remove();\n scopeState.active = null;\n\n session.source.notifyDragEnd(session.draggedId, event);\n\n for (const [handle, beforeOrder] of session.initialOrders) {\n if (!scopeState.handles.has(handle)) continue;\n\n const afterOrder = handle.getOrderedIds();\n\n if (!hasOrderChanged(beforeOrder, afterOrder)) continue;\n\n handle.notifyBeforeReorder(beforeOrder, afterOrder);\n handle.commitReorder(afterOrder);\n }\n}\n\nfunction finishSession(scopeState: SortableScopeState, event: DragEvent, forceCancel: boolean): void {\n if (!scopeState.active) return;\n\n const { active } = scopeState;\n\n // If source or target became disabled during the drag, treat as cancellation.\n if (\n forceCancel ||\n !event.dataTransfer ||\n event.dataTransfer.dropEffect === 'none' ||\n !active.target ||\n !scopeState.handles.has(active.target) ||\n active.source.isDisabled() ||\n active.target.isDisabled()\n ) {\n cancelSession(scopeState, event);\n } else {\n commitSession(scopeState, event);\n }\n}\n\nfunction maybeAutoScroll(\n event: DragEvent,\n container: HTMLElement,\n axis: 'vertical' | 'horizontal',\n options: ResolvedAutoScrollOptions | null,\n): void {\n if (!options) return;\n\n const { container: scrollContainer, edgeThreshold: threshold, speed, viewport } = options;\n const rect = container.getBoundingClientRect();\n\n if (axis === 'vertical') {\n if (scrollContainer && event.clientY < rect.top + threshold) {\n container.scrollTop -= speed;\n } else if (scrollContainer && event.clientY > rect.bottom - threshold) {\n container.scrollTop += speed;\n }\n\n if (viewport && event.clientY < threshold) {\n window.scrollBy({ left: 0, top: -speed });\n } else if (viewport && event.clientY > window.innerHeight - threshold) {\n window.scrollBy({ left: 0, top: speed });\n }\n\n return;\n }\n\n if (scrollContainer && event.clientX < rect.left + threshold) {\n container.scrollLeft -= speed;\n } else if (scrollContainer && event.clientX > rect.right - threshold) {\n container.scrollLeft += speed;\n }\n\n if (viewport && event.clientX < threshold) {\n window.scrollBy({ left: -speed, top: 0 });\n } else if (viewport && event.clientX > window.innerWidth - threshold) {\n window.scrollBy({ left: speed, top: 0 });\n }\n}\n\n// ─── Keyboard reorder ─────────────────────────────────────────────────────────\n\nfunction applyKeyboardReorder(\n item: HTMLElement,\n element: HTMLElement,\n getItems: () => HTMLElement[],\n getOrderedIds: () => string[],\n key: string,\n axis: 'vertical' | 'horizontal',\n): string[] | null {\n const items = getItems();\n const currentIndex = items.indexOf(item);\n\n if (currentIndex < 0) return null;\n\n const isForward = axis === 'vertical' ? key === 'ArrowDown' : key === 'ArrowRight';\n const isBackward = axis === 'vertical' ? key === 'ArrowUp' : key === 'ArrowLeft';\n let targetIndex: number;\n\n if (isForward) {\n targetIndex = Math.min(items.length - 1, currentIndex + 1);\n } else if (isBackward) {\n targetIndex = Math.max(0, currentIndex - 1);\n } else if (key === 'Home') {\n targetIndex = 0;\n } else if (key === 'End') {\n targetIndex = items.length - 1;\n } else {\n return null;\n }\n\n // Already at the boundary — return null so the caller does not call preventDefault\n // and the browser can handle the key (e.g. scrolling the page).\n if (targetIndex === currentIndex) return null;\n\n const targetItem = items[targetIndex];\n\n if (!targetItem) return null;\n\n element.insertBefore(item, targetIndex > currentIndex ? targetItem.nextSibling : targetItem);\n item.focus();\n\n return getOrderedIds();\n}\n\n// ─── createSortableScope ──────────────────────────────────────────────────────\n\n/**\n * Create a shared scope for connected sortable containers.\n *\n * Items can be dragged between all sortables that share the same scope.\n * The scope exposes `isDragging` and `dispose()` — calling `dispose()` tears down\n * all member sortables at once.\n *\n * @example\n * ```ts\n * const scope = createSortableScope();\n * const left = createSortable({ element: leftEl, scope, getKey, onReorder });\n * const right = createSortable({ element: rightEl, scope, getKey, onReorder });\n *\n * // Tear down everything at once:\n * scope.dispose();\n * ```\n */\nexport function createSortableScope(): SortableScope {\n const state: SortableScopeState = { active: null, disposables: new Set(), handles: new Set() };\n const disposable = createDisposable(() => {\n // Dispose all registered sortables (each dispose() call is idempotent)\n for (const disposeFn of state.disposables) {\n disposeFn();\n }\n });\n\n const scope = {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return state.active !== null;\n },\n [SCOPE_BRAND]: true as const,\n [Symbol.dispose]: disposable[Symbol.dispose],\n } as SortableScope;\n\n sortableScopeStates.set(scope, state);\n\n return scope;\n}\n\n// ─── createSortable ───────────────────────────────────────────────────────────\n\n/**\n * Makes a list of items sortable via native HTML drag interactions.\n *\n * Provide `getKey` to map each item element to a stable string identity.\n * `createSortable` marks qualifying children with `data-dnd-item` and sets\n * `draggable=\"true\"` automatically.\n *\n * Style the drop indicator by targeting `.dnd-placeholder` in your CSS.\n * Supports keyboard reordering (arrow keys, Home/End) and auto-scrolling near edges.\n *\n * @example\n * ```ts\n * import { createSortable } from '@vielzeug/dnd';\n *\n * using sortable = createSortable({\n * element: listEl,\n * getKey: (el) => el.dataset.id!,\n * onReorder: ({ ids, setRevert }) => {\n * const prev = order;\n * setOrder(ids);\n * setRevert(() => setOrder(prev));\n * },\n * });\n * ```\n */\nexport function createSortable(options: SortableOptions): Sortable {\n const {\n autoScroll = true,\n axis = 'vertical',\n element,\n getKey,\n handle,\n keyboard = true,\n placeholderClass = 'dnd-placeholder',\n scope = createSortableScope(),\n } = options;\n const autoScrollOptions = resolveAutoScrollOptions(autoScroll);\n const scopeState = getSortableScopeState(scope);\n\n if (handle !== undefined && handle.trim() === '') {\n warn(\n 'handle option is an empty string — no handle elements will be found. Provide a valid CSS selector or omit the option.',\n );\n }\n\n const getItems = (): HTMLElement[] =>\n Array.from(element.children).filter((c) => (c as HTMLElement).hasAttribute(ITEM_ATTR)) as HTMLElement[];\n\n const getOrderedIds = (): string[] => getItems().map((el) => getKey(el));\n\n const syncItems = (): void => {\n clearHandleAttributes(element);\n\n getItems().forEach((el) => {\n el.setAttribute('role', 'listitem');\n el.tabIndex = 0;\n\n if (handle) {\n el.removeAttribute('draggable');\n el.style.touchAction = '';\n el.querySelectorAll<HTMLElement>(handle).forEach((handleEl) => {\n handleEl.setAttribute(HANDLE_ATTR, '');\n handleEl.setAttribute('draggable', 'true');\n // See the matching comment above `cleanupItems()` for why this matters at all.\n handleEl.style.touchAction = 'none';\n });\n } else {\n el.setAttribute('draggable', 'true');\n // A native mouse drag has no competing gesture to arbitrate; touch does. Without this,\n // a mobile browser can decide the very first bit of finger movement is a page\n // scroll/pan — a decision it makes independently of, and before, this library's own\n // touch-shim threshold/`preventDefault()` logic ever runs — and hand the rest of the\n // gesture to native scrolling. Once that happens the item never receives the\n // `dragover` sequence needed to update the drop target, so the session ends up\n // committing back to wherever it started: indistinguishable from the drop \"reverting\".\n // `touch-action: none` opts the element out of every default touch gesture from\n // `touchstart` onward, leaving the whole interaction to this library's own JS.\n el.style.touchAction = 'none';\n }\n });\n };\n\n const markItems = (): void => {\n const seenKeys = new Set<string>();\n\n // Mark all children that have a key as sortable items\n Array.from(element.children).forEach((child) => {\n const el = child as HTMLElement;\n\n try {\n const key = getKey(el);\n\n if (key) {\n if (seenKeys.has(key)) {\n warn(\n `getKey returned the duplicate key \"${key}\" for two sibling items — onReorder's ids and applyReorder may become inconsistent. Ensure getKey returns a unique value per item.`,\n );\n } else {\n seenKeys.add(key);\n }\n\n el.setAttribute(ITEM_ATTR, '');\n }\n } catch (err) {\n warn(\n `getKey threw for a child element — the item will not be sortable. Check your getKey implementation. ${String(err)}`,\n );\n }\n });\n\n syncItems();\n };\n\n const cleanupItems = (): void => {\n clearHandleAttributes(element);\n\n element.querySelectorAll<HTMLElement>(`[${ITEM_ATTR}]`).forEach((item) => {\n item.removeAttribute(ITEM_ATTR);\n item.removeAttribute('draggable');\n item.removeAttribute('role');\n item.removeAttribute('tabindex');\n item.style.touchAction = '';\n });\n };\n\n const createPlaceholder = (source: HTMLElement): HTMLElement => {\n const p = document.createElement('div');\n\n p.className = placeholderClass;\n p.setAttribute('aria-hidden', 'true');\n\n if (axis === 'horizontal') {\n p.style.width = `${source.offsetWidth}px`;\n } else {\n p.style.height = `${source.offsetHeight}px`;\n }\n\n return p;\n };\n\n let lastRevert: (() => void) | null = null;\n\n const handle_: ContainerHandle = {\n commitReorder: (orderedIds) => {\n if (!options.onReorder) return;\n\n const event: ReorderEvent = {\n ids: orderedIds,\n setRevert(fn) {\n lastRevert = fn;\n },\n };\n\n options.onReorder(event);\n },\n getOrderedIds,\n isDisabled: () => resolveDisabled(options.disabled),\n notifyBeforeReorder: (from, to) => options.onBeforeReorder?.(from, to),\n notifyDragEnd: (id, event) => options.onDragEnd?.(id, event),\n notifyDragStart: (id, event) => options.onDragStart?.(id, event),\n };\n\n scopeState.handles.add(handle_);\n\n const handleDragStart = (e: DragEvent): void => {\n if (scopeState.active) return;\n\n if (handle_.isDisabled()) return;\n\n const target = e.target as HTMLElement;\n const item = target.closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item) return;\n\n if (handle && !target.closest(handle)) return;\n\n const originalParent = item.parentElement;\n\n if (!originalParent) return;\n\n const placeholder = createPlaceholder(item);\n const originalNextSibling = item.nextSibling;\n const activeId = getKey(item);\n\n // Snapshot only the source handle at drag start; targets are snapshotted lazily.\n const initialOrders = new Map<ContainerHandle, string[]>();\n\n initialOrders.set(handle_, handle_.getOrderedIds());\n item.setAttribute('data-dragging', '');\n originalParent.insertBefore(placeholder, originalNextSibling);\n\n const session: DragSession = {\n draggedEl: item,\n draggedId: activeId,\n hideFrame: null,\n initialOrders,\n originalDisplay: item.style.display,\n originalNextSibling,\n originalParent,\n placeholder,\n source: handle_,\n target: handle_,\n };\n\n scheduleHide(session);\n scopeState.active = session;\n\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', activeId);\n\n if (options.dragImage) {\n const preview =\n typeof options.dragImage === 'function' ? options.dragImage(activeId, item, e) : options.dragImage;\n const [offsetX, offsetY] = options.dragImageOffset ?? [0, 0];\n\n if (preview) e.dataTransfer.setDragImage(preview, offsetX, offsetY);\n }\n }\n\n handle_.notifyDragStart(session.draggedId, e);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n maybeAutoScroll(e, element, axis, autoScrollOptions);\n\n // Lazily snapshot this handle's order the first time it becomes a target.\n snapshotOrder(session, handle_);\n\n const { draggedEl, placeholder } = session;\n const target = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!target) {\n // Only append placeholder when it isn't already inside this container.\n // Moving it to the end on every over-empty-space event causes the\n // placeholder to oscillate between positions as the cursor moves.\n if (placeholder.parentElement !== element) {\n element.appendChild(placeholder);\n }\n\n session.target = handle_;\n\n return;\n }\n\n if (target === draggedEl || target === placeholder) return;\n\n const rect = target.getBoundingClientRect();\n const insertAfter =\n axis === 'vertical' ? e.clientY >= rect.top + rect.height / 2 : e.clientX >= rect.left + rect.width / 2;\n\n element.insertBefore(placeholder, insertAfter ? target.nextSibling : target);\n session.target = handle_;\n };\n\n const handleDrop = (e: DragEvent): void => {\n const session = scopeState.active;\n\n if (!session) return;\n\n if (session.source.isDisabled() || handle_.isDisabled()) return;\n\n e.preventDefault();\n // Record the drop target; the actual commit happens in handleDragEnd where\n // dataTransfer.dropEffect tells us whether the browser accepted the operation.\n session.target = handle_;\n };\n\n const handleDragEnd = (e: DragEvent): void => {\n if (scopeState.active?.source !== handle_) return;\n\n finishSession(scopeState, e, false);\n };\n\n const handleKeydown = (e: KeyboardEvent): void => {\n if (!keyboard || handle_.isDisabled()) return;\n\n const tagName = (e.target as HTMLElement | null)?.tagName;\n\n if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return;\n\n const item = (e.target as HTMLElement).closest<HTMLElement>(`[${ITEM_ATTR}]`);\n\n if (!item || !element.contains(item)) return;\n\n const prevOrder = getOrderedIds();\n const newOrder = applyKeyboardReorder(item, element, getItems, getOrderedIds, e.key, axis);\n\n // null means unrecognized key or boundary — let the browser handle it (e.g. page scroll)\n if (newOrder === null) return;\n\n e.preventDefault();\n handle_.notifyBeforeReorder(prevOrder, newOrder);\n handle_.commitReorder(newOrder);\n };\n\n markItems();\n\n const disposable = createDisposable(() => {\n scopeState.disposables.delete(disposable.dispose);\n\n if (scopeState.active && (scopeState.active.source === handle_ || scopeState.active.target === handle_)) {\n finishSession(scopeState, new Event('dragend') as DragEvent, true);\n }\n\n scopeState.handles.delete(handle_);\n element.removeAttribute('role');\n cleanupItems();\n });\n\n element.setAttribute('role', 'list');\n element.addEventListener('dragstart', handleDragStart, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n element.addEventListener('dragend', handleDragEnd, { signal: disposable.disposalSignal });\n element.addEventListener('keydown', handleKeydown, { signal: disposable.disposalSignal });\n\n // Register with scope so scope.dispose() can tear this down\n scopeState.disposables.add(disposable.dispose);\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get isDragging() {\n return scopeState.active?.source === handle_;\n },\n revert: () => {\n lastRevert?.();\n lastRevert = null;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n sync: () => {\n markItems();\n },\n };\n}\n\n// ─── applyReorder ─────────────────────────────────────────────────────────────\n\n/**\n * Applies a sorted key array to a backing data array.\n * Unknown keys are ignored; items not present in `ids` are appended in original order.\n */\nexport function applyReorder<T>(items: T[], ids: string[], getKey: (item: T) => string): T[] {\n const byId = new Map(items.map((item) => [getKey(item), item] as const));\n const ordered: T[] = [];\n\n for (const id of ids) {\n if (!byId.has(id)) continue;\n\n const item = byId.get(id) as T;\n\n ordered.push(item);\n byId.delete(id);\n }\n\n for (const item of byId.values()) ordered.push(item);\n\n return ordered;\n}\n","import { warn } from './_dev';\nimport { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\nexport interface TouchDragOptions {\n /**\n * Set to `true` (or mutate this same options object's field later) to pause the shim without\n * disposing it — matches `createSortable`'s/`createDropZone`'s own `disabled` option.\n */\n disabled?: boolean;\n /**\n * CSS selector identifying draggable elements. Defaults to `[draggable=\"true\"]` — the exact\n * attribute `createSortable`/`createDropZone`-managed elements already carry, so the default\n * needs no configuration for the common case of bridging touch to an existing sortable/drop zone.\n */\n draggableSelector?: string;\n /**\n * Renders a floating clone of the dragged element that follows the touch point for the\n * duration of the drag. @default true\n *\n * A native mouse-driven HTML5 drag gets this for free: the browser snapshots the dragged\n * element into its own drag image the moment `dragstart` fires, then keeps that image under\n * the cursor itself for the whole gesture — `createSortable`'s `scheduleHide()` hides the real\n * element a frame later assuming that snapshot already exists. This shim's `dragstart` is a\n * synthetic `Event`, not a real drag, so the browser never creates that image; without this\n * preview, `scheduleHide()` still hides the real element on schedule and touch users are left\n * with *no* visual feedback for the whole gesture — only an empty placeholder box moving\n * between positions — making any drop feel arbitrary regardless of where it actually lands.\n * Set to `false` to render fully custom feedback instead (e.g. toggling a class from your own\n * `dragstart`/`dragend` listeners).\n */\n showDragPreview?: boolean;\n}\n\nconst PREVIEW_Z_INDEX = 2147483647;\n\n/**\n * Floating visual stand-in for the (real, but hidden) dragged element — see `showDragPreview`'s\n * doc comment for why touch needs one at all. `cloneNode(true)` only clones light-DOM content;\n * a draggable item that renders its visible content inside a shadow root (e.g. a shadow-DOM\n * custom element) will clone as an empty shell. This mirrors the same light-DOM-only limitation\n * every `cloneNode`-based drag preview (most JS sortable libraries included) accepts in exchange\n * for never having to special-case every possible element type.\n */\nfunction createPreviewElement(source: HTMLElement): HTMLElement {\n const rect = source.getBoundingClientRect();\n const clone = source.cloneNode(true) as HTMLElement;\n\n clone.removeAttribute('id');\n clone.setAttribute('aria-hidden', 'true');\n clone.setAttribute('inert', '');\n clone.setAttribute('data-dnd-touch-preview', '');\n Object.assign(clone.style, {\n height: `${rect.height}px`,\n left: `${rect.left}px`,\n margin: '0',\n opacity: '0.85',\n pointerEvents: 'none',\n position: 'fixed',\n top: `${rect.top}px`,\n transition: 'none',\n width: `${rect.width}px`,\n zIndex: String(PREVIEW_Z_INDEX),\n });\n\n document.body.appendChild(clone);\n\n return clone;\n}\n\n// Deliberately a plain object, never a real `new DataTransfer()`. Verified directly against a\n// real Chromium build (not just jsdom): a genuine `DataTransfer` instance created outside an\n// active *native* drag operation is permanently in the spec's \"disabled mode\" — every property\n// write, `dropEffect` included, is silently ignored, and every read of it keeps returning the\n// type's default (`'none'`), no matter what's assigned. `createSortable`/`createDropZone`'s own\n// `finishSession()` reads `dropEffect === 'none'` as an explicit cancellation signal, so with a\n// real `DataTransfer` backing this shim's synthetic events, *every* commit — same-column reorder\n// or cross-column move alike — would always revert, regardless of where it was actually dropped.\n// A plain object has no such \"mode\" concept: its `dropEffect` is an ordinary, freely-settable JS\n// property, which is all `createSortable`/`createDropZone`'s handlers ever read or write on the\n// events this shim dispatches. This is also why the previous Safari-<14 fallback (which already\n// used a plain object) never exhibited this bug — it was the correct approach the whole time.\nfunction makeDataTransfer(): DataTransfer {\n return {\n dropEffect: 'move',\n effectAllowed: 'move',\n getData: () => '',\n setData() {},\n setDragImage() {},\n } as unknown as DataTransfer;\n}\n\n/**\n * Bridges touch gestures to the synthetic `DragEvent` sequence `createSortable()`/\n * `createDropZone()` already listen for. HTML5 drag-and-drop has no native touch story —\n * this translates `touchstart`/`touchmove`/`touchend`/`touchcancel` into\n * `dragstart`/`dragover`/`drop`/`dragend` on the same document, so touch devices get\n * sortable/drop-zone behavior for free with no extra wiring at each call site.\n *\n * Listens at the `document` level (matching how `createSortable`/`createDropZone` themselves\n * scope drag detection) — one instance covers the whole page. Dispose to remove all listeners.\n *\n * @example\n * ```ts\n * using touchDrag = createTouchDragShim();\n * // ...later, or via the `using` declaration above:\n * touchDrag.dispose();\n * ```\n */\nexport function createTouchDragShim(options: TouchDragOptions = {}): Disposable {\n const dt = makeDataTransfer();\n const DRAG_START_DISTANCE_PX = 6;\n\n let pendingDraggable: HTMLElement | null = null;\n let pendingStartPoint: { clientX: number; clientY: number } | null = null;\n let dragging: HTMLElement | null = null;\n let lastTarget: Element | null = null;\n\n // The preview follows the touch point via a `translate3d` delta from wherever the touch was\n // when the preview was created (`previewOrigin`), not from the element's own rect — so it never\n // \"jumps\" at drag start (the delta starts at zero regardless of exactly where inside the\n // element the user first touched).\n let previewEl: HTMLElement | null = null;\n let previewOrigin: { clientX: number; clientY: number } | null = null;\n\n function removePreview(): void {\n previewEl?.remove();\n previewEl = null;\n previewOrigin = null;\n }\n\n // Hides both the real dragged element AND the floating preview for the duration of the\n // hit-test, then restores them. Hiding `dragging` alone isn't enough: the preview sits on top\n // of everything (by design, to follow the touch point) and, since `createPreviewElement()`\n // clones the dragged element's subtree, it can carry cloned custom elements (e.g. shadow-DOM\n // components) that re-run their own setup on the clone and may set `pointer-events` on their\n // *internal* shadow content — which a `pointer-events: none` set only on the preview's light-DOM\n // root does not reliably override. Hiding it outright (rather than relying on `pointer-events`)\n // sidesteps that entirely: a hidden element is never returned by `elementFromPoint`, regardless\n // of what any of its descendants — shadow DOM included — set.\n function elementBelow(clientX: number, clientY: number): Element | null {\n const prevDraggingDisplay = dragging?.style.display ?? '';\n const prevPreviewDisplay = previewEl?.style.display ?? '';\n\n if (dragging) dragging.style.display = 'none';\n\n if (previewEl) previewEl.style.display = 'none';\n\n const below = document.elementFromPoint(clientX, clientY);\n\n if (dragging) dragging.style.display = prevDraggingDisplay;\n\n if (previewEl) previewEl.style.display = prevPreviewDisplay;\n\n return below;\n }\n\n const disposable = createDisposable(() => {\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n });\n\n // Built on a plain `Event` rather than `new DragEvent(...)`: jsdom (and potentially other\n // non-browser DOM implementations) doesn't expose a `DragEvent` constructor at all, and\n // `createSortable`/`createDropZone`'s own handlers only ever read `type`/`clientX`/`clientY`/\n // `dataTransfer` off the event object — they never check `instanceof DragEvent` — so a patched\n // plain `Event` is indistinguishable to them and works everywhere a real `DragEvent` would.\n function dispatch(el: Element, type: string, clientX: number, clientY: number): void {\n const event = new Event(type, { bubbles: true, cancelable: true });\n\n Object.defineProperty(event, 'clientX', { configurable: true, value: clientX });\n Object.defineProperty(event, 'clientY', { configurable: true, value: clientY });\n Object.defineProperty(event, 'dataTransfer', { configurable: true, value: dt });\n el.dispatchEvent(event);\n }\n\n document.addEventListener(\n 'touchstart',\n (e: TouchEvent) => {\n if (resolveDisabled(options.disabled)) return;\n\n const touch = e.touches[0];\n\n if (!touch) return;\n\n const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;\n const draggable = target?.closest<HTMLElement>(options.draggableSelector ?? '[draggable=\"true\"]');\n\n if (!draggable) return;\n\n pendingDraggable = draggable;\n pendingStartPoint = { clientX: touch.clientX, clientY: touch.clientY };\n },\n { passive: false, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchmove',\n (e: TouchEvent) => {\n const touch = e.touches[0];\n\n if (!touch) return;\n\n if (!dragging) {\n if (!pendingDraggable || !pendingStartPoint) return;\n\n const dx = touch.clientX - pendingStartPoint.clientX;\n const dy = touch.clientY - pendingStartPoint.clientY;\n const distance = Math.hypot(dx, dy);\n\n if (distance < DRAG_START_DISTANCE_PX) return;\n\n dragging = pendingDraggable;\n lastTarget = pendingDraggable;\n\n // Captured before dispatching 'dragstart': createSortable's own scheduleHide() hides\n // `dragging` a frame after that event fires, and by then its layout position may already\n // reflect the placeholder having been inserted. Reading the rect now, while it's still\n // exactly where the user picked it up, is what createPreviewElement() sizes/positions the\n // clone from.\n //\n // Wrapped in try/catch: `createPreviewElement()` clones the dragged element's whole\n // subtree, which — for a draggable item backed by custom elements — re-instantiates live\n // copies that re-run their own `connectedCallback`/setup once appended to `document.body`.\n // A best-effort *visual* feature must never be able to take down the *functional* one:\n // if any of that throws (a component assuming a page-unique id, a framework-internal\n // invariant, anything), the drag must still start normally with no preview, not silently\n // never start at all.\n if (options.showDragPreview !== false) {\n try {\n previewEl = createPreviewElement(dragging);\n previewOrigin = { clientX: touch.clientX, clientY: touch.clientY };\n } catch (err) {\n warn(`drag preview failed to render, continuing without one: ${String(err)}`);\n removePreview();\n }\n }\n\n dispatch(dragging, 'dragstart', touch.clientX, touch.clientY);\n }\n\n if (previewEl && previewOrigin) {\n const dx = touch.clientX - previewOrigin.clientX;\n const dy = touch.clientY - previewOrigin.clientY;\n\n previewEl.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;\n }\n\n const below = elementBelow(touch.clientX, touch.clientY);\n\n if (below && below !== lastTarget) {\n if (lastTarget) dispatch(lastTarget, 'dragleave', touch.clientX, touch.clientY);\n\n lastTarget = below;\n }\n\n if (below) dispatch(below, 'dragover', touch.clientX, touch.clientY);\n\n pendingDraggable = null;\n pendingStartPoint = null;\n e.preventDefault();\n },\n { passive: false, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchend',\n (e: TouchEvent) => {\n if (!dragging) {\n pendingDraggable = null;\n pendingStartPoint = null;\n\n return;\n }\n\n const touch = e.changedTouches[0];\n\n if (!touch) return;\n\n const below = elementBelow(touch.clientX, touch.clientY);\n\n if (below) dispatch(below, 'drop', touch.clientX, touch.clientY);\n\n dispatch(dragging, 'dragend', touch.clientX, touch.clientY);\n\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n },\n { passive: true, signal: disposable.disposalSignal },\n );\n\n document.addEventListener(\n 'touchcancel',\n (e: TouchEvent) => {\n if (!dragging) {\n pendingDraggable = null;\n pendingStartPoint = null;\n\n return;\n }\n\n const touch = e.changedTouches[0];\n\n if (!touch) return;\n\n dispatch(dragging, 'dragend', touch.clientX, touch.clientY);\n pendingDraggable = null;\n pendingStartPoint = null;\n dragging = null;\n lastTarget = null;\n removePreview();\n },\n { passive: true, signal: disposable.disposalSignal },\n );\n\n return disposable;\n}\n"],"mappings":"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"}
|
package/dist/drop-zone.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./_shared.cjs");function t(e,t){return
|
|
1
|
+
const e=require("./_shared.cjs");function t(e,t){return!t.length||t.some(t=>{let n=t.trim();return n.startsWith(`.`)?e.name.toLowerCase().endsWith(n.toLowerCase()):n.endsWith(`/*`)?e.type.startsWith(n.slice(0,-1)):e.type===n})}function n(e,t){return!t.length||Array.from(e).some(e=>e.kind===`file`&&t.some(t=>{let n=t.trim();return n.endsWith(`/*`)?e.type.startsWith(n.slice(0,-1)):n.startsWith(`.`)?!0:e.type===n}))}function r(e,n,r){let i=[],a=[];for(let r of e)(t(r,n)?i:a).push(r);return r!==void 0&&i.length>r&&a.push(...i.splice(r)),{accepted:i,rejected:a}}function i(t){let{accept:i=[],dropEffect:a=`copy`,element:o,maxFiles:s,onDrop:c,onDropRejected:l,onHoverChange:u,onValidatingChange:d}=t,f=0,p=!1,m=!1,h=e=>{m=e,d?.(e)},g=e=>{let t=f>0&&p;f=Math.max(0,e),f===0&&(p=!1);let n=f>0&&p;n!==t&&u?.(n)},_=()=>{g(0)},v=e.createDisposable(_),y=(e,t)=>{e.length>0&&c?.(e),t.length>0&&l?.(t)},b=(e,n)=>{e.length>0&&(t.onPaste?t.onPaste(e):c?.(e)),n.length>0&&l?.(n)},x=(e,n)=>{let{accepted:a,rejected:o}=r(e,i,s);t.onValidate&&h(!0),Promise.resolve(t.onValidate&&a.length>0?t.onValidate(a):!0).then(e=>{t.onValidate&&h(!1),!v.disposed&&(e?n(a,o):n([],[...o,...a]))}).catch(()=>{t.onValidate&&h(!1),!v.disposed&&n([],[...o,...a])})};return o.addEventListener(`dragenter`,r=>{if(!e.resolveDisabled(t.disabled)){if(r.preventDefault(),f===0){let e=r.dataTransfer?.items;p=!i.length||!e?.length||n(e,i)}!p&&r.dataTransfer&&(r.dataTransfer.dropEffect=`none`),g(f+1)}},{signal:v.disposalSignal}),o.addEventListener(`dragover`,n=>{e.resolveDisabled(t.disabled)||(n.preventDefault(),n.dataTransfer&&(n.dataTransfer.dropEffect=p?a:`none`))},{signal:v.disposalSignal}),o.addEventListener(`dragleave`,e=>{g(f-1)},{signal:v.disposalSignal}),o.addEventListener(`drop`,n=>{if(_(),e.resolveDisabled(t.disabled))return;n.preventDefault();let r=n.dataTransfer?.files;r&&x(Array.from(r),y)},{signal:v.disposalSignal}),t.paste&&window.addEventListener(`paste`,n=>{if(e.resolveDisabled(t.disabled))return;let r=n.clipboardData?.files;r?.length&&(n.preventDefault(),x(Array.from(r),b))},{signal:v.disposalSignal}),window.addEventListener(`dragend`,_,{signal:v.disposalSignal}),window.addEventListener(`drop`,_,{signal:v.disposalSignal}),{get disposalSignal(){return v.disposalSignal},dispose:v.dispose,get disposed(){return v.disposed},get hovered(){return f>0&&p},[Symbol.dispose]:v[Symbol.dispose],get validating(){return m}}}exports.createDropZone=i,exports.matchesAccept=t;
|
|
2
2
|
//# sourceMappingURL=drop-zone.cjs.map
|
package/dist/drop-zone.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drop-zone.cjs","names":[],"sources":["../src/drop-zone.ts"],"sourcesContent":["import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drop-zone.cjs","names":[],"sources":["../src/drop-zone.ts"],"sourcesContent":["import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/drop-zone.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { createDisposable as e, resolveDisabled as t } from "./_shared.js";
|
|
2
2
|
//#region src/drop-zone.ts
|
|
3
3
|
function n(e, t) {
|
|
4
|
-
return t.length
|
|
4
|
+
return !t.length || t.some((t) => {
|
|
5
5
|
let n = t.trim();
|
|
6
6
|
return n.startsWith(".") ? e.name.toLowerCase().endsWith(n.toLowerCase()) : n.endsWith("/*") ? e.type.startsWith(n.slice(0, -1)) : e.type === n;
|
|
7
|
-
})
|
|
7
|
+
});
|
|
8
8
|
}
|
|
9
9
|
function r(e, t) {
|
|
10
|
-
return t.length
|
|
10
|
+
return !t.length || Array.from(e).some((e) => e.kind === "file" && t.some((t) => {
|
|
11
11
|
let n = t.trim();
|
|
12
12
|
return n.endsWith("/*") ? e.type.startsWith(n.slice(0, -1)) : n.startsWith(".") ? !0 : e.type === n;
|
|
13
|
-
}))
|
|
13
|
+
}));
|
|
14
14
|
}
|
|
15
15
|
function i(e, t, r) {
|
|
16
16
|
let i = [], a = [];
|
package/dist/drop-zone.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drop-zone.js","names":[],"sources":["../src/drop-zone.ts"],"sourcesContent":["import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n"],"mappings":";;AAeA,SAAgB,EAAc,GAAY,GAA2B;CAGnE,OAFK,EAAO,SAEL,EAAO,MAAM,MAAY;EAC9B,IAAM,IAAI,EAAQ,KAAK;EAMvB,OAJI,EAAE,WAAW,GAAG,IAAU,EAAK,KAAK,YAAY,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,IAE1E,EAAE,SAAS,IAAI,IAAU,EAAK,KAAK,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,IAEzD,EAAK,SAAS;CACvB,CAAC,IAV0B;AAW7B;AAuGA,SAAS,EAAiB,GAA6B,GAA2B;CAGhF,OAFK,EAAO,SAEL,MAAM,KAAK,CAAK,CAAC,CAAC,MACtB,MACC,EAAK,SAAS,UACd,EAAO,MAAM,MAAY;EACvB,IAAM,IAAI,EAAQ,KAAK;EAMvB,OAJI,EAAE,SAAS,IAAI,IAAU,EAAK,KAAK,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,IAE5D,EAAE,WAAW,GAAG,IAAU,KAEvB,EAAK,SAAS;CACvB,CAAC,CACL,IAd2B;AAe7B;AAMA,SAAS,EACP,GACA,GACA,GACwC;CACxC,IAAM,IAAmB,CAAC,GACpB,IAAmB,CAAC;CAE1B,KAAK,IAAM,KAAK,GACd,CAAC,EAAc,GAAG,CAAM,IAAI,IAAW,EAAA,CAAU,KAAK,CAAC;CAOzD,OAJI,MAAa,KAAA,KAAa,EAAS,SAAS,KAC9C,EAAS,KAAK,GAAG,EAAS,OAAO,CAAQ,CAAC,GAGrC;EAAE;EAAU;CAAS;AAC9B;AAyBA,SAAgB,EAAe,GAAoC;CACjE,IAAM,EACJ,YAAS,CAAC,GACV,gBAAa,QACb,YACA,aACA,WACA,mBACA,kBACA,0BACE,GAEA,IAAc,GAGd,IAAe,IACf,IAAa,IAEX,KAAiB,MAAwB;EAE7C,AADA,IAAa,GACb,IAAqB,CAAI;CAC3B,GAEM,KAAiB,MAAuB;EAC5C,IAAM,IAAa,IAAc,KAAK;EAKtC,AAHA,IAAc,KAAK,IAAI,GAAG,CAAI,GAG1B,MAAgB,MAAG,IAAe;EAEtC,IAAM,IAAU,IAAc,KAAK;EAEnC,AAAI,MAAY,KAAY,IAAgB,CAAO;CACrD,GAEM,UAA2B;EAC/B,EAAc,CAAC;CACjB,GAEM,IAAa,EAAiB,CAAY,GAG1C,KAAU,GAAuB,MAAgC;EAGrE,AAFI,EAAc,SAAS,KAAG,IAAS,CAAa,GAEhD,EAAc,SAAS,KAAG,IAAiB,CAAa;CAC9D,GAGM,KAAkB,GAAuB,MAAgC;EAS7E,AARI,EAAc,SAAS,MACrB,EAAQ,UACV,EAAQ,QAAQ,CAAa,IAE7B,IAAS,CAAa,IAItB,EAAc,SAAS,KAAG,IAAiB,CAAa;CAC9D,GAGM,KAA0B,GAAkB,MAAiE;EACjH,IAAM,EAAE,aAAU,UAAU,MAAQ,EAAiB,GAAU,GAAQ,CAAQ;EAI/E,AAFI,EAAQ,cAAY,EAAc,EAAI,GAE1C,QAAa,QAAQ,EAAQ,cAAc,EAAS,SAAS,IAAI,EAAQ,WAAW,CAAQ,IAAI,EAAI,CAAC,CAClG,MAAM,MAAU;GACf,AAAI,EAAQ,cAAY,EAAc,EAAK,GAEvC,GAAW,aAEX,IACF,EAAS,GAAU,CAAG,IAGtB,EAAS,CAAC,GAAG,CAAC,GAAG,GAAK,GAAG,CAAQ,CAAC;EAEtC,CAAC,CAAC,CACD,YAAY;GACX,AAAI,EAAQ,cAAY,EAAc,EAAK,GAEvC,GAAW,YAEf,EAAS,CAAC,GAAG,CAAC,GAAG,GAAK,GAAG,CAAQ,CAAC;EACpC,CAAC;CACL;CA4EA,OAZA,EAAQ,iBAAiB,cA9DA,MAAuB;EAC1C,OAAgB,EAAQ,QAAQ,GAMpC;OAJA,EAAE,eAAe,GAIb,MAAgB,GAAG;IACrB,IAAM,IAAQ,EAAE,cAAc;IAE9B,IAAe,CAAC,EAAO,UAAU,CAAC,GAAO,UAAU,EAAiB,GAAO,CAAM;GACnF;GAQA,AANI,CAAC,KAAgB,EAAE,iBACrB,EAAE,aAAa,aAAa,SAK9B,EAAc,IAAc,CAAC;EAR7B;CASF,GA0CuD,EAAE,QAAQ,EAAW,eAAe,CAAC,GAC5F,EAAQ,iBAAiB,aAzCD,MAAuB;EACzC,EAAgB,EAAQ,QAAQ,MAEpC,EAAE,eAAe,GAEb,EAAE,iBAAc,EAAE,aAAa,aAAa,IAAe,IAAa;CAC9E,GAmCqD,EAAE,QAAQ,EAAW,eAAe,CAAC,GAC1F,EAAQ,iBAAiB,cAlCA,MAAwB;EAG/C,EAAc,IAAc,CAAC;CAC/B,GA8BuD,EAAE,QAAQ,EAAW,eAAe,CAAC,GAC5F,EAAQ,iBAAiB,SA7BL,MAAuB;EAIzC,IAFA,EAAa,GAET,EAAgB,EAAQ,QAAQ,GAAG;EAEvC,EAAE,eAAe;EAEjB,IAAM,IAAM,EAAE,cAAc;EAEvB,KAEL,EAAuB,MAAM,KAAK,CAAG,GAAG,CAAM;CAChD,GAgB6C,EAAE,QAAQ,EAAW,eAAe,CAAC,GAE9E,EAAQ,SAAO,OAAO,iBAAiB,UAhBtB,MAA4B;EAC/C,IAAI,EAAgB,EAAQ,QAAQ,GAAG;EAEvC,IAAM,IAAY,EAAE,eAAe;EAE9B,GAAW,WAEhB,EAAE,eAAe,GACjB,EAAuB,MAAM,KAAK,CAAS,GAAG,CAAc;CAC9D,GAOiE,EAAE,QAAQ,EAAW,eAAe,CAAC,GAItG,OAAO,iBAAiB,WAAW,GAAc,EAAE,QAAQ,EAAW,eAAe,CAAC,GACtF,OAAO,iBAAiB,QAAQ,GAAc,EAAE,QAAQ,EAAW,eAAe,CAAC,GAE5E;EACL,IAAI,iBAAiB;GACnB,OAAO,EAAW;EACpB;EACA,SAAS,EAAW;EACpB,IAAI,WAAW;GACb,OAAO,EAAW;EACpB;EACA,IAAI,UAAU;GACZ,OAAO,IAAc,KAAK;EAC5B;GACC,OAAO,UAAU,EAAW,OAAO;EACpC,IAAI,aAAa;GACf,OAAO;EACT;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"drop-zone.js","names":[],"sources":["../src/drop-zone.ts"],"sourcesContent":["import { createDisposable, resolveDisabled } from './_shared';\nimport { type Disposable } from './types';\n\n// ─── Accept matching ─────────────────────────────────────────────────────────\n\n/**\n * Test whether a `File` matches an accept pattern list.\n *\n * Each pattern can be:\n * - A MIME type: `'image/png'`\n * - A MIME wildcard: `'image/*'`\n * - A file extension: `'.pdf'`\n *\n * An empty list accepts everything.\n */\nexport function matchesAccept(file: File, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());\n\n if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -1));\n\n return file.type === p;\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DropZoneOptions {\n /** The element to attach drag listeners to. */\n element: HTMLElement;\n /**\n * Accepted file types. Each entry may be:\n * - A MIME type: 'image/png'\n * - A MIME wildcard: 'image/*'\n * - A file extension: '.pdf'\n *\n * When empty the zone accepts everything.\n */\n accept?: string[];\n /**\n * Maximum number of files accepted per drop. Files beyond this limit are\n * treated as rejected and forwarded to `onDropRejected`.\n *\n * When omitted there is no limit.\n */\n maxFiles?: number;\n /**\n * Optional async file gating. Called after type/extension filtering, before `onDrop`.\n * Return (or resolve) `false` to move all type-accepted files to `onDropRejected`.\n *\n * Only receives type-accepted files (after `accept` and `maxFiles` filtering).\n * Files already rejected by the `accept` filter are forwarded to `onDropRejected`\n * unconditionally and are not passed to this function.\n *\n * While validation is in progress `zone.validating` is `true` and `onValidatingChange`\n * is called with `true`.\n *\n * @example\n * ```ts\n * onValidate: async (files) => {\n * const ok = await checkServerQuota(files);\n * return ok;\n * }\n * ```\n */\n onValidate?: (files: File[]) => boolean | Promise<boolean>;\n /**\n * When `true`, all drag events are ignored and hover state does not change.\n *\n * Note: a disabled zone does not call `preventDefault` on drag or paste events,\n * so underlying elements (such as text editors) will still receive them.\n */\n disabled?: boolean;\n /**\n * The `dropEffect` to set on `dataTransfer` during `dragover`.\n * @default 'copy'\n */\n dropEffect?: DataTransfer['dropEffect'];\n /** Called when files are dropped or pasted (when `paste: true` and `onPaste` is omitted). Receives accepted files only. */\n onDrop?: (files: File[]) => void;\n /**\n * Called when dropped or pasted files are rejected by the `accept` filter, `maxFiles` limit, or `onValidate`.\n */\n onDropRejected?: (files: File[]) => void;\n /**\n * Called whenever hover state toggles.\n * Use this for drag-over styling.\n */\n onHoverChange?: (hovered: boolean) => void;\n /**\n * Called whenever the async validation state changes.\n * Use this to drive loading spinners.\n *\n * @example\n * ```ts\n * onValidatingChange: (v) => { spinnerEl.hidden = !v; }\n * ```\n */\n onValidatingChange?: (validating: boolean) => void;\n /**\n * When `true`, a `paste` event listener is added to `window`. Pasted files run\n * through the same `accept`, `maxFiles`, and `onValidate` pipeline as dropped files.\n * @default false\n */\n paste?: boolean;\n /**\n * Called when files are pasted via the clipboard. Falls back to `onDrop` when omitted.\n * Only active when `paste: true`.\n */\n onPaste?: (files: File[]) => void;\n}\n\nexport interface DropZone extends Disposable {\n /** Whether the pointer is currently dragging over the zone. */\n readonly hovered: boolean;\n /** `true` while an `onValidate` promise is pending. */\n readonly validating: boolean;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * DataTransferItem does not expose file names, so extension patterns (for example\n * `.pdf`) cannot be validated during drag-over. We treat these as a permissive\n * pre-check and perform exact filtering in `handleDrop` when `File` objects exist.\n */\nfunction itemsMatchAccept(items: DataTransferItemList, accept: string[]): boolean {\n if (!accept.length) return true;\n\n return Array.from(items).some(\n (item) =>\n item.kind === 'file' &&\n accept.some((pattern) => {\n const p = pattern.trim();\n\n if (p.endsWith('/*')) return item.type.startsWith(p.slice(0, -1));\n\n if (p.startsWith('.')) return true; // extension cannot be checked from DataTransferItem\n\n return item.type === p;\n }),\n );\n}\n\n/**\n * Filter `files` through `accept` patterns and `maxFiles` limit.\n * Returns accepted and rejected arrays.\n */\nfunction applyFileFilters(\n files: File[],\n accept: string[],\n maxFiles: number | undefined,\n): { accepted: File[]; rejected: File[] } {\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const f of files) {\n (matchesAccept(f, accept) ? accepted : rejected).push(f);\n }\n\n if (maxFiles !== undefined && accepted.length > maxFiles) {\n rejected.push(...accepted.splice(maxFiles));\n }\n\n return { accepted, rejected };\n}\n\n// ─── createDropZone ───────────────────────────────────────────────────────────\n\n/**\n * Attach drag-and-drop behaviour to a DOM element.\n *\n * Accepts files and provides hover state management with configurable MIME/extension filtering.\n * Calls `onDrop` only for accepted files and `onDropRejected` for filtered files.\n *\n * @example\n * ```ts\n * import { createDropZone } from '@vielzeug/dnd';\n *\n * using zone = createDropZone({\n * element: dropZoneEl,\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * onDrop: (files) => { console.log('Dropped:', files); },\n * onHoverChange: (hovered) => {\n * dropZoneEl.classList.toggle('drag-over', hovered);\n * },\n * });\n * ```\n */\nexport function createDropZone(options: DropZoneOptions): DropZone {\n const {\n accept = [],\n dropEffect = 'copy',\n element,\n maxFiles,\n onDrop,\n onDropRejected,\n onHoverChange,\n onValidatingChange,\n } = options;\n\n let dragCounter = 0;\n // Whether the *current* drag's payload passes the accept filter.\n // Determined on the first dragenter and held for the duration of the drag.\n let dragAccepted = false;\n let validating = false;\n\n const setValidating = (next: boolean): void => {\n validating = next;\n onValidatingChange?.(next);\n };\n\n const updateCounter = (next: number): void => {\n const wasHovered = dragCounter > 0 && dragAccepted;\n\n dragCounter = Math.max(0, next);\n\n // Reset acceptance state when the drag fully leaves so the next drag starts clean.\n if (dragCounter === 0) dragAccepted = false;\n\n const hovered = dragCounter > 0 && dragAccepted;\n\n if (hovered !== wasHovered) onHoverChange?.(hovered);\n };\n\n const resetCounter = (): void => {\n updateCounter(0);\n };\n\n const disposable = createDisposable(resetCounter);\n\n // Settle the final accepted/rejected split and fire callbacks.\n const settle = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) onDrop?.(acceptedFiles);\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Settle for paste events (which may use onPaste instead of onDrop).\n const settleForPaste = (acceptedFiles: File[], rejectedFiles: File[]): void => {\n if (acceptedFiles.length > 0) {\n if (options.onPaste) {\n options.onPaste(acceptedFiles);\n } else {\n onDrop?.(acceptedFiles);\n }\n }\n\n if (rejectedFiles.length > 0) onDropRejected?.(rejectedFiles);\n };\n\n // Run accept/maxFiles filter, then async onValidate, then settle.\n const dispatchWithValidation = (rawFiles: File[], settleFn: (accepted: File[], rejected: File[]) => void): void => {\n const { accepted, rejected: rej } = applyFileFilters(rawFiles, accept, maxFiles);\n\n if (options.onValidate) setValidating(true);\n\n void Promise.resolve(options.onValidate && accepted.length > 0 ? options.onValidate(accepted) : true)\n .then((valid) => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n if (valid) {\n settleFn(accepted, rej);\n } else {\n // validation failed — all type-accepted files become rejected\n settleFn([], [...rej, ...accepted]);\n }\n })\n .catch(() => {\n if (options.onValidate) setValidating(false);\n\n if (disposable.disposed) return;\n\n settleFn([], [...rej, ...accepted]);\n });\n };\n\n const handleDragEnter = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n // Evaluate the filter once per drag (on first entry) — the payload is\n // constant for the lifetime of a drag operation.\n if (dragCounter === 0) {\n const items = e.dataTransfer?.items;\n\n dragAccepted = !accept.length || !items?.length || itemsMatchAccept(items, accept);\n }\n\n if (!dragAccepted && e.dataTransfer) {\n e.dataTransfer.dropEffect = 'none';\n }\n\n // Always increment so every dragenter is paired with its dragleave,\n // regardless of acceptance. This prevents counter under-runs.\n updateCounter(dragCounter + 1);\n };\n\n const handleDragOver = (e: DragEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n if (e.dataTransfer) e.dataTransfer.dropEffect = dragAccepted ? dropEffect : 'none';\n };\n\n const handleDragLeave = (_e: DragEvent): void => {\n // Always decrement to balance the paired dragenter — disabling after enter\n // must not leave the counter permanently incremented.\n updateCounter(dragCounter - 1);\n };\n\n const handleDrop = (e: DragEvent): void => {\n // Reset counter first (idempotent at 0) so hover never sticks even when disabled.\n resetCounter();\n\n if (resolveDisabled(options.disabled)) return;\n\n e.preventDefault();\n\n const raw = e.dataTransfer?.files;\n\n if (!raw) return;\n\n dispatchWithValidation(Array.from(raw), settle);\n };\n\n const handlePaste = (e: ClipboardEvent): void => {\n if (resolveDisabled(options.disabled)) return;\n\n const clipFiles = e.clipboardData?.files;\n\n if (!clipFiles?.length) return;\n\n e.preventDefault();\n dispatchWithValidation(Array.from(clipFiles), settleForPaste);\n };\n\n element.addEventListener('dragenter', handleDragEnter, { signal: disposable.disposalSignal });\n element.addEventListener('dragover', handleDragOver, { signal: disposable.disposalSignal });\n element.addEventListener('dragleave', handleDragLeave, { signal: disposable.disposalSignal });\n element.addEventListener('drop', handleDrop, { signal: disposable.disposalSignal });\n\n if (options.paste) window.addEventListener('paste', handlePaste, { signal: disposable.disposalSignal });\n\n // These global listeners catch drags that end outside the zone.\n // The window 'drop' also fires for in-zone drops, but resetCounter() is idempotent at counter=0.\n window.addEventListener('dragend', resetCounter, { signal: disposable.disposalSignal });\n window.addEventListener('drop', resetCounter, { signal: disposable.disposalSignal });\n\n return {\n get disposalSignal() {\n return disposable.disposalSignal;\n },\n dispose: disposable.dispose,\n get disposed() {\n return disposable.disposed;\n },\n get hovered() {\n return dragCounter > 0 && dragAccepted;\n },\n [Symbol.dispose]: disposable[Symbol.dispose],\n get validating() {\n return validating;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAiB,KAAM,CAClC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAiB,KAAM,CAClC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CAEA,OAAO,GAAG,EAA+B,CACvC,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAmC,CAAS,CAC1C,aAAc,CACZ,MAAM,6DAA6D,CACrE,CACF"}
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAiB,MAAM;CAClC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all dnd errors. Use `instanceof DndError` to catch any dnd-originated error. */\nexport class DndError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is DndError {\n return err instanceof DndError;\n }\n}\n\n/** Thrown when a sortable action is called with an invalid scope not created by `createSortableScope()`. */\nexport class DndScopeError extends DndError {\n constructor() {\n super('Invalid scope — use createSortableScope() to create scopes.');\n }\n}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAiB,MAAM;CAClC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,WAAW,MACvB,OAAO,eAAe,MAAM,WAAW,SAAS;CAClD;CAEA,OAAO,GAAG,GAA+B;EACvC,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAmC,EAAS;CAC1C,cAAc;EACZ,MAAM,6DAA6D;CACrE;AACF"}
|