dnd-block-tree 1.0.0 → 1.1.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 CHANGED
@@ -1,12 +1,8 @@
1
1
  # dnd-block-tree
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dnd-block-tree.svg)](https://www.npmjs.com/package/dnd-block-tree)
4
- [![npm downloads](https://img.shields.io/npm/dm/dnd-block-tree.svg)](https://www.npmjs.com/package/dnd-block-tree)
5
- [![bundle size](https://img.shields.io/bundlephobia/minzip/dnd-block-tree)](https://bundlephobia.com/package/dnd-block-tree)
6
4
  [![CI](https://github.com/thesandybridge/dnd-block-tree/actions/workflows/ci.yml/badge.svg)](https://github.com/thesandybridge/dnd-block-tree/actions/workflows/ci.yml)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
9
- [![demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://dnd-block-tree.vercel.app)
5
+ [![demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://blocktree.sandybridge.io)
10
6
 
11
7
  A headless React library for building hierarchical drag-and-drop interfaces. Bring your own components, we handle the complexity.
12
8
 
@@ -517,7 +513,7 @@ Enable accessible tree navigation with the `keyboardNavigation` prop:
517
513
  | Home | Focus first block |
518
514
  | End | Focus last block |
519
515
 
520
- Blocks receive `data-block-id` and `tabIndex` attributes for focus management, and the tree root gets `role="tree"`.
516
+ Blocks receive `data-block-id` and `tabIndex` attributes for focus management, and the tree root gets `role="tree"`. Each block element includes WAI-ARIA TreeView attributes: `aria-level`, `aria-posinset`, `aria-setsize`, `aria-expanded` (containers only), and `aria-selected`.
521
517
 
522
518
  ## Multi-Select Drag
523
519
 
@@ -689,6 +685,7 @@ import {
689
685
  deleteBlockAndDescendants, // Remove a block and all its descendants from index
690
686
  getBlockDepth, // Compute depth of a block (root = 1)
691
687
  getSubtreeDepth, // Max depth of a subtree (leaf = 1)
688
+ validateBlockTree, // Validate tree for cycles, orphans, stale refs
692
689
 
693
690
  // Serialization
694
691
  flatToNested, // Convert flat block array to nested tree
@@ -716,10 +713,19 @@ import {
716
713
  createStickyCollision, // Hysteresis wrapper with snapshot support
717
714
  type SnapshotRectsRef, // Ref type for frozen zone rects
718
715
 
716
+ // Internal helpers
717
+ cloneMap, // Clone a Map
718
+ cloneParentMap, // Deep clone a parent->children Map
719
+ debounce, // Debounce with cancel()
720
+
719
721
  // Hooks
722
+ createBlockState, // Factory for block state context + provider
723
+ createTreeState, // Factory for tree UI state context + provider
720
724
  useBlockHistory, // Undo/redo state management
721
725
  useLayoutAnimation, // FLIP-based reorder animations
722
726
  useVirtualTree, // Virtual scrolling primitives
727
+ useConfiguredSensors, // Configure dnd-kit sensors
728
+ getSensorConfig, // Get sensor config from SensorConfig
723
729
 
724
730
  // Components
725
731
  BlockTree, // Main drag-and-drop tree component
@@ -732,7 +738,7 @@ import {
732
738
 
733
739
  ## Demo
734
740
 
735
- Check out the [live demo](https://dnd-block-tree.vercel.app) to see the library in action with two example use cases:
741
+ Check out the [live demo](https://blocktree.sandybridge.io) to see the library in action with two example use cases:
736
742
 
737
743
  - **Productivity** - Sections, tasks, and notes with undo/redo, max depth control, and keyboard navigation
738
744
  - **File System** - Folders and files
package/dist/index.d.mts CHANGED
@@ -389,6 +389,78 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
389
389
  blockMap: Map<string, T>;
390
390
  }
391
391
 
392
+ /**
393
+ * Clone a Map
394
+ */
395
+ declare function cloneMap<K, V>(map: Map<K, V>): Map<K, V>;
396
+ /**
397
+ * Clone a parent map with arrays
398
+ */
399
+ declare function cloneParentMap(map: Map<string | null, string[]>): Map<string | null, string[]>;
400
+ /**
401
+ * Compute normalized index from flat block array.
402
+ *
403
+ * With `orderingStrategy: 'fractional'`, siblings are sorted by their `order`
404
+ * field (lexicographic). With `'integer'` (default), the input order is preserved.
405
+ */
406
+ declare function computeNormalizedIndex<T extends BaseBlock>(blocks: T[], orderingStrategy?: OrderingStrategy): BlockIndex<T>;
407
+ /**
408
+ * Build ordered flat array from BlockIndex.
409
+ *
410
+ * With `'integer'` ordering (default), assigns sequential `order: 0, 1, 2, …`.
411
+ * With `'fractional'` ordering, preserves existing `order` values — the moved
412
+ * block already has its new fractional key set by `reparentBlockIndex`.
413
+ */
414
+ declare function buildOrderedBlocks<T extends BaseBlock>(index: BlockIndex<T>, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy): T[];
415
+ /**
416
+ * Reparent a block based on drop zone ID.
417
+ *
418
+ * @param state - Current block index
419
+ * @param activeId - ID of the dragged block
420
+ * @param targetZone - Drop zone ID (e.g., "after-uuid", "before-uuid", "into-uuid")
421
+ * @param containerTypes - Block types that can have children
422
+ * @param orderingStrategy - Whether to assign a fractional key to the moved block
423
+ */
424
+ declare function reparentBlockIndex<T extends BaseBlock>(state: BlockIndex<T>, activeId: UniqueIdentifier, targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
425
+ /**
426
+ * Compute the depth of a block by walking its parentId chain.
427
+ * Root-level blocks have depth 1.
428
+ */
429
+ declare function getBlockDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
430
+ /**
431
+ * Compute the maximum depth of a subtree rooted at blockId (inclusive).
432
+ * A leaf block returns 1.
433
+ */
434
+ declare function getSubtreeDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string, visited?: Set<string>): number;
435
+ /**
436
+ * Get all descendant IDs of a block
437
+ */
438
+ declare function getDescendantIds<T extends BaseBlock>(state: BlockIndex<T>, parentId: string): Set<string>;
439
+ /**
440
+ * Reparent multiple blocks to a target zone, preserving their relative order.
441
+ * The first block in `blockIds` is treated as the primary (anchor) block.
442
+ */
443
+ declare function reparentMultipleBlocks<T extends BaseBlock>(state: BlockIndex<T>, blockIds: string[], targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
444
+ /**
445
+ * Result of validating a block tree
446
+ */
447
+ interface TreeValidationResult {
448
+ valid: boolean;
449
+ issues: string[];
450
+ }
451
+ /**
452
+ * Validate a block tree index for structural integrity.
453
+ * Checks for cycles, orphans (parentId references non-existent block),
454
+ * and stale refs (byParent lists IDs not present in byId).
455
+ *
456
+ * Opt-in utility — not called automatically.
457
+ */
458
+ declare function validateBlockTree<T extends BaseBlock>(index: BlockIndex<T>): TreeValidationResult;
459
+ /**
460
+ * Delete a block and all its descendants
461
+ */
462
+ declare function deleteBlockAndDescendants<T extends BaseBlock>(state: BlockIndex<T>, id: string): BlockIndex<T>;
463
+
392
464
  type SnapshotRectsRef = {
393
465
  current: Map<UniqueIdentifier, DOMRect> | null;
394
466
  };
@@ -527,7 +599,8 @@ interface TreeRendererProps<T extends BaseBlock> {
527
599
  /**
528
600
  * Recursive tree renderer with smart drop zones
529
601
  */
530
- 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;
602
+ declare function TreeRendererInner<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;
603
+ declare const TreeRenderer: typeof TreeRendererInner;
531
604
 
532
605
  interface DropZoneProps {
533
606
  id: string;
@@ -672,63 +745,6 @@ interface UseVirtualTreeResult {
672
745
  */
673
746
  declare function useVirtualTree({ containerRef, itemCount, itemHeight, overscan, }: UseVirtualTreeOptions): UseVirtualTreeResult;
674
747
 
675
- /**
676
- * Clone a Map
677
- */
678
- declare function cloneMap<K, V>(map: Map<K, V>): Map<K, V>;
679
- /**
680
- * Clone a parent map with arrays
681
- */
682
- declare function cloneParentMap(map: Map<string | null, string[]>): Map<string | null, string[]>;
683
- /**
684
- * Compute normalized index from flat block array.
685
- *
686
- * With `orderingStrategy: 'fractional'`, siblings are sorted by their `order`
687
- * field (lexicographic). With `'integer'` (default), the input order is preserved.
688
- */
689
- declare function computeNormalizedIndex<T extends BaseBlock>(blocks: T[], orderingStrategy?: OrderingStrategy): BlockIndex<T>;
690
- /**
691
- * Build ordered flat array from BlockIndex.
692
- *
693
- * With `'integer'` ordering (default), assigns sequential `order: 0, 1, 2, …`.
694
- * With `'fractional'` ordering, preserves existing `order` values — the moved
695
- * block already has its new fractional key set by `reparentBlockIndex`.
696
- */
697
- declare function buildOrderedBlocks<T extends BaseBlock>(index: BlockIndex<T>, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy): T[];
698
- /**
699
- * Reparent a block based on drop zone ID.
700
- *
701
- * @param state - Current block index
702
- * @param activeId - ID of the dragged block
703
- * @param targetZone - Drop zone ID (e.g., "after-uuid", "before-uuid", "into-uuid")
704
- * @param containerTypes - Block types that can have children
705
- * @param orderingStrategy - Whether to assign a fractional key to the moved block
706
- */
707
- declare function reparentBlockIndex<T extends BaseBlock>(state: BlockIndex<T>, activeId: UniqueIdentifier, targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
708
- /**
709
- * Compute the depth of a block by walking its parentId chain.
710
- * Root-level blocks have depth 1.
711
- */
712
- declare function getBlockDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
713
- /**
714
- * Compute the maximum depth of a subtree rooted at blockId (inclusive).
715
- * A leaf block returns 1.
716
- */
717
- declare function getSubtreeDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
718
- /**
719
- * Get all descendant IDs of a block
720
- */
721
- declare function getDescendantIds<T extends BaseBlock>(state: BlockIndex<T>, parentId: string): Set<string>;
722
- /**
723
- * Reparent multiple blocks to a target zone, preserving their relative order.
724
- * The first block in `blockIds` is treated as the primary (anchor) block.
725
- */
726
- declare function reparentMultipleBlocks<T extends BaseBlock>(state: BlockIndex<T>, blockIds: string[], targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
727
- /**
728
- * Delete a block and all its descendants
729
- */
730
- declare function deleteBlockAndDescendants<T extends BaseBlock>(state: BlockIndex<T>, id: string): BlockIndex<T>;
731
-
732
748
  /**
733
749
  * Extract UUID from a zone ID by removing the prefix (before-, after-, into-, end-)
734
750
  */
@@ -817,4 +833,4 @@ declare function initFractionalOrder<T extends {
817
833
  order: number | string;
818
834
  }>(blocks: T[]): T[];
819
835
 
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 };
836
+ 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 TreeValidationResult, 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, validateBlockTree, weightedVerticalCollision };
package/dist/index.d.ts CHANGED
@@ -389,6 +389,78 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
389
389
  blockMap: Map<string, T>;
390
390
  }
391
391
 
392
+ /**
393
+ * Clone a Map
394
+ */
395
+ declare function cloneMap<K, V>(map: Map<K, V>): Map<K, V>;
396
+ /**
397
+ * Clone a parent map with arrays
398
+ */
399
+ declare function cloneParentMap(map: Map<string | null, string[]>): Map<string | null, string[]>;
400
+ /**
401
+ * Compute normalized index from flat block array.
402
+ *
403
+ * With `orderingStrategy: 'fractional'`, siblings are sorted by their `order`
404
+ * field (lexicographic). With `'integer'` (default), the input order is preserved.
405
+ */
406
+ declare function computeNormalizedIndex<T extends BaseBlock>(blocks: T[], orderingStrategy?: OrderingStrategy): BlockIndex<T>;
407
+ /**
408
+ * Build ordered flat array from BlockIndex.
409
+ *
410
+ * With `'integer'` ordering (default), assigns sequential `order: 0, 1, 2, …`.
411
+ * With `'fractional'` ordering, preserves existing `order` values — the moved
412
+ * block already has its new fractional key set by `reparentBlockIndex`.
413
+ */
414
+ declare function buildOrderedBlocks<T extends BaseBlock>(index: BlockIndex<T>, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy): T[];
415
+ /**
416
+ * Reparent a block based on drop zone ID.
417
+ *
418
+ * @param state - Current block index
419
+ * @param activeId - ID of the dragged block
420
+ * @param targetZone - Drop zone ID (e.g., "after-uuid", "before-uuid", "into-uuid")
421
+ * @param containerTypes - Block types that can have children
422
+ * @param orderingStrategy - Whether to assign a fractional key to the moved block
423
+ */
424
+ declare function reparentBlockIndex<T extends BaseBlock>(state: BlockIndex<T>, activeId: UniqueIdentifier, targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
425
+ /**
426
+ * Compute the depth of a block by walking its parentId chain.
427
+ * Root-level blocks have depth 1.
428
+ */
429
+ declare function getBlockDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
430
+ /**
431
+ * Compute the maximum depth of a subtree rooted at blockId (inclusive).
432
+ * A leaf block returns 1.
433
+ */
434
+ declare function getSubtreeDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string, visited?: Set<string>): number;
435
+ /**
436
+ * Get all descendant IDs of a block
437
+ */
438
+ declare function getDescendantIds<T extends BaseBlock>(state: BlockIndex<T>, parentId: string): Set<string>;
439
+ /**
440
+ * Reparent multiple blocks to a target zone, preserving their relative order.
441
+ * The first block in `blockIds` is treated as the primary (anchor) block.
442
+ */
443
+ declare function reparentMultipleBlocks<T extends BaseBlock>(state: BlockIndex<T>, blockIds: string[], targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
444
+ /**
445
+ * Result of validating a block tree
446
+ */
447
+ interface TreeValidationResult {
448
+ valid: boolean;
449
+ issues: string[];
450
+ }
451
+ /**
452
+ * Validate a block tree index for structural integrity.
453
+ * Checks for cycles, orphans (parentId references non-existent block),
454
+ * and stale refs (byParent lists IDs not present in byId).
455
+ *
456
+ * Opt-in utility — not called automatically.
457
+ */
458
+ declare function validateBlockTree<T extends BaseBlock>(index: BlockIndex<T>): TreeValidationResult;
459
+ /**
460
+ * Delete a block and all its descendants
461
+ */
462
+ declare function deleteBlockAndDescendants<T extends BaseBlock>(state: BlockIndex<T>, id: string): BlockIndex<T>;
463
+
392
464
  type SnapshotRectsRef = {
393
465
  current: Map<UniqueIdentifier, DOMRect> | null;
394
466
  };
@@ -527,7 +599,8 @@ interface TreeRendererProps<T extends BaseBlock> {
527
599
  /**
528
600
  * Recursive tree renderer with smart drop zones
529
601
  */
530
- 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;
602
+ declare function TreeRendererInner<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;
603
+ declare const TreeRenderer: typeof TreeRendererInner;
531
604
 
532
605
  interface DropZoneProps {
533
606
  id: string;
@@ -672,63 +745,6 @@ interface UseVirtualTreeResult {
672
745
  */
673
746
  declare function useVirtualTree({ containerRef, itemCount, itemHeight, overscan, }: UseVirtualTreeOptions): UseVirtualTreeResult;
674
747
 
675
- /**
676
- * Clone a Map
677
- */
678
- declare function cloneMap<K, V>(map: Map<K, V>): Map<K, V>;
679
- /**
680
- * Clone a parent map with arrays
681
- */
682
- declare function cloneParentMap(map: Map<string | null, string[]>): Map<string | null, string[]>;
683
- /**
684
- * Compute normalized index from flat block array.
685
- *
686
- * With `orderingStrategy: 'fractional'`, siblings are sorted by their `order`
687
- * field (lexicographic). With `'integer'` (default), the input order is preserved.
688
- */
689
- declare function computeNormalizedIndex<T extends BaseBlock>(blocks: T[], orderingStrategy?: OrderingStrategy): BlockIndex<T>;
690
- /**
691
- * Build ordered flat array from BlockIndex.
692
- *
693
- * With `'integer'` ordering (default), assigns sequential `order: 0, 1, 2, …`.
694
- * With `'fractional'` ordering, preserves existing `order` values — the moved
695
- * block already has its new fractional key set by `reparentBlockIndex`.
696
- */
697
- declare function buildOrderedBlocks<T extends BaseBlock>(index: BlockIndex<T>, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy): T[];
698
- /**
699
- * Reparent a block based on drop zone ID.
700
- *
701
- * @param state - Current block index
702
- * @param activeId - ID of the dragged block
703
- * @param targetZone - Drop zone ID (e.g., "after-uuid", "before-uuid", "into-uuid")
704
- * @param containerTypes - Block types that can have children
705
- * @param orderingStrategy - Whether to assign a fractional key to the moved block
706
- */
707
- declare function reparentBlockIndex<T extends BaseBlock>(state: BlockIndex<T>, activeId: UniqueIdentifier, targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
708
- /**
709
- * Compute the depth of a block by walking its parentId chain.
710
- * Root-level blocks have depth 1.
711
- */
712
- declare function getBlockDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
713
- /**
714
- * Compute the maximum depth of a subtree rooted at blockId (inclusive).
715
- * A leaf block returns 1.
716
- */
717
- declare function getSubtreeDepth<T extends BaseBlock>(index: BlockIndex<T>, blockId: string): number;
718
- /**
719
- * Get all descendant IDs of a block
720
- */
721
- declare function getDescendantIds<T extends BaseBlock>(state: BlockIndex<T>, parentId: string): Set<string>;
722
- /**
723
- * Reparent multiple blocks to a target zone, preserving their relative order.
724
- * The first block in `blockIds` is treated as the primary (anchor) block.
725
- */
726
- declare function reparentMultipleBlocks<T extends BaseBlock>(state: BlockIndex<T>, blockIds: string[], targetZone: string, containerTypes?: readonly string[], orderingStrategy?: OrderingStrategy, maxDepth?: number): BlockIndex<T>;
727
- /**
728
- * Delete a block and all its descendants
729
- */
730
- declare function deleteBlockAndDescendants<T extends BaseBlock>(state: BlockIndex<T>, id: string): BlockIndex<T>;
731
-
732
748
  /**
733
749
  * Extract UUID from a zone ID by removing the prefix (before-, after-, into-, end-)
734
750
  */
@@ -817,4 +833,4 @@ declare function initFractionalOrder<T extends {
817
833
  order: number | string;
818
834
  }>(blocks: T[]): T[];
819
835
 
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 };
836
+ 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 TreeValidationResult, 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, validateBlockTree, weightedVerticalCollision };
package/dist/index.js CHANGED
@@ -15,6 +15,12 @@ function extractBlockId(zoneId) {
15
15
  }
16
16
 
17
17
  // src/core/collision.ts
18
+ function collisionValue(d) {
19
+ return d.data.value;
20
+ }
21
+ function collisionLeft(d) {
22
+ return d.data.left;
23
+ }
18
24
  function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
19
25
  const pointerX = collisionRect.left + collisionRect.width / 2;
20
26
  const pointerY = collisionRect.top + collisionRect.height / 2;
@@ -43,11 +49,7 @@ function computeCollisionScores(droppableContainers, collisionRect, snapshotRect
43
49
  }
44
50
  };
45
51
  }).filter((c) => c !== null);
46
- candidates.sort((a, b) => {
47
- const aValue = a.data.value;
48
- const bValue = b.data.value;
49
- return aValue - bValue;
50
- });
52
+ candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
51
53
  return candidates;
52
54
  }
53
55
  var weightedVerticalCollision = ({
@@ -68,13 +70,13 @@ function createStickyCollision(threshold = 15, snapshotRef) {
68
70
  const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
69
71
  if (candidates.length === 0) return [];
70
72
  const bestCandidate = candidates[0];
71
- const bestScore = bestCandidate.data.value;
73
+ const bestScore = collisionValue(bestCandidate);
72
74
  if (currentZoneId !== null) {
73
75
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
74
76
  if (currentCandidate) {
75
- const currentScore = currentCandidate.data.value;
76
- const currentLeft = currentCandidate.data.left;
77
- const bestLeft = bestCandidate.data.left;
77
+ const currentScore = collisionValue(currentCandidate);
78
+ const currentLeft = collisionLeft(currentCandidate);
79
+ const bestLeft = collisionLeft(bestCandidate);
78
80
  const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
79
81
  const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
80
82
  if (currentScore - bestScore < effectiveThreshold) {
@@ -113,11 +115,7 @@ var closestCenterCollision = ({
113
115
  }
114
116
  };
115
117
  }).filter((c) => c !== null);
116
- candidates.sort((a, b) => {
117
- const aValue = a.data.value;
118
- const bValue = b.data.value;
119
- return aValue - bValue;
120
- });
118
+ candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
121
119
  return candidates.slice(0, 1);
122
120
  };
123
121
  var DEFAULT_ACTIVATION_DISTANCE = 8;
@@ -248,7 +246,12 @@ function DraggableBlock({
248
246
  disabled,
249
247
  focusedId,
250
248
  isSelected,
251
- onBlockClick
249
+ onBlockClick,
250
+ isContainer,
251
+ isExpanded,
252
+ depth,
253
+ posInSet,
254
+ setSize
252
255
  }) {
253
256
  const { attributes, listeners, setNodeRef, isDragging } = core.useDraggable({
254
257
  id: block.id,
@@ -267,11 +270,16 @@ function DraggableBlock({
267
270
  "data-selected": isSelected || void 0,
268
271
  style: { touchAction: "none", minWidth: 0, outline: "none" },
269
272
  role: "treeitem",
273
+ "aria-level": depth + 1,
274
+ "aria-posinset": posInSet,
275
+ "aria-setsize": setSize,
276
+ "aria-expanded": isContainer ? isExpanded : void 0,
277
+ "aria-selected": isSelected ?? void 0,
270
278
  children: children({ isDragging })
271
279
  }
272
280
  );
273
281
  }
274
- function TreeRenderer({
282
+ function TreeRendererInner({
275
283
  blocks,
276
284
  blocksByParent,
277
285
  parentId,
@@ -341,6 +349,11 @@ function TreeRenderer({
341
349
  focusedId,
342
350
  isSelected: selectedIds?.has(block.id),
343
351
  onBlockClick,
352
+ isContainer,
353
+ isExpanded,
354
+ depth,
355
+ posInSet: originalIndex + 1,
356
+ setSize: items.length,
344
357
  children: ({ isDragging }) => {
345
358
  if (isContainer) {
346
359
  const expandStyle = animation?.expandDuration ? {
@@ -425,6 +438,7 @@ function TreeRenderer({
425
438
  )
426
439
  ] });
427
440
  }
441
+ var TreeRenderer = react.memo(TreeRendererInner);
428
442
  function DragOverlay({
429
443
  activeBlock,
430
444
  children,
@@ -657,6 +671,9 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
657
671
  if (parentDepth + subtreeDepth > maxDepth) return state;
658
672
  }
659
673
  if (dragged.id === zoneTargetId) return state;
674
+ if (newParentId !== null && getDescendantIds(state, dragged.id).has(newParentId)) {
675
+ return state;
676
+ }
660
677
  const oldList = byParent.get(oldParentId) ?? [];
661
678
  const currentIndexInOldParent = oldList.indexOf(dragged.id);
662
679
  const preNewList = byParent.get(newParentId) ?? [];
@@ -709,19 +726,24 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
709
726
  function getBlockDepth(index, blockId) {
710
727
  let depth = 0;
711
728
  let current = blockId;
729
+ const visited = /* @__PURE__ */ new Set();
712
730
  while (current !== null) {
731
+ if (visited.has(current)) break;
732
+ visited.add(current);
713
733
  depth++;
714
734
  const block = index.byId.get(current);
715
735
  current = block?.parentId ?? null;
716
736
  }
717
737
  return depth;
718
738
  }
719
- function getSubtreeDepth(index, blockId) {
739
+ function getSubtreeDepth(index, blockId, visited = /* @__PURE__ */ new Set()) {
740
+ if (visited.has(blockId)) return 0;
741
+ visited.add(blockId);
720
742
  const children = index.byParent.get(blockId) ?? [];
721
743
  if (children.length === 0) return 1;
722
744
  let max = 0;
723
745
  for (const childId of children) {
724
- max = Math.max(max, getSubtreeDepth(index, childId));
746
+ max = Math.max(max, getSubtreeDepth(index, childId, visited));
725
747
  }
726
748
  return 1 + max;
727
749
  }
@@ -748,6 +770,40 @@ function reparentMultipleBlocks(state, blockIds, targetZone, containerTypes = []
748
770
  }
749
771
  return result;
750
772
  }
773
+ function validateBlockTree(index) {
774
+ const issues = [];
775
+ for (const [id] of index.byId) {
776
+ const visited = /* @__PURE__ */ new Set();
777
+ let current = id;
778
+ while (current !== null) {
779
+ if (visited.has(current)) {
780
+ issues.push(`Cycle detected: block "${id}" has a circular parentId chain`);
781
+ break;
782
+ }
783
+ visited.add(current);
784
+ const block = index.byId.get(current);
785
+ current = block?.parentId ?? null;
786
+ }
787
+ }
788
+ for (const [id, block] of index.byId) {
789
+ if (block.parentId !== null && !index.byId.has(block.parentId)) {
790
+ issues.push(`Orphan: block "${id}" references non-existent parent "${block.parentId}"`);
791
+ }
792
+ }
793
+ for (const [parentId, childIds] of index.byParent) {
794
+ for (const childId of childIds) {
795
+ if (!index.byId.has(childId)) {
796
+ issues.push(`Stale ref: byParent key "${parentId}" lists non-existent block "${childId}"`);
797
+ }
798
+ }
799
+ }
800
+ if (issues.length > 0) {
801
+ for (const issue of issues) {
802
+ console.warn(`[dnd-block-tree] ${issue}`);
803
+ }
804
+ }
805
+ return { valid: issues.length === 0, issues };
806
+ }
751
807
  function deleteBlockAndDescendants(state, id) {
752
808
  const byId = cloneMap(state.byId);
753
809
  const byParent = cloneParentMap(state.byParent);
@@ -770,7 +826,12 @@ function getBlockPosition(blocks, blockId) {
770
826
  }
771
827
  function computeInitialExpanded(blocks, containerTypes, initialExpanded) {
772
828
  if (initialExpanded === "none") {
773
- return {};
829
+ const expandedMap2 = {};
830
+ const containers2 = blocks.filter((b) => containerTypes.includes(b.type));
831
+ for (const container of containers2) {
832
+ expandedMap2[container.id] = false;
833
+ }
834
+ return expandedMap2;
774
835
  }
775
836
  const expandedMap = {};
776
837
  const containers = blocks.filter((b) => containerTypes.includes(b.type));
@@ -887,14 +948,17 @@ function BlockTree({
887
948
  onDragMove?.(event);
888
949
  }, 50)
889
950
  ).current;
890
- const originalIndex = computeNormalizedIndex(blocks, orderingStrategy);
891
- const blocksByParent = /* @__PURE__ */ new Map();
892
- for (const [parentId, ids] of originalIndex.byParent.entries()) {
893
- blocksByParent.set(
894
- parentId,
895
- ids.map((id) => originalIndex.byId.get(id)).filter(Boolean)
896
- );
897
- }
951
+ const originalIndex = react.useMemo(
952
+ () => computeNormalizedIndex(blocks, orderingStrategy),
953
+ [blocks, orderingStrategy]
954
+ );
955
+ const blocksByParent = react.useMemo(() => {
956
+ const map = /* @__PURE__ */ new Map();
957
+ for (const [parentId, ids] of originalIndex.byParent.entries()) {
958
+ map.set(parentId, ids.map((id) => originalIndex.byId.get(id)).filter(Boolean));
959
+ }
960
+ return map;
961
+ }, [originalIndex]);
898
962
  const focusedIdRef = react.useRef(null);
899
963
  const rootRef = react.useRef(null);
900
964
  const lastClickedIdRef = react.useRef(null);
@@ -1956,6 +2020,7 @@ exports.useBlockHistory = useBlockHistory;
1956
2020
  exports.useConfiguredSensors = useConfiguredSensors;
1957
2021
  exports.useLayoutAnimation = useLayoutAnimation;
1958
2022
  exports.useVirtualTree = useVirtualTree;
2023
+ exports.validateBlockTree = validateBlockTree;
1959
2024
  exports.weightedVerticalCollision = weightedVerticalCollision;
1960
2025
  //# sourceMappingURL=index.js.map
1961
2026
  //# sourceMappingURL=index.js.map