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/README.md +30 -3
- package/dist/index.d.mts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +61 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +61 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
|
|
16
|
-
- **Ghost Preview** -
|
|
17
|
-
- **
|
|
16
|
+
- **Ghost Preview** - In-flow semi-transparent preview shows where blocks will land with accurate layout
|
|
17
|
+
- **Snapshotted Collision** - Zone rects are frozen on drag start and re-measured after each ghost commit, preventing layout-shift feedback loops
|
|
18
|
+
- **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with cross-depth-aware hysteresis
|
|
18
19
|
- **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
|
|
19
20
|
- **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
|
|
20
21
|
- **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
|
|
@@ -627,6 +628,31 @@ const collision = createStickyCollision(20)
|
|
|
627
628
|
|
|
628
629
|
You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
|
|
629
630
|
|
|
631
|
+
### Snapshotted Zone Rects
|
|
632
|
+
|
|
633
|
+
`createStickyCollision` accepts an optional `SnapshotRectsRef` — a ref to a `Map<string, DOMRect>` of frozen zone positions. When provided, collision detection uses these snapshots instead of live DOM measurements, preventing feedback loops caused by the in-flow ghost preview shifting zone positions.
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import { createStickyCollision, type SnapshotRectsRef } from 'dnd-block-tree'
|
|
637
|
+
|
|
638
|
+
const snapshotRef: SnapshotRectsRef = { current: null }
|
|
639
|
+
const collision = createStickyCollision(20, snapshotRef)
|
|
640
|
+
|
|
641
|
+
// Snapshot all zone rects after drag starts:
|
|
642
|
+
snapshotRef.current = new Map(
|
|
643
|
+
[...document.querySelectorAll('[data-zone-id]')].map(el => [
|
|
644
|
+
el.getAttribute('data-zone-id')!,
|
|
645
|
+
el.getBoundingClientRect(),
|
|
646
|
+
])
|
|
647
|
+
)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`BlockTree` handles this lifecycle automatically — zones are snapshotted on drag start and re-measured via `requestAnimationFrame` after each ghost position commit.
|
|
651
|
+
|
|
652
|
+
### Cross-Depth Hysteresis
|
|
653
|
+
|
|
654
|
+
The sticky collision uses a reduced threshold (25% of normal) when switching between zones at different indentation levels. This makes it easy to drag blocks in and out of containers while still preventing flickering between same-depth adjacent zones.
|
|
655
|
+
|
|
630
656
|
## Type Safety
|
|
631
657
|
|
|
632
658
|
The library provides automatic type inference for container vs non-container renderers:
|
|
@@ -687,7 +713,8 @@ import {
|
|
|
687
713
|
// Collision detection
|
|
688
714
|
weightedVerticalCollision, // Edge-distance collision, depth-aware
|
|
689
715
|
closestCenterCollision, // Simple closest-center collision
|
|
690
|
-
createStickyCollision, // Hysteresis wrapper
|
|
716
|
+
createStickyCollision, // Hysteresis wrapper with snapshot support
|
|
717
|
+
type SnapshotRectsRef, // Ref type for frozen zone rects
|
|
691
718
|
|
|
692
719
|
// Hooks
|
|
693
720
|
useBlockHistory, // Undo/redo state management
|
package/dist/index.d.mts
CHANGED
|
@@ -389,6 +389,9 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
|
|
|
389
389
|
blockMap: Map<string, T>;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
type SnapshotRectsRef = {
|
|
393
|
+
current: Map<UniqueIdentifier, DOMRect> | null;
|
|
394
|
+
};
|
|
392
395
|
/**
|
|
393
396
|
* Custom collision detection that scores drop zones by distance to nearest edge.
|
|
394
397
|
* Uses edge-distance scoring with a bottom bias for more natural drag behavior.
|
|
@@ -404,8 +407,11 @@ declare const weightedVerticalCollision: CollisionDetection;
|
|
|
404
407
|
* between adjacent drop zones.
|
|
405
408
|
*
|
|
406
409
|
* @param threshold - Minimum score improvement required to switch zones (default: 15px)
|
|
410
|
+
* @param snapshotRef - Optional ref to snapshotted zone rects. When populated,
|
|
411
|
+
* collision detection uses these frozen rects instead of live DOM measurements,
|
|
412
|
+
* preventing layout-shift feedback loops from in-flow ghost previews.
|
|
407
413
|
*/
|
|
408
|
-
declare function createStickyCollision(threshold?: number): CollisionDetection & {
|
|
414
|
+
declare function createStickyCollision(threshold?: number, snapshotRef?: SnapshotRectsRef): CollisionDetection & {
|
|
409
415
|
reset: () => void;
|
|
410
416
|
};
|
|
411
417
|
/**
|
|
@@ -811,4 +817,4 @@ declare function initFractionalOrder<T extends {
|
|
|
811
817
|
order: number | string;
|
|
812
818
|
}>(blocks: T[]): T[];
|
|
813
819
|
|
|
814
|
-
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
|
|
820
|
+
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, type SnapshotRectsRef, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
|
package/dist/index.d.ts
CHANGED
|
@@ -389,6 +389,9 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
|
|
|
389
389
|
blockMap: Map<string, T>;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
type SnapshotRectsRef = {
|
|
393
|
+
current: Map<UniqueIdentifier, DOMRect> | null;
|
|
394
|
+
};
|
|
392
395
|
/**
|
|
393
396
|
* Custom collision detection that scores drop zones by distance to nearest edge.
|
|
394
397
|
* Uses edge-distance scoring with a bottom bias for more natural drag behavior.
|
|
@@ -404,8 +407,11 @@ declare const weightedVerticalCollision: CollisionDetection;
|
|
|
404
407
|
* between adjacent drop zones.
|
|
405
408
|
*
|
|
406
409
|
* @param threshold - Minimum score improvement required to switch zones (default: 15px)
|
|
410
|
+
* @param snapshotRef - Optional ref to snapshotted zone rects. When populated,
|
|
411
|
+
* collision detection uses these frozen rects instead of live DOM measurements,
|
|
412
|
+
* preventing layout-shift feedback loops from in-flow ghost previews.
|
|
407
413
|
*/
|
|
408
|
-
declare function createStickyCollision(threshold?: number): CollisionDetection & {
|
|
414
|
+
declare function createStickyCollision(threshold?: number, snapshotRef?: SnapshotRectsRef): CollisionDetection & {
|
|
409
415
|
reset: () => void;
|
|
410
416
|
};
|
|
411
417
|
/**
|
|
@@ -811,4 +817,4 @@ declare function initFractionalOrder<T extends {
|
|
|
811
817
|
order: number | string;
|
|
812
818
|
}>(blocks: T[]): T[];
|
|
813
819
|
|
|
814
|
-
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
|
|
820
|
+
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, type SnapshotRectsRef, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,11 @@ function extractBlockId(zoneId) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/core/collision.ts
|
|
18
|
-
function computeCollisionScores(droppableContainers, collisionRect) {
|
|
18
|
+
function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
|
|
19
19
|
const pointerX = collisionRect.left + collisionRect.width / 2;
|
|
20
20
|
const pointerY = collisionRect.top + collisionRect.height / 2;
|
|
21
21
|
const candidates = droppableContainers.map((container) => {
|
|
22
|
-
const rect = container.rect.current;
|
|
22
|
+
const rect = snapshotRects?.get(container.id) ?? container.rect.current;
|
|
23
23
|
if (!rect) return null;
|
|
24
24
|
const distanceToTop = Math.abs(pointerY - rect.top);
|
|
25
25
|
const distanceToBottom = Math.abs(pointerY - rect.bottom);
|
|
@@ -29,16 +29,17 @@ function computeCollisionScores(droppableContainers, collisionRect) {
|
|
|
29
29
|
const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
|
|
30
30
|
let horizontalScore = 0;
|
|
31
31
|
if (isWithinX) {
|
|
32
|
-
horizontalScore = -rect.left * 0.
|
|
32
|
+
horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
|
|
33
33
|
} else {
|
|
34
34
|
const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
|
|
35
|
-
horizontalScore =
|
|
35
|
+
horizontalScore = distanceToZone * 2;
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
38
38
|
id: container.id,
|
|
39
39
|
data: {
|
|
40
40
|
droppableContainer: container,
|
|
41
|
-
value: edgeDistance + bias + horizontalScore
|
|
41
|
+
value: edgeDistance + bias + horizontalScore,
|
|
42
|
+
left: rect.left
|
|
42
43
|
}
|
|
43
44
|
};
|
|
44
45
|
}).filter((c) => c !== null);
|
|
@@ -57,14 +58,14 @@ var weightedVerticalCollision = ({
|
|
|
57
58
|
const candidates = computeCollisionScores(droppableContainers, collisionRect);
|
|
58
59
|
return candidates.slice(0, 1);
|
|
59
60
|
};
|
|
60
|
-
function createStickyCollision(threshold = 15) {
|
|
61
|
+
function createStickyCollision(threshold = 15, snapshotRef) {
|
|
61
62
|
let currentZoneId = null;
|
|
62
63
|
const detector = ({
|
|
63
64
|
droppableContainers,
|
|
64
65
|
collisionRect
|
|
65
66
|
}) => {
|
|
66
67
|
if (!collisionRect) return [];
|
|
67
|
-
const candidates = computeCollisionScores(droppableContainers, collisionRect);
|
|
68
|
+
const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
|
|
68
69
|
if (candidates.length === 0) return [];
|
|
69
70
|
const bestCandidate = candidates[0];
|
|
70
71
|
const bestScore = bestCandidate.data.value;
|
|
@@ -72,7 +73,11 @@ function createStickyCollision(threshold = 15) {
|
|
|
72
73
|
const currentCandidate = candidates.find((c) => c.id === currentZoneId);
|
|
73
74
|
if (currentCandidate) {
|
|
74
75
|
const currentScore = currentCandidate.data.value;
|
|
75
|
-
|
|
76
|
+
const currentLeft = currentCandidate.data.left;
|
|
77
|
+
const bestLeft = bestCandidate.data.left;
|
|
78
|
+
const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
|
|
79
|
+
const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
|
|
80
|
+
if (currentScore - bestScore < effectiveThreshold) {
|
|
76
81
|
return [currentCandidate];
|
|
77
82
|
}
|
|
78
83
|
}
|
|
@@ -234,6 +239,9 @@ function DropZoneComponent({
|
|
|
234
239
|
);
|
|
235
240
|
}
|
|
236
241
|
var DropZone = react.memo(DropZoneComponent);
|
|
242
|
+
function GhostPreview({ children }) {
|
|
243
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
|
|
244
|
+
}
|
|
237
245
|
function DraggableBlock({
|
|
238
246
|
block,
|
|
239
247
|
children,
|
|
@@ -320,7 +328,7 @@ function TreeRenderer({
|
|
|
320
328
|
}
|
|
321
329
|
const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
|
|
322
330
|
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
323
|
-
ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(
|
|
331
|
+
ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
|
|
324
332
|
block: draggedBlock,
|
|
325
333
|
isDragging: true,
|
|
326
334
|
depth
|
|
@@ -398,7 +406,7 @@ function TreeRenderer({
|
|
|
398
406
|
}),
|
|
399
407
|
showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
|
|
400
408
|
const GhostRenderer = renderers[draggedBlock.type];
|
|
401
|
-
return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
409
|
+
return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
|
|
402
410
|
block: draggedBlock,
|
|
403
411
|
isDragging: true,
|
|
404
412
|
depth
|
|
@@ -650,6 +658,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
|
|
|
650
658
|
}
|
|
651
659
|
if (dragged.id === zoneTargetId) return state;
|
|
652
660
|
const oldList = byParent.get(oldParentId) ?? [];
|
|
661
|
+
const currentIndexInOldParent = oldList.indexOf(dragged.id);
|
|
662
|
+
const preNewList = byParent.get(newParentId) ?? [];
|
|
663
|
+
let targetIndex;
|
|
664
|
+
if (isInto) {
|
|
665
|
+
targetIndex = 0;
|
|
666
|
+
} else if (isEnd) {
|
|
667
|
+
targetIndex = preNewList.length;
|
|
668
|
+
} else {
|
|
669
|
+
const idx = preNewList.indexOf(zoneTargetId);
|
|
670
|
+
targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
|
|
671
|
+
}
|
|
672
|
+
if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
|
|
673
|
+
const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
|
|
674
|
+
if (adjustedTarget === currentIndexInOldParent) {
|
|
675
|
+
return state;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
653
678
|
const filtered = oldList.filter((id) => id !== dragged.id);
|
|
654
679
|
byParent.set(oldParentId, filtered);
|
|
655
680
|
const newList = [...byParent.get(newParentId) ?? []];
|
|
@@ -662,10 +687,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
|
|
|
662
687
|
const idx = newList.indexOf(zoneTargetId);
|
|
663
688
|
insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
|
|
664
689
|
}
|
|
665
|
-
const currentIndex = newList.indexOf(dragged.id);
|
|
666
|
-
if (dragged.parentId === newParentId && currentIndex === insertIndex) {
|
|
667
|
-
return state;
|
|
668
|
-
}
|
|
669
690
|
newList.splice(insertIndex, 0, dragged.id);
|
|
670
691
|
byParent.set(newParentId, newList);
|
|
671
692
|
let newOrder = dragged.order;
|
|
@@ -835,7 +856,20 @@ function BlockTree({
|
|
|
835
856
|
const cachedReorderRef = react.useRef(null);
|
|
836
857
|
const fromPositionRef = react.useRef(null);
|
|
837
858
|
const draggedIdsRef = react.useRef([]);
|
|
838
|
-
const
|
|
859
|
+
const snapshotRectsRef = react.useRef(null);
|
|
860
|
+
const needsResnapshot = react.useRef(false);
|
|
861
|
+
const stickyCollisionRef = react.useRef(createStickyCollision(20, snapshotRectsRef));
|
|
862
|
+
const snapshotZoneRects = react.useCallback(() => {
|
|
863
|
+
const root = rootRef.current;
|
|
864
|
+
if (!root) return;
|
|
865
|
+
const zones = root.querySelectorAll("[data-zone-id]");
|
|
866
|
+
const map = /* @__PURE__ */ new Map();
|
|
867
|
+
zones.forEach((el) => {
|
|
868
|
+
const id = el.getAttribute("data-zone-id");
|
|
869
|
+
if (id) map.set(id, el.getBoundingClientRect());
|
|
870
|
+
});
|
|
871
|
+
snapshotRectsRef.current = map;
|
|
872
|
+
}, []);
|
|
839
873
|
const [, forceRender] = react.useReducer((x) => x + 1, 0);
|
|
840
874
|
const debouncedSetVirtual = react.useRef(
|
|
841
875
|
debounce((newBlocks) => {
|
|
@@ -844,6 +878,7 @@ function BlockTree({
|
|
|
844
878
|
} else {
|
|
845
879
|
stateRef.current.virtualState = null;
|
|
846
880
|
}
|
|
881
|
+
needsResnapshot.current = true;
|
|
847
882
|
forceRender();
|
|
848
883
|
}, previewDebounce)
|
|
849
884
|
).current;
|
|
@@ -982,6 +1017,13 @@ function BlockTree({
|
|
|
982
1017
|
}
|
|
983
1018
|
lastClickedIdRef.current = blockId;
|
|
984
1019
|
}, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
|
|
1020
|
+
react.useEffect(() => {
|
|
1021
|
+
if (!needsResnapshot.current || !stateRef.current.isDragging) return;
|
|
1022
|
+
needsResnapshot.current = false;
|
|
1023
|
+
requestAnimationFrame(() => {
|
|
1024
|
+
snapshotZoneRects();
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
985
1027
|
react.useEffect(() => {
|
|
986
1028
|
if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
|
|
987
1029
|
const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
|
|
@@ -1034,6 +1076,7 @@ function BlockTree({
|
|
|
1034
1076
|
stateRef.current.isDragging = true;
|
|
1035
1077
|
initialBlocksRef.current = [...blocks];
|
|
1036
1078
|
cachedReorderRef.current = null;
|
|
1079
|
+
needsResnapshot.current = true;
|
|
1037
1080
|
forceRender();
|
|
1038
1081
|
}, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
|
|
1039
1082
|
const handleDragMove = react.useCallback((event) => {
|
|
@@ -1109,6 +1152,7 @@ function BlockTree({
|
|
|
1109
1152
|
cachedReorderRef.current = null;
|
|
1110
1153
|
initialBlocksRef.current = [];
|
|
1111
1154
|
fromPositionRef.current = null;
|
|
1155
|
+
snapshotRectsRef.current = null;
|
|
1112
1156
|
forceRender();
|
|
1113
1157
|
return;
|
|
1114
1158
|
}
|
|
@@ -1148,6 +1192,7 @@ function BlockTree({
|
|
|
1148
1192
|
initialBlocksRef.current = [];
|
|
1149
1193
|
fromPositionRef.current = null;
|
|
1150
1194
|
draggedIdsRef.current = [];
|
|
1195
|
+
snapshotRectsRef.current = null;
|
|
1151
1196
|
if (cached && onChange) {
|
|
1152
1197
|
onChange(cached.reorderedBlocks);
|
|
1153
1198
|
}
|
|
@@ -1176,6 +1221,7 @@ function BlockTree({
|
|
|
1176
1221
|
initialBlocksRef.current = [];
|
|
1177
1222
|
fromPositionRef.current = null;
|
|
1178
1223
|
draggedIdsRef.current = [];
|
|
1224
|
+
snapshotRectsRef.current = null;
|
|
1179
1225
|
forceRender();
|
|
1180
1226
|
}, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
|
|
1181
1227
|
const handleHover = react.useCallback((zoneId, _parentId) => {
|