@snapgridjs/dnd 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/dist/index.cjs ADDED
@@ -0,0 +1,676 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _snapgridjs_core = require("@snapgridjs/core");
3
+ let _dnd_kit_collision = require("@dnd-kit/collision");
4
+ let _dnd_kit_abstract = require("@dnd-kit/abstract");
5
+ let _dnd_kit_dom = require("@dnd-kit/dom");
6
+ //#region src/controller/GridController.ts
7
+ function sameItem(a, b) {
8
+ if (a === b) return true;
9
+ if (!a || !b) return false;
10
+ return a.i === b.i && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
11
+ }
12
+ /**
13
+ * Live per-grid drag/resize state as a plain observable: the provider writes
14
+ * (`setSession`/`setKeyboard`/`setCommitted`), hooks subscribe to just their own
15
+ * slice via `useSyncExternalStore`. Value-cached snapshots mean a drag re-renders
16
+ * only the tiles whose slice changed, not the whole subtree (the old
17
+ * context-value model re-rendered every tile every frame).
18
+ */
19
+ var GridController = class {
20
+ id;
21
+ #committed;
22
+ #session = null;
23
+ #keyboard = false;
24
+ #listeners = /* @__PURE__ */ new Set();
25
+ config = null;
26
+ #itemCache = /* @__PURE__ */ new Map();
27
+ #resizeCache = /* @__PURE__ */ new Map();
28
+ #placeholderCache = null;
29
+ #renderedMap = null;
30
+ #renderedMapSource = null;
31
+ #indexById = /* @__PURE__ */ new Map();
32
+ #nextIndex = 0;
33
+ /** The dnd-kit manager this grid is registered with (set by useInstance). */
34
+ manager;
35
+ /**
36
+ * This grid's container element, reported by the host. The engine reads it to
37
+ * map a pointer to a cell when receiving a tile (its `getBoundingClientRect`).
38
+ */
39
+ element = null;
40
+ constructor(id, committed = [], manager) {
41
+ this.id = id;
42
+ this.#committed = committed;
43
+ this.manager = manager;
44
+ }
45
+ /** The committed (base) layout — the engine's source of truth during a drag. */
46
+ getCommitted() {
47
+ return this.#committed;
48
+ }
49
+ /** Replace the per-grid config (called by the container host during render). */
50
+ setConfig(config) {
51
+ this.config = config;
52
+ }
53
+ /**
54
+ * Re-point this grid's id. The container host syncs it (during render, before
55
+ * the droppable/group read it) when the controlled `id` prop changes, so the
56
+ * returned `group`, the droppable id, and the registry key never drift apart.
57
+ */
58
+ setId(id) {
59
+ this.id = id;
60
+ }
61
+ register = () => {};
62
+ subscribe = (listener) => {
63
+ this.#listeners.add(listener);
64
+ return () => {
65
+ this.#listeners.delete(listener);
66
+ };
67
+ };
68
+ #emit() {
69
+ for (const listener of this.#listeners) listener();
70
+ }
71
+ /** The layout currently shown: the drag preview while dragging, else committed. */
72
+ #rendered() {
73
+ return this.#session ? this.#session.preview : this.#committed;
74
+ }
75
+ #renderedById() {
76
+ const rendered = this.#rendered();
77
+ if (this.#renderedMapSource !== rendered) {
78
+ this.#renderedMap = new Map(rendered.map((it) => [it.i, it]));
79
+ this.#renderedMapSource = rendered;
80
+ }
81
+ return this.#renderedMap;
82
+ }
83
+ /**
84
+ * Sync the committed layout from the controlled `layout` prop. Called during
85
+ * the provider's render, so it must NOT notify — emitting here would update
86
+ * subscribed GridItems mid-render (a React "setState while rendering" error).
87
+ * No notify is needed: a `layout` prop change already re-renders the whole
88
+ * provider subtree, so every GridItem re-reads its snapshot on that pass.
89
+ */
90
+ setCommitted(layout) {
91
+ if (this.#committed === layout) return;
92
+ this.#committed = layout;
93
+ const present = new Set(layout.map((it) => it.i));
94
+ for (const id of this.#indexById.keys()) if (!present.has(id)) this.#indexById.delete(id);
95
+ for (const id of this.#itemCache.keys()) if (!present.has(id)) this.#itemCache.delete(id);
96
+ for (const id of this.#resizeCache.keys()) if (!present.has(id)) this.#resizeCache.delete(id);
97
+ }
98
+ setSession(next) {
99
+ this.#session = next;
100
+ this.#emit();
101
+ }
102
+ getSession() {
103
+ return this.#session;
104
+ }
105
+ /** Record whether the active drag is keyboard-driven (drives `hidden`). */
106
+ setKeyboard(value) {
107
+ if (this.#keyboard === value) return;
108
+ this.#keyboard = value;
109
+ this.#emit();
110
+ }
111
+ itemSnapshot = (id) => {
112
+ const item = this.#renderedById().get(id);
113
+ const isDragging = this.#session?.activeId === id;
114
+ const hidden = isDragging && this.#session?.kind === "move" && !this.#keyboard;
115
+ const prev = this.#itemCache.get(id);
116
+ if (prev && prev.isDragging === isDragging && prev.hidden === hidden && sameItem(prev.item, item)) return prev;
117
+ const snap = {
118
+ item,
119
+ isDragging,
120
+ hidden
121
+ };
122
+ this.#itemCache.set(id, snap);
123
+ return snap;
124
+ };
125
+ placeholderSnapshot = () => {
126
+ const next = this.#session?.placeholder ?? null;
127
+ if (sameItem(this.#placeholderCache ?? void 0, next ?? void 0)) return this.#placeholderCache;
128
+ this.#placeholderCache = next;
129
+ return next;
130
+ };
131
+ resizeSnapshot = (itemId) => {
132
+ const isResizing = this.#session?.kind === "resize" && this.#session.activeId === itemId;
133
+ const prev = this.#resizeCache.get(itemId);
134
+ if (prev && prev.isResizing === isResizing) return prev;
135
+ const snap = { isResizing };
136
+ this.#resizeCache.set(itemId, snap);
137
+ return snap;
138
+ };
139
+ renderedSnapshot = () => this.#rendered();
140
+ /** A stable index for `id` (see {@link GridController.#indexById}). */
141
+ itemIndex(id) {
142
+ let i = this.#indexById.get(id);
143
+ if (i === void 0) {
144
+ i = this.#nextIndex++;
145
+ this.#indexById.set(id, i);
146
+ }
147
+ return i;
148
+ }
149
+ };
150
+ //#endregion
151
+ //#region src/controller/registry.ts
152
+ /**
153
+ * Resolves a grid's {@link GridController} by its id, scoped to the dnd-kit
154
+ * manager the grid is registered with. A container registers its controller
155
+ * here (during render, so child items resolve it on their first render); items
156
+ * look it up by their `group` (= the grid id). Replaces the old geometry
157
+ * `GridRegistry` — which grid the pointer is over now comes from the collision
158
+ * target, so the registry's only job is id → controller resolution.
159
+ *
160
+ * Keyed by manager so two apps (or two providers) never collide, and grids in
161
+ * one provider share a map (the cross-grid seam).
162
+ */
163
+ const byManager = /* @__PURE__ */ new WeakMap();
164
+ const noManager = /* @__PURE__ */ new Map();
165
+ function mapFor(manager) {
166
+ if (!manager) return noManager;
167
+ let map = byManager.get(manager);
168
+ if (!map) {
169
+ map = /* @__PURE__ */ new Map();
170
+ byManager.set(manager, map);
171
+ }
172
+ return map;
173
+ }
174
+ /** Register a controller under `id` for `manager`. Returns an unregister fn. */
175
+ function registerController(manager, id, controller) {
176
+ const map = mapFor(manager);
177
+ map.set(id, controller);
178
+ return () => {
179
+ if (map.get(id) === controller) map.delete(id);
180
+ };
181
+ }
182
+ /** The controller registered under `id` for `manager`, or undefined. */
183
+ function getController(manager, id) {
184
+ return mapFor(manager).get(id);
185
+ }
186
+ const grabOffsets = /* @__PURE__ */ new WeakMap();
187
+ const noManagerGrab = { current: null };
188
+ function setGrabOffset(manager, offset) {
189
+ if (!manager) {
190
+ noManagerGrab.current = offset;
191
+ return;
192
+ }
193
+ if (offset) grabOffsets.set(manager, offset);
194
+ else grabOffsets.delete(manager);
195
+ }
196
+ function getGrabOffset(manager) {
197
+ return (manager ? grabOffsets.get(manager) : noManagerGrab.current) ?? {
198
+ x: 0,
199
+ y: 0
200
+ };
201
+ }
202
+ //#endregion
203
+ //#region src/dnd/dragFlow.ts
204
+ /**
205
+ * Pure decision helpers for the drag interaction so the tricky bits — grab-offset
206
+ * cell mapping, the cross-grid drop lifecycle, and external-drop acceptance — are
207
+ * unit-testable without a DOM or dnd-kit.
208
+ */
209
+ /** Read snapgrid's payload off a dnd-kit drag source. */
210
+ function dragData(event) {
211
+ return (event.operation.source?.data)?.snapGrid;
212
+ }
213
+ /** Size/id spec for an external (non-grid) draggable the grid may accept, or null. */
214
+ function externalDropSpec(source, dropConfig) {
215
+ if (!dropConfig?.enabled || !source) return null;
216
+ const data = source.data;
217
+ if (data?.snapGrid) return null;
218
+ if (dropConfig.accept && !dropConfig.accept(source)) return null;
219
+ const spec = data?.snapGridDrop;
220
+ return {
221
+ i: spec?.i,
222
+ w: spec?.w ?? dropConfig.defaultItem?.w ?? 1,
223
+ h: spec?.h ?? dropConfig.defaultItem?.h ?? 1
224
+ };
225
+ }
226
+ /**
227
+ * Map a client-space pointer to a grid cell, accounting for where *within* the
228
+ * dragged tile the pointer grabbed it. Subtracting the grab offset means the
229
+ * tile's top-left (not the cursor) maps to the cell, so a received tile's
230
+ * placeholder aligns with the floating overlay instead of jumping its corner to
231
+ * the cursor. External drops pass `{ x: 0, y: 0 }` (no meaningful grab point).
232
+ */
233
+ function receiveCell(pointer, gridRect, grabOffset, w, h, pp) {
234
+ return (0, _snapgridjs_core.calcXY)(pp, pointer.y - grabOffset.y - gridRect.top, pointer.x - grabOffset.x - gridRect.left, w, h);
235
+ }
236
+ /**
237
+ * Map a keyboard event key to a one-cell grid step while a keyboard drag is
238
+ * active, or null for keys snapgrid doesn't own — Enter/Space (drop) and Escape
239
+ * (cancel) fall through to dnd-kit's KeyboardSensor.
240
+ */
241
+ function arrowStep(key) {
242
+ switch (key) {
243
+ case "ArrowLeft": return [-1, 0];
244
+ case "ArrowRight": return [1, 0];
245
+ case "ArrowUp": return [0, -1];
246
+ case "ArrowDown": return [0, 1];
247
+ default: return null;
248
+ }
249
+ }
250
+ /** Pure classification of a drag end. See {@link DropAction}. */
251
+ function classifyDrop(s) {
252
+ if (s.canceled) {
253
+ if (s.kind === "resize") return "cancel-resize";
254
+ if (s.ownsItem) return "cancel-move";
255
+ return "noop";
256
+ }
257
+ if (s.kind === "resize") return "commit-resize";
258
+ if (s.ownsItem && s.hasData) {
259
+ if (s.dest === s.myId && s.kind === "move") return "commit-in-grid";
260
+ if (s.dest) return "remove-source";
261
+ return "revert";
262
+ }
263
+ if (s.dest === s.myId && s.kind === "move") return s.hasData ? "commit-dest" : "external-drop";
264
+ return "noop";
265
+ }
266
+ //#endregion
267
+ //#region src/dnd/entity.ts
268
+ /**
269
+ * The DOM element of a dnd-kit entity (a draggable, droppable, or drag source).
270
+ * dnd-kit's abstract types don't expose `element`, but the DOM layer snapgrid
271
+ * runs on always sets it — this centralizes that one assumption (and the cast)
272
+ * in a single place instead of scattering it across the drag code.
273
+ */
274
+ function domElement(entity) {
275
+ return entity?.element ?? null;
276
+ }
277
+ //#endregion
278
+ //#region src/dnd/SnapGridEngine.ts
279
+ const hasWindow = typeof window !== "undefined";
280
+ function dragCtx(ctrl) {
281
+ const cfg = ctrl.config;
282
+ return {
283
+ positionParams: cfg.positionParams,
284
+ compactor: cfg.compactor,
285
+ cols: cfg.gridConfig.cols
286
+ };
287
+ }
288
+ /** Map a client-space pointer to a cell within `ctrl`, via its element rect. */
289
+ function cellFromPointer(ctrl, pointer, item, manager) {
290
+ const el = ctrl.element;
291
+ const cfg = ctrl.config;
292
+ if (!el || !cfg) return null;
293
+ return receiveCell(pointer, el.getBoundingClientRect(), getGrabOffset(manager), item.w, item.h, cfg.positionParams);
294
+ }
295
+ var SnapGridEngine = class {
296
+ #manager;
297
+ #unsub = [];
298
+ #source = null;
299
+ #dest = null;
300
+ #keyboard = false;
301
+ #dropSpec = null;
302
+ #dropCounter = 0;
303
+ #lastTargetId = null;
304
+ #lastDest = void 0;
305
+ constructor(manager) {
306
+ this.#manager = manager;
307
+ const mon = manager.monitor;
308
+ this.#unsub.push(mon.addEventListener("dragstart", (event) => {
309
+ const op = event.operation;
310
+ const p = op.position.current;
311
+ this.#start(dragData(event), {
312
+ x: p.x,
313
+ y: p.y
314
+ }, op.source, op.activatorEvent);
315
+ }), mon.addEventListener("dragmove", (event) => {
316
+ const op = event.operation;
317
+ const p = op.position.current;
318
+ this.#move(dragData(event), {
319
+ x: p.x,
320
+ y: p.y
321
+ }, op.source, op.target?.id ?? null, op.activatorEvent);
322
+ }), mon.addEventListener("dragend", (event) => {
323
+ const op = event.operation;
324
+ this.#end(dragData(event), op.target?.id ?? null, event.nativeEvent ?? null, event.canceled);
325
+ }));
326
+ if (hasWindow) window.addEventListener("keydown", this.#onKeyDown, true);
327
+ }
328
+ destroy() {
329
+ for (const u of this.#unsub) u();
330
+ this.#unsub = [];
331
+ if (hasWindow) window.removeEventListener("keydown", this.#onKeyDown, true);
332
+ }
333
+ #reset() {
334
+ this.#source = null;
335
+ this.#dest = null;
336
+ this.#keyboard = false;
337
+ this.#dropSpec = null;
338
+ this.#lastTargetId = null;
339
+ this.#lastDest = void 0;
340
+ }
341
+ /** Resolve the grid under the pointer from the collision target id (cached per drag). */
342
+ #resolveDest(targetId) {
343
+ if (targetId == null) return void 0;
344
+ if (targetId !== this.#lastTargetId) {
345
+ this.#lastTargetId = targetId;
346
+ this.#lastDest = getController(this.#manager, String(targetId));
347
+ }
348
+ return this.#lastDest;
349
+ }
350
+ #setDest(next) {
351
+ if (this.#dest && this.#dest !== next && this.#dest !== this.#source) this.#dest.setSession(null);
352
+ this.#dest = next;
353
+ }
354
+ #start(data, pointer, source, activatorEvent) {
355
+ this.#reset();
356
+ if (!data) return;
357
+ const owner = getController(this.#manager, data.group);
358
+ const cfg = owner?.config;
359
+ if (!owner || !cfg) return;
360
+ const layout = owner.getCommitted();
361
+ const item = layout.find((it) => it.i === data.itemId);
362
+ if (!item) return;
363
+ if (data.kind === "resize") {
364
+ const rect = (0, _snapgridjs_core.calcGridItemPosition)(cfg.positionParams, item.x, item.y, item.w, item.h);
365
+ owner.setSession((0, _snapgridjs_core.beginResize)(layout, {
366
+ item,
367
+ rect,
368
+ pointer
369
+ }, data.handle));
370
+ this.#source = owner;
371
+ owner.setKeyboard(false);
372
+ cfg.callbacks.onResizeStart?.(layout, item, item, item, activatorEvent, null);
373
+ return;
374
+ }
375
+ const isKeyboard = hasWindow && activatorEvent instanceof KeyboardEvent;
376
+ this.#keyboard = isKeyboard;
377
+ owner.setKeyboard(isKeyboard);
378
+ const rect = (0, _snapgridjs_core.calcGridItemPosition)(cfg.positionParams, item.x, item.y, item.w, item.h);
379
+ owner.setSession((0, _snapgridjs_core.beginDrag)(layout, {
380
+ item,
381
+ left: rect.left,
382
+ top: rect.top,
383
+ pointer
384
+ }));
385
+ this.#source = owner;
386
+ const cr = domElement(source)?.getBoundingClientRect();
387
+ if (cr) setGrabOffset(this.#manager, {
388
+ x: pointer.x - cr.left,
389
+ y: pointer.y - cr.top
390
+ });
391
+ cfg.callbacks.onDragStart?.(layout, item, item, item, activatorEvent, null);
392
+ }
393
+ #move(data, pointer, source, targetId, activatorEvent) {
394
+ if (this.#keyboard) return;
395
+ const owner = this.#source;
396
+ const ownerSession = owner?.getSession();
397
+ if (owner && ownerSession?.kind === "resize") {
398
+ const cfg = owner.config;
399
+ const next = (0, _snapgridjs_core.dragResize)(ownerSession, pointer, dragCtx(owner));
400
+ owner.setSession(next);
401
+ cfg.callbacks.onResize?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, activatorEvent, null);
402
+ return;
403
+ }
404
+ const destCtrl = this.#resolveDest(targetId);
405
+ if (!data) {
406
+ this.#setDest(destCtrl ? this.#receiveExternalInto(destCtrl, source, pointer) : null);
407
+ return;
408
+ }
409
+ if (data.kind !== "move") return;
410
+ if (owner && destCtrl === owner) {
411
+ this.#setDest(null);
412
+ const cur = owner.getSession();
413
+ if (!cur) return;
414
+ const cfg = owner.config;
415
+ const next = (0, _snapgridjs_core.dragTo)(cur, pointer, dragCtx(owner));
416
+ owner.setSession(next);
417
+ cfg.callbacks.onDrag?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, activatorEvent, null);
418
+ return;
419
+ }
420
+ if (owner) {
421
+ const cur = owner.getSession();
422
+ if (cur) {
423
+ const cfg = owner.config;
424
+ const hidden = (0, _snapgridjs_core.hideActive)(cur);
425
+ owner.setSession(hidden);
426
+ cfg.callbacks.onDrag?.(hidden.preview, hidden.anchor.item, null, null, activatorEvent, null);
427
+ }
428
+ }
429
+ this.#setDest(destCtrl && destCtrl !== owner ? this.#receiveInto(destCtrl, data.item, pointer) : null);
430
+ }
431
+ /** Build a receive preview for `foreign` in `dest`; returns `dest` on success, else null. */
432
+ #receiveInto(dest, foreign, pointer) {
433
+ if (!dest.config) return null;
434
+ const committed = dest.getCommitted();
435
+ const cell = cellFromPointer(dest, pointer, foreign, this.#manager) ?? {
436
+ x: 0,
437
+ y: 0
438
+ };
439
+ dest.setSession((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, dragCtx(dest)));
440
+ return dest;
441
+ }
442
+ /** Receive an external (non-grid) draggable into `dest`, synthesizing its item. */
443
+ #receiveExternalInto(dest, source, pointer) {
444
+ const spec = this.#externalSpecFor(dest, source);
445
+ if (!spec) return null;
446
+ return this.#receiveInto(dest, {
447
+ i: spec.i,
448
+ x: 0,
449
+ y: 0,
450
+ w: spec.w,
451
+ h: spec.h
452
+ }, pointer);
453
+ }
454
+ #externalSpecFor(dest, source) {
455
+ const spec = externalDropSpec(source, dest.config?.dropConfig);
456
+ if (!spec) return null;
457
+ if (!this.#dropSpec) {
458
+ this.#dropCounter += 1;
459
+ this.#dropSpec = {
460
+ i: spec.i ?? `${dest.id}-dropped-${this.#dropCounter}`,
461
+ w: spec.w,
462
+ h: spec.h
463
+ };
464
+ }
465
+ return this.#dropSpec;
466
+ }
467
+ #end(data, targetId, nativeEvent, canceled) {
468
+ const source = this.#source;
469
+ const dest = this.#dest;
470
+ try {
471
+ if (source) {
472
+ const cfg = source.config;
473
+ const cur = source.getSession();
474
+ const destId = dest ? dest.id : this.#keyboard || targetId != null && String(targetId) === source.id ? source.id : null;
475
+ const action = classifyDrop({
476
+ kind: cur?.kind ?? null,
477
+ canceled,
478
+ ownsItem: true,
479
+ hasData: !!data,
480
+ dest: destId,
481
+ myId: source.id
482
+ });
483
+ switch (action) {
484
+ case "cancel-resize":
485
+ cfg.callbacks.onResizeStop?.(source.getCommitted(), cur?.anchor.item ?? null, null, null, nativeEvent, null);
486
+ break;
487
+ case "cancel-move":
488
+ cfg.callbacks.onDragStop?.(source.getCommitted(), cur?.anchor.item ?? null, null, null, nativeEvent, null);
489
+ break;
490
+ case "commit-resize":
491
+ if (cur) {
492
+ cfg.callbacks.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(cur));
493
+ cfg.callbacks.onResizeStop?.(cur.preview, cur.anchor.item, cur.placeholder, cur.placeholder, nativeEvent, null);
494
+ }
495
+ break;
496
+ case "commit-in-grid":
497
+ case "remove-source":
498
+ case "revert":
499
+ if (action === "commit-in-grid" && cur) cfg.callbacks.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(cur));
500
+ else if (action === "remove-source" && data) {
501
+ const { compactor, cols } = dragCtx(source);
502
+ cfg.callbacks.onLayoutChange?.((0, _snapgridjs_core.removeItemWithCompactor)(source.getCommitted(), data.itemId, {
503
+ compactor,
504
+ cols
505
+ }));
506
+ }
507
+ cfg.callbacks.onDragStop?.(cur?.preview ?? source.getCommitted(), cur?.anchor.item ?? null, cur?.placeholder ?? null, cur?.placeholder ?? null, nativeEvent, null);
508
+ break;
509
+ }
510
+ }
511
+ if (dest && dest !== source) {
512
+ const cfg = dest.config;
513
+ const cur = dest.getSession();
514
+ const action = classifyDrop({
515
+ kind: cur?.kind ?? null,
516
+ canceled,
517
+ ownsItem: false,
518
+ hasData: !!data,
519
+ dest: dest.id,
520
+ myId: dest.id
521
+ });
522
+ if (action === "commit-dest") {
523
+ if (cur) cfg.callbacks.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(cur));
524
+ } else if (action === "external-drop" && cur) {
525
+ const committed = (0, _snapgridjs_core.commitLayout)(cur);
526
+ const dropped = committed.find((it) => it.i === cur.activeId);
527
+ if (dropped) cfg.callbacks.onDrop?.(committed, dropped, nativeEvent);
528
+ }
529
+ }
530
+ } finally {
531
+ setGrabOffset(this.#manager, null);
532
+ source?.setKeyboard(false);
533
+ source?.setSession(null);
534
+ if (dest && dest !== source) dest.setSession(null);
535
+ this.#reset();
536
+ }
537
+ }
538
+ #onKeyDown = (e) => {
539
+ if (!this.#keyboard) return;
540
+ const source = this.#source;
541
+ const session = source?.getSession();
542
+ if (!source || !session || session.kind !== "move") return;
543
+ const step = arrowStep(e.key);
544
+ if (!step) return;
545
+ e.preventDefault();
546
+ e.stopImmediatePropagation();
547
+ source.setSession((0, _snapgridjs_core.nudge)(session, step[0], step[1], dragCtx(source)));
548
+ };
549
+ };
550
+ const engines = /* @__PURE__ */ new WeakMap();
551
+ /** Ensure the engine is attached to `manager`; returns a detach (ref-decrement) fn. */
552
+ function attachEngine(manager) {
553
+ let entry = engines.get(manager);
554
+ if (!entry) {
555
+ entry = {
556
+ engine: new SnapGridEngine(manager),
557
+ refs: 0
558
+ };
559
+ engines.set(manager, entry);
560
+ }
561
+ entry.refs += 1;
562
+ let detached = false;
563
+ return () => {
564
+ if (detached) return;
565
+ detached = true;
566
+ const e = engines.get(manager);
567
+ if (!e) return;
568
+ e.refs -= 1;
569
+ if (e.refs <= 0) {
570
+ e.engine.destroy();
571
+ engines.delete(manager);
572
+ }
573
+ };
574
+ }
575
+ //#endregion
576
+ //#region src/dnd/collision.ts
577
+ /**
578
+ * Marker attribute set on every grid container element. Used by {@link gridDepth}
579
+ * to measure how deeply a grid is nested, purely from the DOM.
580
+ */
581
+ const SNAPGRID_GRID_ATTR = "data-snapgrid-grid";
582
+ const GRID_COLLISION_PRIORITY = 10;
583
+ /**
584
+ * How deeply `el`'s grid is nested: the number of ancestor grid containers above
585
+ * it. A top-level grid is 0; a grid rendered inside another grid's tile is 1; and
586
+ * so on. DOM containment is the ground truth, so this is correct regardless of the
587
+ * React tree shape or how priorities are assigned elsewhere.
588
+ */
589
+ function gridDepth(el) {
590
+ let depth = 0;
591
+ let node = el?.parentElement ?? null;
592
+ while (node) {
593
+ if (node.hasAttribute("data-snapgrid-grid")) depth++;
594
+ node = node.parentElement;
595
+ }
596
+ return depth;
597
+ }
598
+ /**
599
+ * Collision detector for grid droppables. Runs dnd-kit's default detector, then —
600
+ * when nested grid rects overlap (the pointer is over both an inner grid and its
601
+ * outer one) — ranks the **innermost** grid highest by boosting priority with the
602
+ * grid's nesting depth. Without this, overlapping grids tie on priority and the
603
+ * winner is arbitrary. For non-nested grids depth is 0, so priority is unchanged.
604
+ */
605
+ const gridCollisionDetector = (input) => {
606
+ const collision = (0, _dnd_kit_collision.defaultCollisionDetection)(input);
607
+ if (!collision) return null;
608
+ return {
609
+ ...collision,
610
+ priority: GRID_COLLISION_PRIORITY + gridDepth(domElement(input.droppable))
611
+ };
612
+ };
613
+ //#endregion
614
+ //#region src/dnd/snapToGrid.ts
615
+ /**
616
+ * Quantizes the dragged item's transform to whole grid cells, so the floating
617
+ * <DragOverlay> clone jumps cell-to-cell in lockstep with the (always-snapped)
618
+ * placeholder instead of tracking the pointer smoothly. Applied on the item
619
+ * draggable; a no-op unless `dragConfig.snapToGrid` is set.
620
+ */
621
+ var SnapToGrid = class extends _dnd_kit_abstract.Modifier {
622
+ apply({ transform }) {
623
+ const opts = this.options;
624
+ if (!opts?.isEnabled()) return transform;
625
+ const pp = opts.getPositionParams();
626
+ const colStep = (0, _snapgridjs_core.calcGridColWidth)(pp) + pp.margin[0];
627
+ const rowStep = pp.rowHeight + pp.margin[1];
628
+ if (colStep <= 0 || rowStep <= 0) return transform;
629
+ return {
630
+ x: Math.round(transform.x / colStep) * colStep,
631
+ y: Math.round(transform.y / rowStep) * rowStep
632
+ };
633
+ }
634
+ };
635
+ //#endregion
636
+ //#region src/dndShared.ts
637
+ /** Marker attribute placed on resize-handle elements. */
638
+ const RESIZE_HANDLE_ATTR = "data-snapgrid-resize-handle";
639
+ const NO_FEEDBACK = [_dnd_kit_dom.Feedback.configure({ feedback: "none" })];
640
+ /**
641
+ * Whether a pointer-down on `target` should NOT start an item move. Pure and
642
+ * exported for testing. Honors three rules, in order:
643
+ * - never start a move from a resize handle;
644
+ * - never start from a region matching `dragConfig.cancel`;
645
+ * - if `dragConfig.handle` is set, only start from within it.
646
+ */
647
+ function shouldPreventItemDrag(target, cfg) {
648
+ if (!(target instanceof Element)) return false;
649
+ if (target.closest(`[data-snapgrid-resize-handle]`)) return true;
650
+ if (cfg?.cancel && target.closest(cfg.cancel)) return true;
651
+ if (cfg?.handle && !target.closest(cfg.handle)) return true;
652
+ return false;
653
+ }
654
+ /**
655
+ * Sensors for item (move) draggables, built from the drag config: a distance
656
+ * activation threshold (so clicks don't start drags) plus handle/cancel/resize
657
+ * gating, with the keyboard sensor kept for accessibility.
658
+ */
659
+ function buildItemSensors(threshold, getDragConfig) {
660
+ return [_dnd_kit_dom.PointerSensor.configure({
661
+ activationConstraints: () => threshold > 0 ? [new _dnd_kit_dom.PointerActivationConstraints.Distance({ value: threshold })] : void 0,
662
+ preventActivation: (event) => shouldPreventItemDrag(event.target, getDragConfig())
663
+ }), _dnd_kit_dom.KeyboardSensor];
664
+ }
665
+ //#endregion
666
+ exports.GridController = GridController;
667
+ exports.NO_FEEDBACK = NO_FEEDBACK;
668
+ exports.RESIZE_HANDLE_ATTR = RESIZE_HANDLE_ATTR;
669
+ exports.SNAPGRID_GRID_ATTR = SNAPGRID_GRID_ATTR;
670
+ exports.SnapToGrid = SnapToGrid;
671
+ exports.attachEngine = attachEngine;
672
+ exports.buildItemSensors = buildItemSensors;
673
+ exports.domElement = domElement;
674
+ exports.getController = getController;
675
+ exports.gridCollisionDetector = gridCollisionDetector;
676
+ exports.registerController = registerController;