dnd-manager 1.0.0 → 1.2.0
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/LICENSE +0 -0
- package/README.md +101 -17
- package/dist/index.d.mts +53 -9
- package/dist/index.mjs +133 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -21
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,14 +1,99 @@
|
|
|
1
1
|
# dnd-manager
|
|
2
2
|
|
|
3
|
-
Simple, performant, and data-driven Drag-and-Drop library for vanilla JS,
|
|
3
|
+
Simple, performant, and data-driven Drag-and-Drop library for vanilla JS, compatible with any
|
|
4
|
+
framework, and designed for developers who want full control over drag-and-drop UX without being
|
|
5
|
+
locked into specific component patterns or opinionated libraries.
|
|
4
6
|
|
|
5
|
-
This document provides practical examples of using
|
|
6
|
-
|
|
7
|
+
This document provides practical examples of using the library in both vanilla JavaScript and React
|
|
8
|
+
applications.
|
|
7
9
|
|
|
8
10
|
## Table of Contents
|
|
9
11
|
|
|
10
|
-
1. [
|
|
11
|
-
2. [
|
|
12
|
+
1. [Installation](#installation)
|
|
13
|
+
2. [Clone Preview Helper](#clone-preview-helper)
|
|
14
|
+
3. [Vanilla JavaScript Example](#vanilla-javascript-example)
|
|
15
|
+
4. [React Example](#react-example)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add dnd-manager
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with npm/yarn:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install dnd-manager
|
|
29
|
+
yarn add dnd-manager
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Clone Preview Helper
|
|
35
|
+
|
|
36
|
+
Use `new DragPreviewController()` to get a clone of the dragged element that follows the cursor with
|
|
37
|
+
`position: fixed`, `pointer-events: none`, and transform-based movement.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import {
|
|
41
|
+
DragPreviewController,
|
|
42
|
+
DragDropManager,
|
|
43
|
+
type DragDropCallbacks,
|
|
44
|
+
type PointerPosition,
|
|
45
|
+
} from 'dnd-manager'
|
|
46
|
+
|
|
47
|
+
type Item = { index: number }
|
|
48
|
+
type Position = { index: number }
|
|
49
|
+
|
|
50
|
+
const preview = new DragPreviewController({
|
|
51
|
+
zIndex: 9999,
|
|
52
|
+
opacity: 0.95,
|
|
53
|
+
className: 'drop-shadow-xl',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const callbacks: DragDropCallbacks<Item, Position> = {
|
|
57
|
+
getItemPosition: (element) => {
|
|
58
|
+
const raw = element.dataset.index
|
|
59
|
+
if (!raw) return null
|
|
60
|
+
const index = Number.parseInt(raw, 10)
|
|
61
|
+
return Number.isFinite(index) ? { index } : null
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
getItemData: (_element, pos) => ({ index: pos.index }),
|
|
65
|
+
|
|
66
|
+
onDragStart: (element) => {
|
|
67
|
+
preview.startFromElement(element)
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
onDragMove: (pos: PointerPosition) => {
|
|
71
|
+
preview.moveToPointer(pos)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
onDragEnd: () => {
|
|
75
|
+
preview.stop()
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const manager = new DragDropManager<Item, Position>(
|
|
80
|
+
'#container',
|
|
81
|
+
{
|
|
82
|
+
draggableKind: 'ITEM',
|
|
83
|
+
droppableKind: 'ITEM',
|
|
84
|
+
},
|
|
85
|
+
callbacks,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Important cleanup for unmount/page teardown.
|
|
89
|
+
function cleanup() {
|
|
90
|
+
preview.destroy()
|
|
91
|
+
manager.destroy()
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This helper uses the clone-in-`document.body` pattern to avoid drift in scrolled containers and
|
|
96
|
+
keeps cursor tracking smooth via transform updates.
|
|
12
97
|
|
|
13
98
|
---
|
|
14
99
|
|
|
@@ -199,7 +284,7 @@ data to the console, and dragging shows a preview element following the cursor.
|
|
|
199
284
|
**TypeScript (app.ts):**
|
|
200
285
|
|
|
201
286
|
```typescript
|
|
202
|
-
import { DragDropManager, type DragDropCallbacks } from '
|
|
287
|
+
import { DragDropManager, type DragDropCallbacks } from 'dnd-manager'
|
|
203
288
|
|
|
204
289
|
// Types
|
|
205
290
|
type GridItem = {
|
|
@@ -222,11 +307,6 @@ function initDragDropGrid(): void {
|
|
|
222
307
|
|
|
223
308
|
// Setup DragDropManager callbacks
|
|
224
309
|
const callbacks: DragDropCallbacks<GridItem, GridPosition> = {
|
|
225
|
-
canDrag: (element, position) => {
|
|
226
|
-
// Can only drag non-empty cells
|
|
227
|
-
return element.dataset.empty === undefined
|
|
228
|
-
},
|
|
229
|
-
|
|
230
310
|
getItemPosition: (element, kind) => {
|
|
231
311
|
const row = element.dataset.row
|
|
232
312
|
const col = element.dataset.col
|
|
@@ -343,6 +423,8 @@ function initDragDropGrid(): void {
|
|
|
343
423
|
clickThreshold: 10,
|
|
344
424
|
scrollThreshold: 100,
|
|
345
425
|
scrollSpeed: 10,
|
|
426
|
+
cancelOnEscape: true,
|
|
427
|
+
cancelOnPointerLeave: true,
|
|
346
428
|
},
|
|
347
429
|
callbacks,
|
|
348
430
|
)
|
|
@@ -365,7 +447,7 @@ This example shows the same draggable grid implemented as a React component with
|
|
|
365
447
|
|
|
366
448
|
```typescript
|
|
367
449
|
import { useEffect, useRef, useState } from 'react'
|
|
368
|
-
import { DragDropManager, type DragDropCallbacks } from '
|
|
450
|
+
import { DragDropManager, type DragDropCallbacks } from 'dnd-manager'
|
|
369
451
|
|
|
370
452
|
// Types
|
|
371
453
|
type GridItem = {
|
|
@@ -479,10 +561,6 @@ export function DraggableGrid() {
|
|
|
479
561
|
|
|
480
562
|
// Setup callbacks
|
|
481
563
|
const callbacks: DragDropCallbacks<GridItem, GridPosition> = {
|
|
482
|
-
canDrag: (element, position) => {
|
|
483
|
-
return element.dataset.empty === undefined
|
|
484
|
-
},
|
|
485
|
-
|
|
486
564
|
getItemPosition: (element, kind) => {
|
|
487
565
|
const row = element.dataset.row
|
|
488
566
|
const col = element.dataset.col
|
|
@@ -557,6 +635,8 @@ export function DraggableGrid() {
|
|
|
557
635
|
clickThreshold: 10,
|
|
558
636
|
scrollThreshold: 100,
|
|
559
637
|
scrollSpeed: 10,
|
|
638
|
+
cancelOnEscape: true,
|
|
639
|
+
cancelOnPointerLeave: true,
|
|
560
640
|
},
|
|
561
641
|
callbacks,
|
|
562
642
|
)
|
|
@@ -741,7 +821,7 @@ function DragPreview({ item, position, width, height }: DragPreviewProps) {
|
|
|
741
821
|
|
|
742
822
|
### DragDropManager Features Used:
|
|
743
823
|
|
|
744
|
-
- `
|
|
824
|
+
- `data-empty` convention - Elements with `data-empty` are automatically treated as non-draggable
|
|
745
825
|
- `getItemPosition` - Extracts grid coordinates from data attributes
|
|
746
826
|
- `getItemData` - Retrieves item data from grid state
|
|
747
827
|
- `onDragStart` - Shows preview and updates source appearance
|
|
@@ -753,10 +833,14 @@ function DragPreview({ item, position, width, height }: DragPreviewProps) {
|
|
|
753
833
|
|
|
754
834
|
### Configuration:
|
|
755
835
|
|
|
836
|
+
- `draggableKind` / `droppableKind` - Accept `string` or `string[]` to match one or many kinds. Each
|
|
837
|
+
element should still expose a single `data-kind` value.
|
|
756
838
|
- `dragThreshold: 10` - Prevents accidental drags
|
|
757
839
|
- `clickThreshold: 10` - Distinguishes clicks from drags
|
|
758
840
|
- `scrollThreshold: 100` - Auto-scroll near viewport edges
|
|
759
841
|
- `scrollSpeed: 10` - Smooth scroll speed
|
|
842
|
+
- `cancelOnEscape: true` - Cancels active drag when Escape is pressed
|
|
843
|
+
- `cancelOnPointerLeave: true` - Cancels active drag if pointer leaves the window
|
|
760
844
|
|
|
761
845
|
---
|
|
762
846
|
|
package/dist/index.d.mts
CHANGED
|
@@ -47,23 +47,40 @@ type DragDropConfig = {
|
|
|
47
47
|
*/
|
|
48
48
|
scrollSpeed?: number;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* Cancel the current drag operation when Escape is pressed.
|
|
51
|
+
* Useful for explicit keyboard cancellation.
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
cancelOnEscape?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Cancel the current drag operation when the pointer leaves the browser window.
|
|
57
|
+
* Prevents "stuck dragging" when pointerup happens outside the app/window.
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
cancelOnPointerLeave?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The `data-kind` value(s) for draggable elements.
|
|
63
|
+
* Can be a single kind or multiple kinds.
|
|
64
|
+
* Each draggable element should still expose a single `data-kind` value.
|
|
52
65
|
* @example
|
|
53
66
|
* draggableKind: 'cell' // <div data-kind="cell">
|
|
67
|
+
* draggableKind: ['quest', 'bounty'] // <div data-kind="quest">
|
|
54
68
|
* draggableKind: 'card' // <div data-kind="card">
|
|
55
69
|
* draggableKind: 'list-item' // <div data-kind="list-item">
|
|
56
70
|
*/
|
|
57
|
-
draggableKind: string;
|
|
71
|
+
draggableKind: string | string[];
|
|
58
72
|
/**
|
|
59
|
-
* The `data-kind`
|
|
60
|
-
*
|
|
73
|
+
* The `data-kind` value(s) for droppable target elements.
|
|
74
|
+
* Can be a single kind or multiple kinds.
|
|
75
|
+
* Elements with matching kinds can receive dropped items.
|
|
76
|
+
* Each droppable element should still expose a single `data-kind` value.
|
|
61
77
|
* Often the same as `draggableKind` for reorderable lists.
|
|
62
78
|
* @example
|
|
63
79
|
* droppableKind: 'cell' // Can drop on cells
|
|
80
|
+
* droppableKind: ['quest', 'bounty'] // Can drop on either kind
|
|
64
81
|
* droppableKind: 'zone' // Can drop on zones
|
|
65
82
|
*/
|
|
66
|
-
droppableKind: string;
|
|
83
|
+
droppableKind: string | string[];
|
|
67
84
|
};
|
|
68
85
|
type PointerPosition = {
|
|
69
86
|
x: number;
|
|
@@ -84,12 +101,15 @@ type DragState<TItem, TPosition> = {
|
|
|
84
101
|
sourceElement: HTMLElement | null;
|
|
85
102
|
sourcePosition: TPosition | null;
|
|
86
103
|
sourceItem: TItem | null;
|
|
104
|
+
activePointerId: number | null;
|
|
87
105
|
isDragging: boolean;
|
|
88
106
|
lastHoveredElement: HTMLElement | null;
|
|
89
107
|
};
|
|
90
108
|
declare class DragDropManager<TItem = unknown, TPosition = unknown> {
|
|
91
109
|
private container;
|
|
92
110
|
private config;
|
|
111
|
+
private draggableKinds;
|
|
112
|
+
private droppableKinds;
|
|
93
113
|
private callbacks;
|
|
94
114
|
private state;
|
|
95
115
|
private rafId;
|
|
@@ -103,12 +123,17 @@ declare class DragDropManager<TItem = unknown, TPosition = unknown> {
|
|
|
103
123
|
private handlePointerMove;
|
|
104
124
|
private handlePointerUp;
|
|
105
125
|
private handlePointerCancel;
|
|
126
|
+
private handleKeyDown;
|
|
127
|
+
private handleMouseOut;
|
|
106
128
|
private handleAutoScroll;
|
|
107
129
|
private cleanup;
|
|
130
|
+
private getPointerId;
|
|
131
|
+
private isActivePointer;
|
|
132
|
+
private normalizeKindList;
|
|
108
133
|
/**
|
|
109
|
-
* Find closest ancestor element with
|
|
134
|
+
* Find closest ancestor element with a `data-kind` matching any expected kind.
|
|
110
135
|
*/
|
|
111
|
-
private
|
|
136
|
+
private findElementByKinds;
|
|
112
137
|
/**
|
|
113
138
|
* Calculate distance between two points
|
|
114
139
|
*/
|
|
@@ -127,5 +152,24 @@ declare class DragDropManager<TItem = unknown, TPosition = unknown> {
|
|
|
127
152
|
destroy(): void;
|
|
128
153
|
}
|
|
129
154
|
//#endregion
|
|
130
|
-
|
|
155
|
+
//#region src/drag-preview-controller.d.ts
|
|
156
|
+
type DragPreviewControllerOptions = {
|
|
157
|
+
zIndex?: number;
|
|
158
|
+
opacity?: number;
|
|
159
|
+
centerOnCursor?: boolean;
|
|
160
|
+
className?: string | string[];
|
|
161
|
+
copyComputedStyles?: string[];
|
|
162
|
+
appendTo?: HTMLElement;
|
|
163
|
+
};
|
|
164
|
+
declare class DragPreviewController {
|
|
165
|
+
private options;
|
|
166
|
+
private previewElement;
|
|
167
|
+
constructor(options?: DragPreviewControllerOptions);
|
|
168
|
+
startFromElement(element: HTMLElement): void;
|
|
169
|
+
moveToPointer(pos: PointerPosition): void;
|
|
170
|
+
stop(): void;
|
|
171
|
+
destroy(): void;
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { type DragDropCallbacks, type DragDropConfig, DragDropManager, DragPreviewController, type DragPreviewControllerOptions, type PointerPosition };
|
|
131
175
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
var DragDropManager = class {
|
|
3
3
|
container;
|
|
4
4
|
config;
|
|
5
|
+
draggableKinds;
|
|
6
|
+
droppableKinds;
|
|
5
7
|
callbacks;
|
|
6
8
|
state = {
|
|
7
9
|
startPosition: null,
|
|
8
10
|
sourceElement: null,
|
|
9
11
|
sourcePosition: null,
|
|
10
12
|
sourceItem: null,
|
|
13
|
+
activePointerId: null,
|
|
11
14
|
isDragging: false,
|
|
12
15
|
lastHoveredElement: null
|
|
13
16
|
};
|
|
@@ -28,15 +31,21 @@ var DragDropManager = class {
|
|
|
28
31
|
clickThreshold: config.clickThreshold ?? 10,
|
|
29
32
|
scrollThreshold: config.scrollThreshold ?? 100,
|
|
30
33
|
scrollSpeed: config.scrollSpeed ?? 10,
|
|
34
|
+
cancelOnEscape: config.cancelOnEscape ?? true,
|
|
35
|
+
cancelOnPointerLeave: config.cancelOnPointerLeave ?? true,
|
|
31
36
|
draggableKind: config.draggableKind,
|
|
32
37
|
droppableKind: config.droppableKind
|
|
33
38
|
};
|
|
39
|
+
this.draggableKinds = this.normalizeKindList(this.config.draggableKind);
|
|
40
|
+
this.droppableKinds = this.normalizeKindList(this.config.droppableKind);
|
|
34
41
|
this.callbacks = callbacks;
|
|
35
42
|
this.boundHandlers = {
|
|
36
43
|
pointerDown: this.handlePointerDown.bind(this),
|
|
37
44
|
pointerMove: this.handlePointerMove.bind(this),
|
|
38
45
|
pointerUp: this.handlePointerUp.bind(this),
|
|
39
|
-
pointerCancel: this.handlePointerCancel.bind(this)
|
|
46
|
+
pointerCancel: this.handlePointerCancel.bind(this),
|
|
47
|
+
keyDown: this.handleKeyDown.bind(this),
|
|
48
|
+
mouseOut: this.handleMouseOut.bind(this)
|
|
40
49
|
};
|
|
41
50
|
this.attachListeners();
|
|
42
51
|
}
|
|
@@ -45,12 +54,18 @@ var DragDropManager = class {
|
|
|
45
54
|
this.container.addEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
46
55
|
this.container.addEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
47
56
|
this.container.addEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
57
|
+
window.addEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
58
|
+
window.addEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
59
|
+
window.addEventListener("keydown", this.boundHandlers.keyDown);
|
|
60
|
+
window.addEventListener("mouseout", this.boundHandlers.mouseOut);
|
|
48
61
|
}
|
|
49
62
|
handlePointerDown(e) {
|
|
63
|
+
if (this.state.startPosition) return;
|
|
50
64
|
const target = e.target;
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
const
|
|
65
|
+
const draggableMatch = this.findElementByKinds(target, this.draggableKinds);
|
|
66
|
+
if (!draggableMatch) return;
|
|
67
|
+
const { element: draggableElement, kind: sourceKind } = draggableMatch;
|
|
68
|
+
const position = this.callbacks.getItemPosition(draggableElement, sourceKind);
|
|
54
69
|
if (!position) return;
|
|
55
70
|
if (draggableElement.dataset.empty !== void 0) return;
|
|
56
71
|
if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) return;
|
|
@@ -64,8 +79,13 @@ var DragDropManager = class {
|
|
|
64
79
|
this.state.sourceElement = draggableElement;
|
|
65
80
|
this.state.sourcePosition = position;
|
|
66
81
|
this.state.sourceItem = item;
|
|
82
|
+
this.state.activePointerId = this.getPointerId(e);
|
|
83
|
+
if (this.state.activePointerId !== null && typeof draggableElement.setPointerCapture === "function") try {
|
|
84
|
+
draggableElement.setPointerCapture(this.state.activePointerId);
|
|
85
|
+
} catch {}
|
|
67
86
|
}
|
|
68
87
|
handlePointerMove(e) {
|
|
88
|
+
if (!this.isActivePointer(e)) return;
|
|
69
89
|
const currentPos = {
|
|
70
90
|
x: e.clientX,
|
|
71
91
|
y: e.clientY
|
|
@@ -87,7 +107,7 @@ var DragDropManager = class {
|
|
|
87
107
|
if (!this.lastPointerPos) return;
|
|
88
108
|
const pos = this.lastPointerPos;
|
|
89
109
|
const elementUnderCursor = document.elementFromPoint(pos.x, pos.y);
|
|
90
|
-
const droppableElement = this.
|
|
110
|
+
const droppableElement = this.findElementByKinds(elementUnderCursor, this.droppableKinds)?.element ?? null;
|
|
91
111
|
if (droppableElement !== this.state.lastHoveredElement) {
|
|
92
112
|
if (this.state.lastHoveredElement) this.state.lastHoveredElement.removeAttribute("data-hovered");
|
|
93
113
|
if (droppableElement) {
|
|
@@ -100,6 +120,7 @@ var DragDropManager = class {
|
|
|
100
120
|
});
|
|
101
121
|
}
|
|
102
122
|
handlePointerUp(e) {
|
|
123
|
+
if (!this.isActivePointer(e)) return;
|
|
103
124
|
if (this.state.startPosition && !this.state.isDragging) {
|
|
104
125
|
const currentPos = {
|
|
105
126
|
x: e.clientX,
|
|
@@ -114,9 +135,10 @@ var DragDropManager = class {
|
|
|
114
135
|
return;
|
|
115
136
|
}
|
|
116
137
|
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
const droppableMatch = this.findElementByKinds(target, this.droppableKinds);
|
|
139
|
+
const droppableElement = droppableMatch?.element ?? null;
|
|
140
|
+
if (droppableElement && droppableMatch) {
|
|
141
|
+
const targetPosition = this.callbacks.getItemPosition(droppableElement, droppableMatch.kind);
|
|
120
142
|
if (targetPosition) this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem);
|
|
121
143
|
}
|
|
122
144
|
this.cleanup();
|
|
@@ -124,6 +146,18 @@ var DragDropManager = class {
|
|
|
124
146
|
handlePointerCancel() {
|
|
125
147
|
this.cleanup();
|
|
126
148
|
}
|
|
149
|
+
handleKeyDown(e) {
|
|
150
|
+
if (!this.config.cancelOnEscape) return;
|
|
151
|
+
if (e.key !== "Escape") return;
|
|
152
|
+
if (!this.state.startPosition) return;
|
|
153
|
+
this.cleanup();
|
|
154
|
+
}
|
|
155
|
+
handleMouseOut(e) {
|
|
156
|
+
if (!this.config.cancelOnPointerLeave) return;
|
|
157
|
+
if (!this.state.startPosition) return;
|
|
158
|
+
if (e.relatedTarget !== null) return;
|
|
159
|
+
this.cleanup();
|
|
160
|
+
}
|
|
127
161
|
handleAutoScroll(pos) {
|
|
128
162
|
const { scrollThreshold, scrollSpeed } = this.config;
|
|
129
163
|
const viewportWidth = window.innerWidth;
|
|
@@ -137,6 +171,9 @@ var DragDropManager = class {
|
|
|
137
171
|
if (scrollX !== 0 || scrollY !== 0) window.scrollBy(scrollX, scrollY);
|
|
138
172
|
}
|
|
139
173
|
cleanup() {
|
|
174
|
+
if (this.state.sourceElement && this.state.activePointerId !== null && typeof this.state.sourceElement.hasPointerCapture === "function" && typeof this.state.sourceElement.releasePointerCapture === "function" && this.state.sourceElement.hasPointerCapture(this.state.activePointerId)) try {
|
|
175
|
+
this.state.sourceElement.releasePointerCapture(this.state.activePointerId);
|
|
176
|
+
} catch {}
|
|
140
177
|
if (this.state.sourceElement) this.state.sourceElement.removeAttribute("data-dragging");
|
|
141
178
|
if (this.state.lastHoveredElement) this.state.lastHoveredElement.removeAttribute("data-hovered");
|
|
142
179
|
if (this.rafId !== null) {
|
|
@@ -149,18 +186,35 @@ var DragDropManager = class {
|
|
|
149
186
|
sourceElement: null,
|
|
150
187
|
sourcePosition: null,
|
|
151
188
|
sourceItem: null,
|
|
189
|
+
activePointerId: null,
|
|
152
190
|
isDragging: false,
|
|
153
191
|
lastHoveredElement: null
|
|
154
192
|
};
|
|
155
193
|
this.lastPointerPos = null;
|
|
156
194
|
}
|
|
195
|
+
getPointerId(e) {
|
|
196
|
+
return typeof e.pointerId === "number" ? e.pointerId : null;
|
|
197
|
+
}
|
|
198
|
+
isActivePointer(e) {
|
|
199
|
+
if (this.state.activePointerId === null) return true;
|
|
200
|
+
const pointerId = this.getPointerId(e);
|
|
201
|
+
return pointerId === null || pointerId === this.state.activePointerId;
|
|
202
|
+
}
|
|
203
|
+
normalizeKindList(kinds) {
|
|
204
|
+
const values = Array.isArray(kinds) ? kinds : [kinds];
|
|
205
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
206
|
+
}
|
|
157
207
|
/**
|
|
158
|
-
* Find closest ancestor element with
|
|
208
|
+
* Find closest ancestor element with a `data-kind` matching any expected kind.
|
|
159
209
|
*/
|
|
160
|
-
|
|
210
|
+
findElementByKinds(element, kinds) {
|
|
161
211
|
let current = element;
|
|
162
212
|
while (current && current !== this.container) {
|
|
163
|
-
|
|
213
|
+
const elementKind = current.dataset.kind;
|
|
214
|
+
if (elementKind && kinds.includes(elementKind)) return {
|
|
215
|
+
element: current,
|
|
216
|
+
kind: elementKind
|
|
217
|
+
};
|
|
164
218
|
current = current.parentElement;
|
|
165
219
|
}
|
|
166
220
|
return null;
|
|
@@ -192,9 +246,76 @@ var DragDropManager = class {
|
|
|
192
246
|
this.container.removeEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
193
247
|
this.container.removeEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
194
248
|
this.container.removeEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
249
|
+
window.removeEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
250
|
+
window.removeEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
251
|
+
window.removeEventListener("keydown", this.boundHandlers.keyDown);
|
|
252
|
+
window.removeEventListener("mouseout", this.boundHandlers.mouseOut);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/drag-preview-controller.ts
|
|
258
|
+
const OFFSCREEN_TRANSFORM = "translate(-9999px, -9999px)";
|
|
259
|
+
function normalizeClassNames(className) {
|
|
260
|
+
if (!className) return [];
|
|
261
|
+
return Array.isArray(className) ? className : [className];
|
|
262
|
+
}
|
|
263
|
+
var DragPreviewController = class {
|
|
264
|
+
options;
|
|
265
|
+
previewElement = null;
|
|
266
|
+
constructor(options = {}) {
|
|
267
|
+
this.options = {
|
|
268
|
+
zIndex: options.zIndex ?? 9999,
|
|
269
|
+
opacity: options.opacity ?? .95,
|
|
270
|
+
centerOnCursor: options.centerOnCursor ?? true,
|
|
271
|
+
className: options.className,
|
|
272
|
+
copyComputedStyles: options.copyComputedStyles,
|
|
273
|
+
appendTo: options.appendTo
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
startFromElement(element) {
|
|
277
|
+
this.stop();
|
|
278
|
+
const rect = element.getBoundingClientRect();
|
|
279
|
+
const clone = element.cloneNode(true);
|
|
280
|
+
const computed = window.getComputedStyle(element);
|
|
281
|
+
const appendTo = this.options.appendTo ?? document.body;
|
|
282
|
+
clone.setAttribute("aria-hidden", "true");
|
|
283
|
+
clone.style.position = "fixed";
|
|
284
|
+
clone.style.left = "0";
|
|
285
|
+
clone.style.top = "0";
|
|
286
|
+
clone.style.width = `${rect.width}px`;
|
|
287
|
+
clone.style.height = `${rect.height}px`;
|
|
288
|
+
clone.style.pointerEvents = "none";
|
|
289
|
+
clone.style.zIndex = String(this.options.zIndex);
|
|
290
|
+
clone.style.opacity = String(this.options.opacity);
|
|
291
|
+
clone.style.transform = OFFSCREEN_TRANSFORM;
|
|
292
|
+
clone.style.transition = "none";
|
|
293
|
+
clone.style.cursor = "grabbing";
|
|
294
|
+
clone.style.margin = "0";
|
|
295
|
+
clone.style.boxSizing = "border-box";
|
|
296
|
+
clone.style.background = computed.backgroundColor;
|
|
297
|
+
for (const classToken of normalizeClassNames(this.options.className)) clone.classList.add(classToken);
|
|
298
|
+
if (this.options.copyComputedStyles && this.options.copyComputedStyles.length > 0) for (const propertyName of this.options.copyComputedStyles) clone.style.setProperty(propertyName, computed.getPropertyValue(propertyName));
|
|
299
|
+
appendTo.appendChild(clone);
|
|
300
|
+
this.previewElement = clone;
|
|
301
|
+
}
|
|
302
|
+
moveToPointer(pos) {
|
|
303
|
+
if (!this.previewElement) return;
|
|
304
|
+
if (this.options.centerOnCursor) {
|
|
305
|
+
this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px) translate(-50%, -50%)`;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
|
309
|
+
}
|
|
310
|
+
stop() {
|
|
311
|
+
this.previewElement?.remove();
|
|
312
|
+
this.previewElement = null;
|
|
313
|
+
}
|
|
314
|
+
destroy() {
|
|
315
|
+
this.stop();
|
|
195
316
|
}
|
|
196
317
|
};
|
|
197
318
|
|
|
198
319
|
//#endregion
|
|
199
|
-
export { DragDropManager };
|
|
320
|
+
export { DragDropManager, DragPreviewController };
|
|
200
321
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/drag-drop-manager.ts"],"sourcesContent":["/**\n * Framework-agnostic Drag and Drop Manager\n *\n * A data-driven drag-and-drop utility that works with React and vanilla JS.\n * Uses data attributes and direct DOM manipulation for optimal performance.\n *\n * @template TItem - The type of data associated with draggable items\n * @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)\n */\n\nexport type DragDropConfig = {\n /**\n * Minimum distance in pixels the pointer must move before a drag operation starts.\n * Prevents accidental drags from small movements and allows normal scrolling on mobile.\n * Checked during `pointermove` before drag has started.\n * @default 10\n * @example\n * dragThreshold: 15 // Harder to start drag (better for mobile scroll)\n */\n dragThreshold?: number\n\n /**\n * Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.\n * Used to distinguish intentional clicks from cancelled drag attempts.\n * Only checked on `pointerup` if drag never started.\n * @default 10\n * @example\n * clickThreshold: 5 // Stricter click detection\n * clickThreshold: 15 // More forgiving for mobile taps\n */\n clickThreshold?: number\n\n /**\n * Distance in pixels from viewport edge that triggers auto-scrolling during drag.\n * When dragging near screen edges, the window automatically scrolls.\n * @default 100\n * @example\n * scrollThreshold: 50 // Start scrolling closer to edges\n * scrollThreshold: 150 // Larger safe zone before scrolling\n */\n scrollThreshold?: number\n\n /**\n * Scroll speed in pixels per animation frame during auto-scroll.\n * Higher values = faster scrolling when dragging near viewport edges.\n * @default 10\n * @example\n * scrollSpeed: 5 // Slower, smoother scroll\n * scrollSpeed: 20 // Faster scroll\n */\n scrollSpeed?: number\n\n /**\n * The `data-kind` attribute value for draggable elements.\n * Elements with this attribute can be picked up and dragged.\n * @example\n * draggableKind: 'cell' // <div data-kind=\"cell\">\n * draggableKind: 'card' // <div data-kind=\"card\">\n * draggableKind: 'list-item' // <div data-kind=\"list-item\">\n */\n draggableKind: string\n\n /**\n * The `data-kind` attribute value for droppable target elements.\n * Elements with this attribute can receive dropped items.\n * Often the same as `draggableKind` for reorderable lists.\n * @example\n * droppableKind: 'cell' // Can drop on cells\n * droppableKind: 'zone' // Can drop on zones\n */\n droppableKind: string\n}\n\nexport type PointerPosition = {\n x: number\n y: number\n}\n\nexport type DragDropCallbacks<TItem, TPosition> = {\n /** Called when determining if an element can be dragged */\n canDrag?: (element: HTMLElement, position: TPosition) => boolean\n /** Called when drag starts (after threshold is met) */\n onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void\n /** Called continuously during drag */\n onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void\n /** Called when item is dropped on a valid target */\n onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void\n /** Called when drag ends (regardless of success) */\n onDragEnd?: () => void\n /** Called on simple click/tap (no drag) */\n onClick?: (element: HTMLElement, position: TPosition) => void\n /** Called to extract item data from an element */\n getItemData?: (element: HTMLElement, position: TPosition) => TItem | null\n /** Called to get the custom position/coordinates of an element */\n getItemPosition: (element: HTMLElement, kind: string) => TPosition | null\n}\n\ntype DragState<TItem, TPosition> = {\n startPosition: PointerPosition | null\n sourceElement: HTMLElement | null\n sourcePosition: TPosition | null\n sourceItem: TItem | null\n isDragging: boolean\n lastHoveredElement: HTMLElement | null\n}\n\nexport class DragDropManager<TItem = unknown, TPosition = unknown> {\n private container: HTMLElement\n private config: Required<DragDropConfig>\n private callbacks: DragDropCallbacks<TItem, TPosition>\n private state: DragState<TItem, TPosition> = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n private rafId: number | null = null\n private lastPointerPos: PointerPosition | null = null\n private boundHandlers: {\n pointerDown: (e: PointerEvent) => void\n pointerMove: (e: PointerEvent) => void\n pointerUp: (e: PointerEvent) => void\n pointerCancel: (e: PointerEvent) => void\n }\n\n constructor(\n containerRef: { current: HTMLElement | null } | string | HTMLElement,\n config: DragDropConfig,\n callbacks: DragDropCallbacks<TItem, TPosition>,\n ) {\n // Resolve container element\n if (typeof containerRef === 'string') {\n const element = document.querySelector<HTMLElement>(containerRef)\n if (!element) {\n throw new Error(`DragDropManager: Could not find element with selector \"${containerRef}\"`)\n }\n this.container = element\n } else if (typeof containerRef === 'object' && 'current' in containerRef) {\n if (!containerRef.current) {\n throw new Error('DragDropManager: Ref.current is null')\n }\n this.container = containerRef.current\n } else {\n this.container = containerRef as HTMLElement\n }\n\n this.config = {\n dragThreshold: config.dragThreshold ?? 10,\n clickThreshold: config.clickThreshold ?? 10,\n scrollThreshold: config.scrollThreshold ?? 100,\n scrollSpeed: config.scrollSpeed ?? 10,\n draggableKind: config.draggableKind,\n droppableKind: config.droppableKind,\n }\n\n this.callbacks = callbacks\n\n // Bind handlers to preserve context\n this.boundHandlers = {\n pointerDown: this.handlePointerDown.bind(this),\n pointerMove: this.handlePointerMove.bind(this),\n pointerUp: this.handlePointerUp.bind(this),\n pointerCancel: this.handlePointerCancel.bind(this),\n }\n\n this.attachListeners()\n }\n\n private attachListeners(): void {\n this.container.addEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.addEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.addEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n }\n\n private handlePointerDown(e: PointerEvent): void {\n const target = e.target as HTMLElement\n const draggableElement = this.findElementByKind(target, this.config.draggableKind)\n\n if (!draggableElement) return\n\n const position = this.callbacks.getItemPosition(draggableElement, this.config.draggableKind)\n if (!position) return\n\n // Check if element is empty (convention: data-empty=\"any-value\")\n const isEmpty = draggableElement.dataset.empty !== undefined\n if (isEmpty) return\n\n // Check canDrag callback\n if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) {\n return\n }\n\n // Get item data\n const item = this.callbacks.getItemData?.(draggableElement, position) ?? null\n if (!item) return\n\n // Prevent text selection during potential drag\n e.preventDefault()\n\n // Store start position and element info\n this.state.startPosition = { x: e.clientX, y: e.clientY }\n this.state.sourceElement = draggableElement\n this.state.sourcePosition = position\n this.state.sourceItem = item\n }\n\n private handlePointerMove(e: PointerEvent): void {\n const currentPos: PointerPosition = { x: e.clientX, y: e.clientY }\n\n // Check if we should start dragging (threshold not yet met)\n if (this.state.startPosition && !this.state.isDragging) {\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n if (distance >= this.config.dragThreshold) {\n // Start drag\n this.state.isDragging = true\n\n if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {\n // Set data-dragging attribute on source\n this.state.sourceElement.setAttribute('data-dragging', 'true')\n\n // Notify drag start\n this.callbacks.onDragStart?.(\n this.state.sourceElement,\n this.state.sourcePosition,\n this.state.sourceItem,\n )\n }\n }\n return\n }\n\n if (!this.state.isDragging) return\n\n // Store latest position for RAF\n this.lastPointerPos = currentPos\n\n // Throttle updates to 60fps using RAF\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n if (!this.lastPointerPos) return\n\n const pos = this.lastPointerPos\n\n // Find hovered droppable element\n const elementUnderCursor = document.elementFromPoint(pos.x, pos.y) as HTMLElement\n const droppableElement = this.findElementByKind(\n elementUnderCursor,\n this.config.droppableKind,\n )\n\n // Update hover feedback\n if (droppableElement !== this.state.lastHoveredElement) {\n // Remove hover from previous element\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Add hover to current element (including the source element).\n // This enables \"drag over itself\" states (e.g. combined data-dragging + data-hovered styling).\n if (droppableElement) {\n droppableElement.setAttribute('data-hovered', 'true')\n this.state.lastHoveredElement = droppableElement\n } else {\n this.state.lastHoveredElement = null\n }\n }\n\n // Auto-scroll\n this.handleAutoScroll(pos)\n\n // Notify drag move\n this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement)\n })\n }\n }\n\n private handlePointerUp(e: PointerEvent): void {\n // Handle click/tap if we never started dragging\n if (this.state.startPosition && !this.state.isDragging) {\n const currentPos = { x: e.clientX, y: e.clientY }\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n // If distance is very small, treat as click\n if (\n distance < this.config.clickThreshold &&\n this.state.sourceElement &&\n this.state.sourcePosition\n ) {\n this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition)\n }\n\n this.cleanup()\n return\n }\n\n if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {\n this.cleanup()\n return\n }\n\n // Find drop target\n const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement\n const droppableElement = this.findElementByKind(target, this.config.droppableKind)\n\n if (droppableElement) {\n const targetPosition = this.callbacks.getItemPosition(\n droppableElement,\n this.config.droppableKind,\n )\n\n if (targetPosition) {\n // Notify drop\n this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem)\n }\n }\n\n this.cleanup()\n }\n\n private handlePointerCancel(): void {\n this.cleanup()\n }\n\n private handleAutoScroll(pos: PointerPosition): void {\n const { scrollThreshold, scrollSpeed } = this.config\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n let scrollX = 0\n let scrollY = 0\n\n // Check horizontal scroll\n if (pos.x < scrollThreshold) {\n scrollX = -scrollSpeed\n } else if (pos.x > viewportWidth - scrollThreshold) {\n scrollX = scrollSpeed\n }\n\n // Check vertical scroll\n if (pos.y < scrollThreshold) {\n scrollY = -scrollSpeed\n } else if (pos.y > viewportHeight - scrollThreshold) {\n scrollY = scrollSpeed\n }\n\n if (scrollX !== 0 || scrollY !== 0) {\n window.scrollBy(scrollX, scrollY)\n }\n }\n\n private cleanup(): void {\n // Remove data-dragging attribute\n if (this.state.sourceElement) {\n this.state.sourceElement.removeAttribute('data-dragging')\n }\n\n // Remove hover attribute\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Cancel RAF\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n // Notify drag end\n if (this.state.isDragging) {\n this.callbacks.onDragEnd?.()\n }\n\n // Reset state\n this.state = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n this.lastPointerPos = null\n }\n\n /**\n * Find closest ancestor element with matching data-kind attribute\n */\n private findElementByKind(element: HTMLElement | null, kind: string): HTMLElement | null {\n let current = element\n while (current && current !== this.container) {\n if (current.dataset.kind === kind) {\n return current\n }\n current = current.parentElement\n }\n return null\n }\n\n /**\n * Calculate distance between two points\n */\n private getDistance(p1: PointerPosition, p2: PointerPosition): number {\n return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))\n }\n\n /**\n * Get current drag state (read-only)\n */\n public getState(): Readonly<DragState<TItem, TPosition>> {\n return { ...this.state }\n }\n\n /**\n * Check if currently dragging\n */\n public isDragging(): boolean {\n return this.state.isDragging\n }\n\n /**\n * Cleanup and remove event listeners\n */\n public destroy(): void {\n this.cleanup()\n this.container.removeEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.removeEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/drag-drop-manager.ts","../src/drag-preview-controller.ts"],"sourcesContent":["/**\n * Framework-agnostic Drag and Drop Manager\n *\n * A data-driven drag-and-drop utility that works with React and vanilla JS.\n * Uses data attributes and direct DOM manipulation for optimal performance.\n *\n * @template TItem - The type of data associated with draggable items\n * @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)\n */\n\nexport type DragDropConfig = {\n /**\n * Minimum distance in pixels the pointer must move before a drag operation starts.\n * Prevents accidental drags from small movements and allows normal scrolling on mobile.\n * Checked during `pointermove` before drag has started.\n * @default 10\n * @example\n * dragThreshold: 15 // Harder to start drag (better for mobile scroll)\n */\n dragThreshold?: number\n\n /**\n * Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.\n * Used to distinguish intentional clicks from cancelled drag attempts.\n * Only checked on `pointerup` if drag never started.\n * @default 10\n * @example\n * clickThreshold: 5 // Stricter click detection\n * clickThreshold: 15 // More forgiving for mobile taps\n */\n clickThreshold?: number\n\n /**\n * Distance in pixels from viewport edge that triggers auto-scrolling during drag.\n * When dragging near screen edges, the window automatically scrolls.\n * @default 100\n * @example\n * scrollThreshold: 50 // Start scrolling closer to edges\n * scrollThreshold: 150 // Larger safe zone before scrolling\n */\n scrollThreshold?: number\n\n /**\n * Scroll speed in pixels per animation frame during auto-scroll.\n * Higher values = faster scrolling when dragging near viewport edges.\n * @default 10\n * @example\n * scrollSpeed: 5 // Slower, smoother scroll\n * scrollSpeed: 20 // Faster scroll\n */\n scrollSpeed?: number\n\n /**\n * Cancel the current drag operation when Escape is pressed.\n * Useful for explicit keyboard cancellation.\n * @default true\n */\n cancelOnEscape?: boolean\n\n /**\n * Cancel the current drag operation when the pointer leaves the browser window.\n * Prevents \"stuck dragging\" when pointerup happens outside the app/window.\n * @default true\n */\n cancelOnPointerLeave?: boolean\n\n /**\n * The `data-kind` value(s) for draggable elements.\n * Can be a single kind or multiple kinds.\n * Each draggable element should still expose a single `data-kind` value.\n * @example\n * draggableKind: 'cell' // <div data-kind=\"cell\">\n * draggableKind: ['quest', 'bounty'] // <div data-kind=\"quest\">\n * draggableKind: 'card' // <div data-kind=\"card\">\n * draggableKind: 'list-item' // <div data-kind=\"list-item\">\n */\n draggableKind: string | string[]\n\n /**\n * The `data-kind` value(s) for droppable target elements.\n * Can be a single kind or multiple kinds.\n * Elements with matching kinds can receive dropped items.\n * Each droppable element should still expose a single `data-kind` value.\n * Often the same as `draggableKind` for reorderable lists.\n * @example\n * droppableKind: 'cell' // Can drop on cells\n * droppableKind: ['quest', 'bounty'] // Can drop on either kind\n * droppableKind: 'zone' // Can drop on zones\n */\n droppableKind: string | string[]\n}\n\nexport type PointerPosition = {\n x: number\n y: number\n}\n\nexport type DragDropCallbacks<TItem, TPosition> = {\n /** Called when determining if an element can be dragged */\n canDrag?: (element: HTMLElement, position: TPosition) => boolean\n /** Called when drag starts (after threshold is met) */\n onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void\n /** Called continuously during drag */\n onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void\n /** Called when item is dropped on a valid target */\n onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void\n /** Called when drag ends (regardless of success) */\n onDragEnd?: () => void\n /** Called on simple click/tap (no drag) */\n onClick?: (element: HTMLElement, position: TPosition) => void\n /** Called to extract item data from an element */\n getItemData?: (element: HTMLElement, position: TPosition) => TItem | null\n /** Called to get the custom position/coordinates of an element */\n getItemPosition: (element: HTMLElement, kind: string) => TPosition | null\n}\n\ntype DragState<TItem, TPosition> = {\n startPosition: PointerPosition | null\n sourceElement: HTMLElement | null\n sourcePosition: TPosition | null\n sourceItem: TItem | null\n activePointerId: number | null\n isDragging: boolean\n lastHoveredElement: HTMLElement | null\n}\n\nexport class DragDropManager<TItem = unknown, TPosition = unknown> {\n private container: HTMLElement\n private config: Required<DragDropConfig>\n private draggableKinds: string[]\n private droppableKinds: string[]\n private callbacks: DragDropCallbacks<TItem, TPosition>\n private state: DragState<TItem, TPosition> = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n activePointerId: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n private rafId: number | null = null\n private lastPointerPos: PointerPosition | null = null\n private boundHandlers: {\n pointerDown: (e: PointerEvent) => void\n pointerMove: (e: PointerEvent) => void\n pointerUp: (e: PointerEvent) => void\n pointerCancel: (e: PointerEvent) => void\n keyDown: (e: KeyboardEvent) => void\n mouseOut: (e: MouseEvent) => void\n }\n\n constructor(\n containerRef: { current: HTMLElement | null } | string | HTMLElement,\n config: DragDropConfig,\n callbacks: DragDropCallbacks<TItem, TPosition>,\n ) {\n // Resolve container element\n if (typeof containerRef === 'string') {\n const element = document.querySelector<HTMLElement>(containerRef)\n if (!element) {\n throw new Error(`DragDropManager: Could not find element with selector \"${containerRef}\"`)\n }\n this.container = element\n } else if (typeof containerRef === 'object' && 'current' in containerRef) {\n if (!containerRef.current) {\n throw new Error('DragDropManager: Ref.current is null')\n }\n this.container = containerRef.current\n } else {\n this.container = containerRef as HTMLElement\n }\n\n this.config = {\n dragThreshold: config.dragThreshold ?? 10,\n clickThreshold: config.clickThreshold ?? 10,\n scrollThreshold: config.scrollThreshold ?? 100,\n scrollSpeed: config.scrollSpeed ?? 10,\n cancelOnEscape: config.cancelOnEscape ?? true,\n cancelOnPointerLeave: config.cancelOnPointerLeave ?? true,\n draggableKind: config.draggableKind,\n droppableKind: config.droppableKind,\n }\n this.draggableKinds = this.normalizeKindList(this.config.draggableKind)\n this.droppableKinds = this.normalizeKindList(this.config.droppableKind)\n\n this.callbacks = callbacks\n\n // Bind handlers to preserve context\n this.boundHandlers = {\n pointerDown: this.handlePointerDown.bind(this),\n pointerMove: this.handlePointerMove.bind(this),\n pointerUp: this.handlePointerUp.bind(this),\n pointerCancel: this.handlePointerCancel.bind(this),\n keyDown: this.handleKeyDown.bind(this),\n mouseOut: this.handleMouseOut.bind(this),\n }\n\n this.attachListeners()\n }\n\n private attachListeners(): void {\n this.container.addEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.addEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.addEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('pointerup', this.boundHandlers.pointerUp)\n window.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.addEventListener('keydown', this.boundHandlers.keyDown)\n window.addEventListener('mouseout', this.boundHandlers.mouseOut)\n }\n\n private handlePointerDown(e: PointerEvent): void {\n if (this.state.startPosition) return\n\n const target = e.target as HTMLElement\n const draggableMatch = this.findElementByKinds(target, this.draggableKinds)\n\n if (!draggableMatch) return\n\n const { element: draggableElement, kind: sourceKind } = draggableMatch\n\n const position = this.callbacks.getItemPosition(draggableElement, sourceKind)\n if (!position) return\n\n // Check if element is empty (convention: data-empty=\"any-value\")\n const isEmpty = draggableElement.dataset.empty !== undefined\n if (isEmpty) return\n\n // Check canDrag callback\n if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) {\n return\n }\n\n // Get item data\n const item = this.callbacks.getItemData?.(draggableElement, position) ?? null\n if (!item) return\n\n // Prevent text selection during potential drag\n e.preventDefault()\n\n // Store start position and element info\n this.state.startPosition = { x: e.clientX, y: e.clientY }\n this.state.sourceElement = draggableElement\n this.state.sourcePosition = position\n this.state.sourceItem = item\n this.state.activePointerId = this.getPointerId(e)\n\n // Capture pointer events outside container boundaries when supported.\n if (\n this.state.activePointerId !== null &&\n typeof draggableElement.setPointerCapture === 'function'\n ) {\n try {\n draggableElement.setPointerCapture(this.state.activePointerId)\n } catch {\n // Ignore unsupported/inactive-pointer capture errors.\n }\n }\n }\n\n private handlePointerMove(e: PointerEvent): void {\n if (!this.isActivePointer(e)) return\n\n const currentPos: PointerPosition = { x: e.clientX, y: e.clientY }\n\n // Check if we should start dragging (threshold not yet met)\n if (this.state.startPosition && !this.state.isDragging) {\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n if (distance >= this.config.dragThreshold) {\n // Start drag\n this.state.isDragging = true\n\n if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {\n // Set data-dragging attribute on source\n this.state.sourceElement.setAttribute('data-dragging', 'true')\n\n // Notify drag start\n this.callbacks.onDragStart?.(\n this.state.sourceElement,\n this.state.sourcePosition,\n this.state.sourceItem,\n )\n }\n }\n return\n }\n\n if (!this.state.isDragging) return\n\n // Store latest position for RAF\n this.lastPointerPos = currentPos\n\n // Throttle updates to 60fps using RAF\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n if (!this.lastPointerPos) return\n\n const pos = this.lastPointerPos\n\n // Find hovered droppable element\n const elementUnderCursor = document.elementFromPoint(pos.x, pos.y) as HTMLElement\n const droppableMatch = this.findElementByKinds(elementUnderCursor, this.droppableKinds)\n const droppableElement = droppableMatch?.element ?? null\n\n // Update hover feedback\n if (droppableElement !== this.state.lastHoveredElement) {\n // Remove hover from previous element\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Add hover to current element (including the source element).\n // This enables \"drag over itself\" states (e.g. combined data-dragging + data-hovered styling).\n if (droppableElement) {\n droppableElement.setAttribute('data-hovered', 'true')\n this.state.lastHoveredElement = droppableElement\n } else {\n this.state.lastHoveredElement = null\n }\n }\n\n // Auto-scroll\n this.handleAutoScroll(pos)\n\n // Notify drag move\n this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement)\n })\n }\n }\n\n private handlePointerUp(e: PointerEvent): void {\n if (!this.isActivePointer(e)) return\n\n // Handle click/tap if we never started dragging\n if (this.state.startPosition && !this.state.isDragging) {\n const currentPos = { x: e.clientX, y: e.clientY }\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n // If distance is very small, treat as click\n if (\n distance < this.config.clickThreshold &&\n this.state.sourceElement &&\n this.state.sourcePosition\n ) {\n this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition)\n }\n\n this.cleanup()\n return\n }\n\n if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {\n this.cleanup()\n return\n }\n\n // Find drop target\n const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement\n const droppableMatch = this.findElementByKinds(target, this.droppableKinds)\n const droppableElement = droppableMatch?.element ?? null\n\n if (droppableElement && droppableMatch) {\n const targetPosition = this.callbacks.getItemPosition(droppableElement, droppableMatch.kind)\n\n if (targetPosition) {\n // Notify drop\n this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem)\n }\n }\n\n this.cleanup()\n }\n\n private handlePointerCancel(): void {\n this.cleanup()\n }\n\n private handleKeyDown(e: KeyboardEvent): void {\n if (!this.config.cancelOnEscape) return\n if (e.key !== 'Escape') return\n if (!this.state.startPosition) return\n this.cleanup()\n }\n\n private handleMouseOut(e: MouseEvent): void {\n if (!this.config.cancelOnPointerLeave) return\n if (!this.state.startPosition) return\n if (e.relatedTarget !== null) return\n this.cleanup()\n }\n\n private handleAutoScroll(pos: PointerPosition): void {\n const { scrollThreshold, scrollSpeed } = this.config\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n let scrollX = 0\n let scrollY = 0\n\n // Check horizontal scroll\n if (pos.x < scrollThreshold) {\n scrollX = -scrollSpeed\n } else if (pos.x > viewportWidth - scrollThreshold) {\n scrollX = scrollSpeed\n }\n\n // Check vertical scroll\n if (pos.y < scrollThreshold) {\n scrollY = -scrollSpeed\n } else if (pos.y > viewportHeight - scrollThreshold) {\n scrollY = scrollSpeed\n }\n\n if (scrollX !== 0 || scrollY !== 0) {\n window.scrollBy(scrollX, scrollY)\n }\n }\n\n private cleanup(): void {\n // Release pointer capture if we own it.\n if (\n this.state.sourceElement &&\n this.state.activePointerId !== null &&\n typeof this.state.sourceElement.hasPointerCapture === 'function' &&\n typeof this.state.sourceElement.releasePointerCapture === 'function' &&\n this.state.sourceElement.hasPointerCapture(this.state.activePointerId)\n ) {\n try {\n this.state.sourceElement.releasePointerCapture(this.state.activePointerId)\n } catch {\n // Ignore release errors for inactive pointer.\n }\n }\n\n // Remove data-dragging attribute\n if (this.state.sourceElement) {\n this.state.sourceElement.removeAttribute('data-dragging')\n }\n\n // Remove hover attribute\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Cancel RAF\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n // Notify drag end\n if (this.state.isDragging) {\n this.callbacks.onDragEnd?.()\n }\n\n // Reset state\n this.state = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n activePointerId: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n this.lastPointerPos = null\n }\n\n private getPointerId(e: PointerEvent): number | null {\n return typeof e.pointerId === 'number' ? e.pointerId : null\n }\n\n private isActivePointer(e: PointerEvent): boolean {\n if (this.state.activePointerId === null) {\n return true\n }\n const pointerId = this.getPointerId(e)\n return pointerId === null || pointerId === this.state.activePointerId\n }\n\n private normalizeKindList(kinds: string | string[]): string[] {\n const values = Array.isArray(kinds) ? kinds : [kinds]\n return [...new Set(values.map((value) => value.trim()).filter(Boolean))]\n }\n\n /**\n * Find closest ancestor element with a `data-kind` matching any expected kind.\n */\n private findElementByKinds(\n element: HTMLElement | null,\n kinds: string[],\n ): { element: HTMLElement; kind: string } | null {\n let current = element\n while (current && current !== this.container) {\n const elementKind = current.dataset.kind\n if (elementKind && kinds.includes(elementKind)) {\n return { element: current, kind: elementKind }\n }\n current = current.parentElement\n }\n return null\n }\n\n /**\n * Calculate distance between two points\n */\n private getDistance(p1: PointerPosition, p2: PointerPosition): number {\n return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))\n }\n\n /**\n * Get current drag state (read-only)\n */\n public getState(): Readonly<DragState<TItem, TPosition>> {\n return { ...this.state }\n }\n\n /**\n * Check if currently dragging\n */\n public isDragging(): boolean {\n return this.state.isDragging\n }\n\n /**\n * Cleanup and remove event listeners\n */\n public destroy(): void {\n this.cleanup()\n this.container.removeEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.removeEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n window.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n window.removeEventListener('keydown', this.boundHandlers.keyDown)\n window.removeEventListener('mouseout', this.boundHandlers.mouseOut)\n }\n}\n","import type { PointerPosition } from './drag-drop-manager'\n\nexport type DragPreviewControllerOptions = {\n zIndex?: number\n opacity?: number\n centerOnCursor?: boolean\n className?: string | string[]\n copyComputedStyles?: string[]\n appendTo?: HTMLElement\n}\n\nconst OFFSCREEN_TRANSFORM = 'translate(-9999px, -9999px)'\n\nfunction normalizeClassNames(className: string | string[] | undefined): string[] {\n if (!className) return []\n return Array.isArray(className) ? className : [className]\n}\n\nexport class DragPreviewController {\n private options: Required<\n Pick<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n > &\n Omit<DragPreviewControllerOptions, 'zIndex' | 'opacity' | 'centerOnCursor'>\n private previewElement: HTMLElement | null = null\n\n constructor(options: DragPreviewControllerOptions = {}) {\n this.options = {\n zIndex: options.zIndex ?? 9999,\n opacity: options.opacity ?? 0.95,\n centerOnCursor: options.centerOnCursor ?? true,\n className: options.className,\n copyComputedStyles: options.copyComputedStyles,\n appendTo: options.appendTo,\n }\n }\n\n public startFromElement(element: HTMLElement): void {\n this.stop()\n const rect = element.getBoundingClientRect()\n const clone = element.cloneNode(true) as HTMLElement\n const computed = window.getComputedStyle(element)\n const appendTo = this.options.appendTo ?? document.body\n\n clone.setAttribute('aria-hidden', 'true')\n clone.style.position = 'fixed'\n clone.style.left = '0'\n clone.style.top = '0'\n clone.style.width = `${rect.width}px`\n clone.style.height = `${rect.height}px`\n clone.style.pointerEvents = 'none'\n clone.style.zIndex = String(this.options.zIndex)\n clone.style.opacity = String(this.options.opacity)\n clone.style.transform = OFFSCREEN_TRANSFORM\n clone.style.transition = 'none'\n clone.style.cursor = 'grabbing'\n clone.style.margin = '0'\n clone.style.boxSizing = 'border-box'\n clone.style.background = computed.backgroundColor\n\n for (const classToken of normalizeClassNames(this.options.className)) {\n clone.classList.add(classToken)\n }\n\n if (this.options.copyComputedStyles && this.options.copyComputedStyles.length > 0) {\n for (const propertyName of this.options.copyComputedStyles) {\n clone.style.setProperty(propertyName, computed.getPropertyValue(propertyName))\n }\n }\n\n appendTo.appendChild(clone)\n this.previewElement = clone\n }\n\n public moveToPointer(pos: PointerPosition): void {\n if (!this.previewElement) return\n if (this.options.centerOnCursor) {\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px) translate(-50%, -50%)`\n return\n }\n this.previewElement.style.transform = `translate(${pos.x}px, ${pos.y}px)`\n }\n\n public stop(): void {\n this.previewElement?.remove()\n this.previewElement = null\n }\n\n public destroy(): void {\n this.stop()\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,41 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dnd-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"homepage": "https://itsjavi.com/dnd-manager",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "git+https://github.com/itsjavi/dnd-manager.git"
|
|
8
9
|
},
|
|
9
|
-
"
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
10
15
|
"type": "module",
|
|
16
|
+
"main": "./dist/index.mjs",
|
|
17
|
+
"module": "./dist/index.mjs",
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
11
19
|
"exports": {
|
|
12
20
|
".": {
|
|
13
21
|
"import": "./dist/index.mjs",
|
|
14
22
|
"require": "./dist/index.mjs"
|
|
15
23
|
}
|
|
16
24
|
},
|
|
17
|
-
"main": "./dist/index.mjs",
|
|
18
|
-
"module": "./dist/index.mjs",
|
|
19
|
-
"types": "./dist/index.d.mts",
|
|
20
|
-
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsdown src/index.ts --dts --sourcemap --format esm --clean",
|
|
27
|
-
"format": "pnpx sort-package-json && pnpm exec prettier --cache --write .",
|
|
28
|
-
"prepublishOnly": "pnpm typecheck && pnpm build && publint",
|
|
29
|
-
"test": "vitest --run",
|
|
30
|
-
"test:watch": "vitest --watch",
|
|
31
|
-
"typecheck": "tsc --noEmit"
|
|
32
|
-
},
|
|
33
25
|
"devDependencies": {
|
|
34
|
-
"
|
|
26
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
27
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
28
|
+
"jsdom": "^28.1.0",
|
|
29
|
+
"oxfmt": "^0.35.0",
|
|
35
30
|
"publint": "^0.3.17",
|
|
36
|
-
"sort-package-json": "^3.6.1",
|
|
37
31
|
"tsdown": "^0.20.3",
|
|
38
32
|
"typescript": "^5.9.3",
|
|
39
33
|
"vitest": "^4.0.18"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "pnpm --filter dnd-manager-demo dev",
|
|
37
|
+
"build": "tsdown src/index.ts --dts --sourcemap --format esm --clean",
|
|
38
|
+
"build:watch": "pnpm build --watch",
|
|
39
|
+
"build:demo": "pnpm --filter dnd-manager-demo build",
|
|
40
|
+
"build:all": "pnpm build && pnpm build:demo",
|
|
41
|
+
"format": "oxfmt --write .",
|
|
42
|
+
"format:check": "oxfmt --check .",
|
|
43
|
+
"test": "vitest --run",
|
|
44
|
+
"test:coverage": "pnpm build && vitest --run --coverage",
|
|
45
|
+
"test:e2e": "pnpm build && vitest --project e2e",
|
|
46
|
+
"test:unit": "vitest --project unit",
|
|
47
|
+
"test:watch": "vitest --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
40
49
|
}
|
|
41
|
-
}
|
|
50
|
+
}
|