Drag and Drop API

A Simple, lightweight Solution for a Drag & Drop interactive App.

npm install @dflex/dnd

You can achieve a drag and drop with three steps only:

  • Register element in the store.
  • Start dragging when mouse is down.
  • End dragging to release element when mouse is up.
import { store, DnD } from "@dflex/dnd";

Each element should be registered in DnD store in order to be active for Drag and drop later.

store.register(RegisterInput);

Where RegisterInput is an object with the following properties:

  • id?: string is a unique identifier for an element in the registry. Duplicate ids will cause confusion and prevent DnD from working properly.
  • parentID?: string is the parent element id. If you have multiple lists in the same layout you should provide the parent id so the registry can distinguish between elements.
  • ref?: HTMLElement targeted DOM element. Should be provided if you haven't provided the id.
  • depth?: number Element depth in tree. Start from bottom up. So the child is 0 by default. Depth is necessary for nested elements.

The responsive drag and drop event should be created when onmousedown is fired. So you initialized the element and its siblings before start dragging.

const dndEvent = new DnD(id, clickCoordinates, opts);
  • id: string registered element-id in the store.
  • clickCoordinates is an object with {x: number, y: number} contains the coordinates of the mouse/touch click.
  • opts is DnD options object. You can see options full documentation by clicking here.

dndEvent.dragAt(x, y);
  • x: number is event.clientX, the horizontal click coordinate.
  • y: number is event.clientY, the vertical click coordinate.

dndEvent.endDragging();

It's necessary to cleanup the element from store when the element won't be used or will be removed/unmounted from the DOM to prevent memory leaks.

store.unregister(id);
  • id: string registered element-id.

You can pass options when creating DnD event that controls each element individually.

The threshold object defines when the dragging event should be fired and triggers the response of other sibling elements.

interface ThresholdPercentages {
/** vertical threshold in percentage from 0-100 */
vertical: number;
/** horizontal threshold in percentage from 0-100 */
horizontal: number;
}

interface DndOpts {
// ... other options.
threshold?: Partial<ThresholdPercentages>;
}

{
"threshold": {
"vertical": 60,
"horizontal": 60
}
}

You can define the dragging restrictions for each element relative to its position or the parent container. Note that elements with no auto-scroll are automatically restricted to the viewport.

interface Restrictions {
self: {
allowLeavingFromTop: boolean;
allowLeavingFromBottom: boolean;
allowLeavingFromLeft: boolean;
allowLeavingFromRight: boolean;
};
container: {
allowLeavingFromTop: boolean;
allowLeavingFromBottom: boolean;
allowLeavingFromLeft: boolean;
allowLeavingFromRight: boolean;
};
}

interface DndOpts {
// ... other options.
restrictions?: {
self?: Partial<Restrictions["self"]>;
container?: Partial<Restrictions["container"]>;
};
}

{
"restrictions": {
"self": {
"allowLeavingFromTop": true,
"allowLeavingFromBottom": true,
"allowLeavingFromLeft": true,
"allowLeavingFromRight": true,
},
"container": {
"allowLeavingFromTop": true,
"allowLeavingFromBottom": true,
"allowLeavingFromLeft": true,
"allowLeavingFromRight": true,
},
},
}

interface ScrollOptions {
enable?: boolean;
initialSpeed?: number;
threshold?: Partial<ThresholdPercentages>;
}

interface DndOpts {
// ... other options.
scroll?: Partial<ScrollOptions>;
}

{
"scroll": {
"enable": true,
"initialSpeed": 10,
"threshold": {
"vertical": 15, // vertical threshold in percentage from 0-100
"horizontal": 15, // horizontal threshold in percentage from 0-100
},
},
}

There are four(4) categories of DnD custom events: DraggedEvent, InteractivityEvent, SiblingsEvent and LayoutStateEvent.

interface DndOpts {
// ... other options.
events?: Partial<Events>;
}

interface Events {
/** Drag events */
onDragOutContainer: (event: DraggedEvent) => unknown;
onDragOutThreshold: (event: DraggedEvent) => unknown;
/** Interactivity events */
onDragOver: (event: InteractivityEvent) => unknown;
onDragLeave: (event: InteractivityEvent) => unknown;
/** Sibling events */
onLiftUpSiblings: (event: SiblingsEvent) => unknown;
onMoveDownSiblings: (event: SiblingsEvent) => unknown;
/** Layout events */
onStateChange: (layoutState: LayoutStateEvent) => unknown;
}

It's an event related to the dragged element. This event is fired with onDragOutContainer and onDragOutThreshold event listeners.

interface DraggedEvent {
/** Returns the element that is being dragged */
type: "onDragOutContainer" | "onDragOutThreshold";
/** Returns the time at which the event was created */
timeStamp: number;
/** Returns element id in the registry */
id: string;
/** Returns dragged temp index */
index: number;
}

It's an event related to elements that interacted with the dragged. This event is fired with onDragOver and onDragLeave event listeners.

interface InteractivityEvent {
/** Returns the element that is being dragged */
type: "onDragOver" | "onDragLeave";
/** Returns the time at which the event was created */
timeStamp: number;
/** Returns element id in the registry */
id: string;
/** Returns element current index */
index: number;
/** Returns the element that triggered the event */
target: HTMLElement;
}

It's an Events related to the active list/branch: siblings. This event is fired onLiftUpSiblings and onMoveDownSiblings event listeners.

interface SiblingsEvent {
/** Returns the element that is being dragged */
type: "onLiftUpSiblings" | "onMoveDownSiblings";
/** Returns the time at which the event was created */
timeStamp: number;
/** Returns the index where the dragged left */
from: number;
/** Returns the last index effected of the dragged leaving/entering */
to: number;
/** Returns an array of sibling ids in order */
siblings: Array<string>;
}

It's an Events related to the layout current phase. This event is fired onStateChange event listeners.

type LayoutState =
| "pending" // when DnD is initiated but not activated yet.
| "ready" // When clicking over the registered element. The element is ready but not being dragged.
| "dragging" // as expected.
| "dragEnd" // as expected.
| "dragCancel"; // When releasing the drag without settling in the new position.
interface LayoutStateEvent {
/** Returns the element that is being dragged */
type: "onStateChange";
/** Returns the time at which the event was created */
timeStamp: number;
/** Returns the current state of the interactive layout */
layoutState: LayoutState;
}

In case you need to know the current index of dragged element.

dndEvent.getDraggedTempIndex() : number

To cleanup the store from all registered elements. This is probably what you should do when you are done with DnD completely or your app is about to be closed.

store.destroy(): void;

In case you need to know the current translate value as it's stored in the store. This is so much faster than using getElementById or getComputedStyle methods.

store.getELmTranslateById(id: string): {
translateX: number;
translateY: number;
};

The easiest and fastest way to get the siblings of an element in order.

store.getElmSiblingsById(id: string): string | Array<string> | null;

Returns the initial DOMRect object of the element.

getInitialELmRectById(id: string): Rect | undefined;
interface Rect {
height: number;
width: number;
left: number;
top: number;
}

store.getElmTreeById(id: string) : ElmTree
type ElmBranch = string | Array<string> | null;
interface ElmTree {
element: CoreInstanceInterface;
parent: CoreInstanceInterface | null;
branches: {
siblings: ElmBranch;
parents: ElmBranch;
};
}

Sign in