dnd-block-tree 0.4.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 +482 -14
- package/dist/index.d.mts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +311 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -135,7 +140,7 @@ function useConfiguredSensors(config = {}) {
|
|
|
135
140
|
distance: activationDistance
|
|
136
141
|
};
|
|
137
142
|
touchConstraint = {
|
|
138
|
-
delay: 200,
|
|
143
|
+
delay: config.longPressDelay ?? 200,
|
|
139
144
|
tolerance: 5
|
|
140
145
|
};
|
|
141
146
|
}
|
|
@@ -197,6 +202,11 @@ function debounce(fn, delay) {
|
|
|
197
202
|
function generateId() {
|
|
198
203
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
199
204
|
}
|
|
205
|
+
function triggerHaptic(durationMs = 10) {
|
|
206
|
+
if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
|
|
207
|
+
navigator.vibrate(durationMs);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
200
210
|
function DropZoneComponent({
|
|
201
211
|
id,
|
|
202
212
|
parentId,
|
|
@@ -229,6 +239,9 @@ function DropZoneComponent({
|
|
|
229
239
|
);
|
|
230
240
|
}
|
|
231
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
|
+
}
|
|
232
245
|
function DraggableBlock({
|
|
233
246
|
block,
|
|
234
247
|
children,
|
|
@@ -278,10 +291,15 @@ function TreeRenderer({
|
|
|
278
291
|
draggedBlock,
|
|
279
292
|
focusedId,
|
|
280
293
|
selectedIds,
|
|
281
|
-
onBlockClick
|
|
294
|
+
onBlockClick,
|
|
295
|
+
animation,
|
|
296
|
+
virtualVisibleIds
|
|
282
297
|
}) {
|
|
283
298
|
const items = blocksByParent.get(parentId) ?? [];
|
|
284
|
-
|
|
299
|
+
let filteredBlocks = items.filter((block) => block.id !== activeId);
|
|
300
|
+
if (virtualVisibleIds && depth === 0) {
|
|
301
|
+
filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
|
|
302
|
+
}
|
|
285
303
|
const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
|
|
286
304
|
const containerClass = depth === 0 ? rootClassName : indentClassName;
|
|
287
305
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
|
|
@@ -310,7 +328,7 @@ function TreeRenderer({
|
|
|
310
328
|
}
|
|
311
329
|
const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
|
|
312
330
|
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
313
|
-
ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(
|
|
331
|
+
ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
|
|
314
332
|
block: draggedBlock,
|
|
315
333
|
isDragging: true,
|
|
316
334
|
depth
|
|
@@ -325,7 +343,11 @@ function TreeRenderer({
|
|
|
325
343
|
onBlockClick,
|
|
326
344
|
children: ({ isDragging }) => {
|
|
327
345
|
if (isContainer) {
|
|
328
|
-
const
|
|
346
|
+
const expandStyle = animation?.expandDuration ? {
|
|
347
|
+
transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
|
|
348
|
+
opacity: isExpanded ? 1 : 0
|
|
349
|
+
} : void 0;
|
|
350
|
+
const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
329
351
|
TreeRenderer,
|
|
330
352
|
{
|
|
331
353
|
blocks,
|
|
@@ -347,7 +369,9 @@ function TreeRenderer({
|
|
|
347
369
|
draggedBlock,
|
|
348
370
|
focusedId,
|
|
349
371
|
selectedIds,
|
|
350
|
-
onBlockClick
|
|
372
|
+
onBlockClick,
|
|
373
|
+
animation,
|
|
374
|
+
virtualVisibleIds
|
|
351
375
|
}
|
|
352
376
|
) }) : null;
|
|
353
377
|
return Renderer({
|
|
@@ -382,7 +406,7 @@ function TreeRenderer({
|
|
|
382
406
|
}),
|
|
383
407
|
showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
|
|
384
408
|
const GhostRenderer = renderers[draggedBlock.type];
|
|
385
|
-
return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
409
|
+
return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
|
|
386
410
|
block: draggedBlock,
|
|
387
411
|
isDragging: true,
|
|
388
412
|
depth
|
|
@@ -634,6 +658,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
|
|
|
634
658
|
}
|
|
635
659
|
if (dragged.id === zoneTargetId) return state;
|
|
636
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
|
+
}
|
|
637
678
|
const filtered = oldList.filter((id) => id !== dragged.id);
|
|
638
679
|
byParent.set(oldParentId, filtered);
|
|
639
680
|
const newList = [...byParent.get(newParentId) ?? []];
|
|
@@ -646,10 +687,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
|
|
|
646
687
|
const idx = newList.indexOf(zoneTargetId);
|
|
647
688
|
insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
|
|
648
689
|
}
|
|
649
|
-
const currentIndex = newList.indexOf(dragged.id);
|
|
650
|
-
if (dragged.parentId === newParentId && currentIndex === insertIndex) {
|
|
651
|
-
return state;
|
|
652
|
-
}
|
|
653
690
|
newList.splice(insertIndex, 0, dragged.id);
|
|
654
691
|
byParent.set(newParentId, newList);
|
|
655
692
|
let newOrder = dragged.order;
|
|
@@ -786,18 +823,21 @@ function BlockTree({
|
|
|
786
823
|
canDrop,
|
|
787
824
|
collisionDetection,
|
|
788
825
|
sensors: sensorConfig,
|
|
826
|
+
animation,
|
|
789
827
|
initialExpanded,
|
|
790
828
|
orderingStrategy = "integer",
|
|
791
829
|
maxDepth,
|
|
792
830
|
keyboardNavigation = false,
|
|
793
831
|
multiSelect = false,
|
|
794
832
|
selectedIds: externalSelectedIds,
|
|
795
|
-
onSelectionChange
|
|
833
|
+
onSelectionChange,
|
|
834
|
+
virtualize
|
|
796
835
|
}) {
|
|
797
836
|
const sensors = useConfiguredSensors({
|
|
798
837
|
activationDistance: sensorConfig?.activationDistance ?? activationDistance,
|
|
799
838
|
activationDelay: sensorConfig?.activationDelay,
|
|
800
|
-
tolerance: sensorConfig?.tolerance
|
|
839
|
+
tolerance: sensorConfig?.tolerance,
|
|
840
|
+
longPressDelay: sensorConfig?.longPressDelay
|
|
801
841
|
});
|
|
802
842
|
const initialExpandedMap = react.useMemo(
|
|
803
843
|
() => computeInitialExpanded(blocks, containerTypes, initialExpanded),
|
|
@@ -816,7 +856,20 @@ function BlockTree({
|
|
|
816
856
|
const cachedReorderRef = react.useRef(null);
|
|
817
857
|
const fromPositionRef = react.useRef(null);
|
|
818
858
|
const draggedIdsRef = react.useRef([]);
|
|
819
|
-
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
|
+
}, []);
|
|
820
873
|
const [, forceRender] = react.useReducer((x) => x + 1, 0);
|
|
821
874
|
const debouncedSetVirtual = react.useRef(
|
|
822
875
|
debounce((newBlocks) => {
|
|
@@ -825,6 +878,7 @@ function BlockTree({
|
|
|
825
878
|
} else {
|
|
826
879
|
stateRef.current.virtualState = null;
|
|
827
880
|
}
|
|
881
|
+
needsResnapshot.current = true;
|
|
828
882
|
forceRender();
|
|
829
883
|
}, previewDebounce)
|
|
830
884
|
).current;
|
|
@@ -963,6 +1017,13 @@ function BlockTree({
|
|
|
963
1017
|
}
|
|
964
1018
|
lastClickedIdRef.current = blockId;
|
|
965
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
|
+
});
|
|
966
1027
|
react.useEffect(() => {
|
|
967
1028
|
if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
|
|
968
1029
|
const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
|
|
@@ -1008,12 +1069,16 @@ function BlockTree({
|
|
|
1008
1069
|
setSelectedIds(/* @__PURE__ */ new Set([id]));
|
|
1009
1070
|
}
|
|
1010
1071
|
}
|
|
1072
|
+
if (sensorConfig?.hapticFeedback) {
|
|
1073
|
+
triggerHaptic();
|
|
1074
|
+
}
|
|
1011
1075
|
stateRef.current.activeId = id;
|
|
1012
1076
|
stateRef.current.isDragging = true;
|
|
1013
1077
|
initialBlocksRef.current = [...blocks];
|
|
1014
1078
|
cachedReorderRef.current = null;
|
|
1079
|
+
needsResnapshot.current = true;
|
|
1015
1080
|
forceRender();
|
|
1016
|
-
}, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
|
|
1081
|
+
}, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
|
|
1017
1082
|
const handleDragMove = react.useCallback((event) => {
|
|
1018
1083
|
if (!onDragMove) return;
|
|
1019
1084
|
const id = stateRef.current.activeId;
|
|
@@ -1087,6 +1152,7 @@ function BlockTree({
|
|
|
1087
1152
|
cachedReorderRef.current = null;
|
|
1088
1153
|
initialBlocksRef.current = [];
|
|
1089
1154
|
fromPositionRef.current = null;
|
|
1155
|
+
snapshotRectsRef.current = null;
|
|
1090
1156
|
forceRender();
|
|
1091
1157
|
return;
|
|
1092
1158
|
}
|
|
@@ -1126,6 +1192,7 @@ function BlockTree({
|
|
|
1126
1192
|
initialBlocksRef.current = [];
|
|
1127
1193
|
fromPositionRef.current = null;
|
|
1128
1194
|
draggedIdsRef.current = [];
|
|
1195
|
+
snapshotRectsRef.current = null;
|
|
1129
1196
|
if (cached && onChange) {
|
|
1130
1197
|
onChange(cached.reorderedBlocks);
|
|
1131
1198
|
}
|
|
@@ -1154,6 +1221,7 @@ function BlockTree({
|
|
|
1154
1221
|
initialBlocksRef.current = [];
|
|
1155
1222
|
fromPositionRef.current = null;
|
|
1156
1223
|
draggedIdsRef.current = [];
|
|
1224
|
+
snapshotRectsRef.current = null;
|
|
1157
1225
|
forceRender();
|
|
1158
1226
|
}, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
|
|
1159
1227
|
const handleHover = react.useCallback((zoneId, _parentId) => {
|
|
@@ -1207,6 +1275,61 @@ function BlockTree({
|
|
|
1207
1275
|
forceRender();
|
|
1208
1276
|
}, [blocks, onExpandChange]);
|
|
1209
1277
|
toggleExpandRef.current = handleToggleExpand;
|
|
1278
|
+
const virtualContainerRef = react.useRef(null);
|
|
1279
|
+
const [virtualScroll, setVirtualScroll] = react.useState({ scrollTop: 0, clientHeight: 0 });
|
|
1280
|
+
react.useEffect(() => {
|
|
1281
|
+
if (!virtualize) return;
|
|
1282
|
+
const el = virtualContainerRef.current;
|
|
1283
|
+
if (!el) return;
|
|
1284
|
+
setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
|
|
1285
|
+
const onScroll = () => {
|
|
1286
|
+
setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
|
|
1287
|
+
};
|
|
1288
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
1289
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
1290
|
+
}, [virtualize]);
|
|
1291
|
+
const virtualResult = react.useMemo(() => {
|
|
1292
|
+
if (!virtualize) return null;
|
|
1293
|
+
const { itemHeight, overscan = 5 } = virtualize;
|
|
1294
|
+
const { scrollTop, clientHeight } = virtualScroll;
|
|
1295
|
+
const totalHeight = visibleBlockIds.length * itemHeight;
|
|
1296
|
+
const startRaw = Math.floor(scrollTop / itemHeight);
|
|
1297
|
+
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
1298
|
+
const start = Math.max(0, startRaw - overscan);
|
|
1299
|
+
const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
|
|
1300
|
+
const offsetY = start * itemHeight;
|
|
1301
|
+
const visibleSet = /* @__PURE__ */ new Set();
|
|
1302
|
+
for (let i = start; i <= end; i++) {
|
|
1303
|
+
visibleSet.add(visibleBlockIds[i]);
|
|
1304
|
+
}
|
|
1305
|
+
return { totalHeight, offsetY, visibleSet };
|
|
1306
|
+
}, [virtualize, virtualScroll, visibleBlockIds]);
|
|
1307
|
+
const treeContent = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1308
|
+
TreeRenderer,
|
|
1309
|
+
{
|
|
1310
|
+
blocks,
|
|
1311
|
+
blocksByParent,
|
|
1312
|
+
parentId: null,
|
|
1313
|
+
activeId: stateRef.current.activeId,
|
|
1314
|
+
expandedMap: stateRef.current.expandedMap,
|
|
1315
|
+
renderers,
|
|
1316
|
+
containerTypes,
|
|
1317
|
+
onHover: handleHover,
|
|
1318
|
+
onToggleExpand: handleToggleExpand,
|
|
1319
|
+
dropZoneClassName,
|
|
1320
|
+
dropZoneActiveClassName,
|
|
1321
|
+
indentClassName,
|
|
1322
|
+
rootClassName: className,
|
|
1323
|
+
canDrag,
|
|
1324
|
+
previewPosition,
|
|
1325
|
+
draggedBlock,
|
|
1326
|
+
focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
|
|
1327
|
+
selectedIds: multiSelect ? selectedIds : void 0,
|
|
1328
|
+
onBlockClick: multiSelect ? handleBlockClick : void 0,
|
|
1329
|
+
animation,
|
|
1330
|
+
virtualVisibleIds: virtualResult?.visibleSet ?? null
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1210
1333
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1211
1334
|
core.DndContext,
|
|
1212
1335
|
{
|
|
@@ -1218,7 +1341,20 @@ function BlockTree({
|
|
|
1218
1341
|
onDragEnd: handleDragEnd,
|
|
1219
1342
|
onDragCancel: handleDragCancel,
|
|
1220
1343
|
children: [
|
|
1221
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1344
|
+
virtualize ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1345
|
+
"div",
|
|
1346
|
+
{
|
|
1347
|
+
ref: (el) => {
|
|
1348
|
+
virtualContainerRef.current = el;
|
|
1349
|
+
rootRef.current = el;
|
|
1350
|
+
},
|
|
1351
|
+
className,
|
|
1352
|
+
style: { minWidth: 0, overflow: "auto", position: "relative" },
|
|
1353
|
+
onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
|
|
1354
|
+
role: keyboardNavigation ? "tree" : void 0,
|
|
1355
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: virtualResult.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: virtualResult.offsetY, left: 0, right: 0 }, children: treeContent }) })
|
|
1356
|
+
}
|
|
1357
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1222
1358
|
"div",
|
|
1223
1359
|
{
|
|
1224
1360
|
ref: rootRef,
|
|
@@ -1226,30 +1362,7 @@ function BlockTree({
|
|
|
1226
1362
|
style: { minWidth: 0 },
|
|
1227
1363
|
onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
|
|
1228
1364
|
role: keyboardNavigation ? "tree" : void 0,
|
|
1229
|
-
children:
|
|
1230
|
-
TreeRenderer,
|
|
1231
|
-
{
|
|
1232
|
-
blocks,
|
|
1233
|
-
blocksByParent,
|
|
1234
|
-
parentId: null,
|
|
1235
|
-
activeId: stateRef.current.activeId,
|
|
1236
|
-
expandedMap: stateRef.current.expandedMap,
|
|
1237
|
-
renderers,
|
|
1238
|
-
containerTypes,
|
|
1239
|
-
onHover: handleHover,
|
|
1240
|
-
onToggleExpand: handleToggleExpand,
|
|
1241
|
-
dropZoneClassName,
|
|
1242
|
-
dropZoneActiveClassName,
|
|
1243
|
-
indentClassName,
|
|
1244
|
-
rootClassName: className,
|
|
1245
|
-
canDrag,
|
|
1246
|
-
previewPosition,
|
|
1247
|
-
draggedBlock,
|
|
1248
|
-
focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
|
|
1249
|
-
selectedIds: multiSelect ? selectedIds : void 0,
|
|
1250
|
-
onBlockClick: multiSelect ? handleBlockClick : void 0
|
|
1251
|
-
}
|
|
1252
|
-
)
|
|
1365
|
+
children: treeContent
|
|
1253
1366
|
}
|
|
1254
1367
|
),
|
|
1255
1368
|
/* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
|
|
@@ -1257,6 +1370,16 @@ function BlockTree({
|
|
|
1257
1370
|
}
|
|
1258
1371
|
);
|
|
1259
1372
|
}
|
|
1373
|
+
function BlockTreeSSR({ fallback = null, ...props }) {
|
|
1374
|
+
const [mounted, setMounted] = react.useState(false);
|
|
1375
|
+
react.useEffect(() => {
|
|
1376
|
+
setMounted(true);
|
|
1377
|
+
}, []);
|
|
1378
|
+
if (!mounted) {
|
|
1379
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
|
|
1380
|
+
}
|
|
1381
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BlockTree, { ...props });
|
|
1382
|
+
}
|
|
1260
1383
|
function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
|
|
1261
1384
|
switch (action.type) {
|
|
1262
1385
|
case "ADD_ITEM": {
|
|
@@ -1661,8 +1784,143 @@ function useBlockHistory(initialBlocks, options = {}) {
|
|
|
1661
1784
|
canRedo: state.future.length > 0
|
|
1662
1785
|
};
|
|
1663
1786
|
}
|
|
1787
|
+
function useLayoutAnimation(containerRef, options = {}) {
|
|
1788
|
+
const {
|
|
1789
|
+
duration = 200,
|
|
1790
|
+
easing = "ease",
|
|
1791
|
+
selector = "[data-block-id]"
|
|
1792
|
+
} = options;
|
|
1793
|
+
const prevPositions = react.useRef(/* @__PURE__ */ new Map());
|
|
1794
|
+
react.useLayoutEffect(() => {
|
|
1795
|
+
const container = containerRef.current;
|
|
1796
|
+
if (!container) return;
|
|
1797
|
+
const children = container.querySelectorAll(selector);
|
|
1798
|
+
const currentPositions = /* @__PURE__ */ new Map();
|
|
1799
|
+
children.forEach((el) => {
|
|
1800
|
+
const id = el.dataset.blockId;
|
|
1801
|
+
if (id) {
|
|
1802
|
+
currentPositions.set(id, el.getBoundingClientRect());
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
children.forEach((el) => {
|
|
1806
|
+
const htmlEl = el;
|
|
1807
|
+
const id = htmlEl.dataset.blockId;
|
|
1808
|
+
if (!id) return;
|
|
1809
|
+
const prev = prevPositions.current.get(id);
|
|
1810
|
+
const curr = currentPositions.get(id);
|
|
1811
|
+
if (!prev || !curr) return;
|
|
1812
|
+
const deltaY = prev.top - curr.top;
|
|
1813
|
+
const deltaX = prev.left - curr.left;
|
|
1814
|
+
if (deltaY === 0 && deltaX === 0) return;
|
|
1815
|
+
htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
1816
|
+
htmlEl.style.transition = "none";
|
|
1817
|
+
requestAnimationFrame(() => {
|
|
1818
|
+
htmlEl.style.transition = `transform ${duration}ms ${easing}`;
|
|
1819
|
+
htmlEl.style.transform = "";
|
|
1820
|
+
const onEnd = () => {
|
|
1821
|
+
htmlEl.style.transition = "";
|
|
1822
|
+
htmlEl.removeEventListener("transitionend", onEnd);
|
|
1823
|
+
};
|
|
1824
|
+
htmlEl.addEventListener("transitionend", onEnd);
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
prevPositions.current = currentPositions;
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
function useVirtualTree({
|
|
1831
|
+
containerRef,
|
|
1832
|
+
itemCount,
|
|
1833
|
+
itemHeight,
|
|
1834
|
+
overscan = 5
|
|
1835
|
+
}) {
|
|
1836
|
+
const [scrollTop, setScrollTop] = react.useState(0);
|
|
1837
|
+
const [containerHeight, setContainerHeight] = react.useState(0);
|
|
1838
|
+
const rafId = react.useRef(0);
|
|
1839
|
+
const handleScroll = react.useCallback(() => {
|
|
1840
|
+
cancelAnimationFrame(rafId.current);
|
|
1841
|
+
rafId.current = requestAnimationFrame(() => {
|
|
1842
|
+
const el = containerRef.current;
|
|
1843
|
+
if (el) {
|
|
1844
|
+
setScrollTop(el.scrollTop);
|
|
1845
|
+
setContainerHeight(el.clientHeight);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
}, [containerRef]);
|
|
1849
|
+
react.useEffect(() => {
|
|
1850
|
+
const el = containerRef.current;
|
|
1851
|
+
if (!el) return;
|
|
1852
|
+
setScrollTop(el.scrollTop);
|
|
1853
|
+
setContainerHeight(el.clientHeight);
|
|
1854
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
1855
|
+
return () => {
|
|
1856
|
+
el.removeEventListener("scroll", handleScroll);
|
|
1857
|
+
cancelAnimationFrame(rafId.current);
|
|
1858
|
+
};
|
|
1859
|
+
}, [containerRef, handleScroll]);
|
|
1860
|
+
const totalHeight = itemCount * itemHeight;
|
|
1861
|
+
const startRaw = Math.floor(scrollTop / itemHeight);
|
|
1862
|
+
const visibleCount = Math.ceil(containerHeight / itemHeight);
|
|
1863
|
+
const start = Math.max(0, startRaw - overscan);
|
|
1864
|
+
const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
|
|
1865
|
+
const offsetY = start * itemHeight;
|
|
1866
|
+
return {
|
|
1867
|
+
visibleRange: { start, end },
|
|
1868
|
+
totalHeight,
|
|
1869
|
+
offsetY
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/utils/serialization.ts
|
|
1874
|
+
function flatToNested(blocks) {
|
|
1875
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const block of blocks) {
|
|
1877
|
+
const key = block.parentId ?? null;
|
|
1878
|
+
const list = byParent.get(key);
|
|
1879
|
+
if (list) {
|
|
1880
|
+
list.push(block);
|
|
1881
|
+
} else {
|
|
1882
|
+
byParent.set(key, [block]);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
for (const list of byParent.values()) {
|
|
1886
|
+
list.sort((a, b) => {
|
|
1887
|
+
if (typeof a.order === "string" && typeof b.order === "string") {
|
|
1888
|
+
return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
|
|
1889
|
+
}
|
|
1890
|
+
return Number(a.order) - Number(b.order);
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
function buildChildren(parentId) {
|
|
1894
|
+
const siblings = byParent.get(parentId) ?? [];
|
|
1895
|
+
return siblings.map((block) => {
|
|
1896
|
+
const { parentId: _p, order: _o, ...rest } = block;
|
|
1897
|
+
return {
|
|
1898
|
+
...rest,
|
|
1899
|
+
children: buildChildren(block.id)
|
|
1900
|
+
};
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
return buildChildren(null);
|
|
1904
|
+
}
|
|
1905
|
+
function nestedToFlat(nested) {
|
|
1906
|
+
const result = [];
|
|
1907
|
+
function walk(nodes, parentId) {
|
|
1908
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1909
|
+
const { children, ...rest } = nodes[i];
|
|
1910
|
+
result.push({
|
|
1911
|
+
...rest,
|
|
1912
|
+
parentId,
|
|
1913
|
+
order: i
|
|
1914
|
+
});
|
|
1915
|
+
walk(children, rest.id);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
walk(nested, null);
|
|
1919
|
+
return result;
|
|
1920
|
+
}
|
|
1664
1921
|
|
|
1665
1922
|
exports.BlockTree = BlockTree;
|
|
1923
|
+
exports.BlockTreeSSR = BlockTreeSSR;
|
|
1666
1924
|
exports.DragOverlay = DragOverlay;
|
|
1667
1925
|
exports.DropZone = DropZone;
|
|
1668
1926
|
exports.TreeRenderer = TreeRenderer;
|
|
@@ -1679,6 +1937,7 @@ exports.debounce = debounce;
|
|
|
1679
1937
|
exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
|
|
1680
1938
|
exports.extractBlockId = extractBlockId;
|
|
1681
1939
|
exports.extractUUID = extractUUID;
|
|
1940
|
+
exports.flatToNested = flatToNested;
|
|
1682
1941
|
exports.generateId = generateId;
|
|
1683
1942
|
exports.generateInitialKeys = generateInitialKeys;
|
|
1684
1943
|
exports.generateKeyBetween = generateKeyBetween;
|
|
@@ -1689,10 +1948,14 @@ exports.getDropZoneType = getDropZoneType;
|
|
|
1689
1948
|
exports.getSensorConfig = getSensorConfig;
|
|
1690
1949
|
exports.getSubtreeDepth = getSubtreeDepth;
|
|
1691
1950
|
exports.initFractionalOrder = initFractionalOrder;
|
|
1951
|
+
exports.nestedToFlat = nestedToFlat;
|
|
1692
1952
|
exports.reparentBlockIndex = reparentBlockIndex;
|
|
1693
1953
|
exports.reparentMultipleBlocks = reparentMultipleBlocks;
|
|
1954
|
+
exports.triggerHaptic = triggerHaptic;
|
|
1694
1955
|
exports.useBlockHistory = useBlockHistory;
|
|
1695
1956
|
exports.useConfiguredSensors = useConfiguredSensors;
|
|
1957
|
+
exports.useLayoutAnimation = useLayoutAnimation;
|
|
1958
|
+
exports.useVirtualTree = useVirtualTree;
|
|
1696
1959
|
exports.weightedVerticalCollision = weightedVerticalCollision;
|
|
1697
1960
|
//# sourceMappingURL=index.js.map
|
|
1698
1961
|
//# sourceMappingURL=index.js.map
|