dnd-block-tree 0.4.0 → 0.5.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 +453 -12
- package/dist/index.d.mts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +250 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -213,6 +213,10 @@ interface SensorConfig {
|
|
|
213
213
|
activationDistance?: number;
|
|
214
214
|
activationDelay?: number;
|
|
215
215
|
tolerance?: number;
|
|
216
|
+
/** Override the default long-press delay (200ms) for touch sensors */
|
|
217
|
+
longPressDelay?: number;
|
|
218
|
+
/** Trigger haptic feedback (vibration) on drag start for touch devices */
|
|
219
|
+
hapticFeedback?: boolean;
|
|
216
220
|
}
|
|
217
221
|
/**
|
|
218
222
|
* Drop zone configuration
|
|
@@ -466,12 +470,19 @@ interface BlockTreeProps<T extends BaseBlock, C extends readonly T['type'][] = r
|
|
|
466
470
|
selectedIds?: Set<string>;
|
|
467
471
|
/** Called when selection changes (for multi-select) */
|
|
468
472
|
onSelectionChange?: (selectedIds: Set<string>) => void;
|
|
473
|
+
/** Enable virtual scrolling for large trees (fixed item height only) */
|
|
474
|
+
virtualize?: {
|
|
475
|
+
/** Fixed height of each item in pixels */
|
|
476
|
+
itemHeight: number;
|
|
477
|
+
/** Number of extra items to render outside the visible range (default: 5) */
|
|
478
|
+
overscan?: number;
|
|
479
|
+
};
|
|
469
480
|
}
|
|
470
481
|
/**
|
|
471
482
|
* Main BlockTree component
|
|
472
483
|
* Provides drag-and-drop functionality for hierarchical block structures
|
|
473
484
|
*/
|
|
474
|
-
declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
485
|
+
declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, animation, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, virtualize, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
475
486
|
|
|
476
487
|
interface TreeRendererProps<T extends BaseBlock> {
|
|
477
488
|
blocks: T[];
|
|
@@ -502,11 +513,15 @@ interface TreeRendererProps<T extends BaseBlock> {
|
|
|
502
513
|
selectedIds?: Set<string>;
|
|
503
514
|
/** Click handler for multi-select */
|
|
504
515
|
onBlockClick?: (blockId: string, event: React.MouseEvent) => void;
|
|
516
|
+
/** Animation configuration */
|
|
517
|
+
animation?: AnimationConfig;
|
|
518
|
+
/** When virtual scrolling is active, only render blocks in this set */
|
|
519
|
+
virtualVisibleIds?: Set<string> | null;
|
|
505
520
|
}
|
|
506
521
|
/**
|
|
507
522
|
* Recursive tree renderer with smart drop zones
|
|
508
523
|
*/
|
|
509
|
-
declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
|
|
524
|
+
declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, animation, virtualVisibleIds, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
|
|
510
525
|
|
|
511
526
|
interface DropZoneProps {
|
|
512
527
|
id: string;
|
|
@@ -536,6 +551,17 @@ interface DragOverlayProps<T extends BaseBlock> {
|
|
|
536
551
|
*/
|
|
537
552
|
declare function DragOverlay<T extends BaseBlock>({ activeBlock, children, selectedCount, }: DragOverlayProps<T>): react_jsx_runtime.JSX.Element;
|
|
538
553
|
|
|
554
|
+
interface BlockTreeSSRProps<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]> extends BlockTreeProps<T, C> {
|
|
555
|
+
/** Content to render before hydration completes (default: null) */
|
|
556
|
+
fallback?: ReactNode;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Hydration-safe wrapper for BlockTree in SSR environments.
|
|
560
|
+
* Renders the fallback (or nothing) on the server and during initial hydration,
|
|
561
|
+
* then mounts the full BlockTree after the client has hydrated.
|
|
562
|
+
*/
|
|
563
|
+
declare function BlockTreeSSR<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ fallback, ...props }: BlockTreeSSRProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
564
|
+
|
|
539
565
|
/**
|
|
540
566
|
* Create block state context and hooks
|
|
541
567
|
*/
|
|
@@ -588,6 +614,58 @@ interface UseBlockHistoryResult<T extends BaseBlock> {
|
|
|
588
614
|
*/
|
|
589
615
|
declare function useBlockHistory<T extends BaseBlock>(initialBlocks: T[], options?: UseBlockHistoryOptions): UseBlockHistoryResult<T>;
|
|
590
616
|
|
|
617
|
+
interface UseLayoutAnimationOptions {
|
|
618
|
+
/** Duration of the transition in ms (default: 200) */
|
|
619
|
+
duration?: number;
|
|
620
|
+
/** CSS easing function (default: 'ease') */
|
|
621
|
+
easing?: string;
|
|
622
|
+
/** CSS selector for animated children (default: '[data-block-id]') */
|
|
623
|
+
selector?: string;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* FLIP-based layout animation hook for reorder transitions.
|
|
627
|
+
*
|
|
628
|
+
* Captures block positions before render (layout effect cleanup),
|
|
629
|
+
* computes the delta after render, and applies a CSS transform transition.
|
|
630
|
+
*
|
|
631
|
+
* Usage:
|
|
632
|
+
* ```tsx
|
|
633
|
+
* const containerRef = useRef<HTMLDivElement>(null)
|
|
634
|
+
* useLayoutAnimation(containerRef, { duration: 200 })
|
|
635
|
+
* return <div ref={containerRef}>...</div>
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
declare function useLayoutAnimation(containerRef: React.RefObject<HTMLElement | null>, options?: UseLayoutAnimationOptions): void;
|
|
639
|
+
|
|
640
|
+
interface UseVirtualTreeOptions {
|
|
641
|
+
/** Ref to the scrollable container element */
|
|
642
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
643
|
+
/** Total number of items in the tree */
|
|
644
|
+
itemCount: number;
|
|
645
|
+
/** Fixed height of each item in pixels */
|
|
646
|
+
itemHeight: number;
|
|
647
|
+
/** Number of extra items to render outside the visible range (default: 5) */
|
|
648
|
+
overscan?: number;
|
|
649
|
+
}
|
|
650
|
+
interface UseVirtualTreeResult {
|
|
651
|
+
/** Start and end indices of the visible range (inclusive) */
|
|
652
|
+
visibleRange: {
|
|
653
|
+
start: number;
|
|
654
|
+
end: number;
|
|
655
|
+
};
|
|
656
|
+
/** Total height of all items for the spacer div */
|
|
657
|
+
totalHeight: number;
|
|
658
|
+
/** Offset from top for the first rendered item */
|
|
659
|
+
offsetY: number;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Lightweight fixed-height virtual scrolling hook for tree lists.
|
|
663
|
+
*
|
|
664
|
+
* Tracks scroll position on the container and computes which items
|
|
665
|
+
* should be rendered based on the viewport and overscan.
|
|
666
|
+
*/
|
|
667
|
+
declare function useVirtualTree({ containerRef, itemCount, itemHeight, overscan, }: UseVirtualTreeOptions): UseVirtualTreeResult;
|
|
668
|
+
|
|
591
669
|
/**
|
|
592
670
|
* Clone a Map
|
|
593
671
|
*/
|
|
@@ -659,6 +737,29 @@ declare function debounce<Args extends unknown[]>(fn: (...args: Args) => void, d
|
|
|
659
737
|
* Generate a unique ID (simple implementation)
|
|
660
738
|
*/
|
|
661
739
|
declare function generateId(): string;
|
|
740
|
+
/**
|
|
741
|
+
* Trigger haptic feedback (vibration) on supported devices.
|
|
742
|
+
* Safe to call in any environment — no-ops when `navigator.vibrate` is unavailable.
|
|
743
|
+
*/
|
|
744
|
+
declare function triggerHaptic(durationMs?: number): void;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* A nested/tree representation of a block.
|
|
748
|
+
* Omits `parentId` and `order` since they are reconstructed during flattening.
|
|
749
|
+
*/
|
|
750
|
+
type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
|
|
751
|
+
children: NestedBlock<T>[];
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* Convert a flat block array into a nested tree structure.
|
|
755
|
+
* Groups blocks by parentId, sorts siblings by order, and recursively builds children arrays.
|
|
756
|
+
*/
|
|
757
|
+
declare function flatToNested<T extends BaseBlock>(blocks: T[]): NestedBlock<T>[];
|
|
758
|
+
/**
|
|
759
|
+
* Convert a nested tree structure back to a flat block array.
|
|
760
|
+
* DFS walk assigning parentId and integer order on the way down.
|
|
761
|
+
*/
|
|
762
|
+
declare function nestedToFlat<T extends BaseBlock>(nested: NestedBlock<T>[]): T[];
|
|
662
763
|
|
|
663
764
|
/**
|
|
664
765
|
* Fractional indexing utilities
|
|
@@ -710,4 +811,4 @@ declare function initFractionalOrder<T extends {
|
|
|
710
811
|
order: number | string;
|
|
711
812
|
}>(blocks: T[]): T[];
|
|
712
813
|
|
|
713
|
-
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, 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 OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, reparentBlockIndex, reparentMultipleBlocks, useBlockHistory, useConfiguredSensors, weightedVerticalCollision };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -135,7 +135,7 @@ function useConfiguredSensors(config = {}) {
|
|
|
135
135
|
distance: activationDistance
|
|
136
136
|
};
|
|
137
137
|
touchConstraint = {
|
|
138
|
-
delay: 200,
|
|
138
|
+
delay: config.longPressDelay ?? 200,
|
|
139
139
|
tolerance: 5
|
|
140
140
|
};
|
|
141
141
|
}
|
|
@@ -197,6 +197,11 @@ function debounce(fn, delay) {
|
|
|
197
197
|
function generateId() {
|
|
198
198
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
199
199
|
}
|
|
200
|
+
function triggerHaptic(durationMs = 10) {
|
|
201
|
+
if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
|
|
202
|
+
navigator.vibrate(durationMs);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
200
205
|
function DropZoneComponent({
|
|
201
206
|
id,
|
|
202
207
|
parentId,
|
|
@@ -278,10 +283,15 @@ function TreeRenderer({
|
|
|
278
283
|
draggedBlock,
|
|
279
284
|
focusedId,
|
|
280
285
|
selectedIds,
|
|
281
|
-
onBlockClick
|
|
286
|
+
onBlockClick,
|
|
287
|
+
animation,
|
|
288
|
+
virtualVisibleIds
|
|
282
289
|
}) {
|
|
283
290
|
const items = blocksByParent.get(parentId) ?? [];
|
|
284
|
-
|
|
291
|
+
let filteredBlocks = items.filter((block) => block.id !== activeId);
|
|
292
|
+
if (virtualVisibleIds && depth === 0) {
|
|
293
|
+
filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
|
|
294
|
+
}
|
|
285
295
|
const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
|
|
286
296
|
const containerClass = depth === 0 ? rootClassName : indentClassName;
|
|
287
297
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
|
|
@@ -325,7 +335,11 @@ function TreeRenderer({
|
|
|
325
335
|
onBlockClick,
|
|
326
336
|
children: ({ isDragging }) => {
|
|
327
337
|
if (isContainer) {
|
|
328
|
-
const
|
|
338
|
+
const expandStyle = animation?.expandDuration ? {
|
|
339
|
+
transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
|
|
340
|
+
opacity: isExpanded ? 1 : 0
|
|
341
|
+
} : void 0;
|
|
342
|
+
const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
329
343
|
TreeRenderer,
|
|
330
344
|
{
|
|
331
345
|
blocks,
|
|
@@ -347,7 +361,9 @@ function TreeRenderer({
|
|
|
347
361
|
draggedBlock,
|
|
348
362
|
focusedId,
|
|
349
363
|
selectedIds,
|
|
350
|
-
onBlockClick
|
|
364
|
+
onBlockClick,
|
|
365
|
+
animation,
|
|
366
|
+
virtualVisibleIds
|
|
351
367
|
}
|
|
352
368
|
) }) : null;
|
|
353
369
|
return Renderer({
|
|
@@ -786,18 +802,21 @@ function BlockTree({
|
|
|
786
802
|
canDrop,
|
|
787
803
|
collisionDetection,
|
|
788
804
|
sensors: sensorConfig,
|
|
805
|
+
animation,
|
|
789
806
|
initialExpanded,
|
|
790
807
|
orderingStrategy = "integer",
|
|
791
808
|
maxDepth,
|
|
792
809
|
keyboardNavigation = false,
|
|
793
810
|
multiSelect = false,
|
|
794
811
|
selectedIds: externalSelectedIds,
|
|
795
|
-
onSelectionChange
|
|
812
|
+
onSelectionChange,
|
|
813
|
+
virtualize
|
|
796
814
|
}) {
|
|
797
815
|
const sensors = useConfiguredSensors({
|
|
798
816
|
activationDistance: sensorConfig?.activationDistance ?? activationDistance,
|
|
799
817
|
activationDelay: sensorConfig?.activationDelay,
|
|
800
|
-
tolerance: sensorConfig?.tolerance
|
|
818
|
+
tolerance: sensorConfig?.tolerance,
|
|
819
|
+
longPressDelay: sensorConfig?.longPressDelay
|
|
801
820
|
});
|
|
802
821
|
const initialExpandedMap = react.useMemo(
|
|
803
822
|
() => computeInitialExpanded(blocks, containerTypes, initialExpanded),
|
|
@@ -1008,12 +1027,15 @@ function BlockTree({
|
|
|
1008
1027
|
setSelectedIds(/* @__PURE__ */ new Set([id]));
|
|
1009
1028
|
}
|
|
1010
1029
|
}
|
|
1030
|
+
if (sensorConfig?.hapticFeedback) {
|
|
1031
|
+
triggerHaptic();
|
|
1032
|
+
}
|
|
1011
1033
|
stateRef.current.activeId = id;
|
|
1012
1034
|
stateRef.current.isDragging = true;
|
|
1013
1035
|
initialBlocksRef.current = [...blocks];
|
|
1014
1036
|
cachedReorderRef.current = null;
|
|
1015
1037
|
forceRender();
|
|
1016
|
-
}, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
|
|
1038
|
+
}, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
|
|
1017
1039
|
const handleDragMove = react.useCallback((event) => {
|
|
1018
1040
|
if (!onDragMove) return;
|
|
1019
1041
|
const id = stateRef.current.activeId;
|
|
@@ -1207,6 +1229,61 @@ function BlockTree({
|
|
|
1207
1229
|
forceRender();
|
|
1208
1230
|
}, [blocks, onExpandChange]);
|
|
1209
1231
|
toggleExpandRef.current = handleToggleExpand;
|
|
1232
|
+
const virtualContainerRef = react.useRef(null);
|
|
1233
|
+
const [virtualScroll, setVirtualScroll] = react.useState({ scrollTop: 0, clientHeight: 0 });
|
|
1234
|
+
react.useEffect(() => {
|
|
1235
|
+
if (!virtualize) return;
|
|
1236
|
+
const el = virtualContainerRef.current;
|
|
1237
|
+
if (!el) return;
|
|
1238
|
+
setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
|
|
1239
|
+
const onScroll = () => {
|
|
1240
|
+
setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
|
|
1241
|
+
};
|
|
1242
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
1243
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
1244
|
+
}, [virtualize]);
|
|
1245
|
+
const virtualResult = react.useMemo(() => {
|
|
1246
|
+
if (!virtualize) return null;
|
|
1247
|
+
const { itemHeight, overscan = 5 } = virtualize;
|
|
1248
|
+
const { scrollTop, clientHeight } = virtualScroll;
|
|
1249
|
+
const totalHeight = visibleBlockIds.length * itemHeight;
|
|
1250
|
+
const startRaw = Math.floor(scrollTop / itemHeight);
|
|
1251
|
+
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
1252
|
+
const start = Math.max(0, startRaw - overscan);
|
|
1253
|
+
const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
|
|
1254
|
+
const offsetY = start * itemHeight;
|
|
1255
|
+
const visibleSet = /* @__PURE__ */ new Set();
|
|
1256
|
+
for (let i = start; i <= end; i++) {
|
|
1257
|
+
visibleSet.add(visibleBlockIds[i]);
|
|
1258
|
+
}
|
|
1259
|
+
return { totalHeight, offsetY, visibleSet };
|
|
1260
|
+
}, [virtualize, virtualScroll, visibleBlockIds]);
|
|
1261
|
+
const treeContent = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1262
|
+
TreeRenderer,
|
|
1263
|
+
{
|
|
1264
|
+
blocks,
|
|
1265
|
+
blocksByParent,
|
|
1266
|
+
parentId: null,
|
|
1267
|
+
activeId: stateRef.current.activeId,
|
|
1268
|
+
expandedMap: stateRef.current.expandedMap,
|
|
1269
|
+
renderers,
|
|
1270
|
+
containerTypes,
|
|
1271
|
+
onHover: handleHover,
|
|
1272
|
+
onToggleExpand: handleToggleExpand,
|
|
1273
|
+
dropZoneClassName,
|
|
1274
|
+
dropZoneActiveClassName,
|
|
1275
|
+
indentClassName,
|
|
1276
|
+
rootClassName: className,
|
|
1277
|
+
canDrag,
|
|
1278
|
+
previewPosition,
|
|
1279
|
+
draggedBlock,
|
|
1280
|
+
focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
|
|
1281
|
+
selectedIds: multiSelect ? selectedIds : void 0,
|
|
1282
|
+
onBlockClick: multiSelect ? handleBlockClick : void 0,
|
|
1283
|
+
animation,
|
|
1284
|
+
virtualVisibleIds: virtualResult?.visibleSet ?? null
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1210
1287
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1211
1288
|
core.DndContext,
|
|
1212
1289
|
{
|
|
@@ -1218,7 +1295,20 @@ function BlockTree({
|
|
|
1218
1295
|
onDragEnd: handleDragEnd,
|
|
1219
1296
|
onDragCancel: handleDragCancel,
|
|
1220
1297
|
children: [
|
|
1221
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1298
|
+
virtualize ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1299
|
+
"div",
|
|
1300
|
+
{
|
|
1301
|
+
ref: (el) => {
|
|
1302
|
+
virtualContainerRef.current = el;
|
|
1303
|
+
rootRef.current = el;
|
|
1304
|
+
},
|
|
1305
|
+
className,
|
|
1306
|
+
style: { minWidth: 0, overflow: "auto", position: "relative" },
|
|
1307
|
+
onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
|
|
1308
|
+
role: keyboardNavigation ? "tree" : void 0,
|
|
1309
|
+
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 }) })
|
|
1310
|
+
}
|
|
1311
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1222
1312
|
"div",
|
|
1223
1313
|
{
|
|
1224
1314
|
ref: rootRef,
|
|
@@ -1226,30 +1316,7 @@ function BlockTree({
|
|
|
1226
1316
|
style: { minWidth: 0 },
|
|
1227
1317
|
onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
|
|
1228
1318
|
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
|
-
)
|
|
1319
|
+
children: treeContent
|
|
1253
1320
|
}
|
|
1254
1321
|
),
|
|
1255
1322
|
/* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
|
|
@@ -1257,6 +1324,16 @@ function BlockTree({
|
|
|
1257
1324
|
}
|
|
1258
1325
|
);
|
|
1259
1326
|
}
|
|
1327
|
+
function BlockTreeSSR({ fallback = null, ...props }) {
|
|
1328
|
+
const [mounted, setMounted] = react.useState(false);
|
|
1329
|
+
react.useEffect(() => {
|
|
1330
|
+
setMounted(true);
|
|
1331
|
+
}, []);
|
|
1332
|
+
if (!mounted) {
|
|
1333
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
|
|
1334
|
+
}
|
|
1335
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BlockTree, { ...props });
|
|
1336
|
+
}
|
|
1260
1337
|
function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
|
|
1261
1338
|
switch (action.type) {
|
|
1262
1339
|
case "ADD_ITEM": {
|
|
@@ -1661,8 +1738,143 @@ function useBlockHistory(initialBlocks, options = {}) {
|
|
|
1661
1738
|
canRedo: state.future.length > 0
|
|
1662
1739
|
};
|
|
1663
1740
|
}
|
|
1741
|
+
function useLayoutAnimation(containerRef, options = {}) {
|
|
1742
|
+
const {
|
|
1743
|
+
duration = 200,
|
|
1744
|
+
easing = "ease",
|
|
1745
|
+
selector = "[data-block-id]"
|
|
1746
|
+
} = options;
|
|
1747
|
+
const prevPositions = react.useRef(/* @__PURE__ */ new Map());
|
|
1748
|
+
react.useLayoutEffect(() => {
|
|
1749
|
+
const container = containerRef.current;
|
|
1750
|
+
if (!container) return;
|
|
1751
|
+
const children = container.querySelectorAll(selector);
|
|
1752
|
+
const currentPositions = /* @__PURE__ */ new Map();
|
|
1753
|
+
children.forEach((el) => {
|
|
1754
|
+
const id = el.dataset.blockId;
|
|
1755
|
+
if (id) {
|
|
1756
|
+
currentPositions.set(id, el.getBoundingClientRect());
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
children.forEach((el) => {
|
|
1760
|
+
const htmlEl = el;
|
|
1761
|
+
const id = htmlEl.dataset.blockId;
|
|
1762
|
+
if (!id) return;
|
|
1763
|
+
const prev = prevPositions.current.get(id);
|
|
1764
|
+
const curr = currentPositions.get(id);
|
|
1765
|
+
if (!prev || !curr) return;
|
|
1766
|
+
const deltaY = prev.top - curr.top;
|
|
1767
|
+
const deltaX = prev.left - curr.left;
|
|
1768
|
+
if (deltaY === 0 && deltaX === 0) return;
|
|
1769
|
+
htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
1770
|
+
htmlEl.style.transition = "none";
|
|
1771
|
+
requestAnimationFrame(() => {
|
|
1772
|
+
htmlEl.style.transition = `transform ${duration}ms ${easing}`;
|
|
1773
|
+
htmlEl.style.transform = "";
|
|
1774
|
+
const onEnd = () => {
|
|
1775
|
+
htmlEl.style.transition = "";
|
|
1776
|
+
htmlEl.removeEventListener("transitionend", onEnd);
|
|
1777
|
+
};
|
|
1778
|
+
htmlEl.addEventListener("transitionend", onEnd);
|
|
1779
|
+
});
|
|
1780
|
+
});
|
|
1781
|
+
prevPositions.current = currentPositions;
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
function useVirtualTree({
|
|
1785
|
+
containerRef,
|
|
1786
|
+
itemCount,
|
|
1787
|
+
itemHeight,
|
|
1788
|
+
overscan = 5
|
|
1789
|
+
}) {
|
|
1790
|
+
const [scrollTop, setScrollTop] = react.useState(0);
|
|
1791
|
+
const [containerHeight, setContainerHeight] = react.useState(0);
|
|
1792
|
+
const rafId = react.useRef(0);
|
|
1793
|
+
const handleScroll = react.useCallback(() => {
|
|
1794
|
+
cancelAnimationFrame(rafId.current);
|
|
1795
|
+
rafId.current = requestAnimationFrame(() => {
|
|
1796
|
+
const el = containerRef.current;
|
|
1797
|
+
if (el) {
|
|
1798
|
+
setScrollTop(el.scrollTop);
|
|
1799
|
+
setContainerHeight(el.clientHeight);
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
}, [containerRef]);
|
|
1803
|
+
react.useEffect(() => {
|
|
1804
|
+
const el = containerRef.current;
|
|
1805
|
+
if (!el) return;
|
|
1806
|
+
setScrollTop(el.scrollTop);
|
|
1807
|
+
setContainerHeight(el.clientHeight);
|
|
1808
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
1809
|
+
return () => {
|
|
1810
|
+
el.removeEventListener("scroll", handleScroll);
|
|
1811
|
+
cancelAnimationFrame(rafId.current);
|
|
1812
|
+
};
|
|
1813
|
+
}, [containerRef, handleScroll]);
|
|
1814
|
+
const totalHeight = itemCount * itemHeight;
|
|
1815
|
+
const startRaw = Math.floor(scrollTop / itemHeight);
|
|
1816
|
+
const visibleCount = Math.ceil(containerHeight / itemHeight);
|
|
1817
|
+
const start = Math.max(0, startRaw - overscan);
|
|
1818
|
+
const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
|
|
1819
|
+
const offsetY = start * itemHeight;
|
|
1820
|
+
return {
|
|
1821
|
+
visibleRange: { start, end },
|
|
1822
|
+
totalHeight,
|
|
1823
|
+
offsetY
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/utils/serialization.ts
|
|
1828
|
+
function flatToNested(blocks) {
|
|
1829
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1830
|
+
for (const block of blocks) {
|
|
1831
|
+
const key = block.parentId ?? null;
|
|
1832
|
+
const list = byParent.get(key);
|
|
1833
|
+
if (list) {
|
|
1834
|
+
list.push(block);
|
|
1835
|
+
} else {
|
|
1836
|
+
byParent.set(key, [block]);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
for (const list of byParent.values()) {
|
|
1840
|
+
list.sort((a, b) => {
|
|
1841
|
+
if (typeof a.order === "string" && typeof b.order === "string") {
|
|
1842
|
+
return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
|
|
1843
|
+
}
|
|
1844
|
+
return Number(a.order) - Number(b.order);
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
function buildChildren(parentId) {
|
|
1848
|
+
const siblings = byParent.get(parentId) ?? [];
|
|
1849
|
+
return siblings.map((block) => {
|
|
1850
|
+
const { parentId: _p, order: _o, ...rest } = block;
|
|
1851
|
+
return {
|
|
1852
|
+
...rest,
|
|
1853
|
+
children: buildChildren(block.id)
|
|
1854
|
+
};
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
return buildChildren(null);
|
|
1858
|
+
}
|
|
1859
|
+
function nestedToFlat(nested) {
|
|
1860
|
+
const result = [];
|
|
1861
|
+
function walk(nodes, parentId) {
|
|
1862
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1863
|
+
const { children, ...rest } = nodes[i];
|
|
1864
|
+
result.push({
|
|
1865
|
+
...rest,
|
|
1866
|
+
parentId,
|
|
1867
|
+
order: i
|
|
1868
|
+
});
|
|
1869
|
+
walk(children, rest.id);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
walk(nested, null);
|
|
1873
|
+
return result;
|
|
1874
|
+
}
|
|
1664
1875
|
|
|
1665
1876
|
exports.BlockTree = BlockTree;
|
|
1877
|
+
exports.BlockTreeSSR = BlockTreeSSR;
|
|
1666
1878
|
exports.DragOverlay = DragOverlay;
|
|
1667
1879
|
exports.DropZone = DropZone;
|
|
1668
1880
|
exports.TreeRenderer = TreeRenderer;
|
|
@@ -1679,6 +1891,7 @@ exports.debounce = debounce;
|
|
|
1679
1891
|
exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
|
|
1680
1892
|
exports.extractBlockId = extractBlockId;
|
|
1681
1893
|
exports.extractUUID = extractUUID;
|
|
1894
|
+
exports.flatToNested = flatToNested;
|
|
1682
1895
|
exports.generateId = generateId;
|
|
1683
1896
|
exports.generateInitialKeys = generateInitialKeys;
|
|
1684
1897
|
exports.generateKeyBetween = generateKeyBetween;
|
|
@@ -1689,10 +1902,14 @@ exports.getDropZoneType = getDropZoneType;
|
|
|
1689
1902
|
exports.getSensorConfig = getSensorConfig;
|
|
1690
1903
|
exports.getSubtreeDepth = getSubtreeDepth;
|
|
1691
1904
|
exports.initFractionalOrder = initFractionalOrder;
|
|
1905
|
+
exports.nestedToFlat = nestedToFlat;
|
|
1692
1906
|
exports.reparentBlockIndex = reparentBlockIndex;
|
|
1693
1907
|
exports.reparentMultipleBlocks = reparentMultipleBlocks;
|
|
1908
|
+
exports.triggerHaptic = triggerHaptic;
|
|
1694
1909
|
exports.useBlockHistory = useBlockHistory;
|
|
1695
1910
|
exports.useConfiguredSensors = useConfiguredSensors;
|
|
1911
|
+
exports.useLayoutAnimation = useLayoutAnimation;
|
|
1912
|
+
exports.useVirtualTree = useVirtualTree;
|
|
1696
1913
|
exports.weightedVerticalCollision = weightedVerticalCollision;
|
|
1697
1914
|
//# sourceMappingURL=index.js.map
|
|
1698
1915
|
//# sourceMappingURL=index.js.map
|