Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/docs/src/routes/docs/dnd-controller-api/+page.svx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions packages/svelte-dnd/src/lib/core/dnd/dnd-controller.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
DragStartCallback,
DragEndCallback,
DropCallback,
BeforeDropCallback,
DropCommitCallback,
DragOverCallback,
DropCancelledCallback,
ZonesInvalidatedCallback,
Expand Down Expand Up @@ -60,6 +62,8 @@ export type {
DragStartCallback,
DragEndCallback,
DropCallback,
BeforeDropCallback,
DropCommitCallback,
DragOverCallback,
DropCancelledCallback,
ZonesInvalidatedCallback
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions packages/svelte-dnd/src/lib/core/dnd/dnd-event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {
DragStartCallback,
DragEndCallback,
DropCallback,
BeforeDropCallback,
DropCommitCallback,
DragOverCallback,
DropCancelledCallback,
ZonesInvalidatedCallback,
Expand All @@ -16,6 +18,8 @@ export class DndEventEmitter {
private dragStartCallbacks = new Set<DragStartCallback>()
private dragEndCallbacks = new Set<DragEndCallback>()
private dropCallbacks = new Set<DropCallback>()
private beforeDropCallbacks = new Set<BeforeDropCallback>()
private dropCommitCallbacks = new Set<DropCommitCallback>()
private dragOverCallbacks = new Set<DragOverCallback>()
private dropCancelledCallbacks = new Set<DropCancelledCallback>()
private zonesInvalidatedCallbacks = new Set<ZonesInvalidatedCallback>()
Expand All @@ -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 () => {
Expand All @@ -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))
}
Expand All @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte-dnd/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading