dnd-block-tree 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/dist/index.d.mts +366 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +871 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +848 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { useDroppable, useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DragOverlay as DragOverlay$1, DndContext, useDraggable } from '@dnd-kit/core';
|
|
2
|
+
import { memo, useCallback, useEffect, Fragment, useRef, useReducer, createContext, useContext, useState, useMemo } from 'react';
|
|
3
|
+
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/core/types.ts
|
|
6
|
+
function getDropZoneType(zoneId) {
|
|
7
|
+
if (zoneId.startsWith("before-")) return "before";
|
|
8
|
+
if (zoneId.startsWith("into-")) return "into";
|
|
9
|
+
return "after";
|
|
10
|
+
}
|
|
11
|
+
function extractBlockId(zoneId) {
|
|
12
|
+
return zoneId.replace(/^(before|after|into)-/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/core/collision.ts
|
|
16
|
+
var weightedVerticalCollision = ({
|
|
17
|
+
droppableContainers,
|
|
18
|
+
collisionRect
|
|
19
|
+
}) => {
|
|
20
|
+
if (!collisionRect) return [];
|
|
21
|
+
const pointerY = collisionRect.top + collisionRect.height / 2;
|
|
22
|
+
const candidates = droppableContainers.map((container) => {
|
|
23
|
+
const rect = container.rect.current;
|
|
24
|
+
if (!rect) return null;
|
|
25
|
+
const distanceToTop = Math.abs(pointerY - rect.top);
|
|
26
|
+
const distanceToBottom = Math.abs(pointerY - rect.bottom);
|
|
27
|
+
const edgeDistance = Math.min(distanceToTop, distanceToBottom);
|
|
28
|
+
const isBelowCenter = pointerY > rect.top + rect.height / 2;
|
|
29
|
+
const bias = isBelowCenter ? -5 : 0;
|
|
30
|
+
return {
|
|
31
|
+
id: container.id,
|
|
32
|
+
data: {
|
|
33
|
+
droppableContainer: container,
|
|
34
|
+
value: edgeDistance + bias
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}).filter((c) => c !== null);
|
|
38
|
+
candidates.sort((a, b) => {
|
|
39
|
+
const aValue = a.data.value;
|
|
40
|
+
const bValue = b.data.value;
|
|
41
|
+
return aValue - bValue;
|
|
42
|
+
});
|
|
43
|
+
return candidates.slice(0, 1);
|
|
44
|
+
};
|
|
45
|
+
var closestCenterCollision = ({
|
|
46
|
+
droppableContainers,
|
|
47
|
+
collisionRect
|
|
48
|
+
}) => {
|
|
49
|
+
if (!collisionRect) return [];
|
|
50
|
+
const centerY = collisionRect.top + collisionRect.height / 2;
|
|
51
|
+
const centerX = collisionRect.left + collisionRect.width / 2;
|
|
52
|
+
const candidates = droppableContainers.map((container) => {
|
|
53
|
+
const rect = container.rect.current;
|
|
54
|
+
if (!rect) return null;
|
|
55
|
+
const containerCenterX = rect.left + rect.width / 2;
|
|
56
|
+
const containerCenterY = rect.top + rect.height / 2;
|
|
57
|
+
const distance = Math.sqrt(
|
|
58
|
+
Math.pow(centerX - containerCenterX, 2) + Math.pow(centerY - containerCenterY, 2)
|
|
59
|
+
);
|
|
60
|
+
return {
|
|
61
|
+
id: container.id,
|
|
62
|
+
data: {
|
|
63
|
+
droppableContainer: container,
|
|
64
|
+
value: distance
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}).filter((c) => c !== null);
|
|
68
|
+
candidates.sort((a, b) => {
|
|
69
|
+
const aValue = a.data.value;
|
|
70
|
+
const bValue = b.data.value;
|
|
71
|
+
return aValue - bValue;
|
|
72
|
+
});
|
|
73
|
+
return candidates.slice(0, 1);
|
|
74
|
+
};
|
|
75
|
+
var DEFAULT_ACTIVATION_DISTANCE = 8;
|
|
76
|
+
function useConfiguredSensors(config = {}) {
|
|
77
|
+
const { activationDistance = DEFAULT_ACTIVATION_DISTANCE } = config;
|
|
78
|
+
return useSensors(
|
|
79
|
+
useSensor(PointerSensor, {
|
|
80
|
+
activationConstraint: {
|
|
81
|
+
distance: activationDistance
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
useSensor(TouchSensor, {
|
|
85
|
+
activationConstraint: {
|
|
86
|
+
distance: activationDistance
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
useSensor(KeyboardSensor)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function getSensorConfig(activationDistance = DEFAULT_ACTIVATION_DISTANCE) {
|
|
93
|
+
return {
|
|
94
|
+
pointer: {
|
|
95
|
+
activationConstraint: {
|
|
96
|
+
distance: activationDistance
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
touch: {
|
|
100
|
+
activationConstraint: {
|
|
101
|
+
distance: activationDistance
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/utils/helper.ts
|
|
108
|
+
function extractUUID(id, pattern = "^(before|after|into)-") {
|
|
109
|
+
const regex = new RegExp(pattern);
|
|
110
|
+
return id.replace(regex, "");
|
|
111
|
+
}
|
|
112
|
+
function debounce(fn, delay) {
|
|
113
|
+
let timeoutId = null;
|
|
114
|
+
const debounced = ((...args) => {
|
|
115
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
116
|
+
timeoutId = setTimeout(() => {
|
|
117
|
+
fn(...args);
|
|
118
|
+
timeoutId = null;
|
|
119
|
+
}, delay);
|
|
120
|
+
});
|
|
121
|
+
debounced.cancel = () => {
|
|
122
|
+
if (timeoutId) {
|
|
123
|
+
clearTimeout(timeoutId);
|
|
124
|
+
timeoutId = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return debounced;
|
|
128
|
+
}
|
|
129
|
+
function generateId() {
|
|
130
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
131
|
+
}
|
|
132
|
+
function DropZoneComponent({
|
|
133
|
+
id,
|
|
134
|
+
parentId,
|
|
135
|
+
onHover,
|
|
136
|
+
activeId,
|
|
137
|
+
className = "h-1 rounded transition-colors",
|
|
138
|
+
activeClassName = "bg-blue-500",
|
|
139
|
+
height = 4
|
|
140
|
+
}) {
|
|
141
|
+
const { setNodeRef, isOver, active } = useDroppable({ id });
|
|
142
|
+
const handleInternalHover = useCallback(() => {
|
|
143
|
+
onHover(id, parentId);
|
|
144
|
+
}, [onHover, id, parentId]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (isOver) handleInternalHover();
|
|
147
|
+
}, [isOver, handleInternalHover]);
|
|
148
|
+
if (active?.id && extractUUID(id) === String(active.id)) return null;
|
|
149
|
+
if (activeId && extractUUID(id) === activeId) return null;
|
|
150
|
+
return /* @__PURE__ */ jsx(
|
|
151
|
+
"div",
|
|
152
|
+
{
|
|
153
|
+
ref: setNodeRef,
|
|
154
|
+
"data-zone-id": id,
|
|
155
|
+
"data-parent-id": parentId ?? "",
|
|
156
|
+
style: { height: isOver ? height * 2 : height },
|
|
157
|
+
className: `${className} ${isOver ? activeClassName : "bg-transparent"}`
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
var DropZone = memo(DropZoneComponent);
|
|
162
|
+
function DraggableBlock({
|
|
163
|
+
block,
|
|
164
|
+
children
|
|
165
|
+
}) {
|
|
166
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
167
|
+
id: block.id
|
|
168
|
+
});
|
|
169
|
+
return /* @__PURE__ */ jsx("div", { ref: setNodeRef, ...attributes, ...listeners, children: children({ isDragging }) });
|
|
170
|
+
}
|
|
171
|
+
function TreeRenderer({
|
|
172
|
+
blocks,
|
|
173
|
+
blocksByParent,
|
|
174
|
+
parentId,
|
|
175
|
+
activeId,
|
|
176
|
+
expandedMap,
|
|
177
|
+
renderers,
|
|
178
|
+
containerTypes,
|
|
179
|
+
onHover,
|
|
180
|
+
onToggleExpand,
|
|
181
|
+
depth = 0,
|
|
182
|
+
dropZoneClassName,
|
|
183
|
+
dropZoneActiveClassName,
|
|
184
|
+
indentClassName = "ml-6 border-l border-gray-200 pl-4",
|
|
185
|
+
rootClassName = "flex flex-col gap-1"
|
|
186
|
+
}) {
|
|
187
|
+
const items = blocksByParent.get(parentId) ?? [];
|
|
188
|
+
const filteredBlocks = items.filter((block) => block.id !== activeId);
|
|
189
|
+
const firstVisibleBlockId = filteredBlocks[0]?.id;
|
|
190
|
+
const containerClass = depth === 0 ? rootClassName : indentClassName;
|
|
191
|
+
return /* @__PURE__ */ jsx("div", { className: containerClass, children: filteredBlocks.map((block) => {
|
|
192
|
+
const isContainer = containerTypes.includes(block.type);
|
|
193
|
+
const isExpanded = expandedMap[block.id] !== false;
|
|
194
|
+
const Renderer = renderers[block.type];
|
|
195
|
+
if (!Renderer) {
|
|
196
|
+
console.warn(`No renderer found for block type: ${block.type}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
200
|
+
block.id === firstVisibleBlockId && /* @__PURE__ */ jsx(
|
|
201
|
+
DropZone,
|
|
202
|
+
{
|
|
203
|
+
id: `before-${block.id}`,
|
|
204
|
+
parentId: block.parentId,
|
|
205
|
+
onHover,
|
|
206
|
+
activeId,
|
|
207
|
+
className: dropZoneClassName,
|
|
208
|
+
activeClassName: dropZoneActiveClassName
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ jsx(DraggableBlock, { block, children: ({ isDragging }) => {
|
|
212
|
+
if (isContainer) {
|
|
213
|
+
const childContent = isExpanded ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
214
|
+
/* @__PURE__ */ jsx(
|
|
215
|
+
DropZone,
|
|
216
|
+
{
|
|
217
|
+
id: `into-${block.id}`,
|
|
218
|
+
parentId: block.id,
|
|
219
|
+
onHover,
|
|
220
|
+
activeId,
|
|
221
|
+
className: dropZoneClassName,
|
|
222
|
+
activeClassName: dropZoneActiveClassName
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
/* @__PURE__ */ jsx(
|
|
226
|
+
TreeRenderer,
|
|
227
|
+
{
|
|
228
|
+
blocks,
|
|
229
|
+
blocksByParent,
|
|
230
|
+
parentId: block.id,
|
|
231
|
+
activeId,
|
|
232
|
+
expandedMap,
|
|
233
|
+
renderers,
|
|
234
|
+
containerTypes,
|
|
235
|
+
onHover,
|
|
236
|
+
onToggleExpand,
|
|
237
|
+
depth: depth + 1,
|
|
238
|
+
dropZoneClassName,
|
|
239
|
+
dropZoneActiveClassName,
|
|
240
|
+
indentClassName,
|
|
241
|
+
rootClassName
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] }) : null;
|
|
245
|
+
return Renderer({
|
|
246
|
+
block,
|
|
247
|
+
children: childContent,
|
|
248
|
+
isDragging,
|
|
249
|
+
depth,
|
|
250
|
+
isExpanded,
|
|
251
|
+
onToggleExpand: () => onToggleExpand(block.id)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return Renderer({
|
|
255
|
+
block,
|
|
256
|
+
isDragging,
|
|
257
|
+
depth
|
|
258
|
+
});
|
|
259
|
+
} }),
|
|
260
|
+
/* @__PURE__ */ jsx(
|
|
261
|
+
DropZone,
|
|
262
|
+
{
|
|
263
|
+
id: `after-${block.id}`,
|
|
264
|
+
parentId: block.parentId,
|
|
265
|
+
onHover,
|
|
266
|
+
activeId,
|
|
267
|
+
className: dropZoneClassName,
|
|
268
|
+
activeClassName: dropZoneActiveClassName
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
] }, block.id);
|
|
272
|
+
}) });
|
|
273
|
+
}
|
|
274
|
+
function DragOverlay({
|
|
275
|
+
activeBlock,
|
|
276
|
+
children
|
|
277
|
+
}) {
|
|
278
|
+
return /* @__PURE__ */ jsx(DragOverlay$1, { children: activeBlock && (children ? children(activeBlock) : /* @__PURE__ */ jsxs("div", { className: "bg-white border border-gray-300 shadow-md rounded-md p-3 text-sm w-64 pointer-events-none", children: [
|
|
279
|
+
/* @__PURE__ */ jsx("div", { className: "text-gray-500 uppercase text-xs tracking-wide mb-1", children: activeBlock.type }),
|
|
280
|
+
/* @__PURE__ */ jsxs("div", { className: "font-semibold text-gray-800", children: [
|
|
281
|
+
"Block ",
|
|
282
|
+
activeBlock.id.slice(0, 8)
|
|
283
|
+
] })
|
|
284
|
+
] })) });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/utils/blocks.ts
|
|
288
|
+
function cloneMap(map) {
|
|
289
|
+
return new Map(map);
|
|
290
|
+
}
|
|
291
|
+
function cloneParentMap(map) {
|
|
292
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const [k, v] of map.entries()) {
|
|
294
|
+
newMap.set(k, [...v]);
|
|
295
|
+
}
|
|
296
|
+
return newMap;
|
|
297
|
+
}
|
|
298
|
+
function computeNormalizedIndex(blocks) {
|
|
299
|
+
const byId = /* @__PURE__ */ new Map();
|
|
300
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
301
|
+
for (const block of blocks) {
|
|
302
|
+
byId.set(block.id, block);
|
|
303
|
+
const key = block.parentId ?? null;
|
|
304
|
+
const list = byParent.get(key) ?? [];
|
|
305
|
+
byParent.set(key, [...list, block.id]);
|
|
306
|
+
}
|
|
307
|
+
return { byId, byParent };
|
|
308
|
+
}
|
|
309
|
+
function buildOrderedBlocks(index, containerTypes = []) {
|
|
310
|
+
const result = [];
|
|
311
|
+
const walk = (parentId) => {
|
|
312
|
+
const children = index.byParent.get(parentId) ?? [];
|
|
313
|
+
for (let i = 0; i < children.length; i++) {
|
|
314
|
+
const id = children[i];
|
|
315
|
+
const block = index.byId.get(id);
|
|
316
|
+
if (block) {
|
|
317
|
+
result.push({ ...block, order: i });
|
|
318
|
+
if (containerTypes.includes(block.type)) {
|
|
319
|
+
walk(block.id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
walk(null);
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function reparentBlockIndex(state, activeId, targetZone, containerTypes = []) {
|
|
328
|
+
const byId = cloneMap(state.byId);
|
|
329
|
+
const byParent = cloneParentMap(state.byParent);
|
|
330
|
+
const dragged = byId.get(String(activeId));
|
|
331
|
+
if (!dragged) return state;
|
|
332
|
+
const zoneTargetId = extractUUID(targetZone);
|
|
333
|
+
const isAfter = targetZone.startsWith("after-");
|
|
334
|
+
const isInto = targetZone.startsWith("into-");
|
|
335
|
+
const target = byId.get(zoneTargetId);
|
|
336
|
+
const oldParentId = dragged.parentId ?? null;
|
|
337
|
+
const newParentId = isInto ? zoneTargetId : target?.parentId ?? null;
|
|
338
|
+
if (containerTypes.includes(dragged.type) && newParentId !== null) {
|
|
339
|
+
const newParent = byId.get(newParentId);
|
|
340
|
+
if (newParent && !containerTypes.includes(newParent.type)) {
|
|
341
|
+
return state;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (dragged.id === zoneTargetId) return state;
|
|
345
|
+
const oldList = byParent.get(oldParentId) ?? [];
|
|
346
|
+
const filtered = oldList.filter((id) => id !== dragged.id);
|
|
347
|
+
byParent.set(oldParentId, filtered);
|
|
348
|
+
const newList = [...byParent.get(newParentId) ?? []];
|
|
349
|
+
let insertIndex = newList.length;
|
|
350
|
+
if (!isInto) {
|
|
351
|
+
const idx = newList.indexOf(zoneTargetId);
|
|
352
|
+
insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
|
|
353
|
+
}
|
|
354
|
+
const currentIndex = newList.indexOf(dragged.id);
|
|
355
|
+
if (dragged.parentId === newParentId && currentIndex === insertIndex) {
|
|
356
|
+
return state;
|
|
357
|
+
}
|
|
358
|
+
newList.splice(insertIndex, 0, dragged.id);
|
|
359
|
+
byParent.set(newParentId, newList);
|
|
360
|
+
byId.set(dragged.id, {
|
|
361
|
+
...dragged,
|
|
362
|
+
parentId: newParentId
|
|
363
|
+
});
|
|
364
|
+
return { byId, byParent };
|
|
365
|
+
}
|
|
366
|
+
function getDescendantIds(state, parentId) {
|
|
367
|
+
const toDelete = /* @__PURE__ */ new Set();
|
|
368
|
+
const stack = [parentId];
|
|
369
|
+
while (stack.length > 0) {
|
|
370
|
+
const current = stack.pop();
|
|
371
|
+
toDelete.add(current);
|
|
372
|
+
const children = state.byParent.get(current) ?? [];
|
|
373
|
+
stack.push(...children);
|
|
374
|
+
}
|
|
375
|
+
return toDelete;
|
|
376
|
+
}
|
|
377
|
+
function deleteBlockAndDescendants(state, id) {
|
|
378
|
+
const byId = cloneMap(state.byId);
|
|
379
|
+
const byParent = cloneParentMap(state.byParent);
|
|
380
|
+
const idsToDelete = getDescendantIds(state, id);
|
|
381
|
+
for (const deleteId of idsToDelete) {
|
|
382
|
+
byId.delete(deleteId);
|
|
383
|
+
byParent.delete(deleteId);
|
|
384
|
+
}
|
|
385
|
+
for (const [parent, list] of byParent.entries()) {
|
|
386
|
+
byParent.set(parent, list.filter((itemId) => !idsToDelete.has(itemId)));
|
|
387
|
+
}
|
|
388
|
+
return { byId, byParent };
|
|
389
|
+
}
|
|
390
|
+
function BlockTree({
|
|
391
|
+
blocks,
|
|
392
|
+
renderers,
|
|
393
|
+
containerTypes = [],
|
|
394
|
+
onChange,
|
|
395
|
+
dragOverlay,
|
|
396
|
+
activationDistance = 8,
|
|
397
|
+
previewDebounce = 150,
|
|
398
|
+
className = "flex flex-col gap-1",
|
|
399
|
+
dropZoneClassName,
|
|
400
|
+
dropZoneActiveClassName,
|
|
401
|
+
indentClassName
|
|
402
|
+
}) {
|
|
403
|
+
const sensors = useConfiguredSensors({ activationDistance });
|
|
404
|
+
const stateRef = useRef({
|
|
405
|
+
activeId: null,
|
|
406
|
+
hoverZone: null,
|
|
407
|
+
expandedMap: {},
|
|
408
|
+
virtualState: null
|
|
409
|
+
});
|
|
410
|
+
const initialBlocksRef = useRef([]);
|
|
411
|
+
const cachedReorderRef = useRef(null);
|
|
412
|
+
const [, forceRender] = useReducer((x) => x + 1, 0);
|
|
413
|
+
const debouncedSetVirtual = useRef(
|
|
414
|
+
debounce((newBlocks) => {
|
|
415
|
+
if (newBlocks) {
|
|
416
|
+
stateRef.current.virtualState = computeNormalizedIndex(newBlocks);
|
|
417
|
+
} else {
|
|
418
|
+
stateRef.current.virtualState = null;
|
|
419
|
+
}
|
|
420
|
+
forceRender();
|
|
421
|
+
}, previewDebounce)
|
|
422
|
+
).current;
|
|
423
|
+
const effectiveIndex = stateRef.current.virtualState ?? computeNormalizedIndex(blocks);
|
|
424
|
+
const blocksByParent = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const [parentId, ids] of effectiveIndex.byParent.entries()) {
|
|
426
|
+
blocksByParent.set(
|
|
427
|
+
parentId,
|
|
428
|
+
ids.map((id) => effectiveIndex.byId.get(id)).filter(Boolean)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const activeBlock = stateRef.current.activeId ? effectiveIndex.byId.get(stateRef.current.activeId) ?? null : null;
|
|
432
|
+
const handleDragStart = useCallback((event) => {
|
|
433
|
+
const id = String(event.active.id);
|
|
434
|
+
stateRef.current.activeId = id;
|
|
435
|
+
initialBlocksRef.current = [...blocks];
|
|
436
|
+
cachedReorderRef.current = null;
|
|
437
|
+
forceRender();
|
|
438
|
+
}, [blocks]);
|
|
439
|
+
const handleDragOver = useCallback((event) => {
|
|
440
|
+
if (!event.over) return;
|
|
441
|
+
const targetZone = String(event.over.id);
|
|
442
|
+
const activeId = stateRef.current.activeId;
|
|
443
|
+
if (!activeId) return;
|
|
444
|
+
stateRef.current.hoverZone = targetZone;
|
|
445
|
+
const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
|
|
446
|
+
const updatedIndex = reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes);
|
|
447
|
+
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
|
|
448
|
+
cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
|
|
449
|
+
debouncedSetVirtual(orderedBlocks);
|
|
450
|
+
}, [containerTypes, debouncedSetVirtual]);
|
|
451
|
+
const handleDragEnd = useCallback((_event) => {
|
|
452
|
+
debouncedSetVirtual.cancel();
|
|
453
|
+
const cached = cachedReorderRef.current;
|
|
454
|
+
stateRef.current.activeId = null;
|
|
455
|
+
stateRef.current.hoverZone = null;
|
|
456
|
+
stateRef.current.virtualState = null;
|
|
457
|
+
cachedReorderRef.current = null;
|
|
458
|
+
initialBlocksRef.current = [];
|
|
459
|
+
if (cached && onChange) {
|
|
460
|
+
onChange(cached.reorderedBlocks);
|
|
461
|
+
}
|
|
462
|
+
forceRender();
|
|
463
|
+
}, [debouncedSetVirtual, onChange]);
|
|
464
|
+
const handleHover = useCallback((zoneId, _parentId) => {
|
|
465
|
+
const activeId = stateRef.current.activeId;
|
|
466
|
+
if (!activeId) return;
|
|
467
|
+
stateRef.current.hoverZone = zoneId;
|
|
468
|
+
const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
|
|
469
|
+
const updatedIndex = reparentBlockIndex(baseIndex, activeId, zoneId, containerTypes);
|
|
470
|
+
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
|
|
471
|
+
cachedReorderRef.current = { targetId: zoneId, reorderedBlocks: orderedBlocks };
|
|
472
|
+
debouncedSetVirtual(orderedBlocks);
|
|
473
|
+
}, [containerTypes, debouncedSetVirtual]);
|
|
474
|
+
const handleToggleExpand = useCallback((id) => {
|
|
475
|
+
stateRef.current.expandedMap = {
|
|
476
|
+
...stateRef.current.expandedMap,
|
|
477
|
+
[id]: stateRef.current.expandedMap[id] === false
|
|
478
|
+
};
|
|
479
|
+
forceRender();
|
|
480
|
+
}, []);
|
|
481
|
+
return /* @__PURE__ */ jsxs(
|
|
482
|
+
DndContext,
|
|
483
|
+
{
|
|
484
|
+
sensors,
|
|
485
|
+
collisionDetection: weightedVerticalCollision,
|
|
486
|
+
onDragStart: handleDragStart,
|
|
487
|
+
onDragOver: handleDragOver,
|
|
488
|
+
onDragEnd: handleDragEnd,
|
|
489
|
+
children: [
|
|
490
|
+
/* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(
|
|
491
|
+
TreeRenderer,
|
|
492
|
+
{
|
|
493
|
+
blocks,
|
|
494
|
+
blocksByParent,
|
|
495
|
+
parentId: null,
|
|
496
|
+
activeId: stateRef.current.activeId,
|
|
497
|
+
expandedMap: stateRef.current.expandedMap,
|
|
498
|
+
renderers,
|
|
499
|
+
containerTypes,
|
|
500
|
+
onHover: handleHover,
|
|
501
|
+
onToggleExpand: handleToggleExpand,
|
|
502
|
+
dropZoneClassName,
|
|
503
|
+
dropZoneActiveClassName,
|
|
504
|
+
indentClassName,
|
|
505
|
+
rootClassName: className
|
|
506
|
+
}
|
|
507
|
+
) }),
|
|
508
|
+
/* @__PURE__ */ jsx(DragOverlay, { activeBlock, children: dragOverlay })
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
function blockReducer(state, action, containerTypes = []) {
|
|
514
|
+
switch (action.type) {
|
|
515
|
+
case "ADD_ITEM": {
|
|
516
|
+
const byId = cloneMap(state.byId);
|
|
517
|
+
const byParent = cloneParentMap(state.byParent);
|
|
518
|
+
const item = action.payload;
|
|
519
|
+
byId.set(item.id, item);
|
|
520
|
+
const parentKey = item.parentId ?? null;
|
|
521
|
+
const list = byParent.get(parentKey) ?? [];
|
|
522
|
+
const insertAt = typeof item.order === "number" && item.order <= list.length ? item.order : list.length;
|
|
523
|
+
const newList = [...list];
|
|
524
|
+
newList.splice(insertAt, 0, item.id);
|
|
525
|
+
byParent.set(parentKey, newList);
|
|
526
|
+
return { byId, byParent };
|
|
527
|
+
}
|
|
528
|
+
case "INSERT_ITEM": {
|
|
529
|
+
const { item, parentId, index } = action.payload;
|
|
530
|
+
const updated = new Map(state.byParent);
|
|
531
|
+
const siblings = [...updated.get(parentId) ?? []];
|
|
532
|
+
siblings.splice(index, 0, item.id);
|
|
533
|
+
updated.set(parentId, siblings);
|
|
534
|
+
return {
|
|
535
|
+
byId: new Map(state.byId).set(item.id, item),
|
|
536
|
+
byParent: updated
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
case "DELETE_ITEM": {
|
|
540
|
+
return deleteBlockAndDescendants(state, action.payload.id);
|
|
541
|
+
}
|
|
542
|
+
case "SET_ALL": {
|
|
543
|
+
return computeNormalizedIndex(action.payload);
|
|
544
|
+
}
|
|
545
|
+
case "MOVE_ITEM": {
|
|
546
|
+
return reparentBlockIndex(
|
|
547
|
+
state,
|
|
548
|
+
action.payload.activeId,
|
|
549
|
+
action.payload.targetZone,
|
|
550
|
+
containerTypes
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
default:
|
|
554
|
+
return state;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function createBlockState() {
|
|
558
|
+
const BlockContext = createContext(null);
|
|
559
|
+
function useBlockState() {
|
|
560
|
+
const ctx = useContext(BlockContext);
|
|
561
|
+
if (!ctx) throw new Error("useBlockState must be used inside BlockStateProvider");
|
|
562
|
+
return ctx;
|
|
563
|
+
}
|
|
564
|
+
function BlockStateProvider({
|
|
565
|
+
children,
|
|
566
|
+
initialBlocks = [],
|
|
567
|
+
containerTypes = [],
|
|
568
|
+
onChange
|
|
569
|
+
}) {
|
|
570
|
+
const reducerWithContainerTypes = useCallback(
|
|
571
|
+
(state2, action) => blockReducer(state2, action, containerTypes),
|
|
572
|
+
[containerTypes]
|
|
573
|
+
);
|
|
574
|
+
const [state, dispatch] = useReducer(
|
|
575
|
+
reducerWithContainerTypes,
|
|
576
|
+
computeNormalizedIndex(initialBlocks)
|
|
577
|
+
);
|
|
578
|
+
const [lastCreatedItem, setLastCreatedItem] = useState(null);
|
|
579
|
+
const blocks = useMemo(() => {
|
|
580
|
+
const result = [];
|
|
581
|
+
const walk = (parentId) => {
|
|
582
|
+
const children2 = state.byParent.get(parentId) ?? [];
|
|
583
|
+
for (let i = 0; i < children2.length; i++) {
|
|
584
|
+
const id = children2[i];
|
|
585
|
+
const b = state.byId.get(id);
|
|
586
|
+
if (b) {
|
|
587
|
+
result.push({ ...b, order: i });
|
|
588
|
+
if (containerTypes.includes(b.type)) walk(b.id);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
walk(null);
|
|
593
|
+
return result;
|
|
594
|
+
}, [state, containerTypes]);
|
|
595
|
+
useMemo(() => {
|
|
596
|
+
onChange?.(blocks);
|
|
597
|
+
}, [blocks, onChange]);
|
|
598
|
+
const blockMap = useMemo(() => state.byId, [state]);
|
|
599
|
+
const childrenMap = useMemo(() => {
|
|
600
|
+
const map = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const [parentId, ids] of state.byParent.entries()) {
|
|
602
|
+
map.set(
|
|
603
|
+
parentId,
|
|
604
|
+
ids.map((id) => state.byId.get(id)).filter(Boolean)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
return map;
|
|
608
|
+
}, [state]);
|
|
609
|
+
const indexMap = useMemo(() => {
|
|
610
|
+
const map = /* @__PURE__ */ new Map();
|
|
611
|
+
for (const ids of state.byParent.values()) {
|
|
612
|
+
ids.forEach((id, index) => {
|
|
613
|
+
map.set(id, index);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return map;
|
|
617
|
+
}, [state]);
|
|
618
|
+
const createItem = useCallback(
|
|
619
|
+
(type, parentId = null) => {
|
|
620
|
+
const newItem = {
|
|
621
|
+
id: generateId(),
|
|
622
|
+
type,
|
|
623
|
+
parentId,
|
|
624
|
+
order: 0
|
|
625
|
+
};
|
|
626
|
+
dispatch({ type: "ADD_ITEM", payload: newItem });
|
|
627
|
+
setLastCreatedItem(newItem);
|
|
628
|
+
return newItem;
|
|
629
|
+
},
|
|
630
|
+
[]
|
|
631
|
+
);
|
|
632
|
+
const insertItem = useCallback(
|
|
633
|
+
(type, referenceId, position) => {
|
|
634
|
+
const referenceBlock = state.byId.get(referenceId);
|
|
635
|
+
if (!referenceBlock) throw new Error(`Reference block ${referenceId} not found`);
|
|
636
|
+
const parentId = referenceBlock.parentId ?? null;
|
|
637
|
+
const siblings = state.byParent.get(parentId) ?? [];
|
|
638
|
+
const index = siblings.indexOf(referenceId);
|
|
639
|
+
const insertIndex = position === "before" ? index : index + 1;
|
|
640
|
+
const newItem = {
|
|
641
|
+
id: generateId(),
|
|
642
|
+
type,
|
|
643
|
+
parentId,
|
|
644
|
+
order: insertIndex
|
|
645
|
+
};
|
|
646
|
+
dispatch({
|
|
647
|
+
type: "INSERT_ITEM",
|
|
648
|
+
payload: { item: newItem, parentId, index: insertIndex }
|
|
649
|
+
});
|
|
650
|
+
setLastCreatedItem(newItem);
|
|
651
|
+
return newItem;
|
|
652
|
+
},
|
|
653
|
+
[state]
|
|
654
|
+
);
|
|
655
|
+
const deleteItem = useCallback((id) => {
|
|
656
|
+
dispatch({ type: "DELETE_ITEM", payload: { id } });
|
|
657
|
+
}, []);
|
|
658
|
+
const moveItem = useCallback((activeId, targetZone) => {
|
|
659
|
+
dispatch({ type: "MOVE_ITEM", payload: { activeId, targetZone } });
|
|
660
|
+
}, []);
|
|
661
|
+
const setAll = useCallback((all) => {
|
|
662
|
+
dispatch({ type: "SET_ALL", payload: all });
|
|
663
|
+
}, []);
|
|
664
|
+
const value = useMemo(
|
|
665
|
+
() => ({
|
|
666
|
+
blocks,
|
|
667
|
+
blockMap,
|
|
668
|
+
childrenMap,
|
|
669
|
+
indexMap,
|
|
670
|
+
normalizedIndex: state,
|
|
671
|
+
createItem,
|
|
672
|
+
insertItem,
|
|
673
|
+
deleteItem,
|
|
674
|
+
moveItem,
|
|
675
|
+
setAll
|
|
676
|
+
}),
|
|
677
|
+
[
|
|
678
|
+
blocks,
|
|
679
|
+
blockMap,
|
|
680
|
+
childrenMap,
|
|
681
|
+
indexMap,
|
|
682
|
+
state,
|
|
683
|
+
createItem,
|
|
684
|
+
insertItem,
|
|
685
|
+
deleteItem,
|
|
686
|
+
moveItem,
|
|
687
|
+
setAll
|
|
688
|
+
]
|
|
689
|
+
);
|
|
690
|
+
return /* @__PURE__ */ jsx(BlockContext.Provider, { value, children });
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
BlockStateProvider,
|
|
694
|
+
useBlockState
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function expandReducer(state, action) {
|
|
698
|
+
switch (action.type) {
|
|
699
|
+
case "TOGGLE":
|
|
700
|
+
return { ...state, [action.id]: !state[action.id] };
|
|
701
|
+
case "SET_ALL": {
|
|
702
|
+
const newState = {};
|
|
703
|
+
for (const id of action.ids) {
|
|
704
|
+
newState[id] = action.expanded;
|
|
705
|
+
}
|
|
706
|
+
return newState;
|
|
707
|
+
}
|
|
708
|
+
default:
|
|
709
|
+
return state;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function createTreeState(options = {}) {
|
|
713
|
+
const { previewDebounce = 150, containerTypes = [] } = options;
|
|
714
|
+
const TreeContext = createContext(null);
|
|
715
|
+
function useTreeState() {
|
|
716
|
+
const ctx = useContext(TreeContext);
|
|
717
|
+
if (!ctx) throw new Error("useTreeState must be used inside TreeStateProvider");
|
|
718
|
+
return ctx;
|
|
719
|
+
}
|
|
720
|
+
function TreeStateProvider({ children, blocks, blockMap }) {
|
|
721
|
+
const [activeId, setActiveId] = useState(null);
|
|
722
|
+
const [hoverZone, setHoverZone] = useState(null);
|
|
723
|
+
const [virtualState, setVirtualState] = useState(null);
|
|
724
|
+
const [expandedMap, dispatchExpand] = useReducer(expandReducer, {});
|
|
725
|
+
const initialBlocksRef = useRef([]);
|
|
726
|
+
const cachedReorderRef = useRef(null);
|
|
727
|
+
const activeBlock = useMemo(() => {
|
|
728
|
+
if (!activeId) return null;
|
|
729
|
+
return blockMap.get(activeId) ?? null;
|
|
730
|
+
}, [activeId, blockMap]);
|
|
731
|
+
const debouncedSetVirtualBlocks = useMemo(
|
|
732
|
+
() => debounce((newBlocks) => {
|
|
733
|
+
if (!newBlocks) {
|
|
734
|
+
setVirtualState(null);
|
|
735
|
+
} else {
|
|
736
|
+
setVirtualState(computeNormalizedIndex(newBlocks));
|
|
737
|
+
}
|
|
738
|
+
}, previewDebounce),
|
|
739
|
+
[previewDebounce]
|
|
740
|
+
);
|
|
741
|
+
const effectiveState = useMemo(() => {
|
|
742
|
+
return virtualState ?? computeNormalizedIndex(blocks);
|
|
743
|
+
}, [virtualState, blocks]);
|
|
744
|
+
const effectiveBlocks = useMemo(() => {
|
|
745
|
+
return buildOrderedBlocks(effectiveState, containerTypes);
|
|
746
|
+
}, [effectiveState, containerTypes]);
|
|
747
|
+
const blocksByParent = useMemo(() => {
|
|
748
|
+
const map = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const [parentId, ids] of effectiveState.byParent.entries()) {
|
|
750
|
+
map.set(
|
|
751
|
+
parentId,
|
|
752
|
+
ids.map((id) => effectiveState.byId.get(id)).filter(Boolean)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
return map;
|
|
756
|
+
}, [effectiveState]);
|
|
757
|
+
const handleDragStart = useCallback(
|
|
758
|
+
(id) => {
|
|
759
|
+
setActiveId(id);
|
|
760
|
+
if (id) {
|
|
761
|
+
initialBlocksRef.current = [...blocks];
|
|
762
|
+
cachedReorderRef.current = null;
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
[blocks]
|
|
766
|
+
);
|
|
767
|
+
const handleDragOver = useCallback(
|
|
768
|
+
(targetZone) => {
|
|
769
|
+
if (!activeId) return;
|
|
770
|
+
setHoverZone(targetZone);
|
|
771
|
+
const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
|
|
772
|
+
const updatedIndex = reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes);
|
|
773
|
+
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
|
|
774
|
+
cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
|
|
775
|
+
debouncedSetVirtualBlocks(orderedBlocks);
|
|
776
|
+
},
|
|
777
|
+
[activeId, debouncedSetVirtualBlocks, containerTypes]
|
|
778
|
+
);
|
|
779
|
+
useCallback(() => {
|
|
780
|
+
debouncedSetVirtualBlocks.cancel();
|
|
781
|
+
setVirtualState(null);
|
|
782
|
+
setActiveId(null);
|
|
783
|
+
setHoverZone(null);
|
|
784
|
+
const result = cachedReorderRef.current;
|
|
785
|
+
cachedReorderRef.current = null;
|
|
786
|
+
initialBlocksRef.current = [];
|
|
787
|
+
return result;
|
|
788
|
+
}, [debouncedSetVirtualBlocks]);
|
|
789
|
+
const handleHover = useCallback(
|
|
790
|
+
(zoneId, _parentId) => {
|
|
791
|
+
if (!activeId) return;
|
|
792
|
+
handleDragOver(zoneId);
|
|
793
|
+
},
|
|
794
|
+
[activeId, handleDragOver]
|
|
795
|
+
);
|
|
796
|
+
const toggleExpand = useCallback((id) => {
|
|
797
|
+
dispatchExpand({ type: "TOGGLE", id });
|
|
798
|
+
}, []);
|
|
799
|
+
const setExpandAll = useCallback(
|
|
800
|
+
(expanded) => {
|
|
801
|
+
const containerIds = blocks.filter((b) => containerTypes.includes(b.type)).map((b) => b.id);
|
|
802
|
+
dispatchExpand({ type: "SET_ALL", expanded, ids: containerIds });
|
|
803
|
+
},
|
|
804
|
+
[blocks, containerTypes]
|
|
805
|
+
);
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
return () => {
|
|
808
|
+
debouncedSetVirtualBlocks.cancel();
|
|
809
|
+
};
|
|
810
|
+
}, [debouncedSetVirtualBlocks]);
|
|
811
|
+
const value = useMemo(
|
|
812
|
+
() => ({
|
|
813
|
+
activeId,
|
|
814
|
+
activeBlock,
|
|
815
|
+
hoverZone,
|
|
816
|
+
expandedMap,
|
|
817
|
+
effectiveBlocks,
|
|
818
|
+
blocksByParent,
|
|
819
|
+
setActiveId: handleDragStart,
|
|
820
|
+
setHoverZone,
|
|
821
|
+
toggleExpand,
|
|
822
|
+
setExpandAll,
|
|
823
|
+
handleHover
|
|
824
|
+
}),
|
|
825
|
+
[
|
|
826
|
+
activeId,
|
|
827
|
+
activeBlock,
|
|
828
|
+
hoverZone,
|
|
829
|
+
expandedMap,
|
|
830
|
+
effectiveBlocks,
|
|
831
|
+
blocksByParent,
|
|
832
|
+
handleDragStart,
|
|
833
|
+
toggleExpand,
|
|
834
|
+
setExpandAll,
|
|
835
|
+
handleHover
|
|
836
|
+
]
|
|
837
|
+
);
|
|
838
|
+
return /* @__PURE__ */ jsx(TreeContext.Provider, { value, children });
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
TreeStateProvider,
|
|
842
|
+
useTreeState
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export { BlockTree, DragOverlay, DropZone, TreeRenderer, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, computeNormalizedIndex, createBlockState, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, getDescendantIds, getDropZoneType, getSensorConfig, reparentBlockIndex, useConfiguredSensors, weightedVerticalCollision };
|
|
847
|
+
//# sourceMappingURL=index.mjs.map
|
|
848
|
+
//# sourceMappingURL=index.mjs.map
|