@mk-drag-and-drop/dom 0.2.0 → 0.4.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/README.md +88 -3
- package/dist/controller/create-drag-controller.d.ts +4 -0
- package/dist/controller/create-drag-controller.js +9 -0
- package/dist/runtime/drag-runtime-scope.d.ts +1 -0
- package/dist/runtime/drag-runtime-scope.js +1 -0
- package/dist/runtime/drag-runtime.d.ts +1 -0
- package/dist/runtime/drag-runtime.js +12 -0
- package/dist/runtime/lifecycle.d.ts +2 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ The root export includes:
|
|
|
36
36
|
- `createDropContainer`
|
|
37
37
|
- `createSortable`
|
|
38
38
|
- `createDragHandle`
|
|
39
|
+
- controller types: `DragController`, `DragControllerOptions`,
|
|
40
|
+
`DragControllerOverlayInput`, `DragControllerAnnouncements`
|
|
39
41
|
- lifecycle types: `DragStartEvent`, `DragUpdateEvent`, `DragEndEvent`,
|
|
40
42
|
`DropEvent`, `DragSource`, `DragEndResult`, `DragLifecycleCallbacks`,
|
|
41
43
|
`DragLifecycleHelpers`
|
|
@@ -94,6 +96,11 @@ The returned controller has:
|
|
|
94
96
|
|
|
95
97
|
- `remeasureDropTargets(input?)`: remeasures all targets, one target id, an
|
|
96
98
|
array of target ids, or `{ group }`
|
|
99
|
+
- `remeasureOverlay()`: remeasures the currently mounted drag overlay element;
|
|
100
|
+
it is a safe no-op when no overlay is mounted or no drag is active
|
|
101
|
+
- `recomputeActiveDrag()`: reruns active targeting and drag update from the
|
|
102
|
+
last pointer position; it is a safe no-op when no drag is active and does not
|
|
103
|
+
remeasure drop targets
|
|
97
104
|
|
|
98
105
|
The controller does not expose runtime teardown, disposal, update, or broad
|
|
99
106
|
overlay removal methods. Runtime objects are ordinary JavaScript objects; DOM
|
|
@@ -108,6 +115,14 @@ controller calls `dragOverlay` once more for the `"released"` phase and passes
|
|
|
108
115
|
`removeOverlay`; call that function when app-owned release animation is done.
|
|
109
116
|
Pointer movement does not call `dragOverlay`.
|
|
110
117
|
|
|
118
|
+
The overlay input includes:
|
|
119
|
+
|
|
120
|
+
- `dragState`: item id, group, source rect, start pointer, and current pointer.
|
|
121
|
+
- `phase`: `"dragging"` or `"released"`.
|
|
122
|
+
- `remeasureOverlay`: manually refreshes the cached overlay measurement.
|
|
123
|
+
- `removeOverlay`: present only for `"released"` overlays in manual release
|
|
124
|
+
mode; call it when app-owned release animation should remove the overlay.
|
|
125
|
+
|
|
111
126
|
`removeOverlay` is only present for released overlays in manual release mode.
|
|
112
127
|
Calling it more than once is safe. It removes the overlay host state only; it is
|
|
113
128
|
not a controller or runtime teardown API.
|
|
@@ -121,7 +136,31 @@ from the current pointer delta.
|
|
|
121
136
|
The controller measures the returned overlay element on mount/replacement. When
|
|
122
137
|
`ResizeObserver` is available, it also remeasures when that element changes
|
|
123
138
|
size. The runtime derives the current overlay rect from that cached measurement
|
|
124
|
-
plus pointer movement
|
|
139
|
+
plus modified pointer movement, and it does not measure overlay DOM on every
|
|
140
|
+
pointer move.
|
|
141
|
+
|
|
142
|
+
Manual `remeasureOverlay()` is for cases automatic observation cannot cover or
|
|
143
|
+
where the app knows visual geometry changed. CSS transforms can change visual
|
|
144
|
+
bounds without necessarily triggering `ResizeObserver`.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
controller.remeasureOverlay();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const controller = createDragController({
|
|
152
|
+
dragOverlay({ dragState, remeasureOverlay }) {
|
|
153
|
+
const element = document.createElement("div");
|
|
154
|
+
element.textContent = dragState.draggableId;
|
|
155
|
+
|
|
156
|
+
element.addEventListener("transitionend", () => {
|
|
157
|
+
remeasureOverlay();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return element;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
125
164
|
|
|
126
165
|
For dynamic overlay content, keep a reference to the element and update it from
|
|
127
166
|
lifecycle callbacks or subscriptions:
|
|
@@ -164,6 +203,7 @@ type DragUpdateEvent = {
|
|
|
164
203
|
draggableId: string;
|
|
165
204
|
source: DragSource;
|
|
166
205
|
pointerPosition: DragPoint;
|
|
206
|
+
overlayRect: DragRect | null;
|
|
167
207
|
activeDropTargetId: string | null;
|
|
168
208
|
previousDropTargetId: string | null;
|
|
169
209
|
};
|
|
@@ -181,6 +221,7 @@ type DragEndEvent = {
|
|
|
181
221
|
source: DragSource;
|
|
182
222
|
result: DragEndResult;
|
|
183
223
|
dropTargetId: string | null;
|
|
224
|
+
overlayRect: DragRect | null;
|
|
184
225
|
};
|
|
185
226
|
|
|
186
227
|
type DropEvent = {
|
|
@@ -200,6 +241,14 @@ type SortableDropPlacement = {
|
|
|
200
241
|
};
|
|
201
242
|
```
|
|
202
243
|
|
|
244
|
+
`DragUpdateEvent.overlayRect` is the current viewport-space rectangle for the
|
|
245
|
+
drag overlay. `DragEndEvent.overlayRect` is the final viewport-space rectangle
|
|
246
|
+
at the moment the drag ends. Both are `null` when no drag overlay is configured.
|
|
247
|
+
When an overlay is configured, the runtime uses cached overlay measurement plus
|
|
248
|
+
modified pointer movement. Before overlay content has been measured, it uses the
|
|
249
|
+
same translated source-rect fallback as the visual overlay. The drag update and
|
|
250
|
+
drag end paths do not read overlay DOM just to produce this field.
|
|
251
|
+
|
|
203
252
|
Helpers:
|
|
204
253
|
|
|
205
254
|
- `getDropTargetRect(dropTargetId)`
|
|
@@ -339,8 +388,14 @@ modifier state.
|
|
|
339
388
|
## Measurement
|
|
340
389
|
|
|
341
390
|
Targets are measured when registered and remeasured at drag start. The explicit
|
|
342
|
-
remeasurement entry point is `controller.remeasureDropTargets(input?)`;
|
|
343
|
-
when app-owned layout changes during a drag should affect targeting.
|
|
391
|
+
target remeasurement entry point is `controller.remeasureDropTargets(input?)`;
|
|
392
|
+
call it when app-owned layout changes during a drag should affect targeting.
|
|
393
|
+
|
|
394
|
+
Overlay measurement is separate from target measurement. Overlay content is
|
|
395
|
+
measured on mount/replacement and by `ResizeObserver` when available. Use
|
|
396
|
+
`controller.remeasureOverlay()` when the current overlay geometry should be
|
|
397
|
+
refreshed manually, such as after an app-owned transition or transform that
|
|
398
|
+
changes visual bounds without a normal size-observer notification.
|
|
344
399
|
|
|
345
400
|
`RemeasureDropTargetsInput` accepts:
|
|
346
401
|
|
|
@@ -356,6 +411,36 @@ order, so app-owned layout changes such as expanding tree rows or changing
|
|
|
356
411
|
grouped sections during a drag should call the public remeasure API when those
|
|
357
412
|
changes need to affect targeting.
|
|
358
413
|
|
|
414
|
+
## Active Drag Recompute
|
|
415
|
+
|
|
416
|
+
`controller.recomputeActiveDrag()` reruns the active drag update path from the
|
|
417
|
+
last stored raw pointer position. It reapplies modifiers, derives the overlay
|
|
418
|
+
rect from cached overlay measurement, recomputes the active drop target from
|
|
419
|
+
cached target measurements, updates the overlay host, and sends the normal drag
|
|
420
|
+
update notifications. This is the same update path used by pointer movement.
|
|
421
|
+
Calling it with no active drag is safe and does nothing.
|
|
422
|
+
|
|
423
|
+
Use it when something external changes what is under the pointer during an
|
|
424
|
+
active drag without producing a new pointer event.
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
controller.recomputeActiveDrag();
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Recompute does not remeasure drop targets. If target geometry or registrations
|
|
431
|
+
changed and those changes should affect targeting, remeasure first and then
|
|
432
|
+
recompute.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
controller.remeasureDropTargets({ group: "items" });
|
|
436
|
+
controller.recomputeActiveDrag();
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
// Example: after external scrolling changes what is under the pointer
|
|
441
|
+
controller.recomputeActiveDrag();
|
|
442
|
+
```
|
|
443
|
+
|
|
359
444
|
## Data Ownership
|
|
360
445
|
|
|
361
446
|
The DOM package never owns application data. Item ids, target ids, group ids,
|
|
@@ -13,10 +13,12 @@ export type DragControllerAnnouncements = {
|
|
|
13
13
|
export type DragControllerOverlayInput = {
|
|
14
14
|
dragState: DragState;
|
|
15
15
|
phase: "dragging";
|
|
16
|
+
remeasureOverlay: () => void;
|
|
16
17
|
removeOverlay?: never;
|
|
17
18
|
} | {
|
|
18
19
|
dragState: DragState;
|
|
19
20
|
phase: "released";
|
|
21
|
+
remeasureOverlay: () => void;
|
|
20
22
|
removeOverlay: () => void;
|
|
21
23
|
};
|
|
22
24
|
export type DragControllerOptions = {
|
|
@@ -32,5 +34,7 @@ export type DragControllerOptions = {
|
|
|
32
34
|
} & DragLifecycleCallbacks;
|
|
33
35
|
export type DragController = {
|
|
34
36
|
remeasureDropTargets: (input?: RemeasureDropTargetsInput) => void;
|
|
37
|
+
remeasureOverlay: () => void;
|
|
38
|
+
recomputeActiveDrag: () => void;
|
|
35
39
|
};
|
|
36
40
|
export declare function createDragController(options?: DragControllerOptions): DragController;
|
|
@@ -22,6 +22,10 @@ export function createDragController(options = {}) {
|
|
|
22
22
|
remeasureDropTargets: (input) => {
|
|
23
23
|
runtime.remeasureDropTargets(input);
|
|
24
24
|
},
|
|
25
|
+
remeasureOverlay,
|
|
26
|
+
recomputeActiveDrag: () => {
|
|
27
|
+
runtime.recomputeActiveDrag();
|
|
28
|
+
},
|
|
25
29
|
};
|
|
26
30
|
setControllerRuntime(controller, runtime);
|
|
27
31
|
return controller;
|
|
@@ -124,6 +128,9 @@ export function createDragController(options = {}) {
|
|
|
124
128
|
}
|
|
125
129
|
runtime.setOverlayRect(rectToDragRect(overlayElement.getBoundingClientRect()));
|
|
126
130
|
}
|
|
131
|
+
function remeasureOverlay() {
|
|
132
|
+
measureOverlayElement();
|
|
133
|
+
}
|
|
127
134
|
function disconnectOverlayResizeObserver() {
|
|
128
135
|
overlayResizeObserver?.disconnect();
|
|
129
136
|
overlayResizeObserver = null;
|
|
@@ -149,12 +156,14 @@ export function createDragController(options = {}) {
|
|
|
149
156
|
return {
|
|
150
157
|
dragState: overlayState.dragState,
|
|
151
158
|
phase: "released",
|
|
159
|
+
remeasureOverlay,
|
|
152
160
|
removeOverlay: clearOverlayHost,
|
|
153
161
|
};
|
|
154
162
|
}
|
|
155
163
|
return {
|
|
156
164
|
dragState: overlayState.dragState,
|
|
157
165
|
phase: "dragging",
|
|
166
|
+
remeasureOverlay,
|
|
158
167
|
};
|
|
159
168
|
}
|
|
160
169
|
function syncLiveRegion() {
|
|
@@ -11,6 +11,7 @@ export type DragRuntimeScope = DomDraggableRuntime & DomDroppableRuntime & DomDr
|
|
|
11
11
|
configure: (input: DragRuntimeScopeConfigureInput) => void;
|
|
12
12
|
cancelDrag: () => void;
|
|
13
13
|
releaseActiveDragResources: () => void;
|
|
14
|
+
recomputeActiveDrag: () => void;
|
|
14
15
|
setOverlayRect: (overlayRect: DragRect | null) => void;
|
|
15
16
|
};
|
|
16
17
|
export type InternalStaleDomBindingRuntime = {
|
|
@@ -5,6 +5,7 @@ export function createDragRuntimeScope(options) {
|
|
|
5
5
|
configure: (input) => runtime.configure(input),
|
|
6
6
|
cancelDrag: () => runtime.cancelDrag(),
|
|
7
7
|
releaseActiveDragResources: () => runtime.releaseActiveDragResources(),
|
|
8
|
+
recomputeActiveDrag: () => runtime.recomputeActiveDrag(),
|
|
8
9
|
setOverlayRect: (overlayRect) => runtime.setOverlayRect(overlayRect),
|
|
9
10
|
requestDragStart: (input) => runtime.requestDragStart(input),
|
|
10
11
|
isKeyboardDragEnabled: () => runtime.isKeyboardDragEnabled(),
|
|
@@ -43,6 +43,7 @@ export declare class DragRuntime {
|
|
|
43
43
|
requestKeyboardDragStart(input: RequestKeyboardDragStartInput): void;
|
|
44
44
|
moveKeyboardDrag(direction: KeyboardMoveDirection): void;
|
|
45
45
|
updatePointer(rawPointerPosition: Point): void;
|
|
46
|
+
recomputeActiveDrag(): void;
|
|
46
47
|
private updatePointerNow;
|
|
47
48
|
private updatePointerNowOrRelease;
|
|
48
49
|
private finishDragAfterQueuedPointerUpdate;
|
|
@@ -180,6 +180,13 @@ export class DragRuntime {
|
|
|
180
180
|
}
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
|
+
recomputeActiveDrag() {
|
|
184
|
+
const session = this.getDraggingSession();
|
|
185
|
+
if (!session) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.updatePointerNowOrRelease(session.rawPointerPosition);
|
|
189
|
+
}
|
|
183
190
|
updatePointerNow(rawPointerPosition) {
|
|
184
191
|
const session = this.getDraggingSession();
|
|
185
192
|
if (!session) {
|
|
@@ -221,6 +228,7 @@ export class DragRuntime {
|
|
|
221
228
|
draggableId: nextSession.draggableId,
|
|
222
229
|
source: nextSession.source,
|
|
223
230
|
pointerPosition,
|
|
231
|
+
overlayRect,
|
|
224
232
|
activeDropTargetId: nextSession.activeDropTargetId,
|
|
225
233
|
previousDropTargetId,
|
|
226
234
|
};
|
|
@@ -499,6 +507,9 @@ export class DragRuntime {
|
|
|
499
507
|
? "dropped"
|
|
500
508
|
: "invalid-target"
|
|
501
509
|
: null;
|
|
510
|
+
const overlayRect = session && this.hasDragOverlay
|
|
511
|
+
? this.getCurrentDragRectAt(session.pointerPosition)
|
|
512
|
+
: null;
|
|
502
513
|
const sortablePlacement = session && dropTargetId
|
|
503
514
|
? this.getDropEventSortablePlacement(session, dropTargetId)
|
|
504
515
|
: undefined;
|
|
@@ -525,6 +536,7 @@ export class DragRuntime {
|
|
|
525
536
|
draggableId: session.draggableId,
|
|
526
537
|
source: session.source,
|
|
527
538
|
result,
|
|
539
|
+
overlayRect,
|
|
528
540
|
dropTargetId,
|
|
529
541
|
});
|
|
530
542
|
if (result === "dropped" && dropTargetId) {
|
|
@@ -12,6 +12,7 @@ export type DragUpdateEvent = {
|
|
|
12
12
|
draggableId: string;
|
|
13
13
|
source: DragSource;
|
|
14
14
|
pointerPosition: DragPoint;
|
|
15
|
+
overlayRect: DragRect | null;
|
|
15
16
|
activeDropTargetId: string | null;
|
|
16
17
|
previousDropTargetId: string | null;
|
|
17
18
|
};
|
|
@@ -19,6 +20,7 @@ export type DragEndEvent = {
|
|
|
19
20
|
draggableId: string;
|
|
20
21
|
source: DragSource;
|
|
21
22
|
result: DragEndResult;
|
|
23
|
+
overlayRect: DragRect | null;
|
|
22
24
|
dropTargetId: string | null;
|
|
23
25
|
};
|
|
24
26
|
export type DropEvent = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mk-drag-and-drop/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Headless DOM drag-and-drop primitives for draggable, droppable, sortable, and grouped interactions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mkramer.dev",
|
|
@@ -41,13 +41,14 @@
|
|
|
41
41
|
"files": [
|
|
42
42
|
"dist"
|
|
43
43
|
],
|
|
44
|
+
"sideEffects": false,
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"eslint": "^8.57.0",
|
|
46
47
|
"jsdom": "^24.1.3",
|
|
47
48
|
"typescript": "5.5.4",
|
|
48
49
|
"vitest": "^2.1.9",
|
|
49
|
-
"@repo/
|
|
50
|
-
"@repo/
|
|
50
|
+
"@repo/typescript-config": "0.0.0",
|
|
51
|
+
"@repo/eslint-config": "0.0.0"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|