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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { useDroppable, useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DragOverlay as DragOverlay$1, DndContext, useDraggable } from '@dnd-kit/core';
1
+ import { useDroppable, useDraggable, useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DragOverlay as DragOverlay$1, DndContext } from '@dnd-kit/core';
2
2
  import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, useState, createContext, useLayoutEffect, useContext } from 'react';
3
3
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
 
@@ -13,6 +13,12 @@ function extractBlockId(zoneId) {
13
13
  }
14
14
 
15
15
  // src/core/collision.ts
16
+ function collisionValue(d) {
17
+ return d.data.value;
18
+ }
19
+ function collisionLeft(d) {
20
+ return d.data.left;
21
+ }
16
22
  function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
17
23
  const pointerX = collisionRect.left + collisionRect.width / 2;
18
24
  const pointerY = collisionRect.top + collisionRect.height / 2;
@@ -41,11 +47,7 @@ function computeCollisionScores(droppableContainers, collisionRect, snapshotRect
41
47
  }
42
48
  };
43
49
  }).filter((c) => c !== null);
44
- candidates.sort((a, b) => {
45
- const aValue = a.data.value;
46
- const bValue = b.data.value;
47
- return aValue - bValue;
48
- });
50
+ candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
49
51
  return candidates;
50
52
  }
51
53
  var weightedVerticalCollision = ({
@@ -66,13 +68,13 @@ function createStickyCollision(threshold = 15, snapshotRef) {
66
68
  const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
67
69
  if (candidates.length === 0) return [];
68
70
  const bestCandidate = candidates[0];
69
- const bestScore = bestCandidate.data.value;
71
+ const bestScore = collisionValue(bestCandidate);
70
72
  if (currentZoneId !== null) {
71
73
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
72
74
  if (currentCandidate) {
73
- const currentScore = currentCandidate.data.value;
74
- const currentLeft = currentCandidate.data.left;
75
- const bestLeft = bestCandidate.data.left;
75
+ const currentScore = collisionValue(currentCandidate);
76
+ const currentLeft = collisionLeft(currentCandidate);
77
+ const bestLeft = collisionLeft(bestCandidate);
76
78
  const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
77
79
  const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
78
80
  if (currentScore - bestScore < effectiveThreshold) {
@@ -111,11 +113,7 @@ var closestCenterCollision = ({
111
113
  }
112
114
  };
113
115
  }).filter((c) => c !== null);
114
- candidates.sort((a, b) => {
115
- const aValue = a.data.value;
116
- const bValue = b.data.value;
117
- return aValue - bValue;
118
- });
116
+ candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
119
117
  return candidates.slice(0, 1);
120
118
  };
121
119
  var DEFAULT_ACTIVATION_DISTANCE = 8;
@@ -246,7 +244,12 @@ function DraggableBlock({
246
244
  disabled,
247
245
  focusedId,
248
246
  isSelected,
249
- onBlockClick
247
+ onBlockClick,
248
+ isContainer,
249
+ isExpanded,
250
+ depth,
251
+ posInSet,
252
+ setSize
250
253
  }) {
251
254
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
252
255
  id: block.id,
@@ -265,11 +268,16 @@ function DraggableBlock({
265
268
  "data-selected": isSelected || void 0,
266
269
  style: { touchAction: "none", minWidth: 0, outline: "none" },
267
270
  role: "treeitem",
271
+ "aria-level": depth + 1,
272
+ "aria-posinset": posInSet,
273
+ "aria-setsize": setSize,
274
+ "aria-expanded": isContainer ? isExpanded : void 0,
275
+ "aria-selected": isSelected ?? void 0,
268
276
  children: children({ isDragging })
269
277
  }
270
278
  );
271
279
  }
272
- function TreeRenderer({
280
+ function TreeRendererInner({
273
281
  blocks,
274
282
  blocksByParent,
275
283
  parentId,
@@ -339,6 +347,11 @@ function TreeRenderer({
339
347
  focusedId,
340
348
  isSelected: selectedIds?.has(block.id),
341
349
  onBlockClick,
350
+ isContainer,
351
+ isExpanded,
352
+ depth,
353
+ posInSet: originalIndex + 1,
354
+ setSize: items.length,
342
355
  children: ({ isDragging }) => {
343
356
  if (isContainer) {
344
357
  const expandStyle = animation?.expandDuration ? {
@@ -423,6 +436,7 @@ function TreeRenderer({
423
436
  )
424
437
  ] });
425
438
  }
439
+ var TreeRenderer = memo(TreeRendererInner);
426
440
  function DragOverlay({
427
441
  activeBlock,
428
442
  children,
@@ -655,6 +669,9 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
655
669
  if (parentDepth + subtreeDepth > maxDepth) return state;
656
670
  }
657
671
  if (dragged.id === zoneTargetId) return state;
672
+ if (newParentId !== null && getDescendantIds(state, dragged.id).has(newParentId)) {
673
+ return state;
674
+ }
658
675
  const oldList = byParent.get(oldParentId) ?? [];
659
676
  const currentIndexInOldParent = oldList.indexOf(dragged.id);
660
677
  const preNewList = byParent.get(newParentId) ?? [];
@@ -707,19 +724,24 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
707
724
  function getBlockDepth(index, blockId) {
708
725
  let depth = 0;
709
726
  let current = blockId;
727
+ const visited = /* @__PURE__ */ new Set();
710
728
  while (current !== null) {
729
+ if (visited.has(current)) break;
730
+ visited.add(current);
711
731
  depth++;
712
732
  const block = index.byId.get(current);
713
733
  current = block?.parentId ?? null;
714
734
  }
715
735
  return depth;
716
736
  }
717
- function getSubtreeDepth(index, blockId) {
737
+ function getSubtreeDepth(index, blockId, visited = /* @__PURE__ */ new Set()) {
738
+ if (visited.has(blockId)) return 0;
739
+ visited.add(blockId);
718
740
  const children = index.byParent.get(blockId) ?? [];
719
741
  if (children.length === 0) return 1;
720
742
  let max = 0;
721
743
  for (const childId of children) {
722
- max = Math.max(max, getSubtreeDepth(index, childId));
744
+ max = Math.max(max, getSubtreeDepth(index, childId, visited));
723
745
  }
724
746
  return 1 + max;
725
747
  }
@@ -746,6 +768,40 @@ function reparentMultipleBlocks(state, blockIds, targetZone, containerTypes = []
746
768
  }
747
769
  return result;
748
770
  }
771
+ function validateBlockTree(index) {
772
+ const issues = [];
773
+ for (const [id] of index.byId) {
774
+ const visited = /* @__PURE__ */ new Set();
775
+ let current = id;
776
+ while (current !== null) {
777
+ if (visited.has(current)) {
778
+ issues.push(`Cycle detected: block "${id}" has a circular parentId chain`);
779
+ break;
780
+ }
781
+ visited.add(current);
782
+ const block = index.byId.get(current);
783
+ current = block?.parentId ?? null;
784
+ }
785
+ }
786
+ for (const [id, block] of index.byId) {
787
+ if (block.parentId !== null && !index.byId.has(block.parentId)) {
788
+ issues.push(`Orphan: block "${id}" references non-existent parent "${block.parentId}"`);
789
+ }
790
+ }
791
+ for (const [parentId, childIds] of index.byParent) {
792
+ for (const childId of childIds) {
793
+ if (!index.byId.has(childId)) {
794
+ issues.push(`Stale ref: byParent key "${parentId}" lists non-existent block "${childId}"`);
795
+ }
796
+ }
797
+ }
798
+ if (issues.length > 0) {
799
+ for (const issue of issues) {
800
+ console.warn(`[dnd-block-tree] ${issue}`);
801
+ }
802
+ }
803
+ return { valid: issues.length === 0, issues };
804
+ }
749
805
  function deleteBlockAndDescendants(state, id) {
750
806
  const byId = cloneMap(state.byId);
751
807
  const byParent = cloneParentMap(state.byParent);
@@ -768,7 +824,12 @@ function getBlockPosition(blocks, blockId) {
768
824
  }
769
825
  function computeInitialExpanded(blocks, containerTypes, initialExpanded) {
770
826
  if (initialExpanded === "none") {
771
- return {};
827
+ const expandedMap2 = {};
828
+ const containers2 = blocks.filter((b) => containerTypes.includes(b.type));
829
+ for (const container of containers2) {
830
+ expandedMap2[container.id] = false;
831
+ }
832
+ return expandedMap2;
772
833
  }
773
834
  const expandedMap = {};
774
835
  const containers = blocks.filter((b) => containerTypes.includes(b.type));
@@ -885,14 +946,17 @@ function BlockTree({
885
946
  onDragMove?.(event);
886
947
  }, 50)
887
948
  ).current;
888
- const originalIndex = computeNormalizedIndex(blocks, orderingStrategy);
889
- const blocksByParent = /* @__PURE__ */ new Map();
890
- for (const [parentId, ids] of originalIndex.byParent.entries()) {
891
- blocksByParent.set(
892
- parentId,
893
- ids.map((id) => originalIndex.byId.get(id)).filter(Boolean)
894
- );
895
- }
949
+ const originalIndex = useMemo(
950
+ () => computeNormalizedIndex(blocks, orderingStrategy),
951
+ [blocks, orderingStrategy]
952
+ );
953
+ const blocksByParent = useMemo(() => {
954
+ const map = /* @__PURE__ */ new Map();
955
+ for (const [parentId, ids] of originalIndex.byParent.entries()) {
956
+ map.set(parentId, ids.map((id) => originalIndex.byId.get(id)).filter(Boolean));
957
+ }
958
+ return map;
959
+ }, [originalIndex]);
896
960
  const focusedIdRef = useRef(null);
897
961
  const rootRef = useRef(null);
898
962
  const lastClickedIdRef = useRef(null);
@@ -1917,6 +1981,6 @@ function nestedToFlat(nested) {
1917
1981
  return result;
1918
1982
  }
1919
1983
 
1920
- export { BlockTree, BlockTreeSSR, DragOverlay, DropZone, TreeRenderer, 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 };
1984
+ export { BlockTree, BlockTreeSSR, DragOverlay, DropZone, TreeRenderer, 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 };
1921
1985
  //# sourceMappingURL=index.mjs.map
1922
1986
  //# sourceMappingURL=index.mjs.map