dnd-manager 1.2.0 → 1.3.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/CHANGELOG.md +25 -0
- package/LICENSE +0 -0
- package/README.md +290 -768
- package/dist/index.d.mts +146 -12
- package/dist/index.mjs +468 -77
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -13
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,142 @@
|
|
|
1
|
+
//#region src/auto-scroll.ts
|
|
2
|
+
const SCROLLABLE_OVERFLOW = /^(auto|scroll|overlay)$/;
|
|
3
|
+
/** Whether `element` may scroll on an axis: its overflow allows it and its content overflows. */
|
|
4
|
+
function scrollsOn(element, style, axis) {
|
|
5
|
+
return axis === "y" ? SCROLLABLE_OVERFLOW.test(style.overflowY) && element.scrollHeight > element.clientHeight : SCROLLABLE_OVERFLOW.test(style.overflowX) && element.scrollWidth > element.clientWidth;
|
|
6
|
+
}
|
|
7
|
+
/** The step on one axis: toward the edge the pointer is near, if the scroller can still go there. */
|
|
8
|
+
function stepOn(pointer, start, end, zone, speed, canGoBack, canGoForward) {
|
|
9
|
+
if (pointer < start + zone && canGoBack) return -speed;
|
|
10
|
+
if (pointer > end - zone && canGoForward) return speed;
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scrolls one step toward the edge the pointer is near: the closest scroll container around
|
|
15
|
+
* `hit` (the element under the pointer) that can still scroll that way, or else the window.
|
|
16
|
+
* Returns whether anything scrolled, so the caller keeps going while the pointer rests there.
|
|
17
|
+
*/
|
|
18
|
+
function autoScrollStep(pos, hit, { threshold, speed }) {
|
|
19
|
+
if (speed <= 0 || threshold <= 0) return false;
|
|
20
|
+
for (let element = hit; element; element = element.parentElement) {
|
|
21
|
+
if (element === document.body || element === document.documentElement) break;
|
|
22
|
+
const style = getComputedStyle(element);
|
|
23
|
+
const scrollsX = scrollsOn(element, style, "x");
|
|
24
|
+
const scrollsY = scrollsOn(element, style, "y");
|
|
25
|
+
if (!scrollsX && !scrollsY) continue;
|
|
26
|
+
const rect = element.getBoundingClientRect();
|
|
27
|
+
const left = scrollsX ? stepOn(pos.x, rect.left, rect.right, Math.min(threshold, rect.width / 4), speed, element.scrollLeft > 0, element.scrollLeft + element.clientWidth < element.scrollWidth - 1) : 0;
|
|
28
|
+
const top = scrollsY ? stepOn(pos.y, rect.top, rect.bottom, Math.min(threshold, rect.height / 4), speed, element.scrollTop > 0, element.scrollTop + element.clientHeight < element.scrollHeight - 1) : 0;
|
|
29
|
+
if (left !== 0 || top !== 0) {
|
|
30
|
+
element.scrollBy({
|
|
31
|
+
left,
|
|
32
|
+
top,
|
|
33
|
+
behavior: "instant"
|
|
34
|
+
});
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const left = stepOn(pos.x, 0, window.innerWidth, threshold, speed, true, true);
|
|
39
|
+
const top = stepOn(pos.y, 0, window.innerHeight, threshold, speed, true, true);
|
|
40
|
+
if (left === 0 && top === 0) return false;
|
|
41
|
+
const before = [window.scrollX, window.scrollY];
|
|
42
|
+
window.scrollBy({
|
|
43
|
+
left,
|
|
44
|
+
top,
|
|
45
|
+
behavior: "instant"
|
|
46
|
+
});
|
|
47
|
+
return window.scrollX !== before[0] || window.scrollY !== before[1];
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
1
50
|
//#region src/drag-drop-manager.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Framework-agnostic Drag and Drop Manager
|
|
53
|
+
*
|
|
54
|
+
* A data-driven drag-and-drop utility that works with React and vanilla JS.
|
|
55
|
+
* Uses data attributes and direct DOM manipulation for optimal performance.
|
|
56
|
+
*
|
|
57
|
+
* @template TItem - The type of data associated with draggable items
|
|
58
|
+
* @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Presses a manager took: with nested containers the inner manager hears the `pointerdown` first
|
|
62
|
+
* and takes it, and the outer ones leave it alone.
|
|
63
|
+
*/
|
|
64
|
+
const claimedPresses = /* @__PURE__ */ new WeakSet();
|
|
65
|
+
/** The pointer events of a press that a detached source still receives (see handlePointerDown). */
|
|
66
|
+
const SOURCE_POINTER_EVENTS = [
|
|
67
|
+
"pointermove",
|
|
68
|
+
"pointerup",
|
|
69
|
+
"pointercancel"
|
|
70
|
+
];
|
|
71
|
+
/** Moves the boolean attribute `name` from `from` to `to`, and returns `to`. */
|
|
72
|
+
function moveMark(name, from, to) {
|
|
73
|
+
if (from === to) return to;
|
|
74
|
+
from?.removeAttribute(name);
|
|
75
|
+
to?.setAttribute(name, "true");
|
|
76
|
+
return to;
|
|
77
|
+
}
|
|
78
|
+
function idleState() {
|
|
79
|
+
return {
|
|
9
80
|
startPosition: null,
|
|
10
81
|
sourceElement: null,
|
|
11
82
|
sourcePosition: null,
|
|
12
83
|
sourceItem: null,
|
|
13
84
|
activePointerId: null,
|
|
85
|
+
input: null,
|
|
14
86
|
isDragging: false,
|
|
15
|
-
lastHoveredElement: null
|
|
87
|
+
lastHoveredElement: null,
|
|
88
|
+
lastInvalidElement: null,
|
|
89
|
+
lastDropResult: null
|
|
16
90
|
};
|
|
91
|
+
}
|
|
92
|
+
const centerOf = (rect) => ({
|
|
93
|
+
x: rect.left + rect.width / 2,
|
|
94
|
+
y: rect.top + rect.height / 2
|
|
95
|
+
});
|
|
96
|
+
const ARROWS = {
|
|
97
|
+
ArrowLeft: {
|
|
98
|
+
axis: "x",
|
|
99
|
+
sign: -1
|
|
100
|
+
},
|
|
101
|
+
ArrowRight: {
|
|
102
|
+
axis: "x",
|
|
103
|
+
sign: 1
|
|
104
|
+
},
|
|
105
|
+
ArrowUp: {
|
|
106
|
+
axis: "y",
|
|
107
|
+
sign: -1
|
|
108
|
+
},
|
|
109
|
+
ArrowDown: {
|
|
110
|
+
axis: "y",
|
|
111
|
+
sign: 1
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const isActionKey = (e) => (e.key === " " || e.key === "Enter") && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
|
|
115
|
+
var DragDropManager = class {
|
|
116
|
+
container;
|
|
117
|
+
config;
|
|
118
|
+
draggableKinds;
|
|
119
|
+
droppableKinds;
|
|
120
|
+
callbacks;
|
|
121
|
+
state = idleState();
|
|
17
122
|
rafId = null;
|
|
18
123
|
lastPointerPos = null;
|
|
124
|
+
/**
|
|
125
|
+
* Set while a drag holds the pointer down: the browser follows the release with a native
|
|
126
|
+
* `click` (on the source element under pointer capture), which must not activate it.
|
|
127
|
+
*/
|
|
128
|
+
suppressClickOnRelease = false;
|
|
129
|
+
releaseClickBlocker = null;
|
|
130
|
+
/** The pending `dragDelay` of the current press. */
|
|
131
|
+
delayTimer = null;
|
|
132
|
+
activePointerType = "";
|
|
133
|
+
/** The droppable a keyboard drag points at. */
|
|
134
|
+
keyboardTarget = null;
|
|
135
|
+
/**
|
|
136
|
+
* The key that picked up or dropped: its `keyup` would activate a focused button (Space clicks
|
|
137
|
+
* on keyup), so it is swallowed.
|
|
138
|
+
*/
|
|
139
|
+
swallowKeyUp = null;
|
|
19
140
|
boundHandlers;
|
|
20
141
|
constructor(containerRef, config, callbacks) {
|
|
21
142
|
if (typeof containerRef === "string") {
|
|
@@ -28,13 +149,18 @@ var DragDropManager = class {
|
|
|
28
149
|
} else this.container = containerRef;
|
|
29
150
|
this.config = {
|
|
30
151
|
dragThreshold: config.dragThreshold ?? 10,
|
|
152
|
+
dragDelay: config.dragDelay ?? 0,
|
|
153
|
+
dragDelayTolerance: config.dragDelayTolerance ?? 5,
|
|
31
154
|
clickThreshold: config.clickThreshold ?? 10,
|
|
32
155
|
scrollThreshold: config.scrollThreshold ?? 100,
|
|
33
156
|
scrollSpeed: config.scrollSpeed ?? 10,
|
|
34
157
|
cancelOnEscape: config.cancelOnEscape ?? true,
|
|
35
158
|
cancelOnPointerLeave: config.cancelOnPointerLeave ?? true,
|
|
36
159
|
draggableKind: config.draggableKind,
|
|
37
|
-
droppableKind: config.droppableKind
|
|
160
|
+
droppableKind: config.droppableKind,
|
|
161
|
+
dropScope: config.dropScope ?? "document",
|
|
162
|
+
keyboard: config.keyboard ?? false,
|
|
163
|
+
focusOnPress: config.focusOnPress ?? false
|
|
38
164
|
};
|
|
39
165
|
this.draggableKinds = this.normalizeKindList(this.config.draggableKind);
|
|
40
166
|
this.droppableKinds = this.normalizeKindList(this.config.droppableKind);
|
|
@@ -45,44 +171,87 @@ var DragDropManager = class {
|
|
|
45
171
|
pointerUp: this.handlePointerUp.bind(this),
|
|
46
172
|
pointerCancel: this.handlePointerCancel.bind(this),
|
|
47
173
|
keyDown: this.handleKeyDown.bind(this),
|
|
48
|
-
mouseOut: this.handleMouseOut.bind(this)
|
|
174
|
+
mouseOut: this.handleMouseOut.bind(this),
|
|
175
|
+
touchMove: this.handleTouchMove.bind(this),
|
|
176
|
+
detachedSourcePointer: this.handleDetachedSourcePointer.bind(this),
|
|
177
|
+
contextMenu: this.handleContextMenu.bind(this),
|
|
178
|
+
preventDefault: (e) => e.preventDefault(),
|
|
179
|
+
pickUpKey: this.handlePickUpKey.bind(this),
|
|
180
|
+
keyboardDragKey: this.handleKeyboardDragKey.bind(this),
|
|
181
|
+
keyUp: this.handleKeyUp.bind(this),
|
|
182
|
+
focusOut: this.handleFocusOut.bind(this)
|
|
49
183
|
};
|
|
50
184
|
this.attachListeners();
|
|
51
185
|
}
|
|
52
186
|
attachListeners() {
|
|
53
187
|
this.container.addEventListener("pointerdown", this.boundHandlers.pointerDown);
|
|
54
|
-
this.container.addEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
55
188
|
this.container.addEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
56
189
|
this.container.addEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
57
190
|
window.addEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
58
191
|
window.addEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
59
192
|
window.addEventListener("keydown", this.boundHandlers.keyDown);
|
|
60
193
|
window.addEventListener("mouseout", this.boundHandlers.mouseOut);
|
|
194
|
+
this.container.addEventListener("touchmove", this.boundHandlers.touchMove, { passive: false });
|
|
195
|
+
if (this.config.keyboard) {
|
|
196
|
+
this.container.addEventListener("keydown", this.boundHandlers.pickUpKey);
|
|
197
|
+
window.addEventListener("keydown", this.boundHandlers.keyboardDragKey, true);
|
|
198
|
+
window.addEventListener("keyup", this.boundHandlers.keyUp, true);
|
|
199
|
+
window.addEventListener("focusout", this.boundHandlers.focusOut, true);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The source position and item of the draggable `match`, if it may be dragged: it has a
|
|
204
|
+
* position, is not `data-empty`, `canDrag` allows it and it has item data.
|
|
205
|
+
*/
|
|
206
|
+
resolveSource(match) {
|
|
207
|
+
const position = this.callbacks.getItemPosition(match.element, match.kind);
|
|
208
|
+
if (!position) return null;
|
|
209
|
+
if (match.element.dataset.empty !== void 0) return null;
|
|
210
|
+
if (this.callbacks.canDrag && !this.callbacks.canDrag(match.element, position)) return null;
|
|
211
|
+
const item = this.callbacks.getItemData?.(match.element, position) ?? null;
|
|
212
|
+
if (!item) return null;
|
|
213
|
+
return {
|
|
214
|
+
position,
|
|
215
|
+
item
|
|
216
|
+
};
|
|
61
217
|
}
|
|
62
218
|
handlePointerDown(e) {
|
|
63
219
|
if (this.state.startPosition) return;
|
|
64
|
-
|
|
65
|
-
|
|
220
|
+
if (e.button !== 0 || e.pointerType === "touch" && !e.isPrimary) return;
|
|
221
|
+
if (claimedPresses.has(e)) return;
|
|
222
|
+
const draggableMatch = this.findElementByKinds(e.target, this.draggableKinds);
|
|
66
223
|
if (!draggableMatch) return;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
e.preventDefault();
|
|
224
|
+
const source = this.resolveSource(draggableMatch);
|
|
225
|
+
if (!source) return;
|
|
226
|
+
const draggableElement = draggableMatch.element;
|
|
227
|
+
claimedPresses.add(e);
|
|
228
|
+
if (!this.config.focusOnPress) e.preventDefault();
|
|
229
|
+
window.addEventListener("selectstart", this.boundHandlers.preventDefault, true);
|
|
230
|
+
window.addEventListener("dragstart", this.boundHandlers.preventDefault, true);
|
|
75
231
|
this.state.startPosition = {
|
|
76
232
|
x: e.clientX,
|
|
77
233
|
y: e.clientY
|
|
78
234
|
};
|
|
79
235
|
this.state.sourceElement = draggableElement;
|
|
80
|
-
this.state.sourcePosition = position;
|
|
81
|
-
this.state.sourceItem = item;
|
|
236
|
+
this.state.sourcePosition = source.position;
|
|
237
|
+
this.state.sourceItem = source.item;
|
|
238
|
+
this.state.input = "pointer";
|
|
82
239
|
this.state.activePointerId = this.getPointerId(e);
|
|
240
|
+
this.activePointerType = e.pointerType ?? "";
|
|
241
|
+
this.lastPointerPos = this.state.startPosition;
|
|
83
242
|
if (this.state.activePointerId !== null && typeof draggableElement.setPointerCapture === "function") try {
|
|
84
243
|
draggableElement.setPointerCapture(this.state.activePointerId);
|
|
85
244
|
} catch {}
|
|
245
|
+
window.addEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
246
|
+
draggableElement.addEventListener("touchmove", this.boundHandlers.touchMove, { passive: false });
|
|
247
|
+
for (const type of SOURCE_POINTER_EVENTS) draggableElement.addEventListener(type, this.boundHandlers.detachedSourcePointer);
|
|
248
|
+
window.addEventListener("contextmenu", this.boundHandlers.contextMenu);
|
|
249
|
+
const delay = this.delayFor(this.activePointerType);
|
|
250
|
+
if (delay > 0) this.delayTimer = setTimeout(() => {
|
|
251
|
+
this.delayTimer = null;
|
|
252
|
+
this.startDrag();
|
|
253
|
+
this.scheduleFrame();
|
|
254
|
+
}, delay);
|
|
86
255
|
}
|
|
87
256
|
handlePointerMove(e) {
|
|
88
257
|
if (!this.isActivePointer(e)) return;
|
|
@@ -91,36 +260,82 @@ var DragDropManager = class {
|
|
|
91
260
|
y: e.clientY
|
|
92
261
|
};
|
|
93
262
|
if (this.state.startPosition && !this.state.isDragging) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
263
|
+
const distance = this.getDistance(this.state.startPosition, currentPos);
|
|
264
|
+
if (this.delayTimer !== null) {
|
|
265
|
+
if (distance > this.config.dragDelayTolerance) this.cleanup();
|
|
266
|
+
else this.lastPointerPos = currentPos;
|
|
267
|
+
return;
|
|
100
268
|
}
|
|
269
|
+
if (distance >= this.config.dragThreshold) this.startDrag();
|
|
101
270
|
return;
|
|
102
271
|
}
|
|
103
272
|
if (!this.state.isDragging) return;
|
|
104
273
|
this.lastPointerPos = currentPos;
|
|
105
|
-
|
|
274
|
+
this.scheduleFrame();
|
|
275
|
+
}
|
|
276
|
+
startDrag() {
|
|
277
|
+
this.state.isDragging = true;
|
|
278
|
+
this.suppressClickOnRelease = this.state.input === "pointer";
|
|
279
|
+
if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {
|
|
280
|
+
this.state.sourceElement.setAttribute("data-dragging", "true");
|
|
281
|
+
this.callbacks.onDragStart?.(this.state.sourceElement, this.state.sourcePosition, this.state.sourceItem);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** The `dragDelay` for a pointer type (`''` from a constructed event counts as a mouse). */
|
|
285
|
+
delayFor(pointerType) {
|
|
286
|
+
const { dragDelay } = this.config;
|
|
287
|
+
if (typeof dragDelay === "number") return dragDelay;
|
|
288
|
+
return dragDelay[pointerType === "touch" || pointerType === "pen" ? pointerType : "mouse"] ?? 0;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Once a touch press is a drag, the finger moves the item, not the page: a browser that starts
|
|
292
|
+
* panning would cancel the pointer. Before that, touches stay the browser's.
|
|
293
|
+
*/
|
|
294
|
+
handleTouchMove(e) {
|
|
295
|
+
if (this.state.isDragging && e.cancelable) e.preventDefault();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The press's pointer events, heard on the source itself. While it is in the DOM they also reach
|
|
299
|
+
* the window's listeners, which handle them; once it is detached only these hear them.
|
|
300
|
+
*/
|
|
301
|
+
handleDetachedSourcePointer(e) {
|
|
302
|
+
if (this.state.sourceElement?.isConnected !== false) return;
|
|
303
|
+
if (e.type === "pointermove") this.handlePointerMove(e);
|
|
304
|
+
else if (e.type === "pointerup") this.handlePointerUp(e);
|
|
305
|
+
else this.handlePointerCancel();
|
|
306
|
+
}
|
|
307
|
+
/** A long press opens no context menu (Android, iOS) while it is a drag or about to be one. */
|
|
308
|
+
handleContextMenu(e) {
|
|
309
|
+
const pendingTouch = this.delayTimer !== null && this.activePointerType === "touch";
|
|
310
|
+
if (this.state.isDragging || pendingTouch) e.preventDefault();
|
|
311
|
+
}
|
|
312
|
+
scheduleFrame() {
|
|
313
|
+
if (this.rafId !== null) return;
|
|
314
|
+
this.rafId = requestAnimationFrame(() => {
|
|
106
315
|
this.rafId = null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement);
|
|
316
|
+
this.runFrame();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/** One drag frame: hover feedback, auto-scroll and `onDragMove` at the latest pointer position. */
|
|
320
|
+
runFrame() {
|
|
321
|
+
const pos = this.lastPointerPos;
|
|
322
|
+
if (!pos || !this.state.isDragging) return;
|
|
323
|
+
const elementUnderCursor = document.elementFromPoint(pos.x, pos.y);
|
|
324
|
+
this.markTarget(this.findDroppable(elementUnderCursor));
|
|
325
|
+
const scrolled = autoScrollStep(pos, elementUnderCursor, {
|
|
326
|
+
threshold: this.config.scrollThreshold,
|
|
327
|
+
speed: this.config.scrollSpeed
|
|
120
328
|
});
|
|
329
|
+
this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement);
|
|
330
|
+
if (scrolled) this.scheduleFrame();
|
|
121
331
|
}
|
|
122
332
|
handlePointerUp(e) {
|
|
333
|
+
if (this.state.input === "keyboard") return;
|
|
123
334
|
if (!this.isActivePointer(e)) return;
|
|
335
|
+
if (this.suppressClickOnRelease) {
|
|
336
|
+
this.suppressClickOnRelease = false;
|
|
337
|
+
this.blockNextClick();
|
|
338
|
+
}
|
|
124
339
|
if (this.state.startPosition && !this.state.isDragging) {
|
|
125
340
|
const currentPos = {
|
|
126
341
|
x: e.clientX,
|
|
@@ -135,15 +350,24 @@ var DragDropManager = class {
|
|
|
135
350
|
return;
|
|
136
351
|
}
|
|
137
352
|
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
138
|
-
const droppableMatch = this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
353
|
+
const droppableMatch = this.findDroppable(target);
|
|
354
|
+
if ((droppableMatch?.element ?? null) && droppableMatch) {
|
|
355
|
+
const targetPosition = this.acceptedPosition(droppableMatch);
|
|
356
|
+
if (targetPosition) {
|
|
357
|
+
const result = {
|
|
358
|
+
sourcePosition: this.state.sourcePosition,
|
|
359
|
+
targetPosition,
|
|
360
|
+
sourceItem: this.state.sourceItem
|
|
361
|
+
};
|
|
362
|
+
this.state.lastDropResult = result;
|
|
363
|
+
this.callbacks.onDrop?.(result.sourcePosition, result.targetPosition, result.sourceItem);
|
|
364
|
+
}
|
|
143
365
|
}
|
|
144
366
|
this.cleanup();
|
|
145
367
|
}
|
|
146
368
|
handlePointerCancel() {
|
|
369
|
+
if (this.state.input === "keyboard") return;
|
|
370
|
+
this.suppressClickOnRelease = false;
|
|
147
371
|
this.cleanup();
|
|
148
372
|
}
|
|
149
373
|
handleKeyDown(e) {
|
|
@@ -153,44 +377,162 @@ var DragDropManager = class {
|
|
|
153
377
|
this.cleanup();
|
|
154
378
|
}
|
|
155
379
|
handleMouseOut(e) {
|
|
156
|
-
if (!this.config.cancelOnPointerLeave) return;
|
|
157
|
-
if (!this.state.startPosition) return;
|
|
158
380
|
if (e.relatedTarget !== null) return;
|
|
381
|
+
this.suppressClickOnRelease = false;
|
|
382
|
+
if (!this.config.cancelOnPointerLeave) return;
|
|
383
|
+
if (this.state.input !== "pointer") return;
|
|
384
|
+
this.cleanup();
|
|
385
|
+
}
|
|
386
|
+
/** Space or Enter on a focused draggable picks it up (`keyboard: true`). */
|
|
387
|
+
handlePickUpKey(e) {
|
|
388
|
+
if (this.state.startPosition || e.repeat || !isActionKey(e) || claimedPresses.has(e)) return;
|
|
389
|
+
const match = this.findElementByKinds(e.target, this.draggableKinds);
|
|
390
|
+
if (!match) return;
|
|
391
|
+
const source = this.resolveSource(match);
|
|
392
|
+
if (!source) return;
|
|
393
|
+
claimedPresses.add(e);
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
this.swallowKeyUp = e.key;
|
|
396
|
+
this.state.startPosition = centerOf(match.element.getBoundingClientRect());
|
|
397
|
+
this.state.sourceElement = match.element;
|
|
398
|
+
this.state.sourcePosition = source.position;
|
|
399
|
+
this.state.sourceItem = source.item;
|
|
400
|
+
this.state.input = "keyboard";
|
|
401
|
+
this.startDrag();
|
|
402
|
+
this.pointKeyboardAt(this.findDroppable(match.element));
|
|
403
|
+
}
|
|
404
|
+
/** The keys of a keyboard drag, ahead of anything else on the page. */
|
|
405
|
+
handleKeyboardDragKey(e) {
|
|
406
|
+
if (this.state.input !== "keyboard") {
|
|
407
|
+
if (!e.repeat) this.swallowKeyUp = null;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const arrow = ARROWS[e.key];
|
|
411
|
+
if (arrow) this.pointKeyboardAt(this.nearestDroppable(arrow.axis, arrow.sign) ?? this.keyboardTarget);
|
|
412
|
+
else if (isActionKey(e)) {
|
|
413
|
+
this.swallowKeyUp = e.key;
|
|
414
|
+
if (!e.repeat) this.dropWithKeyboard();
|
|
415
|
+
} else if (e.key === "Escape") this.cleanup();
|
|
416
|
+
else return;
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
e.stopPropagation();
|
|
419
|
+
}
|
|
420
|
+
handleKeyUp(e) {
|
|
421
|
+
if (this.swallowKeyUp === null || e.key !== this.swallowKeyUp) return;
|
|
422
|
+
this.swallowKeyUp = null;
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
e.stopPropagation();
|
|
425
|
+
}
|
|
426
|
+
/** Focus leaving the source (Tab, a click elsewhere) cancels a keyboard drag. */
|
|
427
|
+
handleFocusOut(e) {
|
|
428
|
+
const source = this.state.sourceElement;
|
|
429
|
+
if (this.state.input !== "keyboard" || !source) return;
|
|
430
|
+
if (!source.contains(e.target)) return;
|
|
431
|
+
if (source.contains(e.relatedTarget)) return;
|
|
432
|
+
this.cleanup();
|
|
433
|
+
}
|
|
434
|
+
/** Makes `match` the keyboard drag's target: marked, scrolled into view, sent to `onDragMove`. */
|
|
435
|
+
pointKeyboardAt(match) {
|
|
436
|
+
this.keyboardTarget = match;
|
|
437
|
+
this.markTarget(match);
|
|
438
|
+
if (!match) return;
|
|
439
|
+
match.element.scrollIntoView?.({
|
|
440
|
+
block: "nearest",
|
|
441
|
+
inline: "nearest"
|
|
442
|
+
});
|
|
443
|
+
this.callbacks.onDragMove?.(centerOf(match.element.getBoundingClientRect()), this.state.lastHoveredElement);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The droppable nearest the current target (or the source) in a direction: its center lies that
|
|
447
|
+
* way, and it is closest along the axis, with distance across the axis counting double, so the
|
|
448
|
+
* next cell in the same row or column wins over a diagonal one.
|
|
449
|
+
*/
|
|
450
|
+
nearestDroppable(axis, sign) {
|
|
451
|
+
const from = this.keyboardTarget?.element ?? this.state.sourceElement;
|
|
452
|
+
if (!from) return null;
|
|
453
|
+
const origin = centerOf(from.getBoundingClientRect());
|
|
454
|
+
const root = this.config.dropScope === "container" ? this.container : document;
|
|
455
|
+
let best = null;
|
|
456
|
+
let bestScore = Infinity;
|
|
457
|
+
for (const element of root.querySelectorAll("[data-kind]")) {
|
|
458
|
+
const kind = element.dataset.kind;
|
|
459
|
+
if (!kind || element === from || !this.droppableKinds.includes(kind)) continue;
|
|
460
|
+
const rect = element.getBoundingClientRect();
|
|
461
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
462
|
+
const center = centerOf(rect);
|
|
463
|
+
const along = (axis === "x" ? center.x - origin.x : center.y - origin.y) * sign;
|
|
464
|
+
const across = Math.abs(axis === "x" ? center.y - origin.y : center.x - origin.x);
|
|
465
|
+
if (along < 1) continue;
|
|
466
|
+
const score = along + across * 2;
|
|
467
|
+
if (score < bestScore) {
|
|
468
|
+
best = {
|
|
469
|
+
element,
|
|
470
|
+
kind
|
|
471
|
+
};
|
|
472
|
+
bestScore = score;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return best;
|
|
476
|
+
}
|
|
477
|
+
/** Drops a keyboard drag on its target, unless there is none or `canDrop` rejects it. */
|
|
478
|
+
dropWithKeyboard() {
|
|
479
|
+
const target = this.keyboardTarget;
|
|
480
|
+
const targetPosition = target && this.acceptedPosition(target);
|
|
481
|
+
const { sourcePosition, sourceItem } = this.state;
|
|
482
|
+
if (!targetPosition || sourcePosition === null || sourceItem === null) return;
|
|
483
|
+
this.state.lastDropResult = {
|
|
484
|
+
sourcePosition,
|
|
485
|
+
targetPosition,
|
|
486
|
+
sourceItem
|
|
487
|
+
};
|
|
488
|
+
this.callbacks.onDrop?.(sourcePosition, targetPosition, sourceItem);
|
|
159
489
|
this.cleanup();
|
|
160
490
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Swallows the `click` the browser dispatches right after the release that ended a drag, in the
|
|
493
|
+
* same task; a timeout lets the blocker go when no click comes.
|
|
494
|
+
*/
|
|
495
|
+
blockNextClick() {
|
|
496
|
+
this.releaseClickBlocker?.();
|
|
497
|
+
const block = (event) => {
|
|
498
|
+
event.preventDefault();
|
|
499
|
+
event.stopImmediatePropagation();
|
|
500
|
+
release();
|
|
501
|
+
};
|
|
502
|
+
const release = () => {
|
|
503
|
+
window.removeEventListener("click", block, true);
|
|
504
|
+
clearTimeout(timer);
|
|
505
|
+
if (this.releaseClickBlocker === release) this.releaseClickBlocker = null;
|
|
506
|
+
};
|
|
507
|
+
window.addEventListener("click", block, true);
|
|
508
|
+
const timer = setTimeout(release, 0);
|
|
509
|
+
this.releaseClickBlocker = release;
|
|
172
510
|
}
|
|
173
511
|
cleanup() {
|
|
512
|
+
window.removeEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
513
|
+
this.state.sourceElement?.removeEventListener("touchmove", this.boundHandlers.touchMove);
|
|
514
|
+
for (const type of SOURCE_POINTER_EVENTS) this.state.sourceElement?.removeEventListener(type, this.boundHandlers.detachedSourcePointer);
|
|
515
|
+
window.removeEventListener("contextmenu", this.boundHandlers.contextMenu);
|
|
516
|
+
window.removeEventListener("selectstart", this.boundHandlers.preventDefault, true);
|
|
517
|
+
window.removeEventListener("dragstart", this.boundHandlers.preventDefault, true);
|
|
518
|
+
if (this.delayTimer !== null) {
|
|
519
|
+
clearTimeout(this.delayTimer);
|
|
520
|
+
this.delayTimer = null;
|
|
521
|
+
}
|
|
522
|
+
this.activePointerType = "";
|
|
174
523
|
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
524
|
this.state.sourceElement.releasePointerCapture(this.state.activePointerId);
|
|
176
525
|
} catch {}
|
|
177
526
|
if (this.state.sourceElement) this.state.sourceElement.removeAttribute("data-dragging");
|
|
178
|
-
|
|
527
|
+
this.markTarget(null);
|
|
179
528
|
if (this.rafId !== null) {
|
|
180
529
|
cancelAnimationFrame(this.rafId);
|
|
181
530
|
this.rafId = null;
|
|
182
531
|
}
|
|
183
|
-
if (this.state.isDragging) this.callbacks.onDragEnd?.();
|
|
184
|
-
this.state =
|
|
185
|
-
startPosition: null,
|
|
186
|
-
sourceElement: null,
|
|
187
|
-
sourcePosition: null,
|
|
188
|
-
sourceItem: null,
|
|
189
|
-
activePointerId: null,
|
|
190
|
-
isDragging: false,
|
|
191
|
-
lastHoveredElement: null
|
|
192
|
-
};
|
|
532
|
+
if (this.state.isDragging) this.callbacks.onDragEnd?.(this.state.lastDropResult);
|
|
533
|
+
this.state = idleState();
|
|
193
534
|
this.lastPointerPos = null;
|
|
535
|
+
this.keyboardTarget = null;
|
|
194
536
|
}
|
|
195
537
|
getPointerId(e) {
|
|
196
538
|
return typeof e.pointerId === "number" ? e.pointerId : null;
|
|
@@ -201,8 +543,31 @@ var DragDropManager = class {
|
|
|
201
543
|
return pointerId === null || pointerId === this.state.activePointerId;
|
|
202
544
|
}
|
|
203
545
|
normalizeKindList(kinds) {
|
|
204
|
-
|
|
205
|
-
|
|
546
|
+
return [...new Set((Array.isArray(kinds) ? kinds : [kinds]).map((value) => value.trim()).filter(Boolean))];
|
|
547
|
+
}
|
|
548
|
+
/** The target position of the droppable `match` if the current source may drop there. */
|
|
549
|
+
acceptedPosition(match) {
|
|
550
|
+
const { sourcePosition, sourceItem } = this.state;
|
|
551
|
+
const targetPosition = this.callbacks.getItemPosition(match.element, match.kind);
|
|
552
|
+
if (targetPosition === null || sourcePosition === null || sourceItem === null) return null;
|
|
553
|
+
if (this.callbacks.canDrop && !this.callbacks.canDrop(sourcePosition, targetPosition, sourceItem)) return null;
|
|
554
|
+
return targetPosition;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Marks the droppable under the pointer: `data-hovered` when it would receive a drop (also the
|
|
558
|
+
* source itself, for "drag over itself" styling), `data-drop-invalid` when `canDrop` rejects it.
|
|
559
|
+
* Without `canDrop` every droppable is marked hovered.
|
|
560
|
+
*/
|
|
561
|
+
markTarget(match) {
|
|
562
|
+
const accepted = match && (!this.callbacks.canDrop || this.acceptedPosition(match) !== null) ? match.element : null;
|
|
563
|
+
const rejected = match && !accepted ? match.element : null;
|
|
564
|
+
this.state.lastHoveredElement = moveMark("data-hovered", this.state.lastHoveredElement, accepted);
|
|
565
|
+
this.state.lastInvalidElement = moveMark("data-drop-invalid", this.state.lastInvalidElement, rejected);
|
|
566
|
+
}
|
|
567
|
+
/** The droppable around `element` (the one under the pointer), within `dropScope`. */
|
|
568
|
+
findDroppable(element) {
|
|
569
|
+
if (this.config.dropScope === "container" && !(element && this.container.contains(element))) return null;
|
|
570
|
+
return this.findElementByKinds(element, this.droppableKinds);
|
|
206
571
|
}
|
|
207
572
|
/**
|
|
208
573
|
* Find closest ancestor element with a `data-kind` matching any expected kind.
|
|
@@ -242,20 +607,43 @@ var DragDropManager = class {
|
|
|
242
607
|
*/
|
|
243
608
|
destroy() {
|
|
244
609
|
this.cleanup();
|
|
610
|
+
this.suppressClickOnRelease = false;
|
|
611
|
+
this.releaseClickBlocker?.();
|
|
245
612
|
this.container.removeEventListener("pointerdown", this.boundHandlers.pointerDown);
|
|
246
|
-
this.container.removeEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
247
613
|
this.container.removeEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
248
614
|
this.container.removeEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
615
|
+
this.container.removeEventListener("touchmove", this.boundHandlers.touchMove);
|
|
249
616
|
window.removeEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
250
617
|
window.removeEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
251
618
|
window.removeEventListener("keydown", this.boundHandlers.keyDown);
|
|
252
619
|
window.removeEventListener("mouseout", this.boundHandlers.mouseOut);
|
|
620
|
+
if (this.config.keyboard) {
|
|
621
|
+
this.container.removeEventListener("keydown", this.boundHandlers.pickUpKey);
|
|
622
|
+
window.removeEventListener("keydown", this.boundHandlers.keyboardDragKey, true);
|
|
623
|
+
window.removeEventListener("keyup", this.boundHandlers.keyUp, true);
|
|
624
|
+
window.removeEventListener("focusout", this.boundHandlers.focusOut, true);
|
|
625
|
+
}
|
|
253
626
|
}
|
|
254
627
|
};
|
|
255
|
-
|
|
256
628
|
//#endregion
|
|
257
629
|
//#region src/drag-preview-controller.ts
|
|
258
630
|
const OFFSCREEN_TRANSFORM = "translate(-9999px, -9999px)";
|
|
631
|
+
/**
|
|
632
|
+
* Attributes of the source (and its descendants) that belong to its place in the page or to the
|
|
633
|
+
* drag, not to its look: a clone keeping them would duplicate ids, join radio groups, count as a
|
|
634
|
+
* draggable or droppable, and match `[data-dragging]` placeholder styles.
|
|
635
|
+
*/
|
|
636
|
+
const SOURCE_ONLY_ATTRIBUTES = [
|
|
637
|
+
"id",
|
|
638
|
+
"name",
|
|
639
|
+
"data-kind",
|
|
640
|
+
"data-dragging",
|
|
641
|
+
"data-hovered",
|
|
642
|
+
"data-drop-invalid"
|
|
643
|
+
];
|
|
644
|
+
function stripSourceAttributes(clone) {
|
|
645
|
+
for (const element of [clone, ...clone.querySelectorAll("*")]) for (const name of SOURCE_ONLY_ATTRIBUTES) element.removeAttribute(name);
|
|
646
|
+
}
|
|
259
647
|
function normalizeClassNames(className) {
|
|
260
648
|
if (!className) return [];
|
|
261
649
|
return Array.isArray(className) ? className : [className];
|
|
@@ -279,7 +667,10 @@ var DragPreviewController = class {
|
|
|
279
667
|
const clone = element.cloneNode(true);
|
|
280
668
|
const computed = window.getComputedStyle(element);
|
|
281
669
|
const appendTo = this.options.appendTo ?? document.body;
|
|
670
|
+
stripSourceAttributes(clone);
|
|
671
|
+
clone.setAttribute("inert", "");
|
|
282
672
|
clone.setAttribute("aria-hidden", "true");
|
|
673
|
+
clone.setAttribute("data-drag-preview", "");
|
|
283
674
|
clone.style.position = "fixed";
|
|
284
675
|
clone.style.left = "0";
|
|
285
676
|
clone.style.top = "0";
|
|
@@ -315,7 +706,7 @@ var DragPreviewController = class {
|
|
|
315
706
|
this.stop();
|
|
316
707
|
}
|
|
317
708
|
};
|
|
318
|
-
|
|
319
709
|
//#endregion
|
|
320
710
|
export { DragDropManager, DragPreviewController };
|
|
711
|
+
|
|
321
712
|
//# sourceMappingURL=index.mjs.map
|