@mapples/dnd 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/DnDRootView.d.ts +1 -1
- package/dist/components/DnDRootView.d.ts.map +1 -1
- package/dist/components/DnDRootView.js +86 -13
- package/dist/components/DnDRootView.js.map +1 -1
- package/dist/components/DropArea.d.ts.map +1 -1
- package/dist/components/DropArea.js +6 -3
- package/dist/components/DropArea.js.map +1 -1
- package/dist/context/DropContext.d.ts +8 -0
- package/dist/context/DropContext.d.ts.map +1 -1
- package/dist/context/DropContext.js +8 -0
- package/dist/context/DropContext.js.map +1 -1
- package/dist/context/FrameSpaceContext.d.ts +27 -0
- package/dist/context/FrameSpaceContext.d.ts.map +1 -0
- package/dist/context/FrameSpaceContext.js +15 -0
- package/dist/context/FrameSpaceContext.js.map +1 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -1
- package/dist/hooks/useDrag.d.ts.map +1 -1
- package/dist/hooks/useDrag.js +12 -0
- package/dist/hooks/useDrag.js.map +1 -1
- package/dist/hooks/useDrop.d.ts.map +1 -1
- package/dist/hooks/useDrop.js +74 -9
- package/dist/hooks/useDrop.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/monitor/Monitor.d.ts +107 -22
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +378 -111
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/types/index.d.ts +86 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
package/dist/monitor/Monitor.js
CHANGED
|
@@ -22,8 +22,25 @@ export class Monitor {
|
|
|
22
22
|
dragElements = new Map();
|
|
23
23
|
/** Tree structure of registered drop elements */
|
|
24
24
|
dropElementsTree = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Flat index of every registered drop element and its parent id. Kept in
|
|
27
|
+
* lockstep with the tree by register/unregister/update — the only writers —
|
|
28
|
+
* so id lookups and frame-key resolution are O(1)/O(depth) instead of a
|
|
29
|
+
* full recursive tree walk per layout event.
|
|
30
|
+
*/
|
|
31
|
+
dropNodesById = new Map();
|
|
32
|
+
dropParentById = new Map();
|
|
33
|
+
/** Pending coalesced force-layout broadcast (one per animation frame). */
|
|
34
|
+
forceLayoutUpdatePending = false;
|
|
35
|
+
/** Registered frame coordinate spaces by key */
|
|
36
|
+
frames = new Map();
|
|
25
37
|
/** ID of the currently active drag target */
|
|
26
38
|
dragTargetId = null;
|
|
39
|
+
/** Active move-drag source (an existing canvas element being moved) */
|
|
40
|
+
moveSource = null;
|
|
41
|
+
/** Pointer coords at drag start / most recent drag tick (drop space) */
|
|
42
|
+
dragStartCoords = null;
|
|
43
|
+
lastDragCoords = null;
|
|
27
44
|
/** Currently active drop target */
|
|
28
45
|
dropTarget = null;
|
|
29
46
|
/** Index position within the current drop target */
|
|
@@ -40,6 +57,42 @@ export class Monitor {
|
|
|
40
57
|
this.trackOutside = true;
|
|
41
58
|
}
|
|
42
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Registers (or replaces) a frame coordinate space. Drop elements whose
|
|
62
|
+
* config carries the same `frameKey` are stored and hit-tested in
|
|
63
|
+
* coordinates relative to this frame's origin.
|
|
64
|
+
*/
|
|
65
|
+
registerFrame(frameKey, entry) {
|
|
66
|
+
this.frames.set(frameKey, entry);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Moves a frame's world origin. O(1): no element re-measures needed since
|
|
70
|
+
* frame content layouts are origin-relative.
|
|
71
|
+
*/
|
|
72
|
+
updateFrameOrigin(frameKey, origin) {
|
|
73
|
+
const frame = this.frames.get(frameKey);
|
|
74
|
+
if (!frame)
|
|
75
|
+
return;
|
|
76
|
+
this.frames.set(frameKey, { ...frame, origin });
|
|
77
|
+
}
|
|
78
|
+
/** Removes a frame coordinate space. */
|
|
79
|
+
unregisterFrame(frameKey) {
|
|
80
|
+
this.frames.delete(frameKey);
|
|
81
|
+
}
|
|
82
|
+
/** Gets a registered frame space by key. */
|
|
83
|
+
getFrame(frameKey) {
|
|
84
|
+
return this.frames.get(frameKey);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Converts world coordinates into a frame's local space.
|
|
88
|
+
* Falls through unchanged when the frame isn't registered.
|
|
89
|
+
*/
|
|
90
|
+
toFrameLocal(coords, frameKey) {
|
|
91
|
+
const frame = this.frames.get(frameKey);
|
|
92
|
+
if (!frame)
|
|
93
|
+
return coords;
|
|
94
|
+
return { x: coords.x - frame.origin.x, y: coords.y - frame.origin.y };
|
|
95
|
+
}
|
|
43
96
|
/**
|
|
44
97
|
* Gets the ID of the currently active drag target.
|
|
45
98
|
* @returns The drag target ID or null if no drag is active
|
|
@@ -56,11 +109,20 @@ export class Monitor {
|
|
|
56
109
|
}
|
|
57
110
|
/**
|
|
58
111
|
* Triggers a force layout update event.
|
|
59
|
-
* This causes all registered elements to recalculate their layout
|
|
112
|
+
* This causes all registered elements to recalculate their layout
|
|
113
|
+
* information. Every registered element answers with a forced layout read,
|
|
114
|
+
* so calls are coalesced to at most one broadcast per animation frame —
|
|
115
|
+
* scroll handlers and settle timers may call this at input rate.
|
|
60
116
|
*/
|
|
61
117
|
triggerForceLayoutUpdate() {
|
|
62
|
-
this.
|
|
63
|
-
|
|
118
|
+
if (this.forceLayoutUpdatePending)
|
|
119
|
+
return;
|
|
120
|
+
this.forceLayoutUpdatePending = true;
|
|
121
|
+
requestAnimationFrame(() => {
|
|
122
|
+
this.forceLayoutUpdatePending = false;
|
|
123
|
+
this.events.next({
|
|
124
|
+
type: DnDEventType.LayoutForceUpdate,
|
|
125
|
+
});
|
|
64
126
|
});
|
|
65
127
|
}
|
|
66
128
|
/**
|
|
@@ -144,18 +206,63 @@ export class Monitor {
|
|
|
144
206
|
}
|
|
145
207
|
/**
|
|
146
208
|
* Handles the start of a drag operation.
|
|
147
|
-
* @param dragCoords -
|
|
209
|
+
* @param dragCoords - Coordinates in drop-target space (world space when a
|
|
210
|
+
* pointer transform is installed), or `null` when the press began outside
|
|
211
|
+
* that space entirely. A palette press is exactly that case — the palette
|
|
212
|
+
* lives beside the canvas, not on it — so a null here must never abort the
|
|
213
|
+
* drag; it only rules out grabbing an existing element to move.
|
|
214
|
+
* @param windowCoords - Raw window coordinates for drag-*source* hit
|
|
215
|
+
* testing (palette items live outside any frame); defaults to `dragCoords`
|
|
148
216
|
*/
|
|
149
|
-
onDragStart(dragCoords) {
|
|
150
|
-
|
|
151
|
-
|
|
217
|
+
onDragStart(dragCoords, windowCoords) {
|
|
218
|
+
this.dragStartCoords = dragCoords;
|
|
219
|
+
this.lastDragCoords = dragCoords;
|
|
220
|
+
const sourceCoords = windowCoords ?? dragCoords;
|
|
221
|
+
if (!sourceCoords)
|
|
222
|
+
return;
|
|
223
|
+
const dragTargetId = this.getDragTarget(sourceCoords);
|
|
224
|
+
if (dragTargetId) {
|
|
225
|
+
const dragTarget = this.dragElements.get(dragTargetId);
|
|
226
|
+
this.setDragTargetId(dragTargetId);
|
|
227
|
+
this.events.next({
|
|
228
|
+
type: DnDEventType.DragStart,
|
|
229
|
+
target: this.getDragTargetId(),
|
|
230
|
+
data: dragTarget?.item.data,
|
|
231
|
+
kind: 'palette',
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// No palette source under the pointer — a drag that starts on an
|
|
236
|
+
// existing canvas element moves it (Figma-style direct manipulation).
|
|
237
|
+
// A press outside the drop space can't be on such an element.
|
|
238
|
+
if (!dragCoords)
|
|
239
|
+
return;
|
|
240
|
+
const dropTargets = this.getDropTargets(dragCoords, this.dropElementsTree);
|
|
241
|
+
const shallowTarget = this.getShallowTarget(dropTargets, dragCoords);
|
|
242
|
+
const componentId = shallowTarget?.data
|
|
243
|
+
?.id;
|
|
244
|
+
if (!shallowTarget || !componentId)
|
|
245
|
+
return;
|
|
246
|
+
// Root-level elements (page roots) are not movable.
|
|
247
|
+
if (this.dropElementsTree.has(shallowTarget.id))
|
|
152
248
|
return;
|
|
153
|
-
const
|
|
154
|
-
this.
|
|
249
|
+
const hit = shallowTarget.hitCoords ?? dragCoords;
|
|
250
|
+
this.moveSource = {
|
|
251
|
+
dropId: shallowTarget.id,
|
|
252
|
+
componentId,
|
|
253
|
+
frameKey: shallowTarget.frameKey ?? null,
|
|
254
|
+
grabOffset: {
|
|
255
|
+
x: hit.x - shallowTarget.layout.x,
|
|
256
|
+
y: hit.y - shallowTarget.layout.y,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
155
259
|
this.events.next({
|
|
156
260
|
type: DnDEventType.DragStart,
|
|
157
|
-
|
|
158
|
-
data:
|
|
261
|
+
dropTarget: shallowTarget,
|
|
262
|
+
data: shallowTarget.data,
|
|
263
|
+
kind: 'move',
|
|
264
|
+
moveSource: this.moveSource,
|
|
265
|
+
frameKey: shallowTarget.frameKey ?? null,
|
|
159
266
|
});
|
|
160
267
|
}
|
|
161
268
|
/**
|
|
@@ -164,21 +271,124 @@ export class Monitor {
|
|
|
164
271
|
*/
|
|
165
272
|
onDrag(dragCoords) {
|
|
166
273
|
const dragTargetId = this.getDragTargetId();
|
|
167
|
-
if (!dragTargetId)
|
|
274
|
+
if (!dragTargetId && !this.moveSource)
|
|
168
275
|
return;
|
|
169
|
-
|
|
276
|
+
this.lastDragCoords = dragCoords;
|
|
277
|
+
// While moving an element, its own subtree is not a valid drop target.
|
|
278
|
+
const dropTargets = this.getDropTargets(dragCoords, this.dropElementsTree, null, this.moveSource?.dropId ?? null);
|
|
170
279
|
const shallowTarget = this.getShallowTarget(dropTargets, dragCoords);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
280
|
+
// Leaving every droppable area clears the pending drop rather than
|
|
281
|
+
// keeping the last one: the pointer is the only thing that decides where
|
|
282
|
+
// a drop lands, so hovering out must be indistinguishable from never
|
|
283
|
+
// having hovered in. The Drag event still fires (with a null target) so
|
|
284
|
+
// overlays drop their highlight instead of stranding it on the element
|
|
285
|
+
// the pointer last passed over.
|
|
286
|
+
const target = shallowTarget?.canDrop ? shallowTarget : null;
|
|
287
|
+
this.setDropTarget(target);
|
|
288
|
+
this.setDropTargetIndex(target
|
|
289
|
+
? this.getDropTargetIndexData(target.hitCoords ?? dragCoords, target)
|
|
290
|
+
: null);
|
|
176
291
|
this.events.next({
|
|
177
292
|
type: DnDEventType.Drag,
|
|
178
293
|
target: dragTargetId,
|
|
179
294
|
dropTarget: this.getDropTarget(),
|
|
180
295
|
dropTargetIndex: this.getDropTargetIndex(),
|
|
181
296
|
dragCoords: dragCoords,
|
|
297
|
+
frameKey: target?.frameKey ?? null,
|
|
298
|
+
kind: this.moveSource ? 'move' : 'palette',
|
|
299
|
+
moveSource: this.moveSource,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The pointer left the drop coordinate space entirely — reported by the
|
|
304
|
+
* host when its `pointerTransform` cannot place the pointer (e.g. it moved
|
|
305
|
+
* off the canvas viewport onto surrounding chrome). Equivalent to hovering
|
|
306
|
+
* empty space: the pending drop clears, so a release here cancels.
|
|
307
|
+
*/
|
|
308
|
+
onDragOutside() {
|
|
309
|
+
const dragTargetId = this.getDragTargetId();
|
|
310
|
+
if (!dragTargetId && !this.moveSource)
|
|
311
|
+
return;
|
|
312
|
+
// Already cleared — don't republish on every pointer move outside.
|
|
313
|
+
if (!this.getDropTarget() && this.getDropTargetIndex() === null)
|
|
314
|
+
return;
|
|
315
|
+
this.setDropTarget(null);
|
|
316
|
+
this.setDropTargetIndex(null);
|
|
317
|
+
this.events.next({
|
|
318
|
+
type: DnDEventType.Drag,
|
|
319
|
+
target: dragTargetId,
|
|
320
|
+
dropTarget: null,
|
|
321
|
+
dropTargetIndex: null,
|
|
322
|
+
dragCoords: this.lastDragCoords,
|
|
323
|
+
frameKey: null,
|
|
324
|
+
kind: this.moveSource ? 'move' : 'palette',
|
|
325
|
+
moveSource: this.moveSource,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Hit-tests a point in drop space with no drag registered. Drags that start
|
|
330
|
+
* outside the app — files dragged in from the OS — never register a drag
|
|
331
|
+
* element, so they cannot go through {@link onDrag}; this gives them the
|
|
332
|
+
* same target and insert-index resolution the in-app drag path uses.
|
|
333
|
+
* @param dropCoords - The coordinates to hit-test, in drop space
|
|
334
|
+
*/
|
|
335
|
+
hitTest(dropCoords) {
|
|
336
|
+
const dropTargets = this.getDropTargets(dropCoords, this.dropElementsTree);
|
|
337
|
+
const shallowTarget = this.getShallowTarget(dropTargets, dropCoords);
|
|
338
|
+
const dropTarget = shallowTarget?.canDrop ? shallowTarget : null;
|
|
339
|
+
return {
|
|
340
|
+
dropTarget,
|
|
341
|
+
dropTargetIndex: dropTarget
|
|
342
|
+
? this.getDropTargetIndexData(dropTarget.hitCoords ?? dropCoords, dropTarget)
|
|
343
|
+
: null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Publishes an external drag's current position, so the drop chrome
|
|
348
|
+
* (hover highlight, insert line) tracks a file dragged in from the OS
|
|
349
|
+
* exactly as it tracks an in-app drag. Pass `null` coordinates while the
|
|
350
|
+
* pointer sits outside the drop space — that clears the pending drop the
|
|
351
|
+
* same way hovering off a droppable area does mid-drag.
|
|
352
|
+
* @param dropCoords - The pointer position in drop space, or null
|
|
353
|
+
*/
|
|
354
|
+
onExternalDrag(dropCoords) {
|
|
355
|
+
const { dropTarget, dropTargetIndex } = dropCoords
|
|
356
|
+
? this.hitTest(dropCoords)
|
|
357
|
+
: { dropTarget: null, dropTargetIndex: null };
|
|
358
|
+
// Nothing to say when the drag is still over empty space it was already
|
|
359
|
+
// over — pointer moves arrive at input rate.
|
|
360
|
+
if (!dropTarget && !this.getDropTarget())
|
|
361
|
+
return;
|
|
362
|
+
this.lastDragCoords = dropCoords;
|
|
363
|
+
this.setDropTarget(dropTarget);
|
|
364
|
+
this.setDropTargetIndex(dropTargetIndex);
|
|
365
|
+
this.events.next({
|
|
366
|
+
type: DnDEventType.Drag,
|
|
367
|
+
target: null,
|
|
368
|
+
dropTarget,
|
|
369
|
+
dropTargetIndex,
|
|
370
|
+
dragCoords: dropCoords,
|
|
371
|
+
frameKey: dropTarget?.frameKey ?? null,
|
|
372
|
+
kind: 'external',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Ends an external drag — the files were dropped, or the drag left the
|
|
377
|
+
* window. Always `cancelled`: whatever the drop produces is inserted by
|
|
378
|
+
* the host once its uploads finish, never by the DnD path.
|
|
379
|
+
*/
|
|
380
|
+
onExternalDragEnd() {
|
|
381
|
+
this.lastDragCoords = null;
|
|
382
|
+
this.setDropTarget(null);
|
|
383
|
+
this.setDropTargetIndex(null);
|
|
384
|
+
this.events.next({
|
|
385
|
+
type: DnDEventType.DragEnd,
|
|
386
|
+
target: null,
|
|
387
|
+
dropTarget: null,
|
|
388
|
+
dropTargetIndex: null,
|
|
389
|
+
frameKey: null,
|
|
390
|
+
kind: 'external',
|
|
391
|
+
cancelled: true,
|
|
182
392
|
});
|
|
183
393
|
}
|
|
184
394
|
/**
|
|
@@ -186,30 +396,45 @@ export class Monitor {
|
|
|
186
396
|
* @param dragCoords - The coordinates where the selection occurred
|
|
187
397
|
*/
|
|
188
398
|
onSelect(dragCoords) {
|
|
189
|
-
if (!this.
|
|
399
|
+
if (!this.isInsideAnyRootElement(dragCoords) && !this.trackOutside)
|
|
190
400
|
return;
|
|
191
401
|
const dropTargets = this.getDropTargets(dragCoords, this.dropElementsTree);
|
|
192
402
|
const shallowTarget = this.getShallowTarget(dropTargets, dragCoords);
|
|
193
403
|
this.events.next({
|
|
194
404
|
type: DnDEventType.Select,
|
|
195
405
|
dropTarget: shallowTarget,
|
|
406
|
+
frameKey: shallowTarget?.frameKey ?? null,
|
|
196
407
|
});
|
|
197
408
|
}
|
|
198
409
|
/**
|
|
199
|
-
* Checks if the given coordinates are inside
|
|
200
|
-
*
|
|
201
|
-
* @returns True if inside the first root element
|
|
410
|
+
* Checks if the given coordinates are inside any root drop element,
|
|
411
|
+
* converting into frame-local space for frame-rooted elements.
|
|
202
412
|
* @private
|
|
203
413
|
*/
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
414
|
+
isInsideAnyRootElement(dragCoords) {
|
|
415
|
+
for (const [, target] of this.dropElementsTree) {
|
|
416
|
+
const coords = target.frameKey
|
|
417
|
+
? this.toFrameLocal(dragCoords, target.frameKey)
|
|
418
|
+
: dragCoords;
|
|
419
|
+
if (target.layout.x <= coords.x &&
|
|
420
|
+
target.layout.y <= coords.y &&
|
|
421
|
+
target.layout.x + target.layout.width >= coords.x &&
|
|
422
|
+
target.layout.y + target.layout.height >= coords.y) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Looks up a registered drop target (with its current layout and frame
|
|
430
|
+
* key) by matching data properties — e.g. `{ id: componentId }`.
|
|
431
|
+
*
|
|
432
|
+
* @param frameKey - Restrict the search to one frame's subtree. Required
|
|
433
|
+
* whenever ids are only unique per frame (every page root shares the id
|
|
434
|
+
* `root`, so an unscoped lookup can return another frame's element).
|
|
435
|
+
*/
|
|
436
|
+
getDropTargetByData(dataQuery, frameKey) {
|
|
437
|
+
return this.findDropTargetByData(dataQuery, this.dropElementsTree, null, frameKey ?? null);
|
|
213
438
|
}
|
|
214
439
|
/**
|
|
215
440
|
* Handles external selection by data query.
|
|
@@ -220,21 +445,47 @@ export class Monitor {
|
|
|
220
445
|
this.events.next({
|
|
221
446
|
type: DnDEventType.Select,
|
|
222
447
|
dropTarget: dropTarget,
|
|
448
|
+
frameKey: dropTarget?.frameKey ?? null,
|
|
223
449
|
});
|
|
224
450
|
}
|
|
225
451
|
/**
|
|
226
|
-
* Handles the end of a drag operation.
|
|
452
|
+
* Handles the end of a drag operation. Releasing the pointer outside every
|
|
453
|
+
* droppable area cancels the drag: `DragEnd` still fires — overlays, the
|
|
454
|
+
* drag ghost and `isDragging` all reset on it — but carries a null
|
|
455
|
+
* `dropTarget` and `cancelled: true`, so nothing is inserted or moved.
|
|
227
456
|
*/
|
|
228
457
|
onDragEnd() {
|
|
458
|
+
const dragTargetId = this.getDragTargetId();
|
|
459
|
+
const moveSource = this.moveSource;
|
|
229
460
|
const dropTarget = this.getDropTarget();
|
|
230
|
-
|
|
461
|
+
// Idempotent: a pan that never became a drag (canvas panning, or a
|
|
462
|
+
// gesture cancelled before activation) must not emit a phantom DragEnd.
|
|
463
|
+
if (!dragTargetId && !moveSource) {
|
|
464
|
+
this.resetDragState();
|
|
231
465
|
return;
|
|
466
|
+
}
|
|
232
467
|
this.events.next({
|
|
233
468
|
type: DnDEventType.DragEnd,
|
|
234
|
-
target:
|
|
469
|
+
target: dragTargetId,
|
|
235
470
|
dropTarget: dropTarget,
|
|
236
|
-
dropTargetIndex: this.getDropTargetIndex(),
|
|
471
|
+
dropTargetIndex: dropTarget ? this.getDropTargetIndex() : null,
|
|
472
|
+
frameKey: dropTarget?.frameKey ?? null,
|
|
473
|
+
kind: moveSource ? 'move' : 'palette',
|
|
474
|
+
moveSource,
|
|
475
|
+
dragCoords: this.lastDragCoords,
|
|
476
|
+
dragStartCoords: this.dragStartCoords,
|
|
477
|
+
cancelled: !dropTarget,
|
|
237
478
|
});
|
|
479
|
+
this.resetDragState();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Clears every piece of per-drag state.
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
resetDragState() {
|
|
486
|
+
this.moveSource = null;
|
|
487
|
+
this.dragStartCoords = null;
|
|
488
|
+
this.lastDragCoords = null;
|
|
238
489
|
this.setDragTargetId(null);
|
|
239
490
|
this.setDropTarget(null);
|
|
240
491
|
this.setDropTargetIndex(null);
|
|
@@ -250,7 +501,7 @@ export class Monitor {
|
|
|
250
501
|
registerDropElement(layout, parent, config, data) {
|
|
251
502
|
this.dropElementIds++;
|
|
252
503
|
if (!parent) {
|
|
253
|
-
|
|
504
|
+
const item = {
|
|
254
505
|
layout: {
|
|
255
506
|
...layout,
|
|
256
507
|
index: config?.index || 0,
|
|
@@ -259,17 +510,21 @@ export class Monitor {
|
|
|
259
510
|
},
|
|
260
511
|
canDrop: config?.canDrop,
|
|
261
512
|
data,
|
|
262
|
-
|
|
513
|
+
frameKey: config?.frameKey,
|
|
514
|
+
};
|
|
515
|
+
this.dropElementsTree.set(this.dropElementIds, item);
|
|
516
|
+
this.dropNodesById.set(this.dropElementIds, item);
|
|
517
|
+
this.dropParentById.set(this.dropElementIds, null);
|
|
263
518
|
}
|
|
264
519
|
else {
|
|
265
|
-
const parentLayout = this.
|
|
520
|
+
const parentLayout = this.dropNodesById.get(parent);
|
|
266
521
|
if (!parentLayout) {
|
|
267
522
|
throw new Error('Parent element not found');
|
|
268
523
|
}
|
|
269
524
|
if (!parentLayout.children) {
|
|
270
525
|
parentLayout.children = new Map();
|
|
271
526
|
}
|
|
272
|
-
|
|
527
|
+
const item = {
|
|
273
528
|
layout: {
|
|
274
529
|
...layout,
|
|
275
530
|
index: config?.index || 0,
|
|
@@ -278,7 +533,10 @@ export class Monitor {
|
|
|
278
533
|
},
|
|
279
534
|
canDrop: config?.canDrop,
|
|
280
535
|
data,
|
|
281
|
-
}
|
|
536
|
+
};
|
|
537
|
+
parentLayout.children.set(this.dropElementIds, item);
|
|
538
|
+
this.dropNodesById.set(this.dropElementIds, item);
|
|
539
|
+
this.dropParentById.set(this.dropElementIds, parent);
|
|
282
540
|
}
|
|
283
541
|
return this.dropElementIds;
|
|
284
542
|
}
|
|
@@ -290,8 +548,14 @@ export class Monitor {
|
|
|
290
548
|
if (this.dropElementsTree.has(id)) {
|
|
291
549
|
this.dropElementsTree.delete(id);
|
|
292
550
|
}
|
|
293
|
-
|
|
294
|
-
|
|
551
|
+
else {
|
|
552
|
+
const parentId = this.dropParentById.get(id);
|
|
553
|
+
if (parentId != null) {
|
|
554
|
+
this.dropNodesById.get(parentId)?.children?.delete(id);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
this.dropNodesById.delete(id);
|
|
558
|
+
this.dropParentById.delete(id);
|
|
295
559
|
}
|
|
296
560
|
/**
|
|
297
561
|
* Updates the layout and configuration for a registered drop element.
|
|
@@ -301,27 +565,34 @@ export class Monitor {
|
|
|
301
565
|
* @param data - Optional new data
|
|
302
566
|
*/
|
|
303
567
|
updateDropElement(id, layout, config, data) {
|
|
304
|
-
const target = this.
|
|
568
|
+
const target = this.dropNodesById.get(id);
|
|
305
569
|
if (target) {
|
|
570
|
+
const nextIndex = config?.index || target.layout.index;
|
|
571
|
+
const nextZIndex = config?.zIndex || target.layout.zIndex;
|
|
572
|
+
const nextFlexDirection = config?.flexDirection || target.layout.flexDirection;
|
|
573
|
+
const changed = target.layout.x !== layout.x ||
|
|
574
|
+
target.layout.y !== layout.y ||
|
|
575
|
+
target.layout.width !== layout.width ||
|
|
576
|
+
target.layout.height !== layout.height ||
|
|
577
|
+
target.layout.index !== nextIndex ||
|
|
578
|
+
target.layout.zIndex !== nextZIndex ||
|
|
579
|
+
target.layout.flexDirection !== nextFlexDirection;
|
|
306
580
|
target.layout.x = layout.x;
|
|
307
581
|
target.layout.y = layout.y;
|
|
308
582
|
target.layout.width = layout.width;
|
|
309
583
|
target.layout.height = layout.height;
|
|
310
|
-
target.layout.index =
|
|
311
|
-
target.layout.zIndex =
|
|
312
|
-
target.layout.flexDirection =
|
|
313
|
-
config?.flexDirection || target.layout.flexDirection;
|
|
584
|
+
target.layout.index = nextIndex;
|
|
585
|
+
target.layout.zIndex = nextZIndex;
|
|
586
|
+
target.layout.flexDirection = nextFlexDirection;
|
|
314
587
|
target.data = data || target.data;
|
|
315
588
|
target.canDrop = config?.canDrop || target.canDrop;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (parent) {
|
|
322
|
-
parent.children?.set(id, target);
|
|
323
|
-
}
|
|
589
|
+
// Layout listeners (selection overlays, highlight rects) only react to
|
|
590
|
+
// geometry/order. Re-measures land here at input rate and are usually
|
|
591
|
+
// no-ops — broadcasting those floods every subscriber for nothing.
|
|
592
|
+
if (!changed) {
|
|
593
|
+
return;
|
|
324
594
|
}
|
|
595
|
+
const frameKey = this.findFrameKeyFor(id);
|
|
325
596
|
this.events.next({
|
|
326
597
|
type: DnDEventType.Layout,
|
|
327
598
|
target: id,
|
|
@@ -331,31 +602,26 @@ export class Monitor {
|
|
|
331
602
|
layout: target.layout,
|
|
332
603
|
canDrop: target.canDrop,
|
|
333
604
|
data: target.data,
|
|
605
|
+
frameKey,
|
|
334
606
|
},
|
|
607
|
+
frameKey,
|
|
335
608
|
});
|
|
336
609
|
}
|
|
337
610
|
}
|
|
338
611
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* @param tree - The tree to search in
|
|
342
|
-
* @returns The drop item or null if not found
|
|
612
|
+
* Resolves the frame space an element (or any of its ancestors) belongs to
|
|
613
|
+
* by walking the parent chain — O(depth).
|
|
343
614
|
* @private
|
|
344
615
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const target = this.findDropTarget(id, value.children);
|
|
355
|
-
if (target) {
|
|
356
|
-
return target;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
616
|
+
findFrameKeyFor(id) {
|
|
617
|
+
let current = id;
|
|
618
|
+
while (current != null) {
|
|
619
|
+
const node = this.dropNodesById.get(current);
|
|
620
|
+
if (!node)
|
|
621
|
+
return null;
|
|
622
|
+
if (node.frameKey)
|
|
623
|
+
return node.frameKey;
|
|
624
|
+
current = this.dropParentById.get(current);
|
|
359
625
|
}
|
|
360
626
|
return null;
|
|
361
627
|
}
|
|
@@ -366,14 +632,23 @@ export class Monitor {
|
|
|
366
632
|
* @returns The drop target or null if not found
|
|
367
633
|
* @private
|
|
368
634
|
*/
|
|
369
|
-
findDropTargetByData(dataQuery, tree) {
|
|
635
|
+
findDropTargetByData(dataQuery, tree, frameKey, requiredFrameKey) {
|
|
370
636
|
if (!tree) {
|
|
371
637
|
return null;
|
|
372
638
|
}
|
|
373
639
|
let index = 0;
|
|
374
640
|
for (const [id, target] of tree) {
|
|
375
641
|
index++;
|
|
376
|
-
|
|
642
|
+
const targetFrameKey = target.frameKey ?? frameKey ?? null;
|
|
643
|
+
// Prune subtrees belonging to other frames when a frame is required.
|
|
644
|
+
if (requiredFrameKey &&
|
|
645
|
+
targetFrameKey &&
|
|
646
|
+
targetFrameKey !== requiredFrameKey) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (target.data &&
|
|
650
|
+
this.partialMatch(target.data, dataQuery) &&
|
|
651
|
+
(!requiredFrameKey || targetFrameKey === requiredFrameKey)) {
|
|
377
652
|
return {
|
|
378
653
|
id,
|
|
379
654
|
layout: target.layout,
|
|
@@ -381,10 +656,11 @@ export class Monitor {
|
|
|
381
656
|
canDrop: target.canDrop,
|
|
382
657
|
children: target.children,
|
|
383
658
|
data: target.data,
|
|
659
|
+
frameKey: targetFrameKey,
|
|
384
660
|
};
|
|
385
661
|
}
|
|
386
662
|
if (target.children) {
|
|
387
|
-
const childResult = this.findDropTargetByData(dataQuery, target.children);
|
|
663
|
+
const childResult = this.findDropTargetByData(dataQuery, target.children, targetFrameKey, requiredFrameKey);
|
|
388
664
|
if (childResult) {
|
|
389
665
|
return childResult;
|
|
390
666
|
}
|
|
@@ -427,30 +703,6 @@ export class Monitor {
|
|
|
427
703
|
}
|
|
428
704
|
return true;
|
|
429
705
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Finds the parent of a drop target by ID.
|
|
432
|
-
* @param id - The ID of the child drop target
|
|
433
|
-
* @param tree - The tree to search in
|
|
434
|
-
* @returns The parent drop item or null if not found
|
|
435
|
-
* @private
|
|
436
|
-
*/
|
|
437
|
-
findDropTargetParent(id, tree) {
|
|
438
|
-
if (!tree) {
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
for (const [, value] of tree) {
|
|
442
|
-
if (value.children && value.children.has(id)) {
|
|
443
|
-
return value;
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
const parent = this.findDropTargetParent(id, value.children);
|
|
447
|
-
if (parent) {
|
|
448
|
-
return parent;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
706
|
/**
|
|
455
707
|
* Gets all drop targets that contain the given coordinates.
|
|
456
708
|
* @param dropCoords - The coordinates to check
|
|
@@ -458,7 +710,7 @@ export class Monitor {
|
|
|
458
710
|
* @returns Array of drop targets or null if none found
|
|
459
711
|
* @private
|
|
460
712
|
*/
|
|
461
|
-
getDropTargets(dropCoords, tree) {
|
|
713
|
+
getDropTargets(dropCoords, tree, frameKey, excludeId) {
|
|
462
714
|
const targets = [];
|
|
463
715
|
if (!tree) {
|
|
464
716
|
return null;
|
|
@@ -466,10 +718,21 @@ export class Monitor {
|
|
|
466
718
|
let index = 0;
|
|
467
719
|
for (const [id, target] of tree) {
|
|
468
720
|
index++;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
721
|
+
// Skip the excluded subtree entirely (a moved element can't be its
|
|
722
|
+
// own drop target, nor can any of its descendants).
|
|
723
|
+
if (excludeId != null && id === excludeId) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
// Entering a frame-rooted subtree: convert the pointer into that
|
|
727
|
+
// frame's local space once; all descendant layouts share it.
|
|
728
|
+
const targetFrameKey = target.frameKey ?? frameKey ?? null;
|
|
729
|
+
const coords = target.frameKey
|
|
730
|
+
? this.toFrameLocal(dropCoords, target.frameKey)
|
|
731
|
+
: dropCoords;
|
|
732
|
+
if (target.layout.x <= coords.x &&
|
|
733
|
+
target.layout.y <= coords.y &&
|
|
734
|
+
target.layout.x + target.layout.width >= coords.x &&
|
|
735
|
+
target.layout.y + target.layout.height >= coords.y) {
|
|
473
736
|
targets.push({
|
|
474
737
|
id,
|
|
475
738
|
layout: target.layout,
|
|
@@ -477,9 +740,11 @@ export class Monitor {
|
|
|
477
740
|
canDrop: target.canDrop,
|
|
478
741
|
children: target.children,
|
|
479
742
|
data: target.data,
|
|
743
|
+
frameKey: targetFrameKey,
|
|
744
|
+
hitCoords: coords,
|
|
480
745
|
});
|
|
481
746
|
}
|
|
482
|
-
const childTargets = this.getDropTargets(
|
|
747
|
+
const childTargets = this.getDropTargets(coords, target.children, targetFrameKey, excludeId);
|
|
483
748
|
if (childTargets) {
|
|
484
749
|
targets.push(...childTargets);
|
|
485
750
|
}
|
|
@@ -502,12 +767,14 @@ export class Monitor {
|
|
|
502
767
|
if (!lastTarget) {
|
|
503
768
|
return null;
|
|
504
769
|
}
|
|
505
|
-
|
|
770
|
+
// Compare in the target's own coordinate space (frame-local inside
|
|
771
|
+
// frames); `hitCoords` was attached during hit testing.
|
|
772
|
+
const coords = lastTarget.hitCoords ?? dragCoords;
|
|
773
|
+
if (lastTarget.layout.y + this.padding > coords.y ||
|
|
506
774
|
lastTarget.layout.y + lastTarget.layout.height - this.padding <
|
|
507
|
-
|
|
508
|
-
lastTarget.layout.x + this.padding >
|
|
509
|
-
lastTarget.layout.x + lastTarget.layout.width - this.padding <
|
|
510
|
-
dragCoords.x) {
|
|
775
|
+
coords.y ||
|
|
776
|
+
lastTarget.layout.x + this.padding > coords.x ||
|
|
777
|
+
lastTarget.layout.x + lastTarget.layout.width - this.padding < coords.x) {
|
|
511
778
|
return sortedTargets[sortedTargets.length - 2] || lastTarget;
|
|
512
779
|
}
|
|
513
780
|
return lastTarget;
|