@snapgridjs/react 0.1.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/index.cjs +1067 -0
- package/dist/index.d.cts +390 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +390 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +999 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +78 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _dnd_kit_react = require("@dnd-kit/react");
|
|
3
|
+
let _snapgridjs_core = require("@snapgridjs/core");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let _dnd_kit_dom = require("@dnd-kit/dom");
|
|
6
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
|
+
let react_dom = require("react-dom");
|
|
8
|
+
//#region src/context.ts
|
|
9
|
+
const GridContext = (0, react.createContext)(null);
|
|
10
|
+
/** Read the grid runtime; throws if used outside a `SnapGridProvider`. */
|
|
11
|
+
function useGridRuntime() {
|
|
12
|
+
const runtime = (0, react.useContext)(GridContext);
|
|
13
|
+
if (!runtime) throw new Error("snapgrid: hooks and components must be rendered inside a <SnapGridProvider> (or <GridLayout>).");
|
|
14
|
+
return runtime;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/dragFlow.ts
|
|
18
|
+
/**
|
|
19
|
+
* Pure decision helpers for the drag interaction, extracted from
|
|
20
|
+
* {@link SnapGridProvider} so the tricky bits — grab-offset cell mapping and the
|
|
21
|
+
* cross-grid drop lifecycle — are unit-testable without a DOM or dnd-kit.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Map a client-space pointer to a grid cell, accounting for where *within* the
|
|
25
|
+
* dragged tile the pointer grabbed it. Subtracting the grab offset means the
|
|
26
|
+
* tile's top-left (not the cursor) maps to the cell, so a received tile's
|
|
27
|
+
* placeholder aligns with the floating overlay instead of jumping its corner to
|
|
28
|
+
* the cursor. External drops pass `{ x: 0, y: 0 }` (no meaningful grab point).
|
|
29
|
+
*/
|
|
30
|
+
function receiveCell(pointer, gridRect, grabOffset, w, h, pp) {
|
|
31
|
+
return (0, _snapgridjs_core.calcXY)(pp, pointer.y - grabOffset.y - gridRect.top, pointer.x - grabOffset.x - gridRect.left, w, h);
|
|
32
|
+
}
|
|
33
|
+
/** Pure classification of a drag end. See {@link DropAction}. */
|
|
34
|
+
function classifyDrop(s) {
|
|
35
|
+
if (s.canceled) {
|
|
36
|
+
if (s.kind === "resize") return "cancel-resize";
|
|
37
|
+
if (s.ownsItem) return "cancel-move";
|
|
38
|
+
return "noop";
|
|
39
|
+
}
|
|
40
|
+
if (s.kind === "resize") return "commit-resize";
|
|
41
|
+
if (s.ownsItem && s.hasData) {
|
|
42
|
+
if (s.dest === s.myId && s.kind === "move") return "commit-in-grid";
|
|
43
|
+
if (s.dest) return "remove-source";
|
|
44
|
+
return "revert";
|
|
45
|
+
}
|
|
46
|
+
if (s.dest === s.myId && s.kind === "move") return s.hasData ? "commit-dest" : "external-drop";
|
|
47
|
+
return "noop";
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/grouping.ts
|
|
51
|
+
function createGridRegistry() {
|
|
52
|
+
const grids = /* @__PURE__ */ new Map();
|
|
53
|
+
let grabOffset = null;
|
|
54
|
+
return {
|
|
55
|
+
register(id, getRect) {
|
|
56
|
+
grids.set(id, getRect);
|
|
57
|
+
return () => {
|
|
58
|
+
if (grids.get(id) === getRect) grids.delete(id);
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
gridAt(point) {
|
|
62
|
+
for (const [id, getRect] of grids) {
|
|
63
|
+
const r = getRect();
|
|
64
|
+
if (r && point.x >= r.left && point.x <= r.right && point.y >= r.top && point.y <= r.bottom) return id;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
setGrabOffset(offset) {
|
|
69
|
+
grabOffset = offset;
|
|
70
|
+
},
|
|
71
|
+
getGrabOffset() {
|
|
72
|
+
return grabOffset ?? {
|
|
73
|
+
x: 0,
|
|
74
|
+
y: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Non-null when grids are wrapped in a `<SnapGridGroup>` (shared cross-grid registry). */
|
|
80
|
+
const SnapGridGroupContext = (0, react.createContext)(null);
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/hooks/dndShared.ts
|
|
83
|
+
/** Marker attribute placed on resize-handle elements. */
|
|
84
|
+
const RESIZE_HANDLE_ATTR = "data-snapgrid-resize-handle";
|
|
85
|
+
const NO_FEEDBACK = [_dnd_kit_dom.Feedback.configure({ feedback: "none" })];
|
|
86
|
+
/**
|
|
87
|
+
* Whether a pointer-down on `target` should NOT start an item move. Pure and
|
|
88
|
+
* exported for testing. Honors three rules, in order:
|
|
89
|
+
* - never start a move from a resize handle;
|
|
90
|
+
* - never start from a region matching `dragConfig.cancel`;
|
|
91
|
+
* - if `dragConfig.handle` is set, only start from within it.
|
|
92
|
+
*/
|
|
93
|
+
function shouldPreventItemDrag(target, cfg) {
|
|
94
|
+
if (!(target instanceof Element)) return false;
|
|
95
|
+
if (target.closest(`[data-snapgrid-resize-handle]`)) return true;
|
|
96
|
+
if (cfg?.cancel && target.closest(cfg.cancel)) return true;
|
|
97
|
+
if (cfg?.handle && !target.closest(cfg.handle)) return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Sensors for item (move) draggables, built from the drag config: a distance
|
|
102
|
+
* activation threshold (so clicks don't start drags) plus handle/cancel/resize
|
|
103
|
+
* gating, with the keyboard sensor kept for accessibility.
|
|
104
|
+
*/
|
|
105
|
+
function buildItemSensors(threshold, getDragConfig) {
|
|
106
|
+
return [_dnd_kit_dom.PointerSensor.configure({
|
|
107
|
+
activationConstraints: () => threshold > 0 ? [new _dnd_kit_dom.PointerActivationConstraints.Distance({ value: threshold })] : void 0,
|
|
108
|
+
preventActivation: (event) => shouldPreventItemDrag(event.target, getDragConfig())
|
|
109
|
+
}), _dnd_kit_dom.KeyboardSensor];
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/SnapGridProvider.tsx
|
|
113
|
+
const DEFAULT_HANDLES = ["se"];
|
|
114
|
+
/** Read snapgrid's payload off a dnd-kit drag source. */
|
|
115
|
+
function dragData(event) {
|
|
116
|
+
return (event.operation.source?.data)?.snapGrid;
|
|
117
|
+
}
|
|
118
|
+
/** Size/id spec for an external (non-grid) draggable the grid may accept, or null. */
|
|
119
|
+
function externalDropSpec(source, dropConfig) {
|
|
120
|
+
if (!dropConfig?.enabled || !source) return null;
|
|
121
|
+
const data = source.data;
|
|
122
|
+
if (data?.snapGrid) return null;
|
|
123
|
+
if (dropConfig.accept && !dropConfig.accept(source)) return null;
|
|
124
|
+
const spec = data?.snapGridDrop;
|
|
125
|
+
return {
|
|
126
|
+
i: spec?.i,
|
|
127
|
+
w: spec?.w ?? dropConfig.defaultItem?.w ?? 1,
|
|
128
|
+
h: spec?.h ?? dropConfig.defaultItem?.h ?? 1
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Headless provider for a grid. Standalone grids get their own isolated dnd-kit
|
|
133
|
+
* provider; grids inside a {@link SnapGridGroup} share that group's provider and
|
|
134
|
+
* registry so tiles can be dragged between them. Owns this grid's drag/resize
|
|
135
|
+
* session; the consumer owns all markup/styling.
|
|
136
|
+
*/
|
|
137
|
+
function SnapGridProvider(props) {
|
|
138
|
+
const groupRegistry = (0, react.useContext)(SnapGridGroupContext);
|
|
139
|
+
const runtime = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapGridRuntime, {
|
|
140
|
+
groupRegistry,
|
|
141
|
+
...props
|
|
142
|
+
});
|
|
143
|
+
return groupRegistry ? runtime : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_dnd_kit_react.DragDropProvider, { children: runtime });
|
|
144
|
+
}
|
|
145
|
+
function SnapGridRuntime(props) {
|
|
146
|
+
const autoId = (0, react.useId)();
|
|
147
|
+
const containerId = props.id ?? autoId;
|
|
148
|
+
const gridConfig = (0, react.useMemo)(() => ({
|
|
149
|
+
..._snapgridjs_core.defaultGridConfig,
|
|
150
|
+
...props.gridConfig
|
|
151
|
+
}), [props.gridConfig]);
|
|
152
|
+
const positionParams = (0, react.useMemo)(() => (0, _snapgridjs_core.toPositionParams)(gridConfig, props.width), [gridConfig, props.width]);
|
|
153
|
+
const compactor = props.compactor ?? _snapgridjs_core.verticalCompactor;
|
|
154
|
+
const [session, setSession] = (0, react.useState)(null);
|
|
155
|
+
const [overlay, setOverlay] = (0, react.useState)(null);
|
|
156
|
+
const propsRef = (0, react.useRef)(props);
|
|
157
|
+
propsRef.current = props;
|
|
158
|
+
const ppRef = (0, react.useRef)(positionParams);
|
|
159
|
+
ppRef.current = positionParams;
|
|
160
|
+
const gridRef = (0, react.useRef)(gridConfig);
|
|
161
|
+
gridRef.current = gridConfig;
|
|
162
|
+
const compactorRef = (0, react.useRef)(compactor);
|
|
163
|
+
compactorRef.current = compactor;
|
|
164
|
+
const containerIdRef = (0, react.useRef)(containerId);
|
|
165
|
+
containerIdRef.current = containerId;
|
|
166
|
+
const sessionRef = (0, react.useRef)(null);
|
|
167
|
+
const containerElRef = (0, react.useRef)(null);
|
|
168
|
+
const keyboardRef = (0, react.useRef)(false);
|
|
169
|
+
const dropSpecRef = (0, react.useRef)(null);
|
|
170
|
+
const dropCounterRef = (0, react.useRef)(0);
|
|
171
|
+
const localRegistryRef = (0, react.useRef)(null);
|
|
172
|
+
if (!localRegistryRef.current) localRegistryRef.current = createGridRegistry();
|
|
173
|
+
const registry = props.groupRegistry ?? localRegistryRef.current;
|
|
174
|
+
const registryRef = (0, react.useRef)(registry);
|
|
175
|
+
registryRef.current = registry;
|
|
176
|
+
(0, react.useEffect)(() => registry.register(containerId, () => containerElRef.current?.getBoundingClientRect() ?? null), [registry, containerId]);
|
|
177
|
+
const committedById = (0, react.useMemo)(() => new Map(props.layout.map((it) => [it.i, it])), [props.layout]);
|
|
178
|
+
const committedByIdRef = (0, react.useRef)(committedById);
|
|
179
|
+
committedByIdRef.current = committedById;
|
|
180
|
+
const setSessionBoth = (0, react.useCallback)((next) => {
|
|
181
|
+
sessionRef.current = next;
|
|
182
|
+
setSession(next);
|
|
183
|
+
}, []);
|
|
184
|
+
const setContainerElement = (0, react.useCallback)((element) => {
|
|
185
|
+
containerElRef.current = element;
|
|
186
|
+
}, []);
|
|
187
|
+
/**
|
|
188
|
+
* Is THIS grid the one under `p` (client coords)? Resolved through the
|
|
189
|
+
* registry (not a self-only rect test) so the move-phase preview and the
|
|
190
|
+
* drop-phase commit — which both go through `gridAt` — always agree on which
|
|
191
|
+
* grid wins when grids overlap (`gridAt` returns a single first match).
|
|
192
|
+
*/
|
|
193
|
+
const overMe = (0, react.useCallback)((p) => registryRef.current.gridAt(p) === containerIdRef.current, []);
|
|
194
|
+
/** Map a client-space pointer to a grid cell within THIS grid (see {@link receiveCell}). */
|
|
195
|
+
const cellFromPointer = (0, react.useCallback)((p, item) => {
|
|
196
|
+
const el = containerElRef.current;
|
|
197
|
+
if (!el) return null;
|
|
198
|
+
return receiveCell(p, el.getBoundingClientRect(), registryRef.current.getGrabOffset(), item.w, item.h, ppRef.current);
|
|
199
|
+
}, []);
|
|
200
|
+
const ctx = (0, react.useCallback)(() => ({
|
|
201
|
+
positionParams: ppRef.current,
|
|
202
|
+
compactor: compactorRef.current,
|
|
203
|
+
cols: gridRef.current.cols
|
|
204
|
+
}), []);
|
|
205
|
+
const handleDragStart = (0, react.useCallback)((event) => {
|
|
206
|
+
keyboardRef.current = false;
|
|
207
|
+
const data = dragData(event);
|
|
208
|
+
if (!data) {
|
|
209
|
+
const spec = externalDropSpec(event.operation.source, propsRef.current.dropConfig);
|
|
210
|
+
if (spec) {
|
|
211
|
+
dropCounterRef.current += 1;
|
|
212
|
+
dropSpecRef.current = {
|
|
213
|
+
i: spec.i ?? `${containerIdRef.current}-dropped-${dropCounterRef.current}`,
|
|
214
|
+
w: spec.w,
|
|
215
|
+
h: spec.h
|
|
216
|
+
};
|
|
217
|
+
} else dropSpecRef.current = null;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
dropSpecRef.current = null;
|
|
221
|
+
const layout = propsRef.current.layout;
|
|
222
|
+
const item = layout.find((it) => it.i === data.itemId);
|
|
223
|
+
const p = event.operation.position.current;
|
|
224
|
+
const pointer = {
|
|
225
|
+
x: p.x,
|
|
226
|
+
y: p.y
|
|
227
|
+
};
|
|
228
|
+
if (data.kind === "resize") {
|
|
229
|
+
if (!item) return;
|
|
230
|
+
setSessionBoth((0, _snapgridjs_core.beginResize)(layout, {
|
|
231
|
+
item,
|
|
232
|
+
rect: (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h),
|
|
233
|
+
pointer
|
|
234
|
+
}, data.handle));
|
|
235
|
+
propsRef.current.onResizeStart?.(layout, item, item, item, event.operation.activatorEvent, null);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (item) {
|
|
239
|
+
keyboardRef.current = event.operation.activatorEvent instanceof KeyboardEvent;
|
|
240
|
+
const rect = (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h);
|
|
241
|
+
setSessionBoth((0, _snapgridjs_core.beginDrag)(layout, {
|
|
242
|
+
item,
|
|
243
|
+
left: rect.left,
|
|
244
|
+
top: rect.top,
|
|
245
|
+
pointer
|
|
246
|
+
}));
|
|
247
|
+
const cr = (event.operation.source?.element)?.getBoundingClientRect();
|
|
248
|
+
if (cr) {
|
|
249
|
+
registryRef.current.setGrabOffset({
|
|
250
|
+
x: pointer.x - cr.left,
|
|
251
|
+
y: pointer.y - cr.top
|
|
252
|
+
});
|
|
253
|
+
setOverlay({
|
|
254
|
+
item,
|
|
255
|
+
left: cr.left,
|
|
256
|
+
top: cr.top,
|
|
257
|
+
width: cr.width,
|
|
258
|
+
height: cr.height
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
propsRef.current.onDragStart?.(layout, item, item, item, event.operation.activatorEvent, null);
|
|
262
|
+
}
|
|
263
|
+
}, [setSessionBoth]);
|
|
264
|
+
const handleDragMove = (0, react.useCallback)((event) => {
|
|
265
|
+
if (keyboardRef.current) return;
|
|
266
|
+
const p = event.operation.position.current;
|
|
267
|
+
const pointer = {
|
|
268
|
+
x: p.x,
|
|
269
|
+
y: p.y
|
|
270
|
+
};
|
|
271
|
+
const current = sessionRef.current;
|
|
272
|
+
if (current?.kind === "resize") {
|
|
273
|
+
const next = (0, _snapgridjs_core.dragResize)(current, pointer, ctx());
|
|
274
|
+
setSessionBoth(next);
|
|
275
|
+
propsRef.current.onResize?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const data = dragData(event);
|
|
279
|
+
if (!data) {
|
|
280
|
+
const spec = dropSpecRef.current;
|
|
281
|
+
if (spec && overMe(pointer)) {
|
|
282
|
+
const foreign = {
|
|
283
|
+
i: spec.i,
|
|
284
|
+
x: 0,
|
|
285
|
+
y: 0,
|
|
286
|
+
w: spec.w,
|
|
287
|
+
h: spec.h
|
|
288
|
+
};
|
|
289
|
+
const committed = propsRef.current.layout;
|
|
290
|
+
const cell = cellFromPointer(pointer, foreign) ?? {
|
|
291
|
+
x: 0,
|
|
292
|
+
y: 0
|
|
293
|
+
};
|
|
294
|
+
setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
|
|
295
|
+
} else if (sessionRef.current) setSessionBoth(null);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (data.kind !== "move") return;
|
|
299
|
+
const here = overMe(pointer);
|
|
300
|
+
if (committedByIdRef.current.has(data.itemId)) {
|
|
301
|
+
const source = current?.kind === "move" ? current : null;
|
|
302
|
+
if (!source) return;
|
|
303
|
+
let next;
|
|
304
|
+
if (here) next = (0, _snapgridjs_core.dragTo)(source, pointer, ctx());
|
|
305
|
+
else next = {
|
|
306
|
+
...source,
|
|
307
|
+
preview: source.committed,
|
|
308
|
+
placeholder: null
|
|
309
|
+
};
|
|
310
|
+
setSessionBoth(next);
|
|
311
|
+
const grab = registryRef.current.getGrabOffset();
|
|
312
|
+
let oLeft = pointer.x - grab.x;
|
|
313
|
+
let oTop = pointer.y - grab.y;
|
|
314
|
+
if (propsRef.current.dragConfig?.snapToGrid && here && next.placeholder) {
|
|
315
|
+
const rect = containerElRef.current?.getBoundingClientRect();
|
|
316
|
+
if (rect) {
|
|
317
|
+
const cell = (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, next.placeholder.x, next.placeholder.y, next.placeholder.w, next.placeholder.h);
|
|
318
|
+
oLeft = rect.left + cell.left;
|
|
319
|
+
oTop = rect.top + cell.top;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
setOverlay((o) => o ? {
|
|
323
|
+
...o,
|
|
324
|
+
left: oLeft,
|
|
325
|
+
top: oTop
|
|
326
|
+
} : o);
|
|
327
|
+
propsRef.current.onDrag?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (here) {
|
|
331
|
+
const foreign = data.item;
|
|
332
|
+
const committed = propsRef.current.layout;
|
|
333
|
+
const cell = cellFromPointer(pointer, foreign) ?? {
|
|
334
|
+
x: 0,
|
|
335
|
+
y: 0
|
|
336
|
+
};
|
|
337
|
+
setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
|
|
338
|
+
} else if (sessionRef.current) setSessionBoth(null);
|
|
339
|
+
}, [
|
|
340
|
+
setSessionBoth,
|
|
341
|
+
overMe,
|
|
342
|
+
cellFromPointer,
|
|
343
|
+
ctx
|
|
344
|
+
]);
|
|
345
|
+
const handleDragEnd = (0, react.useCallback)((event) => {
|
|
346
|
+
const current = sessionRef.current;
|
|
347
|
+
const data = dragData(event);
|
|
348
|
+
const p = event.operation.position.current;
|
|
349
|
+
const myId = containerIdRef.current;
|
|
350
|
+
const dest = keyboardRef.current ? myId : registryRef.current.gridAt({
|
|
351
|
+
x: p.x,
|
|
352
|
+
y: p.y
|
|
353
|
+
});
|
|
354
|
+
const ownsItem = data ? committedByIdRef.current.has(data.itemId) : false;
|
|
355
|
+
setOverlay(null);
|
|
356
|
+
registryRef.current.setGrabOffset(null);
|
|
357
|
+
const native = event.nativeEvent ?? null;
|
|
358
|
+
const p2 = propsRef.current;
|
|
359
|
+
const action = classifyDrop({
|
|
360
|
+
kind: current?.kind ?? null,
|
|
361
|
+
canceled: event.canceled,
|
|
362
|
+
ownsItem,
|
|
363
|
+
hasData: !!data,
|
|
364
|
+
dest,
|
|
365
|
+
myId
|
|
366
|
+
});
|
|
367
|
+
switch (action) {
|
|
368
|
+
case "cancel-resize":
|
|
369
|
+
p2.onResizeStop?.(p2.layout, current?.anchor.item ?? null, null, null, native, null);
|
|
370
|
+
break;
|
|
371
|
+
case "cancel-move":
|
|
372
|
+
p2.onDragStop?.(p2.layout, current?.anchor.item ?? null, null, null, native, null);
|
|
373
|
+
break;
|
|
374
|
+
case "commit-resize":
|
|
375
|
+
if (current) {
|
|
376
|
+
p2.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
|
|
377
|
+
p2.onResizeStop?.(current.preview, current.anchor.item, current.placeholder, current.placeholder, native, null);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case "commit-in-grid":
|
|
381
|
+
case "remove-source":
|
|
382
|
+
case "revert":
|
|
383
|
+
if (action === "commit-in-grid" && current) p2.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
|
|
384
|
+
else if (action === "remove-source" && data) {
|
|
385
|
+
const { compactor: c, cols } = ctx();
|
|
386
|
+
p2.onLayoutChange?.((0, _snapgridjs_core.removeItemWithCompactor)(p2.layout, data.itemId, {
|
|
387
|
+
compactor: c,
|
|
388
|
+
cols
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
p2.onDragStop?.(current?.preview ?? p2.layout, current?.anchor.item ?? null, current?.placeholder ?? null, current?.placeholder ?? null, native, null);
|
|
392
|
+
break;
|
|
393
|
+
case "commit-dest":
|
|
394
|
+
if (current) p2.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
|
|
395
|
+
break;
|
|
396
|
+
case "external-drop":
|
|
397
|
+
if (current) {
|
|
398
|
+
const committed = (0, _snapgridjs_core.commitLayout)(current);
|
|
399
|
+
const dropped = committed.find((it) => it.i === current.activeId);
|
|
400
|
+
if (dropped) p2.onDrop?.(committed, dropped, native);
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
dropSpecRef.current = null;
|
|
405
|
+
keyboardRef.current = false;
|
|
406
|
+
setSessionBoth(null);
|
|
407
|
+
}, [setSessionBoth, ctx]);
|
|
408
|
+
(0, react.useEffect)(() => {
|
|
409
|
+
const STEP = {
|
|
410
|
+
ArrowLeft: [-1, 0],
|
|
411
|
+
ArrowRight: [1, 0],
|
|
412
|
+
ArrowUp: [0, -1],
|
|
413
|
+
ArrowDown: [0, 1]
|
|
414
|
+
};
|
|
415
|
+
const onKeyDown = (e) => {
|
|
416
|
+
if (!keyboardRef.current) return;
|
|
417
|
+
const session = sessionRef.current;
|
|
418
|
+
if (!session || session.kind !== "move") return;
|
|
419
|
+
const step = STEP[e.key];
|
|
420
|
+
if (!step) return;
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
e.stopImmediatePropagation();
|
|
423
|
+
const next = (0, _snapgridjs_core.nudge)(session, step[0], step[1], ctx());
|
|
424
|
+
setSessionBoth(next);
|
|
425
|
+
const cell = next.placeholder;
|
|
426
|
+
const rect = containerElRef.current?.getBoundingClientRect();
|
|
427
|
+
if (cell && rect) {
|
|
428
|
+
const pos = (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, cell.x, cell.y, cell.w, cell.h);
|
|
429
|
+
setOverlay((o) => o ? {
|
|
430
|
+
...o,
|
|
431
|
+
left: rect.left + pos.left,
|
|
432
|
+
top: rect.top + pos.top,
|
|
433
|
+
width: pos.width,
|
|
434
|
+
height: pos.height
|
|
435
|
+
} : o);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
439
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
440
|
+
}, [ctx, setSessionBoth]);
|
|
441
|
+
(0, _dnd_kit_react.useDragDropMonitor)((0, react.useMemo)(() => ({
|
|
442
|
+
onDragStart: handleDragStart,
|
|
443
|
+
onDragMove: handleDragMove,
|
|
444
|
+
onDragEnd: handleDragEnd
|
|
445
|
+
}), [
|
|
446
|
+
handleDragStart,
|
|
447
|
+
handleDragMove,
|
|
448
|
+
handleDragEnd
|
|
449
|
+
]));
|
|
450
|
+
const dragThreshold = props.dragConfig?.threshold ?? 3;
|
|
451
|
+
const itemSensors = (0, react.useMemo)(() => buildItemSensors(dragThreshold, () => propsRef.current.dragConfig), [dragThreshold]);
|
|
452
|
+
const renderedLayout = session ? session.preview : props.layout;
|
|
453
|
+
const itemsById = (0, react.useMemo)(() => new Map(renderedLayout.map((it) => [it.i, it])), [renderedLayout]);
|
|
454
|
+
const gridDraggable = props.isDraggable ?? true;
|
|
455
|
+
const dragEnabled = props.dragConfig?.enabled ?? true;
|
|
456
|
+
const isItemDraggable = (0, react.useCallback)((id) => {
|
|
457
|
+
const it = committedById.get(id);
|
|
458
|
+
if (!it) return false;
|
|
459
|
+
return gridDraggable && dragEnabled && (it.isDraggable ?? true) && !it.static;
|
|
460
|
+
}, [
|
|
461
|
+
committedById,
|
|
462
|
+
gridDraggable,
|
|
463
|
+
dragEnabled
|
|
464
|
+
]);
|
|
465
|
+
const gridResizable = props.isResizable ?? true;
|
|
466
|
+
const resizeEnabled = props.resizeConfig?.enabled ?? true;
|
|
467
|
+
const isItemResizable = (0, react.useCallback)((id) => {
|
|
468
|
+
const it = committedById.get(id);
|
|
469
|
+
if (!it) return false;
|
|
470
|
+
return gridResizable && resizeEnabled && (it.isResizable ?? true) && !it.static;
|
|
471
|
+
}, [
|
|
472
|
+
committedById,
|
|
473
|
+
gridResizable,
|
|
474
|
+
resizeEnabled
|
|
475
|
+
]);
|
|
476
|
+
const defaultHandles = props.resizeConfig?.handles;
|
|
477
|
+
const resizeHandlesFor = (0, react.useCallback)((id) => committedById.get(id)?.resizeHandles ?? defaultHandles ?? DEFAULT_HANDLES, [committedById, defaultHandles]);
|
|
478
|
+
const runtime = (0, react.useMemo)(() => ({
|
|
479
|
+
containerId,
|
|
480
|
+
width: props.width,
|
|
481
|
+
autoSize: props.autoSize ?? true,
|
|
482
|
+
gridConfig,
|
|
483
|
+
positionParams,
|
|
484
|
+
renderedLayout,
|
|
485
|
+
itemsById,
|
|
486
|
+
session,
|
|
487
|
+
isItemDraggable,
|
|
488
|
+
isItemResizable,
|
|
489
|
+
resizeHandlesFor,
|
|
490
|
+
itemSensors,
|
|
491
|
+
setContainerElement,
|
|
492
|
+
overlay
|
|
493
|
+
}), [
|
|
494
|
+
containerId,
|
|
495
|
+
props.width,
|
|
496
|
+
props.autoSize,
|
|
497
|
+
gridConfig,
|
|
498
|
+
positionParams,
|
|
499
|
+
renderedLayout,
|
|
500
|
+
itemsById,
|
|
501
|
+
session,
|
|
502
|
+
isItemDraggable,
|
|
503
|
+
isItemResizable,
|
|
504
|
+
resizeHandlesFor,
|
|
505
|
+
itemSensors,
|
|
506
|
+
setContainerElement,
|
|
507
|
+
overlay
|
|
508
|
+
]);
|
|
509
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridContext.Provider, {
|
|
510
|
+
value: runtime,
|
|
511
|
+
children: props.children
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/SnapGridGroup.tsx
|
|
516
|
+
/**
|
|
517
|
+
* Wrap multiple grids to let tiles be dragged **between** them. Provides one
|
|
518
|
+
* shared dnd-kit `DragDropProvider` and a registry so each grid can tell which
|
|
519
|
+
* grid the pointer is over.
|
|
520
|
+
*
|
|
521
|
+
* Item ids must be unique across all grids in a group (they share one manager).
|
|
522
|
+
*/
|
|
523
|
+
function SnapGridGroup({ children }) {
|
|
524
|
+
const registryRef = (0, react.useRef)(null);
|
|
525
|
+
if (!registryRef.current) registryRef.current = createGridRegistry();
|
|
526
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_dnd_kit_react.DragDropProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapGridGroupContext.Provider, {
|
|
527
|
+
value: registryRef.current,
|
|
528
|
+
children
|
|
529
|
+
}) });
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/hooks/useGridContainer.ts
|
|
533
|
+
/** Total container height in pixels for the given number of occupied rows. */
|
|
534
|
+
function containerHeight(rows, grid) {
|
|
535
|
+
const padY = (grid.containerPadding ?? grid.margin)[1];
|
|
536
|
+
if (rows <= 0) return padY * 2;
|
|
537
|
+
return padY * 2 + rows * grid.rowHeight + (rows - 1) * grid.margin[1];
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Headless hook for the grid container. Registers the droppable surface (the
|
|
541
|
+
* seam for cross-grid drops) and returns props (ref + sizing style) to spread
|
|
542
|
+
* onto your own container element.
|
|
543
|
+
*/
|
|
544
|
+
function useGridContainer() {
|
|
545
|
+
const rt = useGridRuntime();
|
|
546
|
+
const { ref, isDropTarget } = (0, _dnd_kit_react.useDroppable)({
|
|
547
|
+
id: rt.containerId,
|
|
548
|
+
type: "grid",
|
|
549
|
+
accept: "grid-item"
|
|
550
|
+
});
|
|
551
|
+
const setContainerElement = rt.setContainerElement;
|
|
552
|
+
const setRef = (0, react.useCallback)((element) => {
|
|
553
|
+
ref(element);
|
|
554
|
+
setContainerElement(element);
|
|
555
|
+
}, [ref, setContainerElement]);
|
|
556
|
+
const height = rt.autoSize ? containerHeight((0, _snapgridjs_core.bottom)(rt.renderedLayout), rt.gridConfig) : void 0;
|
|
557
|
+
return {
|
|
558
|
+
containerProps: {
|
|
559
|
+
ref: setRef,
|
|
560
|
+
style: {
|
|
561
|
+
position: "relative",
|
|
562
|
+
width: rt.width,
|
|
563
|
+
height
|
|
564
|
+
},
|
|
565
|
+
"data-drop-target": isDropTarget || void 0
|
|
566
|
+
},
|
|
567
|
+
isDropTarget
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/hooks/useGridItem.ts
|
|
572
|
+
const REFLOW_TRANSITION = "transform 150ms ease, width 150ms ease, height 150ms ease";
|
|
573
|
+
/**
|
|
574
|
+
* Headless hook for a single grid item. Returns a ref, a positioning `style`,
|
|
575
|
+
* and drag state — spread them onto whatever element you render. You own the
|
|
576
|
+
* tag, className, content, and any cosmetic styling.
|
|
577
|
+
*/
|
|
578
|
+
function useGridItem(id) {
|
|
579
|
+
const rt = useGridRuntime();
|
|
580
|
+
const item = rt.itemsById.get(id);
|
|
581
|
+
const { ref, isDragging } = (0, _dnd_kit_react.useDraggable)({
|
|
582
|
+
id,
|
|
583
|
+
type: "grid-item",
|
|
584
|
+
disabled: !rt.isItemDraggable(id),
|
|
585
|
+
sensors: rt.itemSensors,
|
|
586
|
+
plugins: NO_FEEDBACK,
|
|
587
|
+
data: { snapGrid: {
|
|
588
|
+
kind: "move",
|
|
589
|
+
itemId: id,
|
|
590
|
+
item
|
|
591
|
+
} }
|
|
592
|
+
});
|
|
593
|
+
const active = rt.session?.activeId === id;
|
|
594
|
+
let style = {
|
|
595
|
+
position: "absolute",
|
|
596
|
+
touchAction: "none"
|
|
597
|
+
};
|
|
598
|
+
if (item) {
|
|
599
|
+
const pos = (0, _snapgridjs_core.calcGridItemPosition)(rt.positionParams, item.x, item.y, item.w, item.h);
|
|
600
|
+
style = {
|
|
601
|
+
position: "absolute",
|
|
602
|
+
left: 0,
|
|
603
|
+
top: 0,
|
|
604
|
+
width: pos.width,
|
|
605
|
+
height: pos.height,
|
|
606
|
+
transform: `translate(${pos.left}px, ${pos.top}px)`,
|
|
607
|
+
visibility: active ? "hidden" : void 0,
|
|
608
|
+
transition: active ? "none" : REFLOW_TRANSITION,
|
|
609
|
+
touchAction: "none"
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
ref,
|
|
614
|
+
style,
|
|
615
|
+
isDragging,
|
|
616
|
+
item
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/hooks/useGridPlaceholder.ts
|
|
621
|
+
/**
|
|
622
|
+
* Headless hook returning where the drag placeholder should be rendered, or
|
|
623
|
+
* `null` when no drag is in progress. You render the element however you like.
|
|
624
|
+
*/
|
|
625
|
+
function useGridPlaceholder() {
|
|
626
|
+
const rt = useGridRuntime();
|
|
627
|
+
const placeholder = rt.session?.placeholder;
|
|
628
|
+
if (!placeholder) return null;
|
|
629
|
+
const pos = (0, _snapgridjs_core.calcGridItemPosition)(rt.positionParams, placeholder.x, placeholder.y, placeholder.w, placeholder.h);
|
|
630
|
+
return {
|
|
631
|
+
item: placeholder,
|
|
632
|
+
style: {
|
|
633
|
+
position: "absolute",
|
|
634
|
+
left: 0,
|
|
635
|
+
top: 0,
|
|
636
|
+
width: pos.width,
|
|
637
|
+
height: pos.height,
|
|
638
|
+
transform: `translate(${pos.left}px, ${pos.top}px)`,
|
|
639
|
+
pointerEvents: "none"
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/hooks/useGridResizeHandle.ts
|
|
645
|
+
/**
|
|
646
|
+
* Headless hook for a single resize handle. Model a handle as its own draggable;
|
|
647
|
+
* dragging it resizes the item from the given edge/corner. Position and style
|
|
648
|
+
* the handle however you like — spread `ref` and `handleProps` onto it.
|
|
649
|
+
*/
|
|
650
|
+
function useGridResizeHandle(itemId, handle) {
|
|
651
|
+
const rt = useGridRuntime();
|
|
652
|
+
const { ref } = (0, _dnd_kit_react.useDraggable)({
|
|
653
|
+
id: `${itemId}::resize::${handle}`,
|
|
654
|
+
disabled: !rt.isItemResizable(itemId),
|
|
655
|
+
plugins: NO_FEEDBACK,
|
|
656
|
+
data: { snapGrid: {
|
|
657
|
+
kind: "resize",
|
|
658
|
+
itemId,
|
|
659
|
+
handle
|
|
660
|
+
} }
|
|
661
|
+
});
|
|
662
|
+
const isResizing = rt.session?.kind === "resize" && rt.session.activeId === itemId;
|
|
663
|
+
return {
|
|
664
|
+
ref,
|
|
665
|
+
handleProps: { [RESIZE_HANDLE_ATTR]: true },
|
|
666
|
+
isResizing
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/hooks/useGridDragOverlay.ts
|
|
671
|
+
/**
|
|
672
|
+
* Headless hook for the floating drag preview. Returns `null` unless this grid
|
|
673
|
+
* is the source of an in-progress drag. Render the returned `item` with `style`
|
|
674
|
+
* in a portal at `document.body` so it can float across grids unclipped (see
|
|
675
|
+
* {@link GridDragOverlay} for the convenience component).
|
|
676
|
+
*/
|
|
677
|
+
function useGridDragOverlay() {
|
|
678
|
+
const o = useGridRuntime().overlay;
|
|
679
|
+
if (!o) return null;
|
|
680
|
+
return {
|
|
681
|
+
item: o.item,
|
|
682
|
+
style: {
|
|
683
|
+
position: "fixed",
|
|
684
|
+
left: o.left,
|
|
685
|
+
top: o.top,
|
|
686
|
+
width: o.width,
|
|
687
|
+
height: o.height,
|
|
688
|
+
pointerEvents: "none",
|
|
689
|
+
zIndex: 1e3
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/hooks/useResponsiveLayout.ts
|
|
695
|
+
/** react-grid-layout's default breakpoints (px) and column counts. */
|
|
696
|
+
const DEFAULT_BREAKPOINTS = {
|
|
697
|
+
lg: 1200,
|
|
698
|
+
md: 996,
|
|
699
|
+
sm: 768,
|
|
700
|
+
xs: 480,
|
|
701
|
+
xxs: 0
|
|
702
|
+
};
|
|
703
|
+
const DEFAULT_BREAKPOINT_COLS = {
|
|
704
|
+
lg: 12,
|
|
705
|
+
md: 10,
|
|
706
|
+
sm: 6,
|
|
707
|
+
xs: 4,
|
|
708
|
+
xxs: 2
|
|
709
|
+
};
|
|
710
|
+
/**
|
|
711
|
+
* Headless responsive layout engine: resolves the active breakpoint and its
|
|
712
|
+
* column count/layout from the container width, generating a layout for the
|
|
713
|
+
* active breakpoint from the nearest one when missing.
|
|
714
|
+
*/
|
|
715
|
+
function useResponsiveLayout(options) {
|
|
716
|
+
const { width, layouts, breakpoints = DEFAULT_BREAKPOINTS, cols = DEFAULT_BREAKPOINT_COLS, compactor = _snapgridjs_core.verticalCompactor, onLayoutChange, onBreakpointChange } = options;
|
|
717
|
+
const breakpoint = (0, _snapgridjs_core.getBreakpointFromWidth)(breakpoints, width);
|
|
718
|
+
const colCount = (0, _snapgridjs_core.getColsFromBreakpoint)(breakpoint, cols);
|
|
719
|
+
const layout = (0, react.useMemo)(() => {
|
|
720
|
+
let source = breakpoint;
|
|
721
|
+
let sourceWidth = Number.NEGATIVE_INFINITY;
|
|
722
|
+
for (const [bp, minWidth] of Object.entries(breakpoints)) if (layouts[bp] && minWidth > sourceWidth) {
|
|
723
|
+
sourceWidth = minWidth;
|
|
724
|
+
source = bp;
|
|
725
|
+
}
|
|
726
|
+
return (0, _snapgridjs_core.findOrGenerateResponsiveLayout)(layouts, breakpoints, breakpoint, source, colCount, compactor);
|
|
727
|
+
}, [
|
|
728
|
+
layouts,
|
|
729
|
+
breakpoints,
|
|
730
|
+
breakpoint,
|
|
731
|
+
colCount,
|
|
732
|
+
compactor
|
|
733
|
+
]);
|
|
734
|
+
const onBreakpointChangeRef = (0, react.useRef)(onBreakpointChange);
|
|
735
|
+
onBreakpointChangeRef.current = onBreakpointChange;
|
|
736
|
+
const firedBreakpointRef = (0, react.useRef)(breakpoint);
|
|
737
|
+
(0, react.useEffect)(() => {
|
|
738
|
+
if (firedBreakpointRef.current !== breakpoint) {
|
|
739
|
+
firedBreakpointRef.current = breakpoint;
|
|
740
|
+
onBreakpointChangeRef.current?.(breakpoint, colCount);
|
|
741
|
+
}
|
|
742
|
+
}, [breakpoint, colCount]);
|
|
743
|
+
return {
|
|
744
|
+
breakpoint,
|
|
745
|
+
cols: colCount,
|
|
746
|
+
layout,
|
|
747
|
+
onLayoutChange: (0, react.useCallback)((next) => {
|
|
748
|
+
onLayoutChange?.(next, {
|
|
749
|
+
...layouts,
|
|
750
|
+
[breakpoint]: next
|
|
751
|
+
});
|
|
752
|
+
}, [
|
|
753
|
+
onLayoutChange,
|
|
754
|
+
layouts,
|
|
755
|
+
breakpoint
|
|
756
|
+
])
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/GridDragOverlay.tsx
|
|
761
|
+
/**
|
|
762
|
+
* Renders the floating drag preview in a portal at `document.body` — so it
|
|
763
|
+
* follows the pointer across grids without being clipped by any container.
|
|
764
|
+
* Renders nothing when this grid isn't the drag source.
|
|
765
|
+
*/
|
|
766
|
+
function GridDragOverlay({ children, className, style }) {
|
|
767
|
+
const overlay = useGridDragOverlay();
|
|
768
|
+
if (typeof document === "undefined" || !overlay) return null;
|
|
769
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
770
|
+
className: className ? `snapgrid-overlay ${className}` : "snapgrid-overlay",
|
|
771
|
+
style: style ? {
|
|
772
|
+
...overlay.style,
|
|
773
|
+
...style
|
|
774
|
+
} : overlay.style,
|
|
775
|
+
children: children(overlay.item)
|
|
776
|
+
}), document.body);
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/GridItem.tsx
|
|
780
|
+
const HANDLE_CURSOR = {
|
|
781
|
+
n: "ns-resize",
|
|
782
|
+
s: "ns-resize",
|
|
783
|
+
e: "ew-resize",
|
|
784
|
+
w: "ew-resize",
|
|
785
|
+
se: "nwse-resize",
|
|
786
|
+
nw: "nwse-resize",
|
|
787
|
+
ne: "nesw-resize",
|
|
788
|
+
sw: "nesw-resize"
|
|
789
|
+
};
|
|
790
|
+
const SIDE = 14;
|
|
791
|
+
function handleStyle(handle) {
|
|
792
|
+
const s = {
|
|
793
|
+
position: "absolute",
|
|
794
|
+
width: SIDE,
|
|
795
|
+
height: SIDE,
|
|
796
|
+
cursor: HANDLE_CURSOR[handle],
|
|
797
|
+
touchAction: "none",
|
|
798
|
+
zIndex: 4
|
|
799
|
+
};
|
|
800
|
+
if (handle.includes("n")) s.top = -14 / 2;
|
|
801
|
+
if (handle.includes("s")) s.bottom = -14 / 2;
|
|
802
|
+
if (handle.includes("e")) s.right = -14 / 2;
|
|
803
|
+
if (handle.includes("w")) s.left = -14 / 2;
|
|
804
|
+
if (handle === "n" || handle === "s") s.left = `calc(50% - ${SIDE / 2}px)`;
|
|
805
|
+
if (handle === "e" || handle === "w") s.top = `calc(50% - ${SIDE / 2}px)`;
|
|
806
|
+
return s;
|
|
807
|
+
}
|
|
808
|
+
function DefaultResizeHandle({ itemId, handle }) {
|
|
809
|
+
const { ref, handleProps } = useGridResizeHandle(itemId, handle);
|
|
810
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
811
|
+
ref,
|
|
812
|
+
...handleProps,
|
|
813
|
+
className: `snapgrid-resize-handle snapgrid-resize-handle--${handle}`,
|
|
814
|
+
style: handleStyle(handle)
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Convenience wrapper over {@link useGridItem}: an absolutely-positioned `<div>`
|
|
819
|
+
* with stable hooks (`.snapgrid-item`, `data-grid-id`, `data-dragging`) and the
|
|
820
|
+
* configured resize handles. For full control, use the hooks directly.
|
|
821
|
+
*/
|
|
822
|
+
function GridItem({ id, children, className, style }) {
|
|
823
|
+
const rt = useGridRuntime();
|
|
824
|
+
const { ref, style: positionStyle, isDragging } = useGridItem(id);
|
|
825
|
+
const handles = rt.isItemResizable(id) ? rt.resizeHandlesFor(id) : [];
|
|
826
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
827
|
+
ref,
|
|
828
|
+
"data-grid-id": id,
|
|
829
|
+
"data-dragging": isDragging || void 0,
|
|
830
|
+
className: className ? `snapgrid-item ${className}` : "snapgrid-item",
|
|
831
|
+
style: style ? {
|
|
832
|
+
...positionStyle,
|
|
833
|
+
...style
|
|
834
|
+
} : positionStyle,
|
|
835
|
+
children: [children, handles.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultResizeHandle, {
|
|
836
|
+
itemId: id,
|
|
837
|
+
handle
|
|
838
|
+
}, handle))]
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/GridPlaceholder.tsx
|
|
843
|
+
const DEFAULT_LOOK = {
|
|
844
|
+
background: "rgba(99, 102, 241, 0.2)",
|
|
845
|
+
border: "1px dashed rgba(99, 102, 241, 0.6)",
|
|
846
|
+
borderRadius: 4,
|
|
847
|
+
boxSizing: "border-box",
|
|
848
|
+
zIndex: 2,
|
|
849
|
+
transition: "transform 150ms ease, width 150ms ease, height 150ms ease"
|
|
850
|
+
};
|
|
851
|
+
/**
|
|
852
|
+
* Convenience placeholder rendered from {@link useGridPlaceholder}. Renders
|
|
853
|
+
* nothing when no drag is active. For a custom placeholder, call the hook
|
|
854
|
+
* directly and render your own element with the returned `style`.
|
|
855
|
+
*/
|
|
856
|
+
function GridPlaceholder({ className, style }) {
|
|
857
|
+
const placeholder = useGridPlaceholder();
|
|
858
|
+
if (!placeholder) return null;
|
|
859
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
860
|
+
"aria-hidden": "true",
|
|
861
|
+
className: className ? `snapgrid-placeholder ${className}` : "snapgrid-placeholder",
|
|
862
|
+
style: {
|
|
863
|
+
...placeholder.style,
|
|
864
|
+
...DEFAULT_LOOK,
|
|
865
|
+
...style
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/GridLayout.tsx
|
|
871
|
+
/** Strip the namespacing prefix React applies to keys inside `Children.map`. */
|
|
872
|
+
function keyToId(key) {
|
|
873
|
+
return key.startsWith(".$") ? key.slice(2) : key;
|
|
874
|
+
}
|
|
875
|
+
/** The default surface: positioned container + mapped items + placeholder. */
|
|
876
|
+
function GridSurface({ className, style, children }) {
|
|
877
|
+
const { containerProps } = useGridContainer();
|
|
878
|
+
const childById = /* @__PURE__ */ new Map();
|
|
879
|
+
react.Children.forEach(children, (child) => {
|
|
880
|
+
if ((0, react.isValidElement)(child) && child.key != null) childById.set(keyToId(String(child.key)), child);
|
|
881
|
+
});
|
|
882
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
883
|
+
...containerProps,
|
|
884
|
+
className: className ? `snapgrid ${className}` : "snapgrid",
|
|
885
|
+
style: style ? {
|
|
886
|
+
...containerProps.style,
|
|
887
|
+
...style
|
|
888
|
+
} : containerProps.style,
|
|
889
|
+
children: [
|
|
890
|
+
react.Children.map(children, (child) => {
|
|
891
|
+
if (!(0, react.isValidElement)(child) || child.key == null) return child;
|
|
892
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridItem, {
|
|
893
|
+
id: keyToId(String(child.key)),
|
|
894
|
+
children: child
|
|
895
|
+
}, child.key);
|
|
896
|
+
}),
|
|
897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridPlaceholder, {}),
|
|
898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridDragOverlay, { children: (item) => childById.get(item.i) ?? null })
|
|
899
|
+
]
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Drop-in grid component: a controlled, react-grid-layout v2-compatible layout
|
|
904
|
+
* backed by dnd-kit. A thin shell over {@link SnapGridProvider} and the headless
|
|
905
|
+
* hooks — children are keyed by their layout item's `i`. For full control over
|
|
906
|
+
* markup/styling, use the provider + hooks directly.
|
|
907
|
+
*/
|
|
908
|
+
function GridLayout(props) {
|
|
909
|
+
const { className, style, children, ...providerProps } = props;
|
|
910
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapGridProvider, {
|
|
911
|
+
...providerProps,
|
|
912
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridSurface, {
|
|
913
|
+
className,
|
|
914
|
+
style,
|
|
915
|
+
children
|
|
916
|
+
})
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/ResponsiveGridLayout.tsx
|
|
921
|
+
/**
|
|
922
|
+
* A responsive grid: switches column count and layout by breakpoint as `width`
|
|
923
|
+
* changes, generating a breakpoint's layout from the nearest one when absent.
|
|
924
|
+
* A thin wrapper over {@link useResponsiveLayout} + {@link GridLayout}; mirrors
|
|
925
|
+
* react-grid-layout v2's `ResponsiveGridLayout`.
|
|
926
|
+
*/
|
|
927
|
+
function ResponsiveGridLayout(props) {
|
|
928
|
+
const { cols, layout, onLayoutChange } = useResponsiveLayout({
|
|
929
|
+
width: props.width,
|
|
930
|
+
layouts: props.layouts,
|
|
931
|
+
breakpoints: props.breakpoints,
|
|
932
|
+
cols: props.cols,
|
|
933
|
+
compactor: props.compactor,
|
|
934
|
+
onLayoutChange: props.onLayoutChange,
|
|
935
|
+
onBreakpointChange: props.onBreakpointChange
|
|
936
|
+
});
|
|
937
|
+
const gridConfig = (0, react.useMemo)(() => ({
|
|
938
|
+
cols,
|
|
939
|
+
rowHeight: props.rowHeight ?? 150,
|
|
940
|
+
margin: props.margin ?? [10, 10],
|
|
941
|
+
containerPadding: props.containerPadding ?? null
|
|
942
|
+
}), [
|
|
943
|
+
cols,
|
|
944
|
+
props.rowHeight,
|
|
945
|
+
props.margin,
|
|
946
|
+
props.containerPadding
|
|
947
|
+
]);
|
|
948
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridLayout, {
|
|
949
|
+
layout,
|
|
950
|
+
width: props.width,
|
|
951
|
+
onLayoutChange,
|
|
952
|
+
gridConfig,
|
|
953
|
+
compactor: props.compactor,
|
|
954
|
+
dragConfig: props.dragConfig,
|
|
955
|
+
resizeConfig: props.resizeConfig,
|
|
956
|
+
isDraggable: props.isDraggable,
|
|
957
|
+
isResizable: props.isResizable,
|
|
958
|
+
autoSize: props.autoSize,
|
|
959
|
+
className: props.className,
|
|
960
|
+
style: props.style,
|
|
961
|
+
children: props.children
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/hooks/useContainerWidth.ts
|
|
966
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
|
|
967
|
+
/**
|
|
968
|
+
* Measure a container's width with a `ResizeObserver`. Replaces react-grid-layout's
|
|
969
|
+
* `WidthProvider` HOC with a hook, mirroring RGL v2's `useContainerWidth`.
|
|
970
|
+
*/
|
|
971
|
+
function useContainerWidth(options = {}) {
|
|
972
|
+
const { initialWidth = 1280 } = options;
|
|
973
|
+
const [width, setWidth] = (0, react.useState)(initialWidth);
|
|
974
|
+
const [mounted, setMounted] = (0, react.useState)(false);
|
|
975
|
+
const [element, setElement] = (0, react.useState)(null);
|
|
976
|
+
const containerRef = (0, react.useCallback)((node) => setElement(node), []);
|
|
977
|
+
useIsomorphicLayoutEffect(() => {
|
|
978
|
+
if (!element || typeof ResizeObserver === "undefined") return;
|
|
979
|
+
const measure = () => {
|
|
980
|
+
const next = element.getBoundingClientRect().width;
|
|
981
|
+
if (next > 0) {
|
|
982
|
+
setWidth(next);
|
|
983
|
+
setMounted(true);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
measure();
|
|
987
|
+
const observer = new ResizeObserver(measure);
|
|
988
|
+
observer.observe(element);
|
|
989
|
+
return () => observer.disconnect();
|
|
990
|
+
}, [element]);
|
|
991
|
+
return {
|
|
992
|
+
width,
|
|
993
|
+
mounted,
|
|
994
|
+
containerRef
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
exports.DEFAULT_BREAKPOINTS = DEFAULT_BREAKPOINTS;
|
|
999
|
+
exports.DEFAULT_BREAKPOINT_COLS = DEFAULT_BREAKPOINT_COLS;
|
|
1000
|
+
Object.defineProperty(exports, "Feedback", {
|
|
1001
|
+
enumerable: true,
|
|
1002
|
+
get: function() {
|
|
1003
|
+
return _dnd_kit_dom.Feedback;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
exports.GridDragOverlay = GridDragOverlay;
|
|
1007
|
+
exports.GridItem = GridItem;
|
|
1008
|
+
exports.GridLayout = GridLayout;
|
|
1009
|
+
exports.GridPlaceholder = GridPlaceholder;
|
|
1010
|
+
Object.defineProperty(exports, "KeyboardSensor", {
|
|
1011
|
+
enumerable: true,
|
|
1012
|
+
get: function() {
|
|
1013
|
+
return _dnd_kit_dom.KeyboardSensor;
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
Object.defineProperty(exports, "PointerSensor", {
|
|
1017
|
+
enumerable: true,
|
|
1018
|
+
get: function() {
|
|
1019
|
+
return _dnd_kit_dom.PointerSensor;
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
exports.ResponsiveGridLayout = ResponsiveGridLayout;
|
|
1023
|
+
exports.SnapGridGroup = SnapGridGroup;
|
|
1024
|
+
exports.SnapGridProvider = SnapGridProvider;
|
|
1025
|
+
Object.defineProperty(exports, "getCompactor", {
|
|
1026
|
+
enumerable: true,
|
|
1027
|
+
get: function() {
|
|
1028
|
+
return _snapgridjs_core.getCompactor;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
Object.defineProperty(exports, "horizontalCompactor", {
|
|
1032
|
+
enumerable: true,
|
|
1033
|
+
get: function() {
|
|
1034
|
+
return _snapgridjs_core.horizontalCompactor;
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
Object.defineProperty(exports, "noCompactor", {
|
|
1038
|
+
enumerable: true,
|
|
1039
|
+
get: function() {
|
|
1040
|
+
return _snapgridjs_core.noCompactor;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
exports.useContainerWidth = useContainerWidth;
|
|
1044
|
+
Object.defineProperty(exports, "useDraggable", {
|
|
1045
|
+
enumerable: true,
|
|
1046
|
+
get: function() {
|
|
1047
|
+
return _dnd_kit_react.useDraggable;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
Object.defineProperty(exports, "useDroppable", {
|
|
1051
|
+
enumerable: true,
|
|
1052
|
+
get: function() {
|
|
1053
|
+
return _dnd_kit_react.useDroppable;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
exports.useGridContainer = useGridContainer;
|
|
1057
|
+
exports.useGridDragOverlay = useGridDragOverlay;
|
|
1058
|
+
exports.useGridItem = useGridItem;
|
|
1059
|
+
exports.useGridPlaceholder = useGridPlaceholder;
|
|
1060
|
+
exports.useGridResizeHandle = useGridResizeHandle;
|
|
1061
|
+
exports.useResponsiveLayout = useResponsiveLayout;
|
|
1062
|
+
Object.defineProperty(exports, "verticalCompactor", {
|
|
1063
|
+
enumerable: true,
|
|
1064
|
+
get: function() {
|
|
1065
|
+
return _snapgridjs_core.verticalCompactor;
|
|
1066
|
+
}
|
|
1067
|
+
});
|