Draggable
Draggable class acts as an orchestrator for any amount of sensors and moves DOM elements based on drag events emitted by sensors.
Example
import { Draggable } from 'dragdoll/draggable';
import { PointerSensor } from 'dragdoll/sensors/pointer';
import { KeyboardSensor } from 'dragdoll/sensors/keyboard';
const element = document.querySelector('.draggable') as HTMLElement;
const pointerSensor = new PointerSensor(element);
const keyboardSensor = new KeyboardSensor(element);
const draggable = new Draggable([pointerSensor, keyboardSensor], {
elements: () => [element],
});Class
class Draggable<S extends Sensor[] = Sensor[], P extends DraggablePluginMap = {}> {
constructor(sensors: S, options: DraggableOptions<S> = {}) {}
}Type Variables
- S
- The types of the sensors that the Draggable will use as inputs for moving the provided elements around.
- This is inferred from the constructor's
sensorsargument, so you don't have to manually specify this, unless you use this as a type constraint. - Default:
Sensor[].
- P
- The types of the plugins that the Draggable will use.
- This is mainly used for automatic type inference so you don't have to manually specify this, unless you use this as a type constraint.
- Default:
{}.
Constructor Parameters
- sensors
- An array of sensors that the Draggable will use as inputs for moving the provided elements around. The sensors are required and can't be changed after instantiation.
- options
- An optional
DraggableOptionsobject. The provided options (except forid) will be merged with the default options and the result will be used as the draggable'ssettingsproperty. - The
idoption is special as it's not part of thesettingsobject unlike all the other options. It's a unique identifier for the draggable that is assigned as the draggable'sidproperty and should not be changed after instantiation. Default is a unique symbol. - Default:
{}.
- An optional
Settings
Here you can find a more in-depth explanation about the DraggableSettings object with all the available options.
container
type container =
| HTMLElement
| null
| ((data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
element: HTMLElement | SVGSVGElement;
}) => HTMLElement | null);The element the dragged elements should be appended to for the duration of the drag. If set to null the element's current parent element is used.
You can also provide a function that returns the container element. This way you can, if you want, provide a different container element for each dragged element.
Default is null.
INFO
When using a custom container, the dragged element must be either absolute or fixed positioned. You can overcome this limitation by changing the element's CSS position to absolute or fixed just before the drag starts (e.g. using elements) and then back to the original position after the drag ends. Additionally you will need to offset the element's position to match the original position before the drag starts, e.g. by using transform: translate(x, y).
startPredicate
type startPredicate = (data: {
draggable: Draggable<S>;
sensor: S[number];
event: SensorsEventsType<S>['start'] | SensorsEventsType<S>['move'];
}) => boolean | undefined;A function that determines if drag should start or not. An important thing to note is that each sensor attached to the draggable has their own start predicate state. So if you reject the predicate for sensorA then sensorB can still keep trying to resolve its start predicate. However, once any sensor's start predicate is resolved the other sensors' start predicates are automatically rejected.
Return:
trueto resolve the predicate and start drag.falseto reject the predicate and prevent drag from starting during the sensor's current drag process.undefinedto keep the predicate in pending state and try resolving it again after the next "move" event from the sensor.
Default is () => true.
elements
type elements = (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
}) => (HTMLElement | SVGSVGElement)[] | null;A function that should return all the elements you want to move during the drag.
IMPORTANT
This is the single most important setting to define! Unless you explicitly provide a function that returns an array of element(s), no elements will be moved during the drag, but the drag operation will still be carried out.
There are a few use cases where you might not want to move any elements during the drag:
You trigger auto-scrolling via the
autoScrollPluginwithout moving any elements.You can define a virtual box for the draggable using the
computeClientRectsetting without moving any elements. This might be handy if you want to use collision detection without moving any elements.
Default is () => null.
frozenStyles
type frozenStyles = (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
item: DraggableDragItem<S>;
style: CSSStyleDeclaration;
}) => CSSProperties | (keyof CSSProperties)[] | null;A function that should return an array of CSS properties that should be frozen during the drag. By "frozen" we mean that the current computed value of the property is stored and applied to the element's inline styles during the drag. This is usually only needed if you have a drag container and the dragged element has percentage based values for some of its properties. The frozen properties are automatically unfrozen (restored to the original values) when drag ends.
You can also return an object with key-value pairs where the key is the CSS property you want to freeze and the value is the explicit value you want it to be frozen to. This is useful if you want to freeze a property to a specific value instead of the current computed value.
By default nothing is frozen.
applyPosition
type applyPosition = (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
item: DraggableDragItem<S>;
phase: DraggableApplyPositionPhase;
}) => void;A function that should apply the current position to a dragged element.
Default is a (very involved) function that applies the position, container offset, alignment offset and matrix transform offsets the element's transform property, while respecting the element's original transform and transform origin.
Also note that the phase argument is provided to the function to help you determine what phase of the drag process you are in:
start: Called when the drag starts.start-align: Called during the start process afterstartphase if the element is moved within a drag container AND the drag container and the element container have different world matrices.move: Called on every "move" event emitted by the currently tracked sensor during the drag.align: Called when the element's position is realigned (viaalignmethod).end: Called when the drag ends.end-align: Called after theendphase if the element was moved back to the original container from the drag container AND the element's position needs to be realigned to match the current client position.
computeClientRect
type computeClientRect = (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
}) => Readonly<Rect> | null;A function that should return the current bounding client rectangle of the draggable during drag operations. This rectangle is used for collision detection, auto-scrolling, and other features that need to know the draggable's current bounds.
The returned rectangle should have the following properties:
x: The x-coordinate of the rectangle's left edgey: The y-coordinate of the rectangle's top edgewidth: The width of the rectangleheight: The height of the rectangle
When to customize: You might want to customize this function when:
- You're dragging multiple elements and want to use a specific element's bounds
- You want to use a custom bounding area (e.g., smaller or larger than the actual element)
- You're implementing custom collision detection logic
- You need to account for transforms or scaling that affect the visual bounds
Example:
const draggable = new Draggable([pointerSensor], {
elements: () => [element1, element2, element3],
// Use the bounds of the second element instead of the first
computeClientRect: ({ drag }) => {
const secondItem = drag.items[1];
return secondItem?.clientRect || null;
},
});
// The draggable's client rect can be accessed anytime during drag
draggable.on('move', () => {
const rect = draggable.getClientRect();
console.log('Current draggable bounds:', rect);
});Default behavior: By default, this returns the clientRect of the first drag item, or null if no items exist.
positionModifiers
type positionModifiers = DraggableModifier<S>[];An array of position modifier functions that should return the position change of a dragged element. Checkout the Draggable Modifiers page for detailed information.
sensorProcessingMode
type sensorProcessingMode = DraggableSensorProcessingMode;Defines how the sensor events should be processed.
'sampled': Upon receiving a "start" or "move" event from a sensor, the Draggable will schedule a task to process the event during the next tick. If another "start" or "move" event is received before the next tick Draggable will discard the previous event and process the latest event instead during the next tick. This method of "sampling" the sensor events is highly useful to avoid redundant work and layout thrashing, but there may be edge cases where you might want to use the immediate mode instead.'immediate': The sensor events are processed immediately (synchronously) as they are emitted. This might cause performance issues (layout thrashing to be specific) if the sensor events are emitted too frequently.
Default is 'sampled'.
dndGroups
type dndGroups = Set<DraggableDndGroup>;A set of DraggableDndGroup identifiers used by DndObserver when matching a Draggable to Droppable.
If a Droppable includes any of these identifiers in its accept set, the Draggable will be matched to the Droppable and there will be collision detection enabled between the two.
Default is an empty set.
onPrepareStart
type onPrepareStart = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;A callback that is called at the end of drag start preparation phase. In this phase the draggable item instances are created and the initial position is computed. All the required DOM reading (for drag start) is also done in this phase.
This callback is called immediately after any preparestart events that are added via on method.
onStart
type onStart = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;A callback that is called at the end of drag start phase. This phase handles applying the initial positions to the dragged elements, setting up the frozen styles and any other initial setup that require writing to the DOM.
This callback is called immediately after any start events that are added via on method.
onPrepareMove
type onPrepareMove = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;A callback that is called at the end of drag move preparation phase. This phase handles computing the new position of the dragged elements based on the sensor data.
This callback is called immediately after any preparemove events that are added via on method.
onMove
type onMove = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;A callback that is called at the end of drag move phase. This phase applies the new positions to the dragged elements.
This callback is called immediately after any move events that are added via on method.
onEnd
type onEnd = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;A callback that is called at the very end of drag process after all the required cleanup has been done. You will still have access to the drag data when this callback is called, but it will be removed from the draggable instance right after this callback.
This callback is called immediately after any end events that are added via on method.
onDestroy
type onDestroy = (draggable: Draggable<S>) => void;A callback that is called when the draggable is destroyed via destroy method. This is the last callback that is called before the draggable instance is completely disposed. If there is an active drag when the draggable is destroyed, the onEnd callback will be called before this callback.
This callback is called immediately after any destroy events that are added via on method.
Properties
id
type id = DraggableId;The unique identifier for this draggable. Default is a unique symbol. Read-only.
sensors
type sensors = Sensor[];An array of all the sensors attached to the Draggable instance. Read-only.
settings
type settings = DraggableSettings;Current settings of the Draggable instance. Read-only.
drag
type drag = DraggableDrag | null;Current drag data or null if drag is not active.
plugins
type plugins = DraggablePluginMap;An object containing all of the Draggable instance's plugins.
isDestroyed
type isDestroyed = boolean;Is the Draggable instance destroyed or not?
Methods
on
type on<T extends keyof DraggableEventCallbacks<S>> = (
type: T,
listener: DraggableEventCallbacks<S>[T],
listenerId?: SensorEventListenerId,
) => SensorEventListenerId;
// Usage
draggable.on('start', (e) => {
console.log('start', e);
});Adds a listener to a draggable event. Returns a listener id, which can be used to remove this specific listener. By default this will always be a symbol unless manually provided.
Please check the Events section for more information about the events and their payloads.
off
type off<T extends keyof DraggableEventCallbacks<S>> = (
type: T,
listenerId: SensorEventListenerId,
) => void;
// Usage
const id = draggable.on('start', (e) => console.log('start', e));
draggable.off('start', id);Removes a listener (based on listener id) from a draggable event.
stop
type stop = () => void;
draggable.stop();Forcibly stops the draggable's current drag process.
IMPORTANT
You can't call this method within the preparestart or start event listeners nor within the onPrepareStart or onStart callbacks. An error will be thrown if you try to do so. The reason for this is that the drag start process can't be interrupted during the prepare/apply phases. You can call this method before, after and between those phases though.
align
type align = (instant?: boolean) => void;
// Usage: update asynchronously on the next animation frame.
draggable.align();
// Usage: update instantly. May cause extra reflows (jank).
draggable.align(true);Recomputes the positions of all dragged elements if they have drifted due to e.g. scrolling. This should be called if a dragged element's position goes out of sync inadvertently. Draggable is smart enough to call this automatically when scrolling occurs during dragging, but if you need to realign the elements manually you can call this method.
getClientRect
type getClientRect = () => Readonly<{ x: number; y: number; width: number; height: number }> | null;
// Usage
const rect = draggable.getClientRect();
if (rect) {
console.log(`Draggable is at ${rect.x}, ${rect.y} with size ${rect.width}x${rect.height}`);
}Returns the current bounding client rectangle of the draggable during drag operations, or null if no drag is active. This method uses the computeClientRect setting to determine the rectangle.
This method is commonly used by:
- Collision detection systems
- Auto-scroll plugins
- Custom drag logic that needs to know the draggable's current bounds
updateSettings
type updateSettings = (options: Partial<DraggableSettings>) => void;
// Usage
draggable.updateSettings({
startPredicate: () => Math.random() > 0.5,
});Updates the draggable's settings. Accepts a partial DraggableSettings object as the first argument, only the options you provide will be updated. Note that you only need to provide the options you want to change, the rest will be left as default.
use
type use = (plugin: (draggable: Draggable) => Draggable) => Draggable;
// Usage
const draggable = new Draggable(
[
// Sensors here...
],
{
// Options here...
},
)
// Plugins here...
.use(myPlugin)
.use(myOtherPlugin);Registers a plugin to the Draggable instance. Returns the Draggable instance so you can chain the method and get updated typings for the instance based on how the plugin(s) extend the Draggable type.
The plugin system is designed to be used so that you register the plugins right away when you instantiate the Draggable. This way you'll get the correct typings to the variable holding the instance. Also, there's no mechanism to unregister a plugin because there really should be no need for that.
Check out the plugin guide to learn how to build custom plugins.
destroy
type destroy = () => void;
// Usage
draggable.destroy();Destroy the draggable. Disposes all allocated memory and removes all bound event listeners.
Events
These are all the events that the Draggable instance can emit via the on method.
The listener functions receive the sensor event that triggered the Draggable event. See the Sensor docs for the exact event shapes.
preparestart
type preparestart = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;Emitted at the end of the drag start preparation phase, just before the onPrepareStart callback is called.
The start preparation phase is called during the read phase of the ticker and is intended for doing all the necessary DOM reads and computations for the drag start. You should avoid doing any DOM writes in this phase as that would cause layout thrashing.
Parameters:
drag— The drag data.- You can read the
drag.startEventfrom the drag data, which is the sensor event that started the drag.
- You can read the
draggable— The draggable instance.
start
type start = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;Emitted at the end of the drag start apply phase, just before the onStart callback is called.
The start apply phase is called during the write phase of the ticker and is intended for applying the initial positions to the dragged elements, applying the frozen styles and other initial setup that require writing to the DOM. You should avoid doing any DOM reads in this phase as that would cause layout thrashing.
Parameters:
drag— The drag data.- You can read the
drag.startEventfrom the drag data, which is the sensor event that started the drag.
- You can read the
draggable— The draggable instance.
preparemove
type preparemove = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;Emitted at the end of the drag move preparation phase, just before the onPrepareMove callback is called.
The move preparation phase is called during the read phase of the ticker and is intended for computing the new position of the dragged elements based on the sensor data. You should avoid doing any DOM writes in this phase as that would cause layout thrashing.
Parameters:
drag— The drag data.- You can read the
drag.moveEventfrom the drag data, which is the sensor event that caused this event to be emitted.
- You can read the
draggable— The draggable instance.
move
type move = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;Emitted at the end of the drag move apply phase, just before the onMove callback is called.
The move apply phase is called during the write phase of the ticker and is intended for applying the new positions to the dragged elements. You should avoid doing any DOM reads in this phase as that would cause layout thrashing.
Parameters:
drag— The drag data.- You can read the
drag.moveEventfrom the drag data, which is the sensor event that caused this event to be emitted.
- You can read the
draggable— The draggable instance.
end
type end = (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;Emitted at the end of the drag end procedure, just before the onEnd callback is called.
The end procedure is called synchronously after the drag ends and is intended for doing all the necessary cleanup after the drag ends. It is not bound to any phase of the ticker and is called immediately after the drag ends.
Parameters:
drag— The drag data.- You can read the
drag.endEventfrom the drag data, which is the sensor event that caused this event to be emitted. - The
drag.endEventwill benullif the drag was stopped programmatically usingstopmethod without a sensor event.
- You can read the
draggable— The draggable instance.
destroy
type destroy = () => void;Emitted when the Draggable instance is destroyed via destroy method.
Exports
Here's a list of additional exports that are available in the dragdoll/draggable package.
DraggableModifierPhase
// Import
import { DraggableModifierPhase } from 'dragdoll/draggable';
// Enum (object literal)
const DraggableModifierPhase = {
Start: 'start',
Move: 'move',
End: 'end',
} as const;DraggableSensorProcessingMode
// Import
import { DraggableSensorProcessingMode } from 'dragdoll/draggable';
// Enum (object literal)
const DraggableSensorProcessingMode = {
Immediate: 'immediate',
Sampled: 'sampled',
} as const;DraggableApplyPositionPhase
// Import
import { DraggableApplyPositionPhase } from 'dragdoll/draggable';
// Enum (object literal)
const DraggableApplyPositionPhase = {
Start: 'start',
StartAlign: 'start-align',
Move: 'move',
Align: 'align',
End: 'end',
EndAlign: 'end-align',
} as const;DraggableEventType
// Import
import { DraggableEventType } from 'dragdoll/draggable';
// Enum (object literal)
const DraggableEventType = {
PrepareStart: 'preparestart',
Start: 'start',
PrepareMove: 'preparemove',
Move: 'move',
End: 'end',
Destroy: 'destroy',
} as const;DraggableDefaultSettings
// Import
import { DraggableDefaultSettings } from 'dragdoll/draggable';
// Constant
const DraggableDefaultSettings: DraggableSettings<any>;Types
DraggableId
// Import
import type { DraggableId } from 'dragdoll/draggable';
// Type
type DraggableId = string | number | symbol;DraggableDndGroup
// Import
import type { DraggableDndGroup } from 'dragdoll/draggable';
// Type
type DraggableDndGroup = string | number | symbol;DraggableModifierPhase
// Import
import type { DraggableModifierPhase } from 'dragdoll/draggable';
// Type
type DraggableModifierPhase = 'start' | 'move' | 'end';DraggableSensorProcessingMode
// Import
import type { DraggableSensorProcessingMode } from 'dragdoll/draggable';
// Type
type DraggableSensorProcessingMode = 'sampled' | 'immediate';DraggableApplyPositionPhase
// Import
import type { DraggableApplyPositionPhase } from 'dragdoll/draggable';
// Type
type DraggableApplyPositionPhase = 'start' | 'start-align' | 'move' | 'align' | 'end' | 'end-align';DraggableModifierData
// Import
import type { DraggableModifierData } from 'dragdoll/draggable';
// Type
type DraggableModifierData<S extends Sensor[]> = {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
item: DraggableDragItem<S>;
phase: DraggableModifierPhase;
};DraggableModifier
// Import
import type { DraggableModifier } from 'dragdoll/draggable';
// Type
type DraggableModifier<S extends Sensor[]> = (
change: { x: number; y: number },
data: DraggableModifierData<S>,
) => { x: number; y: number };DraggableSettings
// Import
import type { DraggableSettings } from 'dragdoll/draggable';
// Interface
export interface DraggableSettings<S extends Sensor[]> {
container:
| HTMLElement
| null
| ((data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
element: HTMLElement | SVGSVGElement;
}) => HTMLElement | null);
startPredicate: (data: {
draggable: Draggable<S>;
sensor: S[number];
event: SensorsEventsType<S>['start'] | SensorsEventsType<S>['move'];
}) => boolean | undefined;
elements: (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
}) => (HTMLElement | SVGSVGElement)[] | null;
frozenStyles: (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
item: DraggableDragItem<S>;
style: CSSStyleDeclaration;
}) => CSSProperties | (keyof CSSProperties)[] | null;
positionModifiers: DraggableModifier<S>[];
applyPosition: (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
item: DraggableDragItem<S>;
phase: DraggableApplyPositionPhase;
}) => void;
computeClientRect?: (data: {
draggable: Draggable<S>;
drag: DraggableDrag<S>;
}) => Readonly<Rect> | null;
sensorProcessingMode?: DraggableSensorProcessingMode;
dndGroups?: Set<DraggableDndGroup>;
onPrepareStart?: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
onStart?: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
onPrepareMove?: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
onMove?: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
onEnd?: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
onDestroy?: (draggable: Draggable<S>) => void;
}DraggableOptions
// Import
import type { DraggableOptions } from 'dragdoll/draggable';
// Interface
export interface DraggableOptions<S extends Sensor[]> extends Partial<DraggableSettings<S>> {
id?: DraggableId;
}DraggablePlugin
// Import
import type { DraggablePlugin } from 'dragdoll/draggable';
// Interface
interface DraggablePlugin {
name: string;
version: string;
}DraggablePluginMap
// Import
import type { DraggablePluginMap } from 'dragdoll/draggable';
// Type
type DraggablePluginMap = Record<string, DraggablePlugin | undefined>;DraggableEventType
// Import
import type { DraggableEventType } from 'dragdoll/draggable';
// Type
type DraggableEventType = 'preparestart' | 'start' | 'preparemove' | 'move' | 'end' | 'destroy';DraggableEventCallbacks
// Import
import type { DraggableEventCallbacks } from 'dragdoll/draggable';
// Interface
interface DraggableEventCallbacks<S extends Sensor[]> {
[DraggableEventType.PrepareStart]: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
[DraggableEventType.Start]: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
[DraggableEventType.PrepareMove]: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
[DraggableEventType.Move]: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
[DraggableEventType.End]: (drag: DraggableDrag<S>, draggable: Draggable<S>) => void;
[DraggableEventType.Destroy]: () => void;
}DraggableEventCallback
// Import
import type { DraggableEventCallback } from 'dragdoll/draggable';
// Type
type DraggableEventCallback<
S extends Sensor[] = Sensor[],
K extends keyof DraggableEventCallbacks<S> = keyof DraggableEventCallbacks<S>,
> = DraggableEventCallbacks<S>[K];