@ptahjs/dnd 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dnd.css +1 -1
- package/dist/dnd.js +3 -3
- package/package.json +1 -1
- package/readme.md +371 -0
package/dist/dnd.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.draggable-dot-wrap{pointer-events:none;z-index:1;box-sizing:border-box;position:absolute;inset:0}.draggable-dot{z-index:2;pointer-events:auto;background:#3a7afe;border-radius:50%;width:10px;height:10px;display:block;position:absolute;transform:translate(-50%,-50%)}.draggable-dot[data-pos=tl]{cursor:nw-resize;top:0%;left:0%}.draggable-dot[data-pos=tm]{cursor:n-resize;border-radius:8px;width:16px;height:8px;top:0%;left:50%}.draggable-dot[data-pos=tr]{cursor:ne-resize;top:0%;right:0%;transform:translate(50%,-50%)}.draggable-dot[data-pos=rm]{cursor:e-resize;border-radius:8px;width:8px;height:16px;top:50%;right:0%;transform:translate(50%,-50%)}.draggable-dot[data-pos=br]{cursor:se-resize;bottom:0%;right:0%;transform:translate(50%,50%)}.draggable-dot[data-pos=bm]{cursor:s-resize;border-radius:8px;width:16px;height:8px;bottom:0%;left:50%;transform:translate(-50%,50%)}.draggable-dot[data-pos=bl]{cursor:sw-resize;bottom:0%;left:0%;transform:translate(-50%,50%)}.draggable-dot[data-pos=lm]{cursor:w-resize;border-radius:8px;width:8px;height:16px;top:50%;left:0%}.draggable-rotate{z-index:2;cursor:grab;pointer-events:auto;position:absolute;top:0;left:50%;transform:translate(-50%,-200%)}.draggable-rotate:after{content:"";background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%201024%201024%22%3E%3Cpath%20fill%3D%22%233a7afe%22%20d%3D%22M784.512%20230.272v-50.56a32%2032%200%201%201%2064%200v149.056a32%2032%200%200%201-32%2032H667.52a32%2032%200%201%201%200-64h92.992A320%20320%200%201%200%20524.8%20833.152a320%20320%200%200%200%20320-320h64a384%20384%200%200%201-384%20384%20384%20384%200%200%201-384-384%20384%20384%200%200%201%20643.712-282.88z%22/%3E%3C/svg%3E) 50%/contain no-repeat;width:16px;height:16px;display:block}.draggable-markline-x,.draggable-markline-y{z-index:9999;background-color:#3a7afe;display:none;position:absolute;top:0;left:0}.draggable-markline-x{width:1px;height:100%}.draggable-markline-y{width:100%;height:1px}.dnd-mirror{pointer-events:none;z-index:999999;will-change:transform;opacity:.5;background:#fff;margin:0;position:fixed;top:0;left:0;transform:translate(0,0)}
|
|
1
|
+
.draggable-dot-wrap{pointer-events:none;z-index:1;box-sizing:border-box;position:absolute;inset:0}.draggable-dot{z-index:2;pointer-events:auto;background:#3a7afe;border-radius:50%;width:10px;height:10px;display:block;position:absolute;transform:translate(-50%,-50%)}.draggable-dot[data-pos=tl]{cursor:nw-resize;top:0%;left:0%}.draggable-dot[data-pos=tm]{cursor:n-resize;border-radius:8px;width:16px;height:8px;top:0%;left:50%}.draggable-dot[data-pos=tr]{cursor:ne-resize;top:0%;right:0%;transform:translate(50%,-50%)}.draggable-dot[data-pos=rm]{cursor:e-resize;border-radius:8px;width:8px;height:16px;top:50%;right:0%;transform:translate(50%,-50%)}.draggable-dot[data-pos=br]{cursor:se-resize;bottom:0%;right:0%;transform:translate(50%,50%)}.draggable-dot[data-pos=bm]{cursor:s-resize;border-radius:8px;width:16px;height:8px;bottom:0%;left:50%;transform:translate(-50%,50%)}.draggable-dot[data-pos=bl]{cursor:sw-resize;bottom:0%;left:0%;transform:translate(-50%,50%)}.draggable-dot[data-pos=lm]{cursor:w-resize;border-radius:8px;width:8px;height:16px;top:50%;left:0%}.draggable-rotate{z-index:2;cursor:grab;pointer-events:auto;position:absolute;top:0;left:50%;transform:translate(-50%,-200%)}.draggable-rotate:after{content:"";background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%201024%201024%22%3E%3Cpath%20fill%3D%22%233a7afe%22%20d%3D%22M784.512%20230.272v-50.56a32%2032%200%201%201%2064%200v149.056a32%2032%200%200%201-32%2032H667.52a32%2032%200%201%201%200-64h92.992A320%20320%200%201%200%20524.8%20833.152a320%20320%200%200%200%20320-320h64a384%20384%200%200%201-384%20384%20384%20384%200%200%201-384-384%20384%20384%200%200%201%20643.712-282.88z%22/%3E%3C/svg%3E) 50%/contain no-repeat;width:16px;height:16px;display:block}.draggable-markline-x,.draggable-markline-y{z-index:9999;background-color:#3a7afe;display:none;position:absolute;top:0;left:0}.draggable-markline-x{width:1px;height:100%}.draggable-markline-y{width:100%;height:1px}.dnd-mirror{pointer-events:none;z-index:999999;will-change:transform;opacity:.5;background:#fff;margin:0;position:fixed;top:0;left:0;transform:translate(0,0)}.dnd-indicator{--dnd-color:#3a7afe;--dnd-dot:6px;--dnd-line:2px;pointer-events:none;background:var(--dnd-color);position:absolute}.dnd-indicator:before,.dnd-indicator:after{content:"";background:var(--dnd-color);width:var(--dnd-dot);height:var(--dnd-dot);opacity:0;border-radius:50%;transition:opacity .15s;position:absolute}.dnd-indicator-active:before,.dnd-indicator-active:after{opacity:1}.dnd-indicator--top,.dnd-indicator--bottom{height:var(--dnd-line);width:100%;left:0}.dnd-indicator--top{top:0}.dnd-indicator--bottom{bottom:0}.dnd-indicator--top:before,.dnd-indicator--bottom:before{left:-2px}.dnd-indicator--top:after,.dnd-indicator--bottom:after{right:-2px}.dnd-indicator--top:before,.dnd-indicator--top:after{top:-2px}.dnd-indicator--bottom:before,.dnd-indicator--bottom:after{bottom:-2px}.dnd-indicator--left,.dnd-indicator--right{width:var(--dnd-line);height:100%;top:0}.dnd-indicator--left{left:0}.dnd-indicator--right{right:0}.dnd-indicator--left:before,.dnd-indicator--left:after{left:-2px}.dnd-indicator--right:before,.dnd-indicator--right:after{right:-2px}.dnd-indicator--left:before,.dnd-indicator--right:before{top:-2px}.dnd-indicator--left:after,.dnd-indicator--right:after{bottom:-2px}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/dnd.js
CHANGED
|
@@ -314,7 +314,7 @@ var p = class {
|
|
|
314
314
|
let i = document.elementFromPoint(e, t);
|
|
315
315
|
if (!i) return;
|
|
316
316
|
let a = n || "_default", o = i;
|
|
317
|
-
|
|
317
|
+
if (!(r && r.contains(o) && (o = r.parentElement ?? null, !o))) for (; o;) {
|
|
318
318
|
if (this.isDrop(o) && this.getNamespace(o) === a) return o;
|
|
319
319
|
o = o.parentElement;
|
|
320
320
|
}
|
|
@@ -397,7 +397,7 @@ var p = class {
|
|
|
397
397
|
}
|
|
398
398
|
#f = (e) => {
|
|
399
399
|
if (!this.#n || !this.#s || e.pointerId !== this.#r) return;
|
|
400
|
-
let t = e.getCoalescedEvents?.()
|
|
400
|
+
let t = e.getCoalescedEvents?.(), n = t?.length ? t[t.length - 1] : e, r = performance.now(), i = n.clientX, a = n.clientY;
|
|
401
401
|
if (i === this.#i && a === this.#a) {
|
|
402
402
|
e.preventDefault();
|
|
403
403
|
return;
|
|
@@ -450,7 +450,7 @@ var p = class {
|
|
|
450
450
|
commit() {
|
|
451
451
|
for (let e of this.removeOps) e?.parentNode?.removeChild?.(e);
|
|
452
452
|
let e = this.removeOps.size > 0;
|
|
453
|
-
for (let [t, n] of this.appendOps) (!e || !this.removeOps.has(t)) && n?.appendChild?.(t);
|
|
453
|
+
for (let [t, n] of this.appendOps) (!e || !this.removeOps.has(t) && !this.removeOps.has(n)) && n?.appendChild?.(t);
|
|
454
454
|
for (let [t, n] of this.classOps) if (!e || !this.removeOps.has(t)) for (let [e, r] of n) r ? t?.classList?.add?.(e) : t?.classList?.remove?.(e);
|
|
455
455
|
for (let [t, n] of this.styleOps) if (!e || !this.removeOps.has(t)) for (let [e, r] of n) r == null ? e.includes("-") ? t?.style?.removeProperty?.(e) : t?.style && (t.style[e] = "") : e.includes("-") ? t?.style?.setProperty?.(e, String(r)) : t?.style && (t.style[e] = r);
|
|
456
456
|
for (let e of this.fnOps) e?.();
|
package/package.json
CHANGED
package/readme.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# @ptahjs/dnd
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic drag-and-drop library built on the Pointer Events API. Designed with a plugin-first architecture for building canvas editors, sortable lists, and other interactive UIs.
|
|
4
|
+
|
|
5
|
+
**[Live Demo →](https://dnd.ptahjs.com/)**
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Pointer Events based** — unified mouse, touch, and stylus support
|
|
10
|
+
- **Plugin architecture** — all behaviors (mirror, drop feedback, auto-scroll, transform) are optional plugins
|
|
11
|
+
- **RAF-driven rendering** — per-frame measure → compute → commit pipeline prevents layout thrashing
|
|
12
|
+
- **Namespace support** — isolate independent drag-and-drop zones within the same page
|
|
13
|
+
- **Transform controller** — drag-scope mode with built-in move, resize, and rotate for canvas editors
|
|
14
|
+
- **Auto-scroll** — edge-triggered scrolling during drag
|
|
15
|
+
- **Zero dependencies** — no runtime dependencies
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @ptahjs/dnd
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { Dnd, MirrorService, DropService } from '@ptahjs/dnd'
|
|
27
|
+
import '@ptahjs/dnd/style'
|
|
28
|
+
|
|
29
|
+
const dnd = new Dnd({ root: '#container' })
|
|
30
|
+
dnd.use(new MirrorService())
|
|
31
|
+
dnd.use(new DropService())
|
|
32
|
+
|
|
33
|
+
dnd.on('dragstart', (payload) => console.log('drag started', payload))
|
|
34
|
+
dnd.on('drop', (payload) => console.log('dropped', payload))
|
|
35
|
+
dnd.on('cancel', (payload) => console.log('cancelled', payload))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## HTML Markup
|
|
39
|
+
|
|
40
|
+
Use data attributes to declare draggable elements and drop targets:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<!-- Root container -->
|
|
44
|
+
<div id="container">
|
|
45
|
+
|
|
46
|
+
<!-- Drop target -->
|
|
47
|
+
<div drop>
|
|
48
|
+
<!-- Draggable element -->
|
|
49
|
+
<div drag data-data='{"id":1,"type":"card"}'>Drag me</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Combined draggable + drop target -->
|
|
53
|
+
<div dragdrop data-namespace="list-a">Item</div>
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Attribute | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `drag` | Marks an element as draggable |
|
|
61
|
+
| `drop` | Marks an element as a drop target |
|
|
62
|
+
| `dragdrop` | Element is both draggable and a drop target |
|
|
63
|
+
| `drag-handle` | Restricts drag to a specific handle element inside the draggable |
|
|
64
|
+
| `data-namespace` | Groups draggable/drop elements — only elements in the same namespace interact |
|
|
65
|
+
| `data-data` | JSON data payload attached to the draggable |
|
|
66
|
+
| `copy` | Creates a copy on drop instead of moving |
|
|
67
|
+
| `drag-scope` | Defines a canvas-style scope for `TransformControllerService` |
|
|
68
|
+
| `drop-indicator` | Enables directional drop indicator (`top`, `right`, `bottom`, `left`, or `all`) |
|
|
69
|
+
| `ignore-mirror` | Dragging this element does not create a mirror clone |
|
|
70
|
+
| `ignore-click` | Clicks on this element do not affect active selection |
|
|
71
|
+
| `resizable` / `rotatable` | Per-element toggle for resize/rotate handles (defaults to `true` within drag-scope) |
|
|
72
|
+
| `scale-ratio` | Canvas zoom ratio applied to pointer coordinates in drag-scope mode |
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `new Dnd(config?)`
|
|
77
|
+
|
|
78
|
+
Creates a DnD instance.
|
|
79
|
+
|
|
80
|
+
| Option | Type | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `root` | `HTMLElement \| string` | — | Root container element or CSS selector |
|
|
83
|
+
| `threshold` | `number` | `3` | Pixel distance before drag is considered started |
|
|
84
|
+
|
|
85
|
+
### Instance methods
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// Register a plugin
|
|
89
|
+
dnd.use(plugin)
|
|
90
|
+
|
|
91
|
+
// Set or replace the root container
|
|
92
|
+
dnd.setRoot(element)
|
|
93
|
+
|
|
94
|
+
// Override drop permission (called every frame)
|
|
95
|
+
dnd.canDrop = (payload) => payload.data.type !== 'locked'
|
|
96
|
+
|
|
97
|
+
// Custom mirror rendering
|
|
98
|
+
dnd.renderMirror = (ctx) => {
|
|
99
|
+
const el = document.createElement('div')
|
|
100
|
+
el.textContent = 'Custom mirror'
|
|
101
|
+
return el
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Event listeners
|
|
105
|
+
dnd.on('dragstart', handler)
|
|
106
|
+
dnd.on('drag', handler)
|
|
107
|
+
dnd.on('drop', handler)
|
|
108
|
+
dnd.on('cancel', handler)
|
|
109
|
+
dnd.off('drop', handler)
|
|
110
|
+
|
|
111
|
+
// Clean up everything
|
|
112
|
+
dnd.destroy()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `dnd.monitor`
|
|
116
|
+
|
|
117
|
+
The current drag session object. Read-only during an active drag.
|
|
118
|
+
|
|
119
|
+
| Property | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `active` | Whether a drag session is active (pointer is down) |
|
|
122
|
+
| `started` | Whether the drag threshold has been exceeded |
|
|
123
|
+
| `x`, `y` | Current pointer coordinates |
|
|
124
|
+
| `dx`, `dy` | Delta from drag start |
|
|
125
|
+
| `sourceEl` | The element being dragged |
|
|
126
|
+
| `handleEl` | The handle element that was grabbed |
|
|
127
|
+
| `currentDrop` | The drop target currently under the pointer |
|
|
128
|
+
| `currentAllowed` | Whether `canDrop` returned `true` for the current target |
|
|
129
|
+
| `currentDropRect` | Bounding rect of the current drop target |
|
|
130
|
+
| `indicatorRegion` | Active drop indicator direction (`top` / `right` / `bottom` / `left`) |
|
|
131
|
+
| `data` | Parsed data from the draggable's `data-data` attribute |
|
|
132
|
+
| `namespace` | Namespace of the drag session |
|
|
133
|
+
| `isCopy` | Whether the draggable has the `copy` attribute |
|
|
134
|
+
|
|
135
|
+
## Events
|
|
136
|
+
|
|
137
|
+
All event handlers receive a payload object.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
dnd.on('dragstart', ({ source, data, namespace }) => { /* ... */ })
|
|
141
|
+
dnd.on('drag', ({ source, currentDrop, currentAllowed, x, y }) => { /* ... */ })
|
|
142
|
+
dnd.on('drop', ({ source, currentDrop, indicatorRegion, data }) => { /* ... */ })
|
|
143
|
+
dnd.on('cancel', ({ source, data }) => { /* ... */ })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Built-in Plugins (Services)
|
|
147
|
+
|
|
148
|
+
All services are optional. Register them with `dnd.use(new ServiceName())`.
|
|
149
|
+
|
|
150
|
+
### `MirrorService`
|
|
151
|
+
|
|
152
|
+
Creates a floating clone of the dragged element that follows the pointer. Adds `dnd-dragging` class to the source element during drag.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
dnd.use(new MirrorService())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The clone can be customized via `dnd.renderMirror`. To disable the mirror for a specific element, add the `ignore-mirror` attribute to it.
|
|
159
|
+
|
|
160
|
+
### `DropService`
|
|
161
|
+
|
|
162
|
+
Maintains `currentDrop`, `currentDropRect`, and `currentAllowed` on the session. Applies `dnd-canDrop` or `dnd-noDrop` CSS class to the active drop target.
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
dnd.use(new DropService())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `DropIndicatorService`
|
|
169
|
+
|
|
170
|
+
Shows a directional insertion indicator (top/right/bottom/left) on the active drop target. Requires the drop target to have a `drop-indicator` attribute.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
dnd.use(new DropIndicatorService())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<!-- Enable all four directions -->
|
|
178
|
+
<div drop drop-indicator>...</div>
|
|
179
|
+
|
|
180
|
+
<!-- Enable specific directions only -->
|
|
181
|
+
<div drop drop-indicator="top bottom">...</div>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The indicator element has class `dnd-indicator` and toggles `dnd-indicator--top`, `dnd-indicator--right`, `dnd-indicator--bottom`, `dnd-indicator--left` direction classes.
|
|
185
|
+
|
|
186
|
+
### `AutoScrollService`
|
|
187
|
+
|
|
188
|
+
Automatically scrolls the nearest scrollable ancestor (or window) when the pointer approaches the edge of the container during drag.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
dnd.use(new AutoScrollService({
|
|
192
|
+
edge: 48, // Edge trigger distance in px (default: 48)
|
|
193
|
+
minSpeed: 180, // Minimum scroll speed in px/s (default: 180)
|
|
194
|
+
maxSpeed: 600, // Maximum scroll speed in px/s (default: 600)
|
|
195
|
+
allowWindowScroll: true // Allow scrolling window (default: true)
|
|
196
|
+
}))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `ActiveSelectionService`
|
|
200
|
+
|
|
201
|
+
Tracks the selected (active) draggable per namespace. Adds `dnd-active` class to the selected element. Clicking outside clears the selection.
|
|
202
|
+
|
|
203
|
+
Within a `drag-scope` container, selecting an element injects resize/rotate handles into it.
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
dnd.use(new ActiveSelectionService())
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `TransformControllerService`
|
|
210
|
+
|
|
211
|
+
Enables move, resize, and rotate within a `drag-scope` container. Designed for canvas editors with elements that use CSS `transform` for positioning.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
dnd.use(new TransformControllerService({
|
|
215
|
+
resizable: true,
|
|
216
|
+
rotatable: true,
|
|
217
|
+
boundary: true, // Constrain movement within drag-scope
|
|
218
|
+
scaleRatio: 1, // Canvas zoom ratio (can also be set via data-scale-ratio)
|
|
219
|
+
snapToGrid: false,
|
|
220
|
+
gridX: 10,
|
|
221
|
+
gridY: 10,
|
|
222
|
+
aspectRatio: undefined, // Lock aspect ratio during resize
|
|
223
|
+
minWidth: 10,
|
|
224
|
+
minHeight: 10,
|
|
225
|
+
maxWidth: 0, // 0 = no limit
|
|
226
|
+
maxHeight: 0,
|
|
227
|
+
rotateSnap: false,
|
|
228
|
+
rotateStep: 15, // Snap angle in degrees
|
|
229
|
+
snap: true, // Snap-to-element alignment
|
|
230
|
+
snapThreshold: 10, // Snap threshold in px
|
|
231
|
+
markline: true, // Show alignment guide lines
|
|
232
|
+
}))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Emitted events** (via `dnd.on`):
|
|
236
|
+
|
|
237
|
+
| Event | Description |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `draggable:drag` | Element moved — payload includes `el`, `x`, `y`, `width`, `height`, `angle` |
|
|
240
|
+
| `draggable:resize` | Element resized — same payload |
|
|
241
|
+
| `draggable:rotate` | Element rotated — same payload |
|
|
242
|
+
| `draggable:drop` | Drag ended with a committed transform — includes final `x`, `y`, `width`, `height`, `angle` |
|
|
243
|
+
|
|
244
|
+
**HTML markup for drag-scope:**
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<div drag-scope data-scale-ratio="1">
|
|
248
|
+
<div drag resizable rotatable
|
|
249
|
+
style="transform: translate(100px, 50px); width: 200px; height: 120px;">
|
|
250
|
+
Canvas element
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## CSS Classes
|
|
256
|
+
|
|
257
|
+
| Class | Applied to | Description |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `dnd-dragging` | Source element | While drag is active |
|
|
260
|
+
| `dnd-mirror` | Mirror clone | The floating drag ghost |
|
|
261
|
+
| `dnd-active` | Selected draggable | While element is selected |
|
|
262
|
+
| `dnd-canDrop` | Drop target | When `canDrop` returns `true` |
|
|
263
|
+
| `dnd-noDrop` | Drop target | When `canDrop` returns `false` |
|
|
264
|
+
| `dnd-indicator` | Indicator element | The drop direction indicator |
|
|
265
|
+
| `dnd-indicator-active` | Indicator element | When indicator is visible |
|
|
266
|
+
| `dnd-indicator--top/right/bottom/left` | Indicator element | Active direction |
|
|
267
|
+
|
|
268
|
+
## Writing a Custom Plugin
|
|
269
|
+
|
|
270
|
+
A plugin is a plain object or class instance with optional lifecycle hooks. Register it with `dnd.use(plugin)`.
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
const myPlugin = {
|
|
274
|
+
order: 50, // lower = earlier execution
|
|
275
|
+
|
|
276
|
+
onAttach(dnd) {
|
|
277
|
+
// Called once when plugin is registered
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
onRootChange(nextRoot, prevRoot, signal) {
|
|
281
|
+
// Called when dnd.setRoot() is called
|
|
282
|
+
nextRoot.addEventListener('contextmenu', handler, { signal })
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
onDown(ctx, event) {
|
|
286
|
+
// Pointer down — drag not yet started
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
onStart(ctx) {
|
|
290
|
+
// Drag threshold exceeded, drag is now active
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
onMeasure(ctx) {
|
|
294
|
+
// Read-only DOM measurements (called every frame)
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
onCompute(ctx) {
|
|
298
|
+
// Pure calculations based on measurements (no DOM writes)
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
onCommit(ctx) {
|
|
302
|
+
// Write DOM changes via ctx.frame to batch updates
|
|
303
|
+
ctx.frame.toggleClass(element, 'my-class', true)
|
|
304
|
+
ctx.frame.setStyle(element, 'opacity', '0.5')
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
onAfterDrag(ctx) {
|
|
308
|
+
// Post-commit hook, e.g. for auto-scroll
|
|
309
|
+
// Return true or { scrolled: true } to request a re-render next frame
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
onEnd(ctx, meta) {
|
|
313
|
+
// meta.ended: true = drop, false = cancel
|
|
314
|
+
// meta.reason: 'pointerup' | 'blur' | 'destroy'
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
onDestroy(dnd, session) {
|
|
318
|
+
// Cleanup when dnd.destroy() is called
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
dnd.use(myPlugin)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Context object (`ctx`)
|
|
326
|
+
|
|
327
|
+
| Property | Description |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `ctx.session` | The current drag session (monitor) |
|
|
330
|
+
| `ctx.store` | Cross-session store (`selectedByNs` map) |
|
|
331
|
+
| `ctx.frame` | Frame command queue for batched DOM writes |
|
|
332
|
+
| `ctx.adapter` | DOM adapter for measurements and hit testing |
|
|
333
|
+
| `ctx.dnd` | The `Dnd` instance |
|
|
334
|
+
| `ctx.payload(type)` | Builds an event payload for the given event type |
|
|
335
|
+
|
|
336
|
+
## Architecture
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
Dnd (Facade)
|
|
340
|
+
├── PointerSensor — captures pointerdown/move/up events
|
|
341
|
+
├── State — finite state machine (DOWN → MOVE → END)
|
|
342
|
+
├── DomAdapter — DOM queries and per-frame rect caching
|
|
343
|
+
├── RafScheduler — requestAnimationFrame loop (only runs when dirty)
|
|
344
|
+
├── FrameContext — batched DOM command queue for the current frame
|
|
345
|
+
└── PluginRuntime — ordered plugin lifecycle dispatcher
|
|
346
|
+
├── MirrorService
|
|
347
|
+
├── DropService
|
|
348
|
+
├── DropIndicatorService
|
|
349
|
+
├── AutoScrollService
|
|
350
|
+
├── ActiveSelectionService
|
|
351
|
+
└── TransformControllerService
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Per-frame pipeline (while dragging):
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
pointermove → State.dispatch(MOVE) → scheduler.request()
|
|
358
|
+
↓
|
|
359
|
+
requestAnimationFrame
|
|
360
|
+
↓
|
|
361
|
+
PluginRuntime.onMeasure (read DOM)
|
|
362
|
+
PluginRuntime.onCompute (pure math)
|
|
363
|
+
emit('drag', payload)
|
|
364
|
+
PluginRuntime.onCommit (write DOM)
|
|
365
|
+
FrameContext.commit()
|
|
366
|
+
PluginRuntime.onAfterDrag (scroll, etc.)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT
|