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