diff --git a/apps/docs/src/routes/docs/dnd-controller-api/+page.svx b/apps/docs/src/routes/docs/dnd-controller-api/+page.svx index 0adbe13..92d0e23 100644 --- a/apps/docs/src/routes/docs/dnd-controller-api/+page.svx +++ b/apps/docs/src/routes/docs/dnd-controller-api/+page.svx @@ -165,6 +165,32 @@ controller.onDrop(({ item, source, target }) => { > **`position` semantics** — `position` is an **insertion index**, not an index into your data array. `0` means before all items, `items.length` means after all items. For a `target`/`current` position, it's the slot between items where the dragged item would be inserted. +### onBeforeDrop + +Fired when a valid drop is accepted, before the drop animation reaches the target. Use it for preparation or optimistic metadata that does not move the active draggable between containers. + +```ts +controller.onBeforeDrop(({ item, source, target }) => { + console.log("Preparing drop:", item.id, "from", source.id, "to", target.id); +}); +``` + +### onDropCommit + +Fired when the drop animation reaches the target, immediately before the drag session is cleaned up. Use this event when the application state must change at the same moment the ghost lands, for example when assigning a group or status. + +```ts +controller.onDropCommit(({ item, target }) => { + // The ghost has reached the target. Apply the visual state now. + applyGroup(item.id, target.id); +}); + +controller.onDrop(({ item, target }) => { + // Persist the final position in application data. + persistMove(item.id, target.id, target.position); +}); +``` + ### onDragOver Fired each time the drag-over target (container + position) changes. Useful for highlighting the active drop zone in custom UIs. diff --git a/packages/svelte-dnd/src/lib/core/animation/drop-animation-coordinator.ts b/packages/svelte-dnd/src/lib/core/animation/drop-animation-coordinator.ts index 7e112b5..9b5ada3 100644 --- a/packages/svelte-dnd/src/lib/core/animation/drop-animation-coordinator.ts +++ b/packages/svelte-dnd/src/lib/core/animation/drop-animation-coordinator.ts @@ -187,6 +187,15 @@ export class DropAnimationCoordinator { const isCrossContainer = targetContainerId !== originContainerId + if (element && sourceDroppable && targetDroppable) { + const dropEvent: DropEvent = { + item: { id: sourceId, data: sourceData, type, element }, + source: sourceDroppable.toContainerInfo(originPosition), + target: targetDroppable.toContainerInfo(position) + } + this.eventEmitter.notifyBeforeDrop(dropEvent) + } + // Smooth source-slot collapse runs in parallel with the ghost flight so // items below the dragged source move up smoothly while the ghost flies. const collapsePromise = @@ -227,6 +236,7 @@ export class DropAnimationCoordinator { const srcScroll = srcScrollTarget?.scrollTop const tgtScroll = tgtScrollTarget?.scrollTop + this.eventEmitter.notifyDropCommit(dropEvent) this.eventEmitter.notifyDrop(dropEvent) this.finalizeDragEnd(dragEndEvent) diff --git a/packages/svelte-dnd/src/lib/core/dnd/dnd-controller.svelte.ts b/packages/svelte-dnd/src/lib/core/dnd/dnd-controller.svelte.ts index ff8418b..4cbc871 100644 --- a/packages/svelte-dnd/src/lib/core/dnd/dnd-controller.svelte.ts +++ b/packages/svelte-dnd/src/lib/core/dnd/dnd-controller.svelte.ts @@ -16,6 +16,8 @@ import type { DragStartCallback, DragEndCallback, DropCallback, + BeforeDropCallback, + DropCommitCallback, DragOverCallback, DropCancelledCallback, ZonesInvalidatedCallback, @@ -60,6 +62,8 @@ export type { DragStartCallback, DragEndCallback, DropCallback, + BeforeDropCallback, + DropCommitCallback, DragOverCallback, DropCancelledCallback, ZonesInvalidatedCallback @@ -362,6 +366,25 @@ export class DndController { return this.eventEmitter.onDrop(cb) } + /** + * Fired the moment a drop is accepted — before the ghost starts flying to the + * target. The item is still rendered in its source container, so handlers must + * not move it between containers here (that would unmount the element the ghost + * animates from). Use it for projections that don't touch the item itself: + * counters, badges, placeholder state. + */ + onBeforeDrop(cb: BeforeDropCallback) { + return this.eventEmitter.onBeforeDrop(cb) + } + + /** + * Fired when the ghost has landed, immediately before {@link onDrop} — the point + * where the item may safely be moved in your data model. + */ + onDropCommit(cb: DropCommitCallback) { + return this.eventEmitter.onDropCommit(cb) + } + /** Fired each time the drag-over target (container + position) changes. */ onDragOver(cb: DragOverCallback) { return this.eventEmitter.onDragOver(cb) diff --git a/packages/svelte-dnd/src/lib/core/dnd/dnd-event-emitter.ts b/packages/svelte-dnd/src/lib/core/dnd/dnd-event-emitter.ts index f07be55..bcd8539 100644 --- a/packages/svelte-dnd/src/lib/core/dnd/dnd-event-emitter.ts +++ b/packages/svelte-dnd/src/lib/core/dnd/dnd-event-emitter.ts @@ -2,6 +2,8 @@ import type { DragStartCallback, DragEndCallback, DropCallback, + BeforeDropCallback, + DropCommitCallback, DragOverCallback, DropCancelledCallback, ZonesInvalidatedCallback, @@ -16,6 +18,8 @@ export class DndEventEmitter { private dragStartCallbacks = new Set() private dragEndCallbacks = new Set() private dropCallbacks = new Set() + private beforeDropCallbacks = new Set() + private dropCommitCallbacks = new Set() private dragOverCallbacks = new Set() private dropCancelledCallbacks = new Set() private zonesInvalidatedCallbacks = new Set() @@ -41,6 +45,16 @@ export class DndEventEmitter { } } + onBeforeDrop(cb: BeforeDropCallback): () => void { + this.beforeDropCallbacks.add(cb) + return () => this.beforeDropCallbacks.delete(cb) + } + + onDropCommit(cb: DropCommitCallback): () => void { + this.dropCommitCallbacks.add(cb) + return () => this.dropCommitCallbacks.delete(cb) + } + onDragOver(cb: DragOverCallback): () => void { this.dragOverCallbacks.add(cb) return () => { @@ -67,6 +81,14 @@ export class DndEventEmitter { this.dropCallbacks.forEach((cb) => cb(event)) } + notifyBeforeDrop(event: DropEvent) { + this.beforeDropCallbacks.forEach((cb) => cb(event)) + } + + notifyDropCommit(event: DropEvent) { + this.dropCommitCallbacks.forEach((cb) => cb(event)) + } + notifyDragOver(event: DragOverEvent) { this.dragOverCallbacks.forEach((cb) => cb(event)) } @@ -90,6 +112,8 @@ export class DndEventEmitter { this.dragStartCallbacks.clear() this.dragEndCallbacks.clear() this.dropCallbacks.clear() + this.beforeDropCallbacks.clear() + this.dropCommitCallbacks.clear() this.dragOverCallbacks.clear() this.dropCancelledCallbacks.clear() this.zonesInvalidatedCallbacks.clear() diff --git a/packages/svelte-dnd/src/lib/types.ts b/packages/svelte-dnd/src/lib/types.ts index 1800737..e3d91e2 100644 --- a/packages/svelte-dnd/src/lib/types.ts +++ b/packages/svelte-dnd/src/lib/types.ts @@ -62,6 +62,8 @@ export interface DropCancelledEvent { export type DragStartCallback = (event: DragStartEvent) => void export type DragEndCallback = (event: DragEndEvent) => void export type DropCallback = (event: DropEvent) => void +export type BeforeDropCallback = (event: DropEvent) => void +export type DropCommitCallback = (event: DropEvent) => void export type DragOverCallback = (event: DragOverEvent) => void export type DropCancelledCallback = (event: DropCancelledEvent) => void export type ZonesInvalidatedCallback = () => void