@neodrag/vanilla

@neodrag/vanilla

2.0.5 1.84KB

A lightweight library to make your elements draggable.

pnpm add @neodrag/vanilla

# or

bun install @neodrag/vanilla

# or

npm install @neodrag/vanilla

Usage

Basic usage

import { Draggable } from '@neodrag/vanilla';

const dragInstance = new Draggable(document.querySelector('#drag'));

With options

import { Draggable } from '@neodrag/vanilla';

const dragInstance = new Draggable(document.querySelector('#drag'), {
  axis: 'x',
  grid: [10, 10],
});

Defining options elsewhere with typescript

import { Draggable } from '@neodrag/vanilla';

const options: DragOptions = {
  axis: 'y',
  bounds: 'parent',
};

const dragInstance = new Draggable(document.querySelector('#drag'), options);

Update options:

import { Draggable } from '@neodrag/vanilla';

const dragInstance = new Draggable(document.querySelector('#drag'), {
  axis: 'x',
  grid: [10, 10],
});

// Update the specific options. Will be merged with the existing options.
dragInstance.updateOptions({
  axis: 'y',
});

// Completely overrides existing options, in this case, the `grid` property is removed
dragInstance.options = {
  axis: 'y',
};

Using via CDN

For users who prefer not to install the package and instead use it directly in their projects via a CDN, you can include @neodrag/vanilla directly in your HTML files. This is particularly useful for quick prototyping or projects where you want to avoid a build step. Here’s how to include it using different CDNs:

Using Unpkg

Include the library in your HTML using the following <script> tag. This will load the latest version of @neodrag/vanilla directly from unpkg:

<script src="https://unpkg.com/@neodrag/vanilla@latest/dist/umd/index.js"> </script>

Using jsDelivr

Alternatively, you can use jsDelivr as a CDN to load @neodrag/vanilla. Include the following line in your HTML:

<script src="https://cdn.jsdelivr.net/npm/@neodrag/vanilla@latest/dist/umd/index.js"> </script>

Usage with CDN

After including the library via a CDN, @neodrag/vanilla will be available as a global variable NeoDrag. Here’s how you can use it to make an element draggable:

<div id="drag">Drag me!</div> <script>   var dragInstance = new NeoDrag.Draggable(document.getElementById('drag')); </script>

This method allows you to use @neodrag/vanilla without any build tools or npm installations, directly in your browser.

Options

axis

Type: 'both' | 'x' | 'y' | 'none'
Default Value: 'both'

Axis on which the element can be dragged on. Valid values: both, x, y, none.

  • both - Element can move in any direction
  • x - Only horizontal movement possible
  • y - Only vertical movement possible
  • none - No movement at all
Both directions
0, 0
new Draggable(el, { axis: 'both' });
Horizontal
0, 0
new Draggable(el, { axis: 'x' });
Vertical
0, 0
new Draggable(el, { axis: 'y' });
None axis: Won't drag
0, 0
new Draggable(el, { axis: 'none' });

bounds

Type: HTMLElement | 'parent' | string | Partial<DragBoundsCoords>
Default Value: undefined

Optionally limit the drag area

parent: Limit to parent

Or, you can specify any selector and it will be bound to that.

Note: This library doesn’t check whether the selector is bigger than the node element. You yourself will have to make sure of that, or it may lead to unexpected behavior.

Or, finally, you can pass an object of type DragBoundsCoords. These mimic the css top, right, bottom and left, in the sense that bottom starts from the bottom of the window, and right from right of window. If any of these properties are unspecified, they are assumed to be 0.

export type DragBoundsCoords = {
  /** Number of pixels from left of the document */
  left: number;

  /** Number of pixels from top of the document */
  top: number;

  /** Number of pixels from the right side of document */
  right: number;

  /** Number of pixels from the bottom of the document */
  bottom: number;
};
Can't go outside the parent element
0, 0
new Draggable(el, { bounds: 'parent' });
Can't go outside <body>
0, 0
new Draggable(el, { bounds: 'body' });
Limited to: top: 60
left: 20
bottom: 35
right: 30
Bounded by these coordinates from the window's edges.
0, 0
new Draggable(el, { bounds: { top: 60, left: 20, bottom: 35, right: 30 } });

recomputeBounds

Type: { dragStart?: boolean; drag?: boolean; dragEnd?: boolean; }
Default Value: { dragStart: true, drag: false, dragEnd: false }

When to recalculate the dimensions of the `bounds` element.

By default, bounds are recomputed only on dragStart. Use this options to change that behavior.

grid

Type: [number, number]
Default Value: undefined

Applies a grid on the page to which the element snaps to when dragging, rather than the default continuous grid.

If you’re programmatically creating the grid, do not set it to [0, 0] ever, that will stop drag at all. Set it to undefined to make it continuous once again

Snaps to 50x50 grid
0, 0
new Draggable(el, { grid: [50, 50] });
Snaps to 72x91 grid
0, 0
new Draggable(el, { grid: [72, 91] });
Snaps to 0x0 grid - Won't drag at all
0, 0
new Draggable(el, { grid: [0, 0] });

defaultPosition

Type: { x: number; y: number }
Default Value: { x: 0, y: 0 }

Offsets your element to the position you specify in the very beginning. x and y should be in pixels. Ignored if position is passed.

Shifted by (100, 40)
100, 40
new Draggable(el, { defaultPosition: { x: 100, y: 40 } });

position

Type: { x: number; y: number }
Default Value: undefined

Controls the position of the element programmatically. Fully reactive.

Read about Controlled vs Uncontrolled components.

I can be moved with the slider too
X:    Y:
0, 0
<div class="box">I am draggable</div>

X: <input type="range" min="0" max="300" value="0" id="x" />
Y: <input type="range" min="0" max="300" value="0" id="y" />
const draggableEl = document.querySelector<HTMLDivElement>('.box')!;
const xSlider = document.querySelector<HTMLInputElement>('#x')!;
const ySlider = document.querySelector<HTMLInputElement>('#y')!;

let position = { x: 0, y: 0 };

const dragInstance = new Draggable(draggableEl, {
  position,
  onDrag: ({ offsetX, offsetY }) => {
    position = { x: offsetX, y: offsetY };

    xSlider.value = offsetX.toString();
    ySlider.value = offsetY.toString();
  },
});

xSlider.addEventListener('input', (e: Event) => {
  position.x = +e.target.value;
  dragInstance.updateOptions({ position });
});

ySlider.addEventListener('input', (e: Event) => {
  position.y = +e.target.value;
  dragInstance.updateOptions({ position });
});
I can be moved only with the slider
X:    Y:
0, 0
<div class="box">I am draggable</div>

X: <input type="range" min="0" max="300" value="0" id="x" />
Y: <input type="range" min="0" max="300" value="0" id="y" />
const draggableEl = document.querySelector<HTMLDivElement>('.box')!;
const xSlider = document.querySelector<HTMLInputElement>('#x')!;
const ySlider = document.querySelector<HTMLInputElement>('#y')!;

let position = { x: 0, y: 0 };

const dragInstance = new Draggable(draggableEl, {
  position,
  disabled: true
});

xSlider.addEventListener('input', (e: Event) => {
  position.x = +e.target.value;
  dragInstance.updateOptions({ position });
});

ySlider.addEventListener('input', (e: Event) => {
  position.y = +e.target.value;
  dragInstance.updateOptions({ position });
});

gpuAcceleration

Type: boolean
Default Value: true

If true, uses `translate3d` instead of `translate` to move the element around, and the hardware acceleration kicks in.

true by default, but can be set to false if blurry text issue occurs.

Depending on the device, you may not see much difference

GPU acceleration on
transform: translate3d(0px, 0px, 0)
new Draggable(el, { gpuAcceleration: true });
GPU acceleration off
transform: translate(0px, 0px)
new Draggable(el, { gpuAcceleration: false });

legacyTranslate

Type: boolean
Default Value: true

Whether to use the new translate property or transform: translate().

At the time of writing, translate property has less than 90% browsers availability. So, for now, this library will use the transform: translate() property to move the element. In future, when translate property will be more widely available, this library will switch to it by default. legacyTranslate’s default value will become false

Legacy translate with GPU acceleration
transform: translate3d(0px, 0px, 0)
new Draggable(el, { legacyTranslate: true, gpuAcceleration: true });
Modern translate with GPU acceleration
translate: 0px 0px 1px
new Draggable(el, { legacyTranslate: false, gpuAcceleration: true });
Legacy translate with no GPU acceleration
transform: translate(0px, 0px)
new Draggable(el, { legacyTranslate: true, gpuAcceleration: false });
Modern translate with no GPU acceleration
translate: 0px 0px
new Draggable(el, { legacyTranslate: false, gpuAcceleration: false });

transform

Type: ({ offsetX, offsetY, rootNode }: { offsetX: number; offsetY: number; rootNode: HTMLElement; }) => string | undefined | void
Default Value: undefined

Custom transform function. If provided, this function will be used to apply the DOM transformations to the root node to move it.

Existing transform logic, including gpuAcceleration and legacyTranslate, will be ignored. You can return a string to apply to a transform property, or not return anything and apply your transformations using rootNode.style.transform = VALUE

Moving by returning transform string
0, 0
new Draggable(el, {
  transform: ({ offsetX, offsetY }) => `translate(${offsetX + 50}px, ${offsetY + 20}px)`
});
Moving by manually setting rootNode.style
0, 0
new Draggable(el, {
  transform: ({ offsetX, offsetY, rootNode }) => {
    rootNode.style.translate = `${offsetX + 50}px ${offsetY + 20}px`;
  },
});

applyUserSelectHack

Type: boolean
Default Value: true

Applies user-select: none on <body /> element when dragging, to prevent the irritating effect where dragging doesn’t happen and the text is selected. Applied when dragging starts and removed when it stops.

User Select disabled
Hit ctrl + A while dragging - Nothing will be selected
0, 0
new Draggable(el, { applyUserSelectHack: true });
User Select enabled
Hit ctrl + A while dragging - Text will be selected
0, 0
new Draggable(el, { applyUserSelectHack: false });

ignoreMultitouch

Type: boolean
Default Value: true

Ignores touch events with more than 1 touch.

This helps when you have multiple elements on a canvas where you want to implement pinch-to-zoom behaviour.

Multi touch ignored
0, 0
new Draggable(el, { ignoreMultitouch: true });
Multi touch allowed
0, 0
new Draggable(el, { ignoreMultitouch: false });

disabled

Type: boolean
Default Value: false

Disables dragging.

Disabled. Won't drag, won't trigger any events
0, 0
new Draggable(el, { disabled: true });

handle

Type: string | HTMLElement | HTMLElement[]
Default Value: undefined

CSS Selector of an element or multiple elements inside the parent node(on which use:draggable is applied). Can be an element or elements too. If it is provided, Only clicking and dragging on this element will allow the parent to drag, anywhere else on the parent won’t work.

Won't drag ❌

Drag me ✅
Single handle with selector
0, 0
new Draggable(draggableEl, { handle: '.handle' });
<div class="draggable">
  Won't drag ❌

  <div class="handle">Drag me ✅</div>
</div>
Won't drag ❌

Drag me ✅
Single handle with element
0, 0
const handleEl = document.querySelector('.handle');
new Draggable(draggableEl, { handle: handleEl });
<div class="draggable">
  Won't drag ❌

  <div class="handle">Drag me ✅</div>
</div>
Won't drag ❌

Drag me ✅
Drag me ✅
Multiple handle with selector
0, 0
new Draggable(draggableEl, { handle: '.handle' });
<div class="draggable">
  Won't drag ❌

  <div class="handle">Drag me ✅</div>
  <div class="handle">Drag me ✅</div>
</div>
Won't drag ❌

Drag me ✅
Drag me ✅
Multiple handle with element
0, 0
const handle1 = document.querySelector('.handle1');
const handle2 = document.querySelector('.handle2');
new Draggable(draggableEl, { handle: [handle1, handle2] });
<div class="draggable">
  Won't drag ❌

  <div class="handle1">Drag me ✅</div>
  <div class="handle2">Drag me ✅</div>
</div>

cancel

Type: string | HTMLElement | HTMLElement[]
Default Value: undefined

CSS Selector of an element or multiple elements inside the parent node(on which use:draggable is applied). Can be an element or elements too. If it is provided, Trying to drag inside the cancel element(s) will prevent dragging.

This will drag!

This won't drag
Single cancel with selector
0, 0
new Draggable(draggableEl, { cancel: '.cancel' });
<div class="draggable">
  This will drag!

  <div class="cancel">This won't drag</div>
</div>
This will drag!

This won't drag
Single cancel passed as element.
0, 0
const cancelEl = document.querySelector('.cancel');
new Draggable(draggableEl, { cancel: cancelEl });
<div class="draggable">
  This will drag!

  <div class="cancel">This won't drag</div>
</div>
This will drag!

This won't drag
This won't drag
Multiple cancel passed as element.
0, 0
new Draggable(draggableEl, { cancel: '.cancel' });
<div class="draggable">
  This will drag!

  <div class="cancel">This won't drag</div>
  <div class="cancel">This won't drag</div>
</div>
This will drag!

This won't drag
This won't drag
Multiple cancel passed as array of elements.
0, 0
const cancel1 = document.querySelector('.cancel1');
const cancel2 = document.querySelector('.cancel2');
new Draggable(draggableEl, { cancel: [cancel1, cancel2] });
<div class="draggable">
  This will drag!

  <div class="cancel1">This won't drag</div>
  <div class="cancel2">This won't drag</div>
</div>

defaultClass

Type: string
Default Value: undefined

Class to apply to draggable element.

If handle is provided, it will still apply class on the parent element, NOT the handle.

defaultClassDragging

Type: string
Default Value: 'neodrag-dragging'

Class to apply on the parent element when it is dragging

defaultClassDragged

Type: string
Default Value: 'neodrag-dragged'

Class to apply on the parent element if it has been dragged at least once. Removed once dragging stops.

onDragStart

Type: (data: DragEventData) => void
Default Value: undefined

Fires when dragging start.

onDrag

Type: (data: DragEventData) => void
Default Value: undefined

Fires when dragging is going on.

onDragEnd

Type: (data: DragEventData) => void
Default Value: undefined

Fires when dragging stops.

Events

@neodrag/vanilla emits 3 events, onDrag, onDragStart & onDragEnd. Example:

new Draggable({
  onDragStart: (data) => console.log('Dragging started', data),
  onDrag: (data) => console.log('Dragging', data),
  onDragEnd: (data) => console.log('Dragging stopped', data),
});

TypeScript

This library ships with proper TypeScript typings, for the best Developer Experience, whether authoring JS or TS.

Types Exported from package

This package exports these types you can use:

import type {
  DragAxis,
  DragBounds,
  DragBoundsCoords,
  DragOptions,
  DragEventData,
} from '@neodrag/undefined';

DragOptions is the documented list of all options provided by the component.

DragAxis is the type of axis option, and is equal to 'both' | 'x' | 'y' | 'none'.

DragBounds is 'parent' | string | Partial<DragBoundsCoords>, the complete type of bounds option.

DragBoundsCoords is when you’re specifying the bounds field using an object, this is the type needed for that.

DragEventData is the data provided during the events

export type DragAxis = 'both' | 'x' | 'y' | 'none';

export type DragBounds = 'parent' | string | Partial<DragBoundsCoords>;

export type DragEventData = {
  /** How much element moved from its original position horizontally */
  offsetX: number;

  /** How much element moved from its original position vertically */
  offsetY: number;

  /** The node on which the draggable is applied */
  rootNode: HTMLElement;

  /** The element being dragged */
  currentNode: HTMLElement;
};

export type DragBoundsCoords = {
  /** Number of pixels from left of the window */
  left: number;

  /** Number of pixels from top of the window */
  top: number;

  /** Number of pixels from the right side of window */
  right: number;

  /** Number of pixels from the bottom of the window */
  bottom: number;
};

Controlled vs Uncontrolled

This is taken straight from React’s philosophy(After all, this package is inspired from react-draggable).

Uncontrolled means your app doesn’t control the dragging of the app. Meaning, the user drags the element, it changes position, and you do something with that action. You yourself don’t change position of the element or anything. This is the default behavior of this library.

Controlled means your app, using state variables, changes the position of the element, or in simple terms, programmatically drag the element. You basically set the position property to { x: 10, y: 50 }(or any other numbers), and voila! yur now controlling the position of the element programmatically 🥳🥳

OFC, this library doesn’t go fully Controlled. The user can still drag it around even when position is set.

So, when you change position, the element position changes. However, when the element is dragged by user interaction, position is not changed. This is done intentionally, as two-way data binding here isn’t possible and also will lead to unexpected behavior. To keep the position variable up to date, use the onDrag event to keep your state up to date to the draggable’s internal state.

To have it be strictly Controlled, meaning it can only be moved programmatically, add the disabled option to your draggable element’s config

new Draggable({ position: { x: 0, y: 10 }, disabled: true });

Credits

Inspired from the amazing react-draggable library, and implements the same API.