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.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