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