Skip to content

DndObserver

The DndObserver class tracks collisions between draggables and droppables and dispatches events during the drag and drop lifecycle.

Example

ts
import { DndObserver } from 'dragdoll/dnd-observer';
import { CollisionDetector } from 'dragdoll/dnd-observer/collision-detector';
import { Draggable } from 'dragdoll/draggable';
import { Droppable } from 'dragdoll/droppable';
import { PointerSensor } from 'dragdoll/sensors/pointer';

// Create a DndObserver instance.
const dndObserver = new DndObserver();

// Create a draggable
const draggableElement = document.querySelector('.draggable') as HTMLElement;
const pointerSensor = new PointerSensor(draggableElement);
const draggable = new Draggable([pointerSensor], {
  elements: () => [draggableElement],
  dndGroups: new Set(['groupA']),
});

// Create a droppable
const dropZoneElement = document.querySelector('.drop-zone') as HTMLElement;
const droppable = new Droppable(dropZoneElement, {
  accept: new Set(['groupA']),
});

// Add draggables and droppables to the observer.
dndObserver.addDraggables([draggable]);
dndObserver.addDroppables([droppable]);

// Listen to events.
dndObserver.on('start', ({ draggable, targets }) => {
  console.log('Drag Started', draggable, targets);
});
dndObserver.on('move', ({ draggable, targets }) => {
  console.log('Drag Moved', draggable, targets);
});
dndObserver.on('enter', ({ draggable, targets, collisions, contacts, addedContacts }) => {
  console.log('Enter', { draggable, targets, collisions, contacts, addedContacts });
});
dndObserver.on('leave', ({ draggable, targets, collisions, contacts, removedContacts }) => {
  console.log('Leave', { draggable, targets, collisions, contacts, removedContacts });
});
dndObserver.on(
  'collide',
  ({
    draggable,
    targets,
    collisions,
    contacts,
    addedContacts,
    removedContacts,
    persistedContacts,
  }) => {
    console.log('Collide', {
      draggable,
      targets,
      collisions,
      contacts,
      addedContacts,
      removedContacts,
      persistedContacts,
    });
  },
);
dndObserver.on('end', ({ canceled, draggable, targets, collisions, contacts }) => {
  console.log('End', { canceled, draggable, targets, collisions, contacts });
});

Class

ts
class DndObserver<T extends CollisionData = CollisionData> {
  constructor(options: DndObserverOptions<T> = {}) {}
}

Type Variables

  1. T

Constructor Parameters

  1. options
    • An optional configuration object with the following properties:
      • collisionDetector
        • A factory function that receives the DndObserver instance and returns a CollisionDetector.
        • If not provided, a default CollisionDetector will be created.
        • See the CollisionDetector docs for subclassing examples.

Properties

draggables

ts
type draggables = ReadonlyMap<DraggableId, AnyDraggable>;

A read-only map containing all registered draggable instances, keyed by their unique ID.

droppables

ts
type droppables = ReadonlyMap<DroppableId, Droppable>;

A read-only map containing all registered droppable instances, keyed by their unique ID.

drags

ts
type drags = ReadonlyMap<AnyDraggable, DndObserverDragData>;

A read-only map of all currently dragged draggables and their public drag data.

Example

ts
// Get all draggables that are currently being dragged.
const draggedDraggables = dndObserver.drags.keys();

// Get the drag data object for a specific draggable.
const dragData = dndObserver.drags.get(draggable);

Methods

on

ts
type on<K extends keyof DndObserverEventCallbacks<T>> = (
  type: K,
  listener: DndObserverEventCallbacks<T>[K],
  listenerId?: SensorEventListenerId,
) => SensorEventListenerId;

Adds an event listener to the DndObserver for the specified event type.

The method returns a listener id, which can be used to remove this specific listener. By default this will always be a symbol unless manually provided.

Example

ts
const id = dndObserver.on('end', ({ draggable, collisions }) => {
  console.log('Dropped on', collisions);
});

off

ts
type off<K extends keyof DndObserverEventCallbacks<T>> = (
  type: K,
  listenerId: SensorEventListenerId,
) => void;

Removes an event listener from the DndObserver based on its listener id.

Example

ts
dndObserver.off('end', id);

addDraggables

ts
type addDraggables = (draggables: AnyDraggable[] | Set<AnyDraggable>) => void;

Registers one or more draggable instances with the observer. This adds the draggables to the internal registry, binds to their events, and emits the addDraggables event.

If any of the draggables are already being dragged, dnd observer will start the drag process for them manually.

Example

ts
dndObserver.addDraggables([draggable]);

removeDraggables

ts
type removeDraggables = (draggables: AnyDraggable[] | Set<AnyDraggable>) => void;

Deregisters one or more draggable instances from the observer. This removes all bound event listeners, cleans up drag data, and emits the appropriate events.

Example

ts
dndObserver.removeDraggables([draggable]);

addDroppables

ts
type addDroppables = (droppables: Droppable[] | Set<Droppable>) => void;

Registers one or more droppables with the observer. This adds them to the internal registry, binds their destroy event, and updates any active draggables with the new droppables as potential targets.

If any active draggable now matches the newly added droppables, a collision check is queued automatically.

Example

ts
dndObserver.addDroppables([droppable]);

removeDroppables

ts
type removeDroppables = (droppables: Droppable[] | Set<Droppable>) => void;

Deregisters one or more droppables from the observer. This removes them from the internal registry, unbinds their destroy event, and updates affected draggables by removing the droppables from their targets.

If any active draggable had an ongoing collision with the removed droppables, a collision check is queued automatically, which may emit leave events on the next tick.

Example

ts
dndObserver.removeDroppables([droppable]);

updateDroppableClientRects

ts
type updateDroppableClientRects = () => void;

Updates the cached client rectangles for all registered droppables.

Droppable rects are recomputed automatically on each collision detection cycle (every drag move). However, if you provide a custom computeClientRect on your droppables whose output depends on external state (e.g., computing rects arithmetically from item indices), you must call this method whenever that state changes — for example, after reordering DOM elements or when the layout shifts due to scrolling.

When you must call this manually:

  • After any DOM mutation that changes a droppable's layout position (e.g., reordering list items in a sortable)
  • After scroll events if your custom computeClientRect depends on viewport-relative positions

Example

ts
// After reordering items in a sortable list:
moveItemAnimated(currentIdx, targetIdx);
dndObserver.updateDroppableClientRects();

// During scroll if droppable rects depend on scroll position:
window.addEventListener('scroll', () => {
  dndObserver.updateDroppableClientRects();
});

detectCollisions

ts
type detectCollisions = (draggable?: AnyDraggable) => void;

Queues collision detection for either a specific draggable, or for all currently active draggables when called without an argument. This compares current and new collisions and emits the appropriate events (enter, leave, collide).

Example

ts
// Specific draggable.
dndObserver.detectCollisions(draggable);

// All active (dragged) draggables.
dndObserver.detectCollisions();

clearTargets

ts
type clearTargets = (draggable?: AnyDraggable) => void;

Clears cached target information for the specified draggable (or all active draggables when called without an argument), forcing re-evaluation on the next detection. Call this if the draggable's dndGroups changes or if any droppable's accept criteria changes during a drag. Targets are computed on drag start and cached for performance.

Example

ts
// Specific draggable.
dndObserver.clearTargets(draggable);

// All active (dragged) draggables.
dndObserver.clearTargets();

destroy

ts
type destroy = () => void;

Destroys the DndObserver by emitting the destroy event, unbinding all event listeners, clearing all internal data structures, and destroying the collision detector.

Example

ts
dndObserver.destroy();

Events

NOTE

To minimize memory allocations, event data objects are pooled and mutated between events. Treat them as read-only and do not store them for later use. If you need to persist the data, extract the specific values you need or clone the object.

For a quick reference of all events and their listener function signatures you can glance at the DndObserverEventCallbacks interface. Below you will find a more detailed description of each event.

start

ts
type start = (data: {
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
}) => void;

Emitted when a draggable starts dragging.

Parameters:

  1. data
    • An object with the following properties:
      • draggable
        • The draggable instance that started dragging.
      • targets
        • Map (read-only) of all droppable instances that accept this draggable (based on the droppable's accept criteria and the draggable's group). Keyed by droppable.id.

move

ts
type move = (data: {
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
}) => void;

Emitted when a draggable moves during dragging.

Parameters:

  1. data
    • An object with the following properties:
      • draggable
        • The draggable instance that is moving.
      • targets
        • A (read-only) map of all droppable instances that accept this draggable (based on the droppable's accept criteria and the draggable's group). Keyed by droppable.id.

enter

ts
type enter = (data: {
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
  collisions: ReadonlyArray<CollisionData>;
  contacts: ReadonlySet<Droppable>;
  addedContacts: ReadonlySet<Droppable>;
}) => void;

Emitted when a draggable first collides with one or more droppables.

Parameters:

  1. data
    • An object with the following properties:
      • draggable
        • The draggable instance that entered collision with droppables.
      • targets
        • A (read-only) map of all droppable instances that accept this draggable. Keyed by droppable.id.
      • collisions
        • An array (read-only) of collision data for all current collisions (includes the newly added ones). Each collision contains droppableId.
      • contacts
        • A set (read-only) of droppable instances currently in collision.
      • addedContacts
        • A set (read-only) of droppable instances that newly entered collision in this cycle.

leave

ts
type leave = (data: {
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
  collisions: ReadonlyArray<CollisionData>;
  contacts: ReadonlySet<Droppable>;
  removedContacts: ReadonlySet<Droppable>;
}) => void;

Emitted when a draggable stops colliding with one or more droppables.

Parameters:

  1. data
    • An object with the following properties:
      • draggable
        • The draggable instance that left collision with droppables.
      • targets
        • A (read-only) map of all droppable instances that accept this draggable. Keyed by droppable.id.
      • collisions
        • An array (read-only) of collision data for current collisions (excludes removed ones). Each collision contains droppableId.
      • contacts
        • A set (read-only) of droppable instances currently in collision.
      • removedContacts
        • A set (read-only) of droppables that exited collision in this cycle.

collide

ts
type collide = (data: {
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
  collisions: ReadonlyArray<CollisionData>;
  contacts: ReadonlySet<Droppable>;
  addedContacts: ReadonlySet<Droppable>;
  removedContacts: ReadonlySet<Droppable>;
  persistedContacts: ReadonlySet<Droppable>;
}) => void;

Emitted each collision cycle if there are any current collisions or removed collisions. Use this event to process all contact changes transactionally in one hook. A final collision pass also runs during drag end (before the end event) to ensure collision state is up-to-date — your handler should work correctly regardless of whether the drag is active or ending.

Parameters:

  1. data
    • An object with the following properties:
      • draggable
        • The draggable instance for this cycle.
      • targets
        • A (read-only) map of all droppable instances that accept this draggable. Keyed by droppable.id.
      • collisions
        • An array (read-only) of collision data for current collisions.
      • contacts
        • A set (read-only) of droppable instances currently in collision.
      • addedContacts
        • A set (read-only) of droppables newly entered this cycle.
      • removedContacts
        • A set (read-only) of droppables left this cycle.
      • persistedContacts
        • A set (read-only) of droppables that remained in collision from the previous cycle.

end

ts
type end = (data: {
  canceled: boolean;
  draggable: AnyDraggable;
  targets: ReadonlyMap<DroppableId, Droppable>;
  collisions: ReadonlyArray<CollisionData>;
  contacts: ReadonlySet<Droppable>;
}) => void;

Emitted when the drag ends, regardless of whether there are active collisions. If canceled is true, the drag ended due to cancellation. The end event is emitted after a final collision detection and emission pass. Cleanup happens synchronously. Ending during collision emission is disallowed and throws an error.

Parameters:

  1. data
    • An object with the following properties:
      • canceled
        • Whether the drag ended due to cancellation.
      • draggable
        • The draggable instance that ended dragging.
      • targets
        • A (read-only) map of all droppable instances that accept this draggable. Keyed by droppable.id.
      • collisions
        • An array (read-only) of collision data captured at the time of end.
      • contacts
        • A set (read-only) of droppable instances currently in collision at the time of end.

addDraggables

ts
type addDraggables = (data: { draggables: ReadonlySet<AnyDraggable> }) => void;

Emitted when one or more draggables are registered with the observer.

Parameters:

  1. data
    • An object with the following properties:
      • draggables
        • A set (read-only) of draggable instances that were added to the observer.

removeDraggables

ts
type removeDraggables = (data: { draggables: ReadonlySet<AnyDraggable> }) => void;

Emitted when one or more draggables are deregistered from the observer.

Parameters:

  1. data
    • An object with the following properties:
      • draggables
        • A set (read-only) of draggable instances that were removed from the observer.

addDroppables

ts
type addDroppables = (data: { droppables: ReadonlySet<Droppable> }) => void;

Emitted when one or more droppables are registered with the observer.

Parameters:

  1. data
    • An object with the following properties:
      • droppables
        • A set (read-only) of droppable instances that were added to the observer.

removeDroppables

ts
type removeDroppables = (data: { droppables: ReadonlySet<Droppable> }) => void;

Emitted when one or more droppables are deregistered from the observer.

Parameters:

  1. data
    • An object with the following properties:
      • droppables
        • A set (read-only) of droppable instances that were removed from the observer.

destroy

ts
type destroy = () => void;

Emitted when the DndObserver is destroyed.

Exports

DndObserverEventType

ts
// Import
import { DndObserverEventType } from 'dragdoll/dnd-observer';

// Enum (object literal)
const DndObserverEventType = {
  Start: 'start',
  Move: 'move',
  Enter: 'enter',
  Leave: 'leave',
  Collide: 'collide',
  End: 'end',
  AddDraggables: 'addDraggables',
  RemoveDraggables: 'removeDraggables',
  AddDroppables: 'addDroppables',
  RemoveDroppables: 'removeDroppables',
  Destroy: 'destroy',
} as const;

Types

DndObserverEventType

ts
// Import
import type { DndObserverEventType } from 'dragdoll/dnd-observer';

// Type
type DndObserverEventType =
  | 'start'
  | 'move'
  | 'end'
  | 'destroy'
  | 'enter'
  | 'leave'
  | 'collide'
  | 'addDraggables'
  | 'removeDraggables'
  | 'addDroppables'
  | 'removeDroppables';

DndObserverEventCallbacks

ts
// Import
import type { DndObserverEventCallbacks } from 'dragdoll/dnd-observer';

// Interface
interface DndObserverEventCallbacks<T extends CollisionData = CollisionData> {
  [DndObserverEventType.Start]: (data: {
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
  }) => void;
  [DndObserverEventType.Move]: (data: {
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
  }) => void;
  [DndObserverEventType.Enter]: (data: {
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
    collisions: ReadonlyArray<T>;
    contacts: ReadonlySet<Droppable>;
    addedContacts: ReadonlySet<Droppable>;
  }) => void;
  [DndObserverEventType.Leave]: (data: {
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
    collisions: ReadonlyArray<T>;
    contacts: ReadonlySet<Droppable>;
    removedContacts: ReadonlySet<Droppable>;
  }) => void;
  [DndObserverEventType.Collide]: (data: {
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
    collisions: ReadonlyArray<T>;
    contacts: ReadonlySet<Droppable>;
    addedContacts: ReadonlySet<Droppable>;
    removedContacts: ReadonlySet<Droppable>;
    persistedContacts: ReadonlySet<Droppable>;
  }) => void;
  [DndObserverEventType.End]: (data: {
    canceled: boolean;
    draggable: AnyDraggable;
    targets: ReadonlyMap<DroppableId, Droppable>;
    collisions: ReadonlyArray<T>;
    contacts: ReadonlySet<Droppable>;
  }) => void;
  [DndObserverEventType.AddDraggables]: (data: { draggables: ReadonlySet<AnyDraggable> }) => void;
  [DndObserverEventType.RemoveDraggables]: (data: {
    draggables: ReadonlySet<AnyDraggable>;
  }) => void;
  [DndObserverEventType.AddDroppables]: (data: { droppables: ReadonlySet<Droppable> }) => void;
  [DndObserverEventType.RemoveDroppables]: (data: { droppables: ReadonlySet<Droppable> }) => void;
  [DndObserverEventType.Destroy]: () => void;
}

DndObserverDragData

ts
// Import
import type { DndObserverDragData } from 'dragdoll/dnd-observer';

// Type
type DndObserverDragData = Readonly<{
  isEnded: boolean;
  data: { [key: string]: any };
}>;
  • isEnded: true when the drag is ending (set before the final collision pass and the end event).
  • data: A mutable object for storing custom data associated with this drag. Useful for passing state between event handlers.

DndObserverOptions

ts
// Import
import type { DndObserverOptions } from 'dragdoll/dnd-observer';

// Interface
interface DndObserverOptions<T extends CollisionData = CollisionData> {
  collisionDetector?: (dndObserver: DndObserver<T>) => CollisionDetector<T>;
}

DragDoll is released under the MIT License.