dnd-block-tree 0.5.0 → 1.0.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
@@ -13,11 +13,11 @@ function extractBlockId(zoneId) {
13
13
  }
14
14
 
15
15
  // src/core/collision.ts
16
- function computeCollisionScores(droppableContainers, collisionRect) {
16
+ function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
17
17
  const pointerX = collisionRect.left + collisionRect.width / 2;
18
18
  const pointerY = collisionRect.top + collisionRect.height / 2;
19
19
  const candidates = droppableContainers.map((container) => {
20
- const rect = container.rect.current;
20
+ const rect = snapshotRects?.get(container.id) ?? container.rect.current;
21
21
  if (!rect) return null;
22
22
  const distanceToTop = Math.abs(pointerY - rect.top);
23
23
  const distanceToBottom = Math.abs(pointerY - rect.bottom);
@@ -27,16 +27,17 @@ function computeCollisionScores(droppableContainers, collisionRect) {
27
27
  const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
28
28
  let horizontalScore = 0;
29
29
  if (isWithinX) {
30
- horizontalScore = -rect.left * 0.1;
30
+ horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
31
31
  } else {
32
32
  const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
33
- horizontalScore = Math.min(distanceToZone, 50);
33
+ horizontalScore = distanceToZone * 2;
34
34
  }
35
35
  return {
36
36
  id: container.id,
37
37
  data: {
38
38
  droppableContainer: container,
39
- value: edgeDistance + bias + horizontalScore
39
+ value: edgeDistance + bias + horizontalScore,
40
+ left: rect.left
40
41
  }
41
42
  };
42
43
  }).filter((c) => c !== null);
@@ -55,14 +56,14 @@ var weightedVerticalCollision = ({
55
56
  const candidates = computeCollisionScores(droppableContainers, collisionRect);
56
57
  return candidates.slice(0, 1);
57
58
  };
58
- function createStickyCollision(threshold = 15) {
59
+ function createStickyCollision(threshold = 15, snapshotRef) {
59
60
  let currentZoneId = null;
60
61
  const detector = ({
61
62
  droppableContainers,
62
63
  collisionRect
63
64
  }) => {
64
65
  if (!collisionRect) return [];
65
- const candidates = computeCollisionScores(droppableContainers, collisionRect);
66
+ const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
66
67
  if (candidates.length === 0) return [];
67
68
  const bestCandidate = candidates[0];
68
69
  const bestScore = bestCandidate.data.value;
@@ -70,7 +71,11 @@ function createStickyCollision(threshold = 15) {
70
71
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
71
72
  if (currentCandidate) {
72
73
  const currentScore = currentCandidate.data.value;
73
- if (currentScore - bestScore < threshold) {
74
+ const currentLeft = currentCandidate.data.left;
75
+ const bestLeft = bestCandidate.data.left;
76
+ const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
77
+ const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
78
+ if (currentScore - bestScore < effectiveThreshold) {
74
79
  return [currentCandidate];
75
80
  }
76
81
  }
@@ -232,6 +237,9 @@ function DropZoneComponent({
232
237
  );
233
238
  }
234
239
  var DropZone = memo(DropZoneComponent);
240
+ function GhostPreview({ children }) {
241
+ return /* @__PURE__ */ jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
242
+ }
235
243
  function DraggableBlock({
236
244
  block,
237
245
  children,
@@ -318,7 +326,7 @@ function TreeRenderer({
318
326
  }
319
327
  const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
320
328
  return /* @__PURE__ */ jsxs(Fragment, { children: [
321
- ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
329
+ ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsx(GhostPreview, { children: GhostRenderer({
322
330
  block: draggedBlock,
323
331
  isDragging: true,
324
332
  depth
@@ -396,7 +404,7 @@ function TreeRenderer({
396
404
  }),
397
405
  showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
398
406
  const GhostRenderer = renderers[draggedBlock.type];
399
- return GhostRenderer ? /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
407
+ return GhostRenderer ? /* @__PURE__ */ jsx(GhostPreview, { children: GhostRenderer({
400
408
  block: draggedBlock,
401
409
  isDragging: true,
402
410
  depth
@@ -648,6 +656,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
648
656
  }
649
657
  if (dragged.id === zoneTargetId) return state;
650
658
  const oldList = byParent.get(oldParentId) ?? [];
659
+ const currentIndexInOldParent = oldList.indexOf(dragged.id);
660
+ const preNewList = byParent.get(newParentId) ?? [];
661
+ let targetIndex;
662
+ if (isInto) {
663
+ targetIndex = 0;
664
+ } else if (isEnd) {
665
+ targetIndex = preNewList.length;
666
+ } else {
667
+ const idx = preNewList.indexOf(zoneTargetId);
668
+ targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
669
+ }
670
+ if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
671
+ const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
672
+ if (adjustedTarget === currentIndexInOldParent) {
673
+ return state;
674
+ }
675
+ }
651
676
  const filtered = oldList.filter((id) => id !== dragged.id);
652
677
  byParent.set(oldParentId, filtered);
653
678
  const newList = [...byParent.get(newParentId) ?? []];
@@ -660,10 +685,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
660
685
  const idx = newList.indexOf(zoneTargetId);
661
686
  insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
662
687
  }
663
- const currentIndex = newList.indexOf(dragged.id);
664
- if (dragged.parentId === newParentId && currentIndex === insertIndex) {
665
- return state;
666
- }
667
688
  newList.splice(insertIndex, 0, dragged.id);
668
689
  byParent.set(newParentId, newList);
669
690
  let newOrder = dragged.order;
@@ -833,7 +854,20 @@ function BlockTree({
833
854
  const cachedReorderRef = useRef(null);
834
855
  const fromPositionRef = useRef(null);
835
856
  const draggedIdsRef = useRef([]);
836
- const stickyCollisionRef = useRef(createStickyCollision(20));
857
+ const snapshotRectsRef = useRef(null);
858
+ const needsResnapshot = useRef(false);
859
+ const stickyCollisionRef = useRef(createStickyCollision(20, snapshotRectsRef));
860
+ const snapshotZoneRects = useCallback(() => {
861
+ const root = rootRef.current;
862
+ if (!root) return;
863
+ const zones = root.querySelectorAll("[data-zone-id]");
864
+ const map = /* @__PURE__ */ new Map();
865
+ zones.forEach((el) => {
866
+ const id = el.getAttribute("data-zone-id");
867
+ if (id) map.set(id, el.getBoundingClientRect());
868
+ });
869
+ snapshotRectsRef.current = map;
870
+ }, []);
837
871
  const [, forceRender] = useReducer((x) => x + 1, 0);
838
872
  const debouncedSetVirtual = useRef(
839
873
  debounce((newBlocks) => {
@@ -842,6 +876,7 @@ function BlockTree({
842
876
  } else {
843
877
  stateRef.current.virtualState = null;
844
878
  }
879
+ needsResnapshot.current = true;
845
880
  forceRender();
846
881
  }, previewDebounce)
847
882
  ).current;
@@ -980,6 +1015,13 @@ function BlockTree({
980
1015
  }
981
1016
  lastClickedIdRef.current = blockId;
982
1017
  }, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1018
+ useEffect(() => {
1019
+ if (!needsResnapshot.current || !stateRef.current.isDragging) return;
1020
+ needsResnapshot.current = false;
1021
+ requestAnimationFrame(() => {
1022
+ snapshotZoneRects();
1023
+ });
1024
+ });
983
1025
  useEffect(() => {
984
1026
  if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
985
1027
  const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
@@ -1032,6 +1074,7 @@ function BlockTree({
1032
1074
  stateRef.current.isDragging = true;
1033
1075
  initialBlocksRef.current = [...blocks];
1034
1076
  cachedReorderRef.current = null;
1077
+ needsResnapshot.current = true;
1035
1078
  forceRender();
1036
1079
  }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1037
1080
  const handleDragMove = useCallback((event) => {
@@ -1107,6 +1150,7 @@ function BlockTree({
1107
1150
  cachedReorderRef.current = null;
1108
1151
  initialBlocksRef.current = [];
1109
1152
  fromPositionRef.current = null;
1153
+ snapshotRectsRef.current = null;
1110
1154
  forceRender();
1111
1155
  return;
1112
1156
  }
@@ -1146,6 +1190,7 @@ function BlockTree({
1146
1190
  initialBlocksRef.current = [];
1147
1191
  fromPositionRef.current = null;
1148
1192
  draggedIdsRef.current = [];
1193
+ snapshotRectsRef.current = null;
1149
1194
  if (cached && onChange) {
1150
1195
  onChange(cached.reorderedBlocks);
1151
1196
  }
@@ -1174,6 +1219,7 @@ function BlockTree({
1174
1219
  initialBlocksRef.current = [];
1175
1220
  fromPositionRef.current = null;
1176
1221
  draggedIdsRef.current = [];
1222
+ snapshotRectsRef.current = null;
1177
1223
  forceRender();
1178
1224
  }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
1179
1225
  const handleHover = useCallback((zoneId, _parentId) => {