dnd-block-tree 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
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';
2
+ import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, createContext, useContext, useState } from 'react';
3
3
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
 
5
5
  // src/core/types.ts
@@ -13,11 +13,8 @@ function extractBlockId(zoneId) {
13
13
  }
14
14
 
15
15
  // src/core/collision.ts
16
- var weightedVerticalCollision = ({
17
- droppableContainers,
18
- collisionRect
19
- }) => {
20
- if (!collisionRect) return [];
16
+ function computeCollisionScores(droppableContainers, collisionRect) {
17
+ const pointerX = collisionRect.left + collisionRect.width / 2;
21
18
  const pointerY = collisionRect.top + collisionRect.height / 2;
22
19
  const candidates = droppableContainers.map((container) => {
23
20
  const rect = container.rect.current;
@@ -27,11 +24,19 @@ var weightedVerticalCollision = ({
27
24
  const edgeDistance = Math.min(distanceToTop, distanceToBottom);
28
25
  const isBelowCenter = pointerY > rect.top + rect.height / 2;
29
26
  const bias = isBelowCenter ? -5 : 0;
27
+ const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
28
+ let horizontalScore = 0;
29
+ if (isWithinX) {
30
+ horizontalScore = -rect.left * 0.1;
31
+ } else {
32
+ const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
33
+ horizontalScore = Math.min(distanceToZone, 50);
34
+ }
30
35
  return {
31
36
  id: container.id,
32
37
  data: {
33
38
  droppableContainer: container,
34
- value: edgeDistance + bias
39
+ value: edgeDistance + bias + horizontalScore
35
40
  }
36
41
  };
37
42
  }).filter((c) => c !== null);
@@ -40,8 +45,44 @@ var weightedVerticalCollision = ({
40
45
  const bValue = b.data.value;
41
46
  return aValue - bValue;
42
47
  });
48
+ return candidates;
49
+ }
50
+ var weightedVerticalCollision = ({
51
+ droppableContainers,
52
+ collisionRect
53
+ }) => {
54
+ if (!collisionRect) return [];
55
+ const candidates = computeCollisionScores(droppableContainers, collisionRect);
43
56
  return candidates.slice(0, 1);
44
57
  };
58
+ function createStickyCollision(threshold = 15) {
59
+ let currentZoneId = null;
60
+ const detector = ({
61
+ droppableContainers,
62
+ collisionRect
63
+ }) => {
64
+ if (!collisionRect) return [];
65
+ const candidates = computeCollisionScores(droppableContainers, collisionRect);
66
+ if (candidates.length === 0) return [];
67
+ const bestCandidate = candidates[0];
68
+ const bestScore = bestCandidate.data.value;
69
+ if (currentZoneId !== null) {
70
+ const currentCandidate = candidates.find((c) => c.id === currentZoneId);
71
+ if (currentCandidate) {
72
+ const currentScore = currentCandidate.data.value;
73
+ if (currentScore - bestScore < threshold) {
74
+ return [currentCandidate];
75
+ }
76
+ }
77
+ }
78
+ currentZoneId = bestCandidate.id;
79
+ return [bestCandidate];
80
+ };
81
+ detector.reset = () => {
82
+ currentZoneId = null;
83
+ };
84
+ return detector;
85
+ }
45
86
  var closestCenterCollision = ({
46
87
  droppableContainers,
47
88
  collisionRect
@@ -74,38 +115,57 @@ var closestCenterCollision = ({
74
115
  };
75
116
  var DEFAULT_ACTIVATION_DISTANCE = 8;
76
117
  function useConfiguredSensors(config = {}) {
77
- const { activationDistance = DEFAULT_ACTIVATION_DISTANCE } = config;
118
+ const {
119
+ activationDistance = DEFAULT_ACTIVATION_DISTANCE,
120
+ activationDelay,
121
+ tolerance
122
+ } = config;
123
+ let activationConstraint;
124
+ if (activationDelay !== void 0) {
125
+ activationConstraint = {
126
+ delay: activationDelay,
127
+ tolerance: tolerance ?? 5
128
+ };
129
+ } else {
130
+ activationConstraint = {
131
+ distance: activationDistance
132
+ };
133
+ }
78
134
  return useSensors(
79
135
  useSensor(PointerSensor, {
80
- activationConstraint: {
81
- distance: activationDistance
82
- }
136
+ activationConstraint
83
137
  }),
84
138
  useSensor(TouchSensor, {
85
- activationConstraint: {
86
- distance: activationDistance
87
- }
139
+ activationConstraint
88
140
  }),
89
141
  useSensor(KeyboardSensor)
90
142
  );
91
143
  }
92
- function getSensorConfig(activationDistance = DEFAULT_ACTIVATION_DISTANCE) {
144
+ function getSensorConfig(config = {}) {
145
+ const {
146
+ activationDistance = DEFAULT_ACTIVATION_DISTANCE,
147
+ activationDelay,
148
+ tolerance
149
+ } = config;
150
+ let activationConstraint;
151
+ if (activationDelay !== void 0) {
152
+ activationConstraint = {
153
+ delay: activationDelay,
154
+ tolerance: tolerance ?? 5
155
+ };
156
+ } else {
157
+ activationConstraint = {
158
+ distance: activationDistance
159
+ };
160
+ }
93
161
  return {
94
- pointer: {
95
- activationConstraint: {
96
- distance: activationDistance
97
- }
98
- },
99
- touch: {
100
- activationConstraint: {
101
- distance: activationDistance
102
- }
103
- }
162
+ pointer: { activationConstraint },
163
+ touch: { activationConstraint }
104
164
  };
105
165
  }
106
166
 
107
167
  // src/utils/helper.ts
108
- function extractUUID(id, pattern = "^(before|after|into)-") {
168
+ function extractUUID(id, pattern = "^(before|after|into|end)-") {
109
169
  const regex = new RegExp(pattern);
110
170
  return id.replace(regex, "");
111
171
  }
@@ -145,8 +205,10 @@ function DropZoneComponent({
145
205
  useEffect(() => {
146
206
  if (isOver) handleInternalHover();
147
207
  }, [isOver, handleInternalHover]);
148
- if (active?.id && extractUUID(id) === String(active.id)) return null;
149
- if (activeId && extractUUID(id) === activeId) return null;
208
+ const zoneBlockId = extractUUID(id);
209
+ const isIntoZone = id.startsWith("into-");
210
+ if (isIntoZone && active?.id && zoneBlockId === String(active.id)) return null;
211
+ if (isIntoZone && activeId && zoneBlockId === activeId) return null;
150
212
  return /* @__PURE__ */ jsx(
151
213
  "div",
152
214
  {
@@ -161,10 +223,12 @@ function DropZoneComponent({
161
223
  var DropZone = memo(DropZoneComponent);
162
224
  function DraggableBlock({
163
225
  block,
164
- children
226
+ children,
227
+ disabled
165
228
  }) {
166
229
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
167
- id: block.id
230
+ id: block.id,
231
+ disabled
168
232
  });
169
233
  return /* @__PURE__ */ jsx("div", { ref: setNodeRef, ...attributes, ...listeners, children: children({ isDragging }) });
170
234
  }
@@ -182,47 +246,49 @@ function TreeRenderer({
182
246
  dropZoneClassName,
183
247
  dropZoneActiveClassName,
184
248
  indentClassName = "ml-6 border-l border-gray-200 pl-4",
185
- rootClassName = "flex flex-col gap-1"
249
+ rootClassName = "flex flex-col gap-1",
250
+ canDrag,
251
+ previewPosition,
252
+ draggedBlock
186
253
  }) {
187
254
  const items = blocksByParent.get(parentId) ?? [];
188
255
  const filteredBlocks = items.filter((block) => block.id !== activeId);
189
- const firstVisibleBlockId = filteredBlocks[0]?.id;
256
+ const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
190
257
  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(
258
+ return /* @__PURE__ */ jsxs("div", { className: containerClass, children: [
259
+ /* @__PURE__ */ jsx(
260
+ DropZone,
261
+ {
262
+ id: parentId ? `into-${parentId}` : "root-start",
263
+ parentId,
264
+ onHover,
265
+ activeId,
266
+ className: dropZoneClassName,
267
+ activeClassName: dropZoneActiveClassName
268
+ }
269
+ ),
270
+ filteredBlocks.map((block, index) => {
271
+ const isContainer = containerTypes.includes(block.type);
272
+ const isExpanded = expandedMap[block.id] !== false;
273
+ const Renderer = renderers[block.type];
274
+ const isDragDisabled = canDrag ? !canDrag(block) : false;
275
+ const ghostBeforeThis = showGhostHere && previewPosition.index === index;
276
+ const originalIndex = items.findIndex((b) => b.id === block.id);
277
+ const isLastInOriginal = originalIndex === items.length - 1;
278
+ if (!Renderer) {
279
+ console.warn(`No renderer found for block type: ${block.type}`);
280
+ return null;
281
+ }
282
+ const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
283
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
284
+ ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", children: GhostRenderer({
285
+ block: draggedBlock,
286
+ isDragging: true,
287
+ depth
288
+ }) }),
289
+ /* @__PURE__ */ jsx(DraggableBlock, { block, disabled: isDragDisabled, children: ({ isDragging }) => {
290
+ if (isContainer) {
291
+ const childContent = isExpanded ? /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
226
292
  TreeRenderer,
227
293
  {
228
294
  blocks,
@@ -238,38 +304,60 @@ function TreeRenderer({
238
304
  dropZoneClassName,
239
305
  dropZoneActiveClassName,
240
306
  indentClassName,
241
- rootClassName
307
+ rootClassName,
308
+ canDrag,
309
+ previewPosition,
310
+ draggedBlock
242
311
  }
243
- )
244
- ] }) : null;
312
+ ) }) : null;
313
+ return Renderer({
314
+ block,
315
+ children: childContent,
316
+ isDragging,
317
+ depth,
318
+ isExpanded,
319
+ onToggleExpand: () => onToggleExpand(block.id)
320
+ });
321
+ }
245
322
  return Renderer({
246
323
  block,
247
- children: childContent,
248
324
  isDragging,
249
- depth,
250
- isExpanded,
251
- onToggleExpand: () => onToggleExpand(block.id)
325
+ depth
252
326
  });
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
- }) });
327
+ } }),
328
+ !isLastInOriginal && /* @__PURE__ */ jsx(
329
+ DropZone,
330
+ {
331
+ id: `after-${block.id}`,
332
+ parentId: block.parentId,
333
+ onHover,
334
+ activeId,
335
+ className: dropZoneClassName,
336
+ activeClassName: dropZoneActiveClassName
337
+ }
338
+ )
339
+ ] }, block.id);
340
+ }),
341
+ showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
342
+ const GhostRenderer = renderers[draggedBlock.type];
343
+ return GhostRenderer ? /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", children: GhostRenderer({
344
+ block: draggedBlock,
345
+ isDragging: true,
346
+ depth
347
+ }) }) : null;
348
+ })(),
349
+ /* @__PURE__ */ jsx(
350
+ DropZone,
351
+ {
352
+ id: parentId ? `end-${parentId}` : "root-end",
353
+ parentId,
354
+ onHover,
355
+ activeId,
356
+ className: dropZoneClassName,
357
+ activeClassName: dropZoneActiveClassName
358
+ }
359
+ )
360
+ ] });
273
361
  }
274
362
  function DragOverlay({
275
363
  activeBlock,
@@ -329,12 +417,15 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = []) {
329
417
  const byParent = cloneParentMap(state.byParent);
330
418
  const dragged = byId.get(String(activeId));
331
419
  if (!dragged) return state;
420
+ const isRootStart = targetZone === "root-start";
421
+ const isRootEnd = targetZone === "root-end";
422
+ const isEnd = targetZone.startsWith("end-") || isRootEnd;
332
423
  const zoneTargetId = extractUUID(targetZone);
333
424
  const isAfter = targetZone.startsWith("after-");
334
- const isInto = targetZone.startsWith("into-");
425
+ const isInto = targetZone.startsWith("into-") || isRootStart;
335
426
  const target = byId.get(zoneTargetId);
336
427
  const oldParentId = dragged.parentId ?? null;
337
- const newParentId = isInto ? zoneTargetId : target?.parentId ?? null;
428
+ const newParentId = isRootStart || isRootEnd ? null : isInto || isEnd ? zoneTargetId : target?.parentId ?? null;
338
429
  if (containerTypes.includes(dragged.type) && newParentId !== null) {
339
430
  const newParent = byId.get(newParentId);
340
431
  if (newParent && !containerTypes.includes(newParent.type)) {
@@ -346,8 +437,12 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = []) {
346
437
  const filtered = oldList.filter((id) => id !== dragged.id);
347
438
  byParent.set(oldParentId, filtered);
348
439
  const newList = [...byParent.get(newParentId) ?? []];
349
- let insertIndex = newList.length;
350
- if (!isInto) {
440
+ let insertIndex;
441
+ if (isInto) {
442
+ insertIndex = 0;
443
+ } else if (isEnd) {
444
+ insertIndex = newList.length;
445
+ } else {
351
446
  const idx = newList.indexOf(zoneTargetId);
352
447
  insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
353
448
  }
@@ -387,6 +482,30 @@ function deleteBlockAndDescendants(state, id) {
387
482
  }
388
483
  return { byId, byParent };
389
484
  }
485
+ function getBlockPosition(blocks, blockId) {
486
+ const block = blocks.find((b) => b.id === blockId);
487
+ if (!block) return { parentId: null, index: 0 };
488
+ const siblings = blocks.filter((b) => b.parentId === block.parentId);
489
+ const index = siblings.findIndex((b) => b.id === blockId);
490
+ return { parentId: block.parentId, index };
491
+ }
492
+ function computeInitialExpanded(blocks, containerTypes, initialExpanded) {
493
+ if (initialExpanded === "none") {
494
+ return {};
495
+ }
496
+ const expandedMap = {};
497
+ const containers = blocks.filter((b) => containerTypes.includes(b.type));
498
+ if (initialExpanded === "all" || initialExpanded === void 0) {
499
+ for (const container of containers) {
500
+ expandedMap[container.id] = true;
501
+ }
502
+ } else if (Array.isArray(initialExpanded)) {
503
+ for (const id of initialExpanded) {
504
+ expandedMap[id] = true;
505
+ }
506
+ }
507
+ return expandedMap;
508
+ }
390
509
  function BlockTree({
391
510
  blocks,
392
511
  renderers,
@@ -398,17 +517,45 @@ function BlockTree({
398
517
  className = "flex flex-col gap-1",
399
518
  dropZoneClassName,
400
519
  dropZoneActiveClassName,
401
- indentClassName
520
+ indentClassName,
521
+ showDropPreview = true,
522
+ // Callbacks
523
+ onDragStart,
524
+ onDragMove,
525
+ onDragEnd,
526
+ onDragCancel,
527
+ onBlockMove,
528
+ onExpandChange,
529
+ onHoverChange,
530
+ // Customization
531
+ canDrag,
532
+ canDrop,
533
+ collisionDetection,
534
+ sensors: sensorConfig,
535
+ initialExpanded
402
536
  }) {
403
- const sensors = useConfiguredSensors({ activationDistance });
537
+ const sensors = useConfiguredSensors({
538
+ activationDistance: sensorConfig?.activationDistance ?? activationDistance,
539
+ activationDelay: sensorConfig?.activationDelay,
540
+ tolerance: sensorConfig?.tolerance
541
+ });
542
+ const initialExpandedMap = useMemo(
543
+ () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
544
+ // Only compute on mount
545
+ // eslint-disable-next-line react-hooks/exhaustive-deps
546
+ []
547
+ );
404
548
  const stateRef = useRef({
405
549
  activeId: null,
406
550
  hoverZone: null,
407
- expandedMap: {},
408
- virtualState: null
551
+ expandedMap: initialExpandedMap,
552
+ virtualState: null,
553
+ isDragging: false
409
554
  });
410
555
  const initialBlocksRef = useRef([]);
411
556
  const cachedReorderRef = useRef(null);
557
+ const fromPositionRef = useRef(null);
558
+ const stickyCollisionRef = useRef(createStickyCollision(20));
412
559
  const [, forceRender] = useReducer((x) => x + 1, 0);
413
560
  const debouncedSetVirtual = useRef(
414
561
  debounce((newBlocks) => {
@@ -420,72 +567,218 @@ function BlockTree({
420
567
  forceRender();
421
568
  }, previewDebounce)
422
569
  ).current;
423
- const effectiveIndex = stateRef.current.virtualState ?? computeNormalizedIndex(blocks);
570
+ const debouncedDragMove = useRef(
571
+ debounce((event) => {
572
+ onDragMove?.(event);
573
+ }, 50)
574
+ ).current;
575
+ const originalIndex = computeNormalizedIndex(blocks);
424
576
  const blocksByParent = /* @__PURE__ */ new Map();
425
- for (const [parentId, ids] of effectiveIndex.byParent.entries()) {
577
+ for (const [parentId, ids] of originalIndex.byParent.entries()) {
426
578
  blocksByParent.set(
427
579
  parentId,
428
- ids.map((id) => effectiveIndex.byId.get(id)).filter(Boolean)
580
+ ids.map((id) => originalIndex.byId.get(id)).filter(Boolean)
429
581
  );
430
582
  }
431
- const activeBlock = stateRef.current.activeId ? effectiveIndex.byId.get(stateRef.current.activeId) ?? null : null;
583
+ const previewPosition = useMemo(() => {
584
+ if (!showDropPreview || !stateRef.current.virtualState || !stateRef.current.activeId) {
585
+ return null;
586
+ }
587
+ const virtualIndex = stateRef.current.virtualState;
588
+ const activeId = stateRef.current.activeId;
589
+ const block = virtualIndex.byId.get(activeId);
590
+ if (!block) return null;
591
+ const parentId = block.parentId ?? null;
592
+ const siblings = virtualIndex.byParent.get(parentId) ?? [];
593
+ const index = siblings.indexOf(activeId);
594
+ return { parentId, index };
595
+ }, [showDropPreview, stateRef.current.virtualState, stateRef.current.activeId]);
596
+ const activeBlock = stateRef.current.activeId ? originalIndex.byId.get(stateRef.current.activeId) ?? null : null;
597
+ const draggedBlock = activeBlock;
432
598
  const handleDragStart = useCallback((event) => {
433
599
  const id = String(event.active.id);
600
+ const block = blocks.find((b) => b.id === id);
601
+ if (!block) return;
602
+ if (canDrag && !canDrag(block)) {
603
+ return;
604
+ }
605
+ const dragEvent = {
606
+ block,
607
+ blockId: id
608
+ };
609
+ const result = onDragStart?.(dragEvent);
610
+ if (result === false) {
611
+ return;
612
+ }
613
+ fromPositionRef.current = getBlockPosition(blocks, id);
614
+ stickyCollisionRef.current.reset();
434
615
  stateRef.current.activeId = id;
616
+ stateRef.current.isDragging = true;
435
617
  initialBlocksRef.current = [...blocks];
436
618
  cachedReorderRef.current = null;
437
619
  forceRender();
438
- }, [blocks]);
620
+ }, [blocks, canDrag, onDragStart]);
621
+ const handleDragMove = useCallback((event) => {
622
+ if (!onDragMove) return;
623
+ const id = stateRef.current.activeId;
624
+ if (!id) return;
625
+ const block = blocks.find((b) => b.id === id);
626
+ if (!block) return;
627
+ const moveEvent = {
628
+ block,
629
+ blockId: id,
630
+ overZone: stateRef.current.hoverZone,
631
+ coordinates: {
632
+ x: event.delta.x,
633
+ y: event.delta.y
634
+ }
635
+ };
636
+ debouncedDragMove(moveEvent);
637
+ }, [blocks, onDragMove, debouncedDragMove]);
439
638
  const handleDragOver = useCallback((event) => {
440
639
  if (!event.over) return;
441
640
  const targetZone = String(event.over.id);
442
641
  const activeId = stateRef.current.activeId;
443
642
  if (!activeId) return;
643
+ const activeBlock2 = blocks.find((b) => b.id === activeId);
644
+ const targetBlockId = extractBlockId(targetZone);
645
+ const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
646
+ if (canDrop && activeBlock2 && !canDrop(activeBlock2, targetZone, targetBlock)) {
647
+ return;
648
+ }
649
+ if (stateRef.current.hoverZone !== targetZone) {
650
+ const zoneType = getDropZoneType(targetZone);
651
+ const hoverEvent = {
652
+ zoneId: targetZone,
653
+ zoneType,
654
+ targetBlock
655
+ };
656
+ onHoverChange?.(hoverEvent);
657
+ }
444
658
  stateRef.current.hoverZone = targetZone;
445
659
  const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
446
660
  const updatedIndex = reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes);
447
661
  const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
448
662
  cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
449
- debouncedSetVirtual(orderedBlocks);
450
- }, [containerTypes, debouncedSetVirtual]);
663
+ if (showDropPreview) {
664
+ debouncedSetVirtual(orderedBlocks);
665
+ }
666
+ }, [blocks, containerTypes, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview]);
451
667
  const handleDragEnd = useCallback((_event) => {
452
668
  debouncedSetVirtual.cancel();
669
+ debouncedDragMove.cancel();
453
670
  const cached = cachedReorderRef.current;
671
+ const activeId = stateRef.current.activeId;
672
+ const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
673
+ if (activeBlockData) {
674
+ const endEvent = {
675
+ block: activeBlockData,
676
+ blockId: activeId,
677
+ targetZone: cached?.targetId ?? null,
678
+ cancelled: false
679
+ };
680
+ onDragEnd?.(endEvent);
681
+ }
682
+ if (cached && activeBlockData && fromPositionRef.current) {
683
+ const toPosition = getBlockPosition(cached.reorderedBlocks, activeBlockData.id);
684
+ const moveEvent = {
685
+ block: activeBlockData,
686
+ from: fromPositionRef.current,
687
+ to: toPosition,
688
+ blocks: cached.reorderedBlocks
689
+ };
690
+ onBlockMove?.(moveEvent);
691
+ }
454
692
  stateRef.current.activeId = null;
455
693
  stateRef.current.hoverZone = null;
456
694
  stateRef.current.virtualState = null;
695
+ stateRef.current.isDragging = false;
457
696
  cachedReorderRef.current = null;
458
697
  initialBlocksRef.current = [];
698
+ fromPositionRef.current = null;
459
699
  if (cached && onChange) {
460
700
  onChange(cached.reorderedBlocks);
461
701
  }
462
702
  forceRender();
463
- }, [debouncedSetVirtual, onChange]);
703
+ }, [blocks, debouncedSetVirtual, debouncedDragMove, onChange, onDragEnd, onBlockMove]);
704
+ const handleDragCancel = useCallback((_event) => {
705
+ debouncedSetVirtual.cancel();
706
+ debouncedDragMove.cancel();
707
+ const activeId = stateRef.current.activeId;
708
+ const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
709
+ if (activeBlockData) {
710
+ const cancelEvent = {
711
+ block: activeBlockData,
712
+ blockId: activeId,
713
+ targetZone: null,
714
+ cancelled: true
715
+ };
716
+ onDragCancel?.(cancelEvent);
717
+ onDragEnd?.(cancelEvent);
718
+ }
719
+ stateRef.current.activeId = null;
720
+ stateRef.current.hoverZone = null;
721
+ stateRef.current.virtualState = null;
722
+ stateRef.current.isDragging = false;
723
+ cachedReorderRef.current = null;
724
+ initialBlocksRef.current = [];
725
+ fromPositionRef.current = null;
726
+ forceRender();
727
+ }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
464
728
  const handleHover = useCallback((zoneId, _parentId) => {
465
729
  const activeId = stateRef.current.activeId;
466
730
  if (!activeId) return;
731
+ const activeBlockData = blocks.find((b) => b.id === activeId);
732
+ const targetBlockId = extractBlockId(zoneId);
733
+ const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
734
+ if (canDrop && activeBlockData && !canDrop(activeBlockData, zoneId, targetBlock)) {
735
+ return;
736
+ }
737
+ if (stateRef.current.hoverZone !== zoneId) {
738
+ const zoneType = getDropZoneType(zoneId);
739
+ const hoverEvent = {
740
+ zoneId,
741
+ zoneType,
742
+ targetBlock
743
+ };
744
+ onHoverChange?.(hoverEvent);
745
+ }
467
746
  stateRef.current.hoverZone = zoneId;
468
747
  const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
469
748
  const updatedIndex = reparentBlockIndex(baseIndex, activeId, zoneId, containerTypes);
470
749
  const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
471
750
  cachedReorderRef.current = { targetId: zoneId, reorderedBlocks: orderedBlocks };
472
- debouncedSetVirtual(orderedBlocks);
473
- }, [containerTypes, debouncedSetVirtual]);
751
+ if (showDropPreview) {
752
+ debouncedSetVirtual(orderedBlocks);
753
+ }
754
+ }, [blocks, containerTypes, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview]);
474
755
  const handleToggleExpand = useCallback((id) => {
756
+ const newExpanded = stateRef.current.expandedMap[id] === false;
475
757
  stateRef.current.expandedMap = {
476
758
  ...stateRef.current.expandedMap,
477
- [id]: stateRef.current.expandedMap[id] === false
759
+ [id]: newExpanded
478
760
  };
761
+ const block = blocks.find((b) => b.id === id);
762
+ if (block && onExpandChange) {
763
+ const expandEvent = {
764
+ block,
765
+ blockId: id,
766
+ expanded: newExpanded
767
+ };
768
+ onExpandChange(expandEvent);
769
+ }
479
770
  forceRender();
480
- }, []);
771
+ }, [blocks, onExpandChange]);
481
772
  return /* @__PURE__ */ jsxs(
482
773
  DndContext,
483
774
  {
484
775
  sensors,
485
- collisionDetection: weightedVerticalCollision,
776
+ collisionDetection: collisionDetection ?? stickyCollisionRef.current,
486
777
  onDragStart: handleDragStart,
778
+ onDragMove: handleDragMove,
487
779
  onDragOver: handleDragOver,
488
780
  onDragEnd: handleDragEnd,
781
+ onDragCancel: handleDragCancel,
489
782
  children: [
490
783
  /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(
491
784
  TreeRenderer,
@@ -502,7 +795,10 @@ function BlockTree({
502
795
  dropZoneClassName,
503
796
  dropZoneActiveClassName,
504
797
  indentClassName,
505
- rootClassName: className
798
+ rootClassName: className,
799
+ canDrag,
800
+ previewPosition,
801
+ draggedBlock
506
802
  }
507
803
  ) }),
508
804
  /* @__PURE__ */ jsx(DragOverlay, { activeBlock, children: dragOverlay })
@@ -575,7 +871,6 @@ function createBlockState() {
575
871
  reducerWithContainerTypes,
576
872
  computeNormalizedIndex(initialBlocks)
577
873
  );
578
- const [lastCreatedItem, setLastCreatedItem] = useState(null);
579
874
  const blocks = useMemo(() => {
580
875
  const result = [];
581
876
  const walk = (parentId) => {
@@ -624,7 +919,6 @@ function createBlockState() {
624
919
  order: 0
625
920
  };
626
921
  dispatch({ type: "ADD_ITEM", payload: newItem });
627
- setLastCreatedItem(newItem);
628
922
  return newItem;
629
923
  },
630
924
  []
@@ -647,7 +941,6 @@ function createBlockState() {
647
941
  type: "INSERT_ITEM",
648
942
  payload: { item: newItem, parentId, index: insertIndex }
649
943
  });
650
- setLastCreatedItem(newItem);
651
944
  return newItem;
652
945
  },
653
946
  [state]
@@ -843,6 +1136,6 @@ function createTreeState(options = {}) {
843
1136
  };
844
1137
  }
845
1138
 
846
- export { BlockTree, DragOverlay, DropZone, TreeRenderer, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, computeNormalizedIndex, createBlockState, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, getDescendantIds, getDropZoneType, getSensorConfig, reparentBlockIndex, useConfiguredSensors, weightedVerticalCollision };
1139
+ export { BlockTree, DragOverlay, DropZone, TreeRenderer, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, getDescendantIds, getDropZoneType, getSensorConfig, reparentBlockIndex, useConfiguredSensors, weightedVerticalCollision };
847
1140
  //# sourceMappingURL=index.mjs.map
848
1141
  //# sourceMappingURL=index.mjs.map