@snapgridjs/react 0.3.0 → 0.5.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 CHANGED
@@ -1,335 +1,27 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _dnd_kit_react = require("@dnd-kit/react");
3
3
  let _snapgridjs_core = require("@snapgridjs/core");
4
+ let _snapgridjs_dnd = require("@snapgridjs/dnd");
4
5
  let react = require("react");
5
- let _dnd_kit_abstract = require("@dnd-kit/abstract");
6
6
  let _dnd_kit_dom = require("@dnd-kit/dom");
7
7
  let _dnd_kit_dom_utilities = require("@dnd-kit/dom/utilities");
8
8
  let _dnd_kit_react_sortable = require("@dnd-kit/react/sortable");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
- //#region src/controller/GridController.ts
11
- function sameItem(a, b) {
12
- if (a === b) return true;
13
- if (!a || !b) return false;
14
- return a.i === b.i && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
15
- }
16
- /**
17
- * Live per-grid drag/resize state as a plain observable: the provider writes
18
- * (`setSession`/`setKeyboard`/`setCommitted`), hooks subscribe to just their own
19
- * slice via `useSyncExternalStore`. Value-cached snapshots mean a drag re-renders
20
- * only the tiles whose slice changed, not the whole subtree (the old
21
- * context-value model re-rendered every tile every frame).
22
- */
23
- var GridController = class {
24
- id;
25
- #committed;
26
- #session = null;
27
- #keyboard = false;
28
- #listeners = /* @__PURE__ */ new Set();
29
- config = null;
30
- #itemCache = /* @__PURE__ */ new Map();
31
- #resizeCache = /* @__PURE__ */ new Map();
32
- #placeholderCache = null;
33
- #renderedMap = null;
34
- #renderedMapSource = null;
35
- #indexById = /* @__PURE__ */ new Map();
36
- #nextIndex = 0;
37
- /** The dnd-kit manager this grid is registered with (set by useInstance). */
38
- manager;
39
- constructor(id, committed = [], manager) {
40
- this.id = id;
41
- this.#committed = committed;
42
- this.manager = manager;
43
- }
44
- /** Replace the per-grid config (called by the container host during render). */
45
- setConfig(config) {
46
- this.config = config;
47
- }
48
- /**
49
- * Re-point this grid's id. The container host syncs it (during render, before
50
- * the droppable/group read it) when the controlled `id` prop changes, so the
51
- * returned `group`, the droppable id, and the registry key never drift apart.
52
- */
53
- setId(id) {
54
- this.id = id;
55
- }
56
- register = () => {};
57
- subscribe = (listener) => {
58
- this.#listeners.add(listener);
59
- return () => {
60
- this.#listeners.delete(listener);
61
- };
62
- };
63
- #emit() {
64
- for (const listener of this.#listeners) listener();
65
- }
66
- /** The layout currently shown: the drag preview while dragging, else committed. */
67
- #rendered() {
68
- return this.#session ? this.#session.preview : this.#committed;
69
- }
70
- #renderedById() {
71
- const rendered = this.#rendered();
72
- if (this.#renderedMapSource !== rendered) {
73
- this.#renderedMap = new Map(rendered.map((it) => [it.i, it]));
74
- this.#renderedMapSource = rendered;
75
- }
76
- return this.#renderedMap;
77
- }
78
- /**
79
- * Sync the committed layout from the controlled `layout` prop. Called during
80
- * the provider's render, so it must NOT notify — emitting here would update
81
- * subscribed GridItems mid-render (a React "setState while rendering" error).
82
- * No notify is needed: a `layout` prop change already re-renders the whole
83
- * provider subtree, so every GridItem re-reads its snapshot on that pass.
84
- */
85
- setCommitted(layout) {
86
- if (this.#committed === layout) return;
87
- this.#committed = layout;
88
- const present = new Set(layout.map((it) => it.i));
89
- for (const id of this.#indexById.keys()) if (!present.has(id)) this.#indexById.delete(id);
90
- for (const id of this.#itemCache.keys()) if (!present.has(id)) this.#itemCache.delete(id);
91
- for (const id of this.#resizeCache.keys()) if (!present.has(id)) this.#resizeCache.delete(id);
92
- }
93
- setSession(next) {
94
- this.#session = next;
95
- this.#emit();
96
- }
97
- getSession() {
98
- return this.#session;
99
- }
100
- /** Record whether the active drag is keyboard-driven (drives `hidden`). */
101
- setKeyboard(value) {
102
- if (this.#keyboard === value) return;
103
- this.#keyboard = value;
104
- this.#emit();
105
- }
106
- itemSnapshot = (id) => {
107
- const item = this.#renderedById().get(id);
108
- const isDragging = this.#session?.activeId === id;
109
- const hidden = isDragging && this.#session?.kind === "move" && !this.#keyboard;
110
- const prev = this.#itemCache.get(id);
111
- if (prev && prev.isDragging === isDragging && prev.hidden === hidden && sameItem(prev.item, item)) return prev;
112
- const snap = {
113
- item,
114
- isDragging,
115
- hidden
116
- };
117
- this.#itemCache.set(id, snap);
118
- return snap;
119
- };
120
- placeholderSnapshot = () => {
121
- const next = this.#session?.placeholder ?? null;
122
- if (sameItem(this.#placeholderCache ?? void 0, next ?? void 0)) return this.#placeholderCache;
123
- this.#placeholderCache = next;
124
- return next;
125
- };
126
- resizeSnapshot = (itemId) => {
127
- const isResizing = this.#session?.kind === "resize" && this.#session.activeId === itemId;
128
- const prev = this.#resizeCache.get(itemId);
129
- if (prev && prev.isResizing === isResizing) return prev;
130
- const snap = { isResizing };
131
- this.#resizeCache.set(itemId, snap);
132
- return snap;
133
- };
134
- renderedSnapshot = () => this.#rendered();
135
- /** A stable index for `id` (see {@link GridController.#indexById}). */
136
- itemIndex(id) {
137
- let i = this.#indexById.get(id);
138
- if (i === void 0) {
139
- i = this.#nextIndex++;
140
- this.#indexById.set(id, i);
141
- }
142
- return i;
143
- }
144
- };
145
- //#endregion
146
- //#region src/controller/registry.ts
147
- /**
148
- * Resolves a grid's {@link GridController} by its id, scoped to the dnd-kit
149
- * manager the grid is registered with. A container registers its controller
150
- * here (during render, so child items resolve it on their first render); items
151
- * look it up by their `group` (= the grid id). Replaces the old geometry
152
- * `GridRegistry` — which grid the pointer is over now comes from the collision
153
- * target, so the registry's only job is id → controller resolution.
154
- *
155
- * Keyed by manager so two apps (or two providers) never collide, and grids in
156
- * one provider share a map (the cross-grid seam).
157
- */
158
- const byManager = /* @__PURE__ */ new WeakMap();
159
- const noManager = /* @__PURE__ */ new Map();
160
- function mapFor(manager) {
161
- if (!manager) return noManager;
162
- let map = byManager.get(manager);
163
- if (!map) {
164
- map = /* @__PURE__ */ new Map();
165
- byManager.set(manager, map);
166
- }
167
- return map;
168
- }
169
- /** Register a controller under `id` for `manager`. Returns an unregister fn. */
170
- function registerController(manager, id, controller) {
171
- const map = mapFor(manager);
172
- map.set(id, controller);
173
- return () => {
174
- if (map.get(id) === controller) map.delete(id);
175
- };
176
- }
177
- /** The controller registered under `id` for `manager`, or undefined. */
178
- function getController(manager, id) {
179
- return mapFor(manager).get(id);
180
- }
181
- const grabOffsets = /* @__PURE__ */ new WeakMap();
182
- const noManagerGrab = { current: null };
183
- function setGrabOffset(manager, offset) {
184
- if (!manager) {
185
- noManagerGrab.current = offset;
186
- return;
187
- }
188
- if (offset) grabOffsets.set(manager, offset);
189
- else grabOffsets.delete(manager);
190
- }
191
- function getGrabOffset(manager) {
192
- return (manager ? grabOffsets.get(manager) : noManagerGrab.current) ?? {
193
- x: 0,
194
- y: 0
195
- };
196
- }
197
- //#endregion
198
- //#region src/dnd/dragFlow.ts
199
- /**
200
- * Pure decision helpers for the drag interaction so the tricky bits — grab-offset
201
- * cell mapping, the cross-grid drop lifecycle, and external-drop acceptance — are
202
- * unit-testable without a DOM or dnd-kit.
203
- */
204
- /** Read snapgrid's payload off a dnd-kit drag source. */
205
- function dragData(event) {
206
- return (event.operation.source?.data)?.snapGrid;
207
- }
208
- /** Size/id spec for an external (non-grid) draggable the grid may accept, or null. */
209
- function externalDropSpec(source, dropConfig) {
210
- if (!dropConfig?.enabled || !source) return null;
211
- const data = source.data;
212
- if (data?.snapGrid) return null;
213
- if (dropConfig.accept && !dropConfig.accept(source)) return null;
214
- const spec = data?.snapGridDrop;
215
- return {
216
- i: spec?.i,
217
- w: spec?.w ?? dropConfig.defaultItem?.w ?? 1,
218
- h: spec?.h ?? dropConfig.defaultItem?.h ?? 1
219
- };
220
- }
221
- /**
222
- * Map a client-space pointer to a grid cell, accounting for where *within* the
223
- * dragged tile the pointer grabbed it. Subtracting the grab offset means the
224
- * tile's top-left (not the cursor) maps to the cell, so a received tile's
225
- * placeholder aligns with the floating overlay instead of jumping its corner to
226
- * the cursor. External drops pass `{ x: 0, y: 0 }` (no meaningful grab point).
227
- */
228
- function receiveCell(pointer, gridRect, grabOffset, w, h, pp) {
229
- return (0, _snapgridjs_core.calcXY)(pp, pointer.y - grabOffset.y - gridRect.top, pointer.x - grabOffset.x - gridRect.left, w, h);
230
- }
231
- /**
232
- * Map a keyboard event key to a one-cell grid step while a keyboard drag is
233
- * active, or null for keys snapgrid doesn't own — Enter/Space (drop) and Escape
234
- * (cancel) fall through to dnd-kit's KeyboardSensor.
235
- */
236
- function arrowStep(key) {
237
- switch (key) {
238
- case "ArrowLeft": return [-1, 0];
239
- case "ArrowRight": return [1, 0];
240
- case "ArrowUp": return [0, -1];
241
- case "ArrowDown": return [0, 1];
242
- default: return null;
243
- }
244
- }
245
- /**
246
- * Which grid a drop commits to, as fed to {@link classifyDrop} as `dest`. A
247
- * keyboard drag has no pointer, so it can only ever land in its own grid; a
248
- * pointer drag lands in whichever grid the collision observer resolved (or none).
249
- */
250
- function dropDestination(opts) {
251
- if (opts.keyboard) return opts.myId;
252
- return opts.targetId != null ? String(opts.targetId) : null;
253
- }
254
- /** Pure classification of a drag end. See {@link DropAction}. */
255
- function classifyDrop(s) {
256
- if (s.canceled) {
257
- if (s.kind === "resize") return "cancel-resize";
258
- if (s.ownsItem) return "cancel-move";
259
- return "noop";
260
- }
261
- if (s.kind === "resize") return "commit-resize";
262
- if (s.ownsItem && s.hasData) {
263
- if (s.dest === s.myId && s.kind === "move") return "commit-in-grid";
264
- if (s.dest) return "remove-source";
265
- return "revert";
266
- }
267
- if (s.dest === s.myId && s.kind === "move") return s.hasData ? "commit-dest" : "external-drop";
268
- return "noop";
269
- }
270
- //#endregion
271
- //#region src/dnd/snapToGrid.ts
272
- /**
273
- * Quantizes the dragged item's transform to whole grid cells, so the floating
274
- * <DragOverlay> clone jumps cell-to-cell in lockstep with the (always-snapped)
275
- * placeholder instead of tracking the pointer smoothly. Applied on the item
276
- * draggable; a no-op unless `dragConfig.snapToGrid` is set.
277
- */
278
- var SnapToGrid = class extends _dnd_kit_abstract.Modifier {
279
- apply({ transform }) {
280
- const opts = this.options;
281
- if (!opts?.isEnabled()) return transform;
282
- const pp = opts.getPositionParams();
283
- const colStep = (0, _snapgridjs_core.calcGridColWidth)(pp) + pp.margin[0];
284
- const rowStep = pp.rowHeight + pp.margin[1];
285
- if (colStep <= 0 || rowStep <= 0) return transform;
286
- return {
287
- x: Math.round(transform.x / colStep) * colStep,
288
- y: Math.round(transform.y / rowStep) * rowStep
289
- };
290
- }
291
- };
292
- //#endregion
293
- //#region src/hooks/dndShared.ts
294
- /** Marker attribute placed on resize-handle elements. */
295
- const RESIZE_HANDLE_ATTR = "data-snapgrid-resize-handle";
296
- const NO_FEEDBACK = [_dnd_kit_dom.Feedback.configure({ feedback: "none" })];
297
- /**
298
- * Whether a pointer-down on `target` should NOT start an item move. Pure and
299
- * exported for testing. Honors three rules, in order:
300
- * - never start a move from a resize handle;
301
- * - never start from a region matching `dragConfig.cancel`;
302
- * - if `dragConfig.handle` is set, only start from within it.
303
- */
304
- function shouldPreventItemDrag(target, cfg) {
305
- if (!(target instanceof Element)) return false;
306
- if (target.closest(`[data-snapgrid-resize-handle]`)) return true;
307
- if (cfg?.cancel && target.closest(cfg.cancel)) return true;
308
- if (cfg?.handle && !target.closest(cfg.handle)) return true;
309
- return false;
310
- }
311
- /**
312
- * Sensors for item (move) draggables, built from the drag config: a distance
313
- * activation threshold (so clicks don't start drags) plus handle/cancel/resize
314
- * gating, with the keyboard sensor kept for accessibility.
315
- */
316
- function buildItemSensors(threshold, getDragConfig) {
317
- return [_dnd_kit_dom.PointerSensor.configure({
318
- activationConstraints: () => threshold > 0 ? [new _dnd_kit_dom.PointerActivationConstraints.Distance({ value: threshold })] : void 0,
319
- preventActivation: (event) => shouldPreventItemDrag(event.target, getDragConfig())
320
- }), _dnd_kit_dom.KeyboardSensor];
321
- }
322
- //#endregion
323
10
  //#region src/hooks/useGridController.ts
324
11
  const DEFAULT_HANDLES = ["se"];
325
12
  function itemGateOpen(flag, isStatic) {
326
13
  return isStatic ? flag === true : flag ?? true;
327
14
  }
328
15
  /**
329
- * The grid's brain: owns the {@link GridController}, runs the dnd-kit drag/resize
330
- * monitor for this grid, and writes per-grid config to the controller each render.
331
- * Created by {@link useGridContainer}; items resolve the same controller by their
332
- * `group` (= this grid's id) from the per-manager registry. Consumes the ambient
16
+ * The grid's React seam: owns the {@link GridController} (an observable render
17
+ * bridge), publishes per-grid config to it each render, registers it for id →
18
+ * controller resolution, and attaches the manager-wide {@link SnapGridEngine}.
19
+ *
20
+ * The drag/resize *orchestration* lives in the engine (one per manager), not
21
+ * here — this hook only wires the React-specific parts: the controller, the item
22
+ * sensors/modifiers descriptors, the draggable/resizable gates, and the config
23
+ * the engine reads. Created by {@link useGridContainer}; items resolve the same
24
+ * controller by their `group` (= this grid's id). Consumes the ambient
333
25
  * `DragDropProvider` — it does not mint one.
334
26
  */
335
27
  function useGridController(opts) {
@@ -342,271 +34,27 @@ function useGridController(opts) {
342
34
  const positionParams = (0, react.useMemo)(() => (0, _snapgridjs_core.toPositionParams)(gridConfig, opts.width), [gridConfig, opts.width]);
343
35
  const compactor = opts.compactor ?? _snapgridjs_core.verticalCompactor;
344
36
  const manager = (0, _dnd_kit_react.useDragDropManager)();
345
- const controller = (0, _dnd_kit_react.useInstance)((m) => new GridController(containerId, opts.layout, m ?? void 0));
37
+ const controller = (0, _dnd_kit_react.useInstance)((m) => new _snapgridjs_dnd.GridController(containerId, opts.layout, m ?? void 0));
346
38
  controller.setCommitted(opts.layout);
347
39
  if (controller.id !== containerId) controller.setId(containerId);
348
40
  const optsRef = (0, react.useRef)(opts);
349
41
  optsRef.current = opts;
350
42
  const ppRef = (0, react.useRef)(positionParams);
351
43
  ppRef.current = positionParams;
352
- const gridRef = (0, react.useRef)(gridConfig);
353
- gridRef.current = gridConfig;
354
- const compactorRef = (0, react.useRef)(compactor);
355
- compactorRef.current = compactor;
356
- const containerIdRef = (0, react.useRef)(containerId);
357
- containerIdRef.current = containerId;
358
- const managerRef = (0, react.useRef)(manager);
359
- managerRef.current = manager;
360
- const sessionRef = (0, react.useRef)(null);
361
- const containerElRef = (0, react.useRef)(null);
362
- const keyboardRef = (0, react.useRef)(false);
363
- const dropSpecRef = (0, react.useRef)(null);
364
- const dropCounterRef = (0, react.useRef)(0);
365
- registerController(manager, containerId, controller);
366
- (0, react.useEffect)(() => registerController(manager, containerId, controller), [
44
+ (0, _snapgridjs_dnd.registerController)(manager, containerId, controller);
45
+ (0, react.useEffect)(() => (0, _snapgridjs_dnd.registerController)(manager, containerId, controller), [
367
46
  manager,
368
47
  containerId,
369
48
  controller
370
49
  ]);
371
- const committedById = (0, react.useMemo)(() => new Map(opts.layout.map((it) => [it.i, it])), [opts.layout]);
372
- const committedByIdRef = (0, react.useRef)(committedById);
373
- committedByIdRef.current = committedById;
374
- const setSessionBoth = (0, react.useCallback)((next) => {
375
- sessionRef.current = next;
376
- controller.setSession(next);
377
- }, [controller]);
378
- const setKeyboard = (0, react.useCallback)((value) => {
379
- keyboardRef.current = value;
380
- controller.setKeyboard(value);
381
- }, [controller]);
382
- const setContainerElement = (0, react.useCallback)((element) => {
383
- containerElRef.current = element;
384
- }, []);
385
- /**
386
- * Is THIS grid the drop target dnd-kit's collision observer resolved? Both the
387
- * move-phase preview and the drop-phase commit read `operation.target`, so they
388
- * always agree on which grid wins (one oracle), including when grids overlap.
389
- */
390
- const overMe = (0, react.useCallback)((target) => target?.id === containerIdRef.current, []);
391
- /** Map a client-space pointer to a grid cell within THIS grid (see {@link receiveCell}). */
392
- const cellFromPointer = (0, react.useCallback)((p, item) => {
393
- const el = containerElRef.current;
394
- if (!el) return null;
395
- return receiveCell(p, el.getBoundingClientRect(), getGrabOffset(managerRef.current), item.w, item.h, ppRef.current);
396
- }, []);
397
- const ctx = (0, react.useCallback)(() => ({
398
- positionParams: ppRef.current,
399
- compactor: compactorRef.current,
400
- cols: gridRef.current.cols
401
- }), []);
402
- const handleDragStart = (0, react.useCallback)((event) => {
403
- setKeyboard(false);
404
- const data = dragData(event);
405
- if (!data) {
406
- const spec = externalDropSpec(event.operation.source, optsRef.current.dropConfig);
407
- if (spec) {
408
- dropCounterRef.current += 1;
409
- dropSpecRef.current = {
410
- i: spec.i ?? `${containerIdRef.current}-dropped-${dropCounterRef.current}`,
411
- w: spec.w,
412
- h: spec.h
413
- };
414
- } else dropSpecRef.current = null;
415
- return;
416
- }
417
- dropSpecRef.current = null;
418
- const layout = optsRef.current.layout;
419
- const item = layout.find((it) => it.i === data.itemId);
420
- const p = event.operation.position.current;
421
- const pointer = {
422
- x: p.x,
423
- y: p.y
424
- };
425
- if (data.kind === "resize") {
426
- if (!item) return;
427
- setSessionBoth((0, _snapgridjs_core.beginResize)(layout, {
428
- item,
429
- rect: (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h),
430
- pointer
431
- }, data.handle));
432
- optsRef.current.onResizeStart?.(layout, item, item, item, event.operation.activatorEvent, null);
433
- return;
434
- }
435
- if (item) {
436
- setKeyboard(event.operation.activatorEvent instanceof KeyboardEvent);
437
- const rect = (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h);
438
- setSessionBoth((0, _snapgridjs_core.beginDrag)(layout, {
439
- item,
440
- left: rect.left,
441
- top: rect.top,
442
- pointer
443
- }));
444
- const cr = (event.operation.source?.element)?.getBoundingClientRect();
445
- if (cr) setGrabOffset(managerRef.current, {
446
- x: pointer.x - cr.left,
447
- y: pointer.y - cr.top
448
- });
449
- optsRef.current.onDragStart?.(layout, item, item, item, event.operation.activatorEvent, null);
450
- }
451
- }, [setSessionBoth, setKeyboard]);
452
- const handleDragMove = (0, react.useCallback)((event) => {
453
- if (keyboardRef.current) return;
454
- const p = event.operation.position.current;
455
- const pointer = {
456
- x: p.x,
457
- y: p.y
458
- };
459
- const current = sessionRef.current;
460
- if (current?.kind === "resize") {
461
- const next = (0, _snapgridjs_core.dragResize)(current, pointer, ctx());
462
- setSessionBoth(next);
463
- optsRef.current.onResize?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
464
- return;
465
- }
466
- const target = event.operation.target;
467
- const data = dragData(event);
468
- if (!data) {
469
- const spec = dropSpecRef.current;
470
- if (spec && overMe(target)) {
471
- const foreign = {
472
- i: spec.i,
473
- x: 0,
474
- y: 0,
475
- w: spec.w,
476
- h: spec.h
477
- };
478
- const committed = optsRef.current.layout;
479
- const cell = cellFromPointer(pointer, foreign) ?? {
480
- x: 0,
481
- y: 0
482
- };
483
- setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
484
- } else if (sessionRef.current) setSessionBoth(null);
485
- return;
486
- }
487
- if (data.kind !== "move") return;
488
- const here = overMe(target);
489
- if (committedByIdRef.current.has(data.itemId)) {
490
- const source = current?.kind === "move" ? current : null;
491
- if (!source) return;
492
- let next;
493
- if (here) next = (0, _snapgridjs_core.dragTo)(source, pointer, ctx());
494
- else next = {
495
- ...source,
496
- preview: source.committed,
497
- placeholder: null
498
- };
499
- setSessionBoth(next);
500
- optsRef.current.onDrag?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
501
- return;
502
- }
503
- if (here) {
504
- const foreign = data.item;
505
- const committed = optsRef.current.layout;
506
- const cell = cellFromPointer(pointer, foreign) ?? {
507
- x: 0,
508
- y: 0
509
- };
510
- setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
511
- } else if (sessionRef.current) setSessionBoth(null);
512
- }, [
513
- setSessionBoth,
514
- overMe,
515
- cellFromPointer,
516
- ctx
517
- ]);
518
- const handleDragEnd = (0, react.useCallback)((event) => {
519
- const current = sessionRef.current;
520
- const data = dragData(event);
521
- const myId = containerIdRef.current;
522
- const dest = dropDestination({
523
- keyboard: keyboardRef.current,
524
- targetId: event.operation.target?.id,
525
- myId
526
- });
527
- const ownsItem = data ? committedByIdRef.current.has(data.itemId) : false;
528
- setGrabOffset(managerRef.current, null);
529
- const native = event.nativeEvent ?? null;
530
- const o = optsRef.current;
531
- const action = classifyDrop({
532
- kind: current?.kind ?? null,
533
- canceled: event.canceled,
534
- ownsItem,
535
- hasData: !!data,
536
- dest,
537
- myId
538
- });
539
- switch (action) {
540
- case "cancel-resize":
541
- o.onResizeStop?.(o.layout, current?.anchor.item ?? null, null, null, native, null);
542
- break;
543
- case "cancel-move":
544
- o.onDragStop?.(o.layout, current?.anchor.item ?? null, null, null, native, null);
545
- break;
546
- case "commit-resize":
547
- if (current) {
548
- o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
549
- o.onResizeStop?.(current.preview, current.anchor.item, current.placeholder, current.placeholder, native, null);
550
- }
551
- break;
552
- case "commit-in-grid":
553
- case "remove-source":
554
- case "revert":
555
- if (action === "commit-in-grid" && current) o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
556
- else if (action === "remove-source" && data) {
557
- const { compactor: c, cols } = ctx();
558
- o.onLayoutChange?.((0, _snapgridjs_core.removeItemWithCompactor)(o.layout, data.itemId, {
559
- compactor: c,
560
- cols
561
- }));
562
- }
563
- o.onDragStop?.(current?.preview ?? o.layout, current?.anchor.item ?? null, current?.placeholder ?? null, current?.placeholder ?? null, native, null);
564
- break;
565
- case "commit-dest":
566
- if (current) o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
567
- break;
568
- case "external-drop":
569
- if (current) {
570
- const committed = (0, _snapgridjs_core.commitLayout)(current);
571
- const dropped = committed.find((it) => it.i === current.activeId);
572
- if (dropped) o.onDrop?.(committed, dropped, native);
573
- }
574
- break;
575
- }
576
- dropSpecRef.current = null;
577
- setKeyboard(false);
578
- setSessionBoth(null);
579
- }, [
580
- setSessionBoth,
581
- setKeyboard,
582
- ctx
583
- ]);
584
50
  (0, react.useEffect)(() => {
585
- const onKeyDown = (e) => {
586
- if (!keyboardRef.current) return;
587
- const session = sessionRef.current;
588
- if (!session || session.kind !== "move") return;
589
- const step = arrowStep(e.key);
590
- if (!step) return;
591
- e.preventDefault();
592
- e.stopImmediatePropagation();
593
- setSessionBoth((0, _snapgridjs_core.nudge)(session, step[0], step[1], ctx()));
594
- };
595
- window.addEventListener("keydown", onKeyDown, true);
596
- return () => window.removeEventListener("keydown", onKeyDown, true);
597
- }, [ctx, setSessionBoth]);
598
- (0, _dnd_kit_react.useDragDropMonitor)((0, react.useMemo)(() => ({
599
- onDragStart: handleDragStart,
600
- onDragMove: handleDragMove,
601
- onDragEnd: handleDragEnd
602
- }), [
603
- handleDragStart,
604
- handleDragMove,
605
- handleDragEnd
606
- ]));
51
+ if (!manager) return;
52
+ return (0, _snapgridjs_dnd.attachEngine)(manager);
53
+ }, [manager]);
54
+ const committedById = (0, react.useMemo)(() => new Map(opts.layout.map((it) => [it.i, it])), [opts.layout]);
607
55
  const dragThreshold = opts.dragConfig?.threshold ?? 3;
608
- const itemSensors = (0, react.useMemo)(() => buildItemSensors(dragThreshold, () => optsRef.current.dragConfig), [dragThreshold]);
609
- const itemModifiers = (0, react.useMemo)(() => [SnapToGrid.configure({
56
+ const itemSensors = (0, react.useMemo)(() => (0, _snapgridjs_dnd.buildItemSensors)(dragThreshold, () => optsRef.current.dragConfig), [dragThreshold]);
57
+ const itemModifiers = (0, react.useMemo)(() => [_snapgridjs_dnd.SnapToGrid.configure({
610
58
  getPositionParams: () => ppRef.current,
611
59
  isEnabled: () => optsRef.current.dragConfig?.snapToGrid ?? false
612
60
  })], []);
@@ -634,6 +82,25 @@ function useGridController(opts) {
634
82
  ]);
635
83
  const defaultHandles = opts.resizeConfig?.handles;
636
84
  const resizeHandlesFor = (0, react.useCallback)((id) => committedById.get(id)?.resizeHandles ?? defaultHandles ?? DEFAULT_HANDLES, [committedById, defaultHandles]);
85
+ const callbacks = (0, react.useMemo)(() => ({
86
+ onDragStart: opts.onDragStart,
87
+ onDrag: opts.onDrag,
88
+ onDragStop: opts.onDragStop,
89
+ onResizeStart: opts.onResizeStart,
90
+ onResize: opts.onResize,
91
+ onResizeStop: opts.onResizeStop,
92
+ onLayoutChange: opts.onLayoutChange,
93
+ onDrop: opts.onDrop
94
+ }), [
95
+ opts.onDragStart,
96
+ opts.onDrag,
97
+ opts.onDragStop,
98
+ opts.onResizeStart,
99
+ opts.onResize,
100
+ opts.onResizeStop,
101
+ opts.onLayoutChange,
102
+ opts.onDrop
103
+ ]);
637
104
  controller.setConfig({
638
105
  positionParams,
639
106
  gridConfig,
@@ -644,13 +111,15 @@ function useGridController(opts) {
644
111
  isItemDraggable,
645
112
  isItemResizable,
646
113
  resizeHandlesFor,
647
- setContainerElement
114
+ compactor,
115
+ dragConfig: opts.dragConfig,
116
+ dropConfig: opts.dropConfig,
117
+ callbacks
648
118
  });
649
119
  return controller;
650
120
  }
651
121
  //#endregion
652
122
  //#region src/hooks/useGridContainer.ts
653
- const GRID_COLLISION_PRIORITY = 10;
654
123
  /** Total container height in pixels for the given number of occupied rows. */
655
124
  function containerHeight(rows, grid) {
656
125
  const padY = (grid.containerPadding ?? grid.margin)[1];
@@ -665,20 +134,25 @@ function containerHeight(rows, grid) {
665
134
  */
666
135
  function useGridContainer(opts) {
667
136
  const controller = useGridController(opts);
668
- const { width, autoSize, gridConfig, setContainerElement } = controller.config;
137
+ const { width, autoSize, gridConfig } = controller.config;
138
+ const gridElRef = (0, react.useRef)(null);
669
139
  const { ref, isDropTarget } = (0, _dnd_kit_react.useDroppable)({
670
140
  id: controller.id,
671
141
  type: "grid",
672
142
  accept: (source) => {
143
+ const srcEl = (0, _snapgridjs_dnd.domElement)(source);
144
+ if (srcEl && gridElRef.current && srcEl.contains(gridElRef.current)) return false;
673
145
  if (source.type === "grid-item") return true;
674
146
  return source.data?.snapGridDrop != null;
675
147
  },
676
- collisionPriority: GRID_COLLISION_PRIORITY
148
+ collisionDetector: _snapgridjs_dnd.gridCollisionDetector
677
149
  });
678
150
  const setRef = (0, react.useCallback)((element) => {
679
151
  ref(element);
680
- setContainerElement(element);
681
- }, [ref, setContainerElement]);
152
+ controller.element = element;
153
+ gridElRef.current = element;
154
+ if (element) element.setAttribute(_snapgridjs_dnd.SNAPGRID_GRID_ATTR, "");
155
+ }, [ref, controller]);
682
156
  const renderedLayout = (0, react.useSyncExternalStore)(controller.subscribe, controller.renderedSnapshot, controller.renderedSnapshot);
683
157
  return {
684
158
  containerProps: {
@@ -707,7 +181,7 @@ const REFLOW_TRANSITION = `transform 150ms ${REFLOW_EASING}, width 150ms ${REFLO
707
181
  * grid / `DragDropProvider`.
708
182
  */
709
183
  function useResolveController(group) {
710
- const controller = getController((0, _dnd_kit_react.useDragDropManager)(), group);
184
+ const controller = (0, _snapgridjs_dnd.getController)((0, _dnd_kit_react.useDragDropManager)(), group);
711
185
  if (!controller) throw new Error(`snapgrid: no grid found for group "${group}". A grid item must pass the group returned by its grid's useGridContainer, and render inside a <DragDropProvider> (or use <GridLayout>, which wires this for you).`);
712
186
  return controller;
713
187
  }
@@ -745,8 +219,13 @@ function useGridItem(id, group) {
745
219
  const data = (0, react.useMemo)(() => ({ snapGrid: {
746
220
  kind: "move",
747
221
  itemId: id,
748
- item
749
- } }), [id, item]);
222
+ item,
223
+ group
224
+ } }), [
225
+ id,
226
+ item,
227
+ group
228
+ ]);
750
229
  const { ref: sortableRef, handleRef, isDragging } = (0, _dnd_kit_react_sortable.useSortable)({
751
230
  id,
752
231
  index: controller.itemIndex(id),
@@ -860,17 +339,18 @@ function useGridResizeHandle(itemId, handle, group) {
860
339
  const { ref } = (0, _dnd_kit_react.useDraggable)({
861
340
  id: `${itemId}::resize::${handle}`,
862
341
  disabled: !controller.config?.isItemResizable(itemId),
863
- plugins: NO_FEEDBACK,
342
+ plugins: _snapgridjs_dnd.NO_FEEDBACK,
864
343
  data: { snapGrid: {
865
344
  kind: "resize",
866
345
  itemId,
867
- handle
346
+ handle,
347
+ group
868
348
  } }
869
349
  });
870
350
  const { isResizing } = (0, react.useSyncExternalStore)(controller.subscribe, () => controller.resizeSnapshot(itemId), () => controller.resizeSnapshot(itemId));
871
351
  return {
872
352
  ref,
873
- handleProps: { [RESIZE_HANDLE_ATTR]: true },
353
+ handleProps: { [_snapgridjs_dnd.RESIZE_HANDLE_ATTR]: true },
874
354
  isResizing
875
355
  };
876
356
  }