react-native-tree-multi-select 3.0.0-beta.3 → 3.0.0-beta.4

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.
Files changed (96) hide show
  1. package/README.md +84 -24
  2. package/lib/module/TreeView.js +36 -31
  3. package/lib/module/TreeView.js.map +1 -1
  4. package/lib/module/components/CheckboxView.js +8 -4
  5. package/lib/module/components/CheckboxView.js.map +1 -1
  6. package/lib/module/components/CustomExpandCollapseIcon.js +2 -2
  7. package/lib/module/components/CustomExpandCollapseIcon.js.map +1 -1
  8. package/lib/module/components/DragOverlay.js +17 -5
  9. package/lib/module/components/DragOverlay.js.map +1 -1
  10. package/lib/module/components/DropIndicator.js +2 -2
  11. package/lib/module/components/DropIndicator.js.map +1 -1
  12. package/lib/module/components/NodeList.js +74 -57
  13. package/lib/module/components/NodeList.js.map +1 -1
  14. package/lib/module/constants/treeView.constants.js +3 -0
  15. package/lib/module/constants/treeView.constants.js.map +1 -1
  16. package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
  17. package/lib/module/helpers/moveTreeNode.helper.js +30 -0
  18. package/lib/module/helpers/moveTreeNode.helper.js.map +1 -1
  19. package/lib/module/helpers/selectAll.helper.js.map +1 -1
  20. package/lib/module/helpers/toggleCheckbox.helper.js +43 -60
  21. package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
  22. package/lib/module/hooks/useDragDrop.js +114 -19
  23. package/lib/module/hooks/useDragDrop.js.map +1 -1
  24. package/lib/module/{handlers/ScrollToNodeHandler.js → hooks/useScrollToNode.js} +27 -26
  25. package/lib/module/hooks/useScrollToNode.js.map +1 -0
  26. package/lib/module/index.js +1 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/jest.setup.js +14 -1
  29. package/lib/module/jest.setup.js.map +1 -1
  30. package/lib/module/store/treeView.store.js +3 -0
  31. package/lib/module/store/treeView.store.js.map +1 -1
  32. package/lib/module/utils/typedMemo.js +3 -3
  33. package/lib/module/utils/typedMemo.js.map +1 -1
  34. package/lib/module/utils/useDeepCompareEffect.js +5 -5
  35. package/lib/module/utils/useDeepCompareEffect.js.map +1 -1
  36. package/lib/typescript/src/TreeView.d.ts +3 -3
  37. package/lib/typescript/src/TreeView.d.ts.map +1 -1
  38. package/lib/typescript/src/components/CheckboxView.d.ts +1 -2
  39. package/lib/typescript/src/components/CheckboxView.d.ts.map +1 -1
  40. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts +1 -2
  41. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts.map +1 -1
  42. package/lib/typescript/src/components/DragOverlay.d.ts +1 -0
  43. package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -1
  44. package/lib/typescript/src/components/DropIndicator.d.ts +1 -2
  45. package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -1
  46. package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
  47. package/lib/typescript/src/constants/treeView.constants.d.ts +2 -0
  48. package/lib/typescript/src/constants/treeView.constants.d.ts.map +1 -1
  49. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts +2 -2
  50. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts.map +1 -1
  51. package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -1
  52. package/lib/typescript/src/helpers/selectAll.helper.d.ts +4 -4
  53. package/lib/typescript/src/helpers/selectAll.helper.d.ts.map +1 -1
  54. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +3 -0
  55. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks/useDragDrop.d.ts +18 -7
  57. package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -1
  58. package/lib/typescript/src/{handlers/ScrollToNodeHandler.d.ts → hooks/useScrollToNode.d.ts} +13 -15
  59. package/lib/typescript/src/hooks/useScrollToNode.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +4 -3
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/jest.setup.d.ts +1 -1
  63. package/lib/typescript/src/jest.setup.d.ts.map +1 -1
  64. package/lib/typescript/src/store/treeView.store.d.ts +2 -1
  65. package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
  66. package/lib/typescript/src/types/dragDrop.types.d.ts +10 -0
  67. package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
  68. package/lib/typescript/src/types/treeView.types.d.ts +69 -41
  69. package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/typedMemo.d.ts +1 -1
  71. package/lib/typescript/src/utils/typedMemo.d.ts.map +1 -1
  72. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts +2 -2
  73. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts.map +1 -1
  74. package/package.json +32 -15
  75. package/src/TreeView.tsx +57 -35
  76. package/src/components/CheckboxView.tsx +7 -4
  77. package/src/components/CustomExpandCollapseIcon.tsx +2 -2
  78. package/src/components/DragOverlay.tsx +19 -6
  79. package/src/components/DropIndicator.tsx +2 -2
  80. package/src/components/NodeList.tsx +83 -59
  81. package/src/constants/treeView.constants.ts +4 -1
  82. package/src/helpers/expandCollapse.helper.ts +5 -5
  83. package/src/helpers/moveTreeNode.helper.ts +33 -0
  84. package/src/helpers/selectAll.helper.ts +10 -10
  85. package/src/helpers/toggleCheckbox.helper.ts +56 -68
  86. package/src/hooks/useDragDrop.ts +152 -30
  87. package/src/{handlers/ScrollToNodeHandler.tsx → hooks/useScrollToNode.ts} +48 -45
  88. package/src/index.tsx +9 -0
  89. package/src/jest.setup.ts +14 -1
  90. package/src/store/treeView.store.ts +6 -1
  91. package/src/types/dragDrop.types.ts +12 -0
  92. package/src/types/treeView.types.ts +76 -43
  93. package/src/utils/typedMemo.ts +3 -3
  94. package/src/utils/useDeepCompareEffect.ts +13 -7
  95. package/lib/module/handlers/ScrollToNodeHandler.js.map +0 -1
  96. package/lib/typescript/src/handlers/ScrollToNodeHandler.d.ts.map +0 -1
@@ -1,5 +1,56 @@
1
+ import type { CheckboxValueType, TreeNode } from "../types/treeView.types";
1
2
  import { getTreeViewStore } from "../store/treeView.store";
2
3
 
4
+ /** Derive the tri-state checkbox value from checked and indeterminate booleans. */
5
+ export function getCheckboxValue(
6
+ isChecked: boolean,
7
+ isIndeterminate: boolean
8
+ ): CheckboxValueType {
9
+ if (isIndeterminate) return "indeterminate";
10
+ return isChecked;
11
+ }
12
+
13
+ /**
14
+ * Update a parent node's checked/indeterminate state based on its children.
15
+ * Mutates tempChecked and tempIndeterminate in-place.
16
+ */
17
+ function updateParentCheckState<ID>(
18
+ parentId: ID,
19
+ children: TreeNode<ID>[],
20
+ tempChecked: Set<ID>,
21
+ tempIndeterminate: Set<ID>,
22
+ ): void {
23
+ let allChecked = true;
24
+ let anyCheckedOrIndeterminate = false;
25
+
26
+ for (const child of children) {
27
+ const isChecked = tempChecked.has(child.id);
28
+ const isIndeterminate = tempIndeterminate.has(child.id);
29
+
30
+ if (isChecked) {
31
+ anyCheckedOrIndeterminate = true;
32
+ } else if (isIndeterminate) {
33
+ anyCheckedOrIndeterminate = true;
34
+ allChecked = false;
35
+ } else {
36
+ allChecked = false;
37
+ }
38
+
39
+ if (!allChecked && anyCheckedOrIndeterminate) break;
40
+ }
41
+
42
+ if (allChecked) {
43
+ tempChecked.add(parentId);
44
+ tempIndeterminate.delete(parentId);
45
+ } else if (anyCheckedOrIndeterminate) {
46
+ tempChecked.delete(parentId);
47
+ tempIndeterminate.add(parentId);
48
+ } else {
49
+ tempChecked.delete(parentId);
50
+ tempIndeterminate.delete(parentId);
51
+ }
52
+ }
53
+
3
54
  /**
4
55
  * Function to toggle checkbox state for a tree structure.
5
56
  * It sets the checked and indeterminate state for all affected nodes in the tree after an action to check/uncheck is made.
@@ -106,7 +157,7 @@ export function toggleCheckboxes<ID>(
106
157
  while (stack.length > 0) {
107
158
  const nodeId = stack.pop()!;
108
159
  const node = nodeMap.get(nodeId);
109
- if (!node) continue; // Node does not exist; skip
160
+ if (!node) continue;
110
161
 
111
162
  if (childrenChecked) {
112
163
  tempChecked.add(nodeId);
@@ -157,43 +208,8 @@ export function toggleCheckboxes<ID>(
157
208
  */
158
209
  function updateNodeState(nodeId: ID) {
159
210
  const node = nodeMap.get(nodeId);
160
- if (!node || !node.children || node.children.length === 0) {
161
- // Leaf nodes are already updated.
162
- return;
163
- }
164
-
165
- let allChildrenChecked = true;
166
- let anyChildCheckedOrIndeterminate = false;
167
-
168
- for (const child of node.children) {
169
- const isChecked = tempChecked.has(child.id);
170
- const isIndeterminate = tempIndeterminate.has(child.id);
171
-
172
- if (isChecked) {
173
- anyChildCheckedOrIndeterminate = true;
174
- } else if (isIndeterminate) {
175
- anyChildCheckedOrIndeterminate = true;
176
- allChildrenChecked = false;
177
- } else {
178
- allChildrenChecked = false;
179
- }
180
-
181
- // If both conditions are met, we can break early.
182
- if (!allChildrenChecked && anyChildCheckedOrIndeterminate) {
183
- break;
184
- }
185
- }
186
-
187
- if (allChildrenChecked) {
188
- tempChecked.add(nodeId);
189
- tempIndeterminate.delete(nodeId);
190
- } else if (anyChildCheckedOrIndeterminate) {
191
- tempChecked.delete(nodeId);
192
- tempIndeterminate.add(nodeId);
193
- } else {
194
- tempChecked.delete(nodeId);
195
- tempIndeterminate.delete(nodeId);
196
- }
211
+ if (!node?.children?.length) return;
212
+ updateParentCheckState(nodeId, node.children, tempChecked, tempIndeterminate);
197
213
  }
198
214
 
199
215
  // Update the state object with the new checked and indeterminate sets.
@@ -260,37 +276,9 @@ export function recalculateCheckedStates<ID>(storeId: string) {
260
276
  // Update each parent based on its children's current state
261
277
  for (const parentId of parentNodes) {
262
278
  const node = nodeMap.get(parentId);
279
+ /* istanbul ignore next -- parentNodes is built from nodeMap entries with children above; same nodeMap, same iteration — unreachable unless nodeMap mutates between loops */
263
280
  if (!node?.children?.length) continue;
264
-
265
- let allChecked = true;
266
- let anyCheckedOrIndeterminate = false;
267
-
268
- for (const child of node.children) {
269
- const isChecked = tempChecked.has(child.id);
270
- const isIndeterminate = tempIndeterminate.has(child.id);
271
-
272
- if (isChecked) {
273
- anyCheckedOrIndeterminate = true;
274
- } else if (isIndeterminate) {
275
- anyCheckedOrIndeterminate = true;
276
- allChecked = false;
277
- } else {
278
- allChecked = false;
279
- }
280
-
281
- if (!allChecked && anyCheckedOrIndeterminate) break;
282
- }
283
-
284
- if (allChecked) {
285
- tempChecked.add(parentId);
286
- tempIndeterminate.delete(parentId);
287
- } else if (anyCheckedOrIndeterminate) {
288
- tempChecked.delete(parentId);
289
- tempIndeterminate.add(parentId);
290
- } else {
291
- tempChecked.delete(parentId);
292
- tempIndeterminate.delete(parentId);
293
- }
281
+ updateParentCheckState(parentId, node.children, tempChecked, tempIndeterminate);
294
282
  }
295
283
 
296
284
  updateChecked(tempChecked);
@@ -1,4 +1,11 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import {
2
+ MutableRefObject,
3
+ RefObject,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState
8
+ } from "react";
2
9
  import {
3
10
  Animated,
4
11
  PanResponder,
@@ -7,27 +14,44 @@ import {
7
14
  import type { FlashList } from "@shopify/flash-list";
8
15
 
9
16
  import type { __FlattenedTreeNode__, TreeNode } from "../types/treeView.types";
10
- import type { DragEndEvent, DropTarget } from "../types/dragDrop.types";
17
+ import type { DragCancelEvent, DragEndEvent, DragStartEvent, DropTarget } from "../types/dragDrop.types";
11
18
  import { getTreeViewStore } from "../store/treeView.store";
12
- import { collapseNodes, expandNodes, handleToggleExpand, initializeNodeMaps, recalculateCheckedStates } from "../helpers";
19
+ import {
20
+ collapseNodes,
21
+ expandNodes,
22
+ handleToggleExpand,
23
+ initializeNodeMaps,
24
+ recalculateCheckedStates
25
+ } from "../helpers";
13
26
  import { moveTreeNode } from "../helpers/moveTreeNode.helper";
27
+ import { listHeaderFooterPadding } from "../constants/treeView.constants";
14
28
 
15
29
  interface UseDragDropParams<ID> {
16
30
  storeId: string;
17
31
  flattenedNodes: __FlattenedTreeNode__<ID>[];
18
- flashListRef: React.RefObject<FlashList<__FlattenedTreeNode__<ID>> | null>;
19
- containerRef: React.RefObject<{ measureInWindow: (cb: (x: number, y: number, w: number, h: number) => void) => void; } | null>;
32
+ flashListRef: RefObject<FlashList<__FlattenedTreeNode__<ID>> | null>;
33
+ containerRef: RefObject<{ measureInWindow: (cb: (x: number, y: number, w: number, h: number) => void) => void; } | null>;
20
34
  dragEnabled: boolean;
35
+ onDragStart?: (event: DragStartEvent<ID>) => void;
21
36
  onDragEnd?: (event: DragEndEvent<ID>) => void;
37
+ onDragCancel?: (event: DragCancelEvent<ID>) => void;
22
38
  longPressDuration: number;
23
39
  autoScrollThreshold: number;
24
40
  autoScrollSpeed: number;
25
- internalDataRef: React.MutableRefObject<TreeNode<ID>[] | null>;
26
- measuredItemHeightRef: React.MutableRefObject<number>;
41
+ internalDataRef: MutableRefObject<TreeNode<ID>[] | null>;
42
+ measuredItemHeightRef: MutableRefObject<number>;
27
43
  dragOverlayOffset: number;
28
44
  autoExpandDelay: number;
29
45
  /** Pixels per nesting level, used for magnetic overlay shift. */
30
46
  indentationMultiplier: number;
47
+ /** Callback to determine if a drop is allowed on a specific target. */
48
+ canDrop?: (draggedNode: TreeNode<ID>, targetNode: TreeNode<ID>, position: "above" | "below" | "inside") => boolean;
49
+ /** Maximum nesting depth allowed. */
50
+ maxDepth?: number;
51
+ /** Callback to determine if a node can accept children. */
52
+ canNodeHaveChildren?: (node: TreeNode<ID>) => boolean;
53
+ /** Callback to determine if a node can be dragged. */
54
+ canDrag?: (node: TreeNode<ID>) => boolean;
31
55
  }
32
56
 
33
57
  interface UseDragDropReturn<ID> {
@@ -46,8 +70,8 @@ interface UseDragDropReturn<ID> {
46
70
  ) => void;
47
71
  handleNodeTouchEnd: () => void;
48
72
  cancelLongPressTimer: () => void;
49
- scrollOffsetRef: React.MutableRefObject<number>;
50
- headerOffsetRef: React.MutableRefObject<number>;
73
+ scrollOffsetRef: MutableRefObject<number>;
74
+ headerOffsetRef: MutableRefObject<number>;
51
75
  }
52
76
 
53
77
  export function useDragDrop<ID>(
@@ -59,7 +83,9 @@ export function useDragDrop<ID>(
59
83
  flashListRef,
60
84
  containerRef,
61
85
  dragEnabled,
86
+ onDragStart,
62
87
  onDragEnd,
88
+ onDragCancel,
63
89
  longPressDuration,
64
90
  autoScrollThreshold,
65
91
  autoScrollSpeed,
@@ -68,6 +94,10 @@ export function useDragDrop<ID>(
68
94
  dragOverlayOffset,
69
95
  autoExpandDelay,
70
96
  indentationMultiplier,
97
+ canDrop: canDropCallback,
98
+ maxDepth,
99
+ canNodeHaveChildren,
100
+ canDrag,
71
101
  } = params;
72
102
 
73
103
  // --- Refs for mutable state (no stale closures in PanResponder) ---
@@ -109,13 +139,40 @@ export function useDragDrop<ID>(
109
139
  // Previous drop target for hysteresis (prevents flicker between "below N" / "above N+1")
110
140
  const prevDropTargetRef = useRef<{ targetIndex: number; position: "above" | "below" | "inside"; } | null>(null);
111
141
 
142
+ // Depth of the dragged subtree (computed once at drag start, used for maxDepth check)
143
+ const draggedSubtreeDepthRef = useRef(0);
144
+
112
145
  // Keep flattenedNodes ref current for PanResponder closures
113
146
  const flattenedNodesRef = useRef(flattenedNodes);
114
147
  flattenedNodesRef.current = flattenedNodes;
115
148
 
116
149
  // Keep callbacks current
150
+ const onDragStartRef = useRef(onDragStart);
151
+ onDragStartRef.current = onDragStart;
117
152
  const onDragEndRef = useRef(onDragEnd);
118
153
  onDragEndRef.current = onDragEnd;
154
+ const onDragCancelRef = useRef(onDragCancel);
155
+ onDragCancelRef.current = onDragCancel;
156
+ const canDropRef = useRef(canDropCallback);
157
+ canDropRef.current = canDropCallback;
158
+ const canNodeHaveChildrenRef = useRef(canNodeHaveChildren);
159
+ canNodeHaveChildrenRef.current = canNodeHaveChildren;
160
+ const canDragRef = useRef(canDrag);
161
+ canDragRef.current = canDrag;
162
+
163
+ // Keep config values current for PanResponder closures
164
+ const dragOverlayOffsetRef = useRef(dragOverlayOffset);
165
+ dragOverlayOffsetRef.current = dragOverlayOffset;
166
+ const autoScrollThresholdRef = useRef(autoScrollThreshold);
167
+ autoScrollThresholdRef.current = autoScrollThreshold;
168
+ const autoScrollSpeedParamRef = useRef(autoScrollSpeed);
169
+ autoScrollSpeedParamRef.current = autoScrollSpeed;
170
+ const autoExpandDelayRef = useRef(autoExpandDelay);
171
+ autoExpandDelayRef.current = autoExpandDelay;
172
+ const indentationMultiplierRef = useRef(indentationMultiplier);
173
+ indentationMultiplierRef.current = indentationMultiplier;
174
+ const maxDepthRef = useRef(maxDepth);
175
+ maxDepthRef.current = maxDepth;
119
176
 
120
177
  // --- React state (triggers re-renders only at drag start/end + indicator changes) ---
121
178
  const [isDragging, setIsDragging] = useState(false);
@@ -157,6 +214,22 @@ export function useDragDrop<ID>(
157
214
  [storeId]
158
215
  );
159
216
 
217
+ // --- Get the maximum depth of a subtree (0 for leaf nodes) ---
218
+ const getSubtreeDepth = useCallback(
219
+ (nodeId: ID): number => {
220
+ const store = getTreeViewStore<ID>(storeId);
221
+ const { nodeMap } = store.getState();
222
+ const node = nodeMap.get(nodeId);
223
+ if (!node?.children?.length) return 0;
224
+ let max = 0;
225
+ for (const child of node.children) {
226
+ max = Math.max(max, 1 + getSubtreeDepth(child.id));
227
+ }
228
+ return max;
229
+ },
230
+ [storeId]
231
+ );
232
+
160
233
  // --- Initiate drag ---
161
234
  const initiateDrag = useCallback(
162
235
  (nodeId: ID, pageY: number, locationY: number, nodeIndex: number) => {
@@ -187,12 +260,11 @@ export function useDragDrop<ID>(
187
260
  draggedNodeRef.current = node;
188
261
  draggedNodeIdRef.current = nodeId;
189
262
  draggedNodeIndexRef.current = nodeIndex;
263
+ draggedSubtreeDepthRef.current = getSubtreeDepth(nodeId);
190
264
 
191
- // Use measured item height if available, fall back to estimatedItemSize
265
+ // Use measured item height if available, fall back to default
192
266
  const measured = measuredItemHeightRef.current;
193
- const estimatedSize =
194
- (flashListRef.current as any)?.props?.estimatedItemSize ?? 36;
195
- itemHeightRef.current = measured > 0 ? measured : estimatedSize;
267
+ itemHeightRef.current = measured > 0 ? measured : 36;
196
268
 
197
269
  // Calculate headerOffset dynamically:
198
270
  // fingerLocalY = pageY - containerPageY
@@ -207,9 +279,8 @@ export function useDragDrop<ID>(
207
279
 
208
280
  // Delta-based auto-scroll: compute finger's position in the container
209
281
  // from the node's known index (avoids unreliable containerPageY).
210
- // The FlashList header (padding:5 → ~10px) + nodeIndex * itemHeight - scroll + locationY
211
282
  const iH = itemHeightRef.current;
212
- const listHeaderHeight = 10; // HeaderFooterView has padding: 5 → 10px total
283
+ const listHeaderHeight = listHeaderFooterPadding * 2;
213
284
  initialFingerPageYRef.current = pageY;
214
285
  initialFingerContainerYRef.current =
215
286
  listHeaderHeight + nodeIndex * iH - scrollOffsetRef.current + locationY;
@@ -223,7 +294,7 @@ export function useDragDrop<ID>(
223
294
  store.getState().updateInvalidDragTargetIds(descendants);
224
295
 
225
296
  // Set overlay initial position (with configurable offset)
226
- const overlayLocalY = fingerLocalY - locationY + dragOverlayOffset * itemHeightRef.current;
297
+ const overlayLocalY = fingerLocalY - locationY + dragOverlayOffsetRef.current * itemHeightRef.current;
227
298
  overlayY.setValue(overlayLocalY);
228
299
 
229
300
  // Reset magnetic overlay
@@ -238,6 +309,9 @@ export function useDragDrop<ID>(
238
309
  setEffectiveDropLevel(node.level ?? 0);
239
310
  setDropTarget(null);
240
311
 
312
+ // Notify consumer that drag has started
313
+ onDragStartRef.current?.({ draggedNodeId: nodeId });
314
+
241
315
  // Start auto-scroll loop
242
316
  startAutoScrollLoop();
243
317
  });
@@ -249,6 +323,7 @@ export function useDragDrop<ID>(
249
323
  containerRef,
250
324
  flashListRef,
251
325
  getDescendantIds,
326
+ getSubtreeDepth,
252
327
  overlayY,
253
328
  ]
254
329
  );
@@ -258,6 +333,12 @@ export function useDragDrop<ID>(
258
333
  (nodeId: ID, pageY: number, locationY: number, nodeIndex: number) => {
259
334
  if (!dragEnabled) return;
260
335
 
336
+ // Check if this node can be dragged
337
+ if (canDragRef.current) {
338
+ const node = flattenedNodesRef.current[nodeIndex];
339
+ if (node && !canDragRef.current(node)) return;
340
+ }
341
+
261
342
  // Cancel any existing timer
262
343
  cancelLongPressTimer();
263
344
 
@@ -281,7 +362,7 @@ export function useDragDrop<ID>(
281
362
  scrollOffsetRef.current + autoScrollSpeedRef.current
282
363
  );
283
364
  scrollOffsetRef.current = newOffset;
284
- (flashListRef.current as any)?.scrollToOffset?.({
365
+ flashListRef.current?.scrollToOffset?.({
285
366
  offset: newOffset,
286
367
  animated: false,
287
368
  });
@@ -302,8 +383,8 @@ export function useDragDrop<ID>(
302
383
 
303
384
  const updateAutoScroll = useCallback(
304
385
  (fingerInContainer: number) => {
305
- const threshold = autoScrollThreshold;
306
- const maxSpeed = 8 * autoScrollSpeed;
386
+ const threshold = autoScrollThresholdRef.current;
387
+ const maxSpeed = 8 * autoScrollSpeedParamRef.current;
307
388
  const containerH = containerHeightRef.current;
308
389
 
309
390
  if (fingerInContainer < threshold) {
@@ -319,7 +400,7 @@ export function useDragDrop<ID>(
319
400
  autoScrollSpeedRef.current = 0;
320
401
  }
321
402
  },
322
- [autoScrollThreshold, autoScrollSpeed]
403
+ []
323
404
  );
324
405
 
325
406
  // --- Cancel auto-expand timer ---
@@ -365,6 +446,26 @@ export function useDragDrop<ID>(
365
446
  position = "inside";
366
447
  }
367
448
 
449
+ // --- Determine if "inside" drop is allowed for this target ---
450
+ const canDropInsideTarget = (() => {
451
+ // canNodeHaveChildren: structural constraint
452
+ if (canNodeHaveChildrenRef.current && !canNodeHaveChildrenRef.current(targetNode)) {
453
+ return false;
454
+ }
455
+ // maxDepth: the dragged subtree at (targetLevel + 1) must not exceed maxDepth
456
+ if (maxDepthRef.current !== undefined) {
457
+ const targetLevel = targetNode.level ?? 0;
458
+ const deepest = targetLevel + 1 + draggedSubtreeDepthRef.current;
459
+ if (deepest > maxDepthRef.current) return false;
460
+ }
461
+ return true;
462
+ })();
463
+
464
+ // If "inside" is not allowed, convert to nearest zone
465
+ if (position === "inside" && !canDropInsideTarget) {
466
+ position = positionInItem < 0.5 ? "above" : "below";
467
+ }
468
+
368
469
  // --- Horizontal control at level cliffs ---
369
470
  // At the boundary between nodes at different depths, the user's
370
471
  // horizontal finger position decides the drop level:
@@ -398,9 +499,10 @@ export function useDragDrop<ID>(
398
499
 
399
500
  if (isCliff) {
400
501
  // Generous threshold: midpoint of the two levels + 2× indent buffer
502
+ const indent = indentationMultiplierRef.current;
401
503
  const threshold =
402
- ((currentLevel + shallowLevel) / 2) * indentationMultiplier
403
- + indentationMultiplier * 2;
504
+ ((currentLevel + shallowLevel) / 2) * indent
505
+ + indent * 2;
404
506
 
405
507
  if (fingerLocalX < threshold) {
406
508
  // User wants the shallow level
@@ -435,9 +537,10 @@ export function useDragDrop<ID>(
435
537
  const currentLevel = targetNode.level ?? 0;
436
538
  if (prevNode && prevLevel > currentLevel) {
437
539
  // Level cliff above - same generous threshold
540
+ const indent = indentationMultiplierRef.current;
438
541
  const threshold =
439
- ((prevLevel + currentLevel) / 2) * indentationMultiplier
440
- + indentationMultiplier * 2;
542
+ ((prevLevel + currentLevel) / 2) * indent
543
+ + indent * 2;
441
544
 
442
545
  if (fingerLocalX >= threshold) {
443
546
  clampedIndex = clampedIndex - 1;
@@ -450,7 +553,8 @@ export function useDragDrop<ID>(
450
553
  // --- Suppress "below" when it's redundant or confusing ---
451
554
  // After horizontal control, any remaining "below" that isn't at a
452
555
  // cliff is redundant with "above" on the next node → show "inside".
453
- if (position === "below") {
556
+ // Only convert to "inside" if inside drops are allowed for this target.
557
+ if (position === "below" && canDropInsideTarget) {
454
558
  const expandedSet = getTreeViewStore<ID>(storeId).getState().expanded;
455
559
 
456
560
  // (a) Expanded parent: "below" visually sits at the parent/child junction
@@ -500,9 +604,24 @@ export function useDragDrop<ID>(
500
604
  const store = getTreeViewStore<ID>(storeId);
501
605
  const { invalidDragTargetIds, draggedNodeId, expanded } =
502
606
  store.getState();
607
+
608
+ // maxDepth check for above/below (sibling) positions
609
+ let maxDepthValid = true;
610
+ if (maxDepthRef.current !== undefined && (position === "above" || position === "below")) {
611
+ const targetLevel = targetNode.level ?? 0;
612
+ const deepest = targetLevel + draggedSubtreeDepthRef.current;
613
+ if (deepest > maxDepthRef.current) maxDepthValid = false;
614
+ }
615
+
503
616
  const isValid =
504
617
  targetNode.id !== draggedNodeId &&
505
- !invalidDragTargetIds.has(targetNode.id);
618
+ !invalidDragTargetIds.has(targetNode.id) &&
619
+ maxDepthValid &&
620
+ (!canDropRef.current || canDropRef.current(
621
+ draggedNodeRef.current!,
622
+ targetNode,
623
+ position
624
+ ));
506
625
 
507
626
  // --- Auto-expand: if hovering "inside" a collapsed expandable node ---
508
627
  if (isValid && position === "inside" && targetNode.children?.length && !expanded.has(targetNode.id)) {
@@ -515,7 +634,7 @@ export function useDragDrop<ID>(
515
634
  // Expand the node and track it
516
635
  handleToggleExpand(storeId, targetNode.id);
517
636
  autoExpandedDuringDragRef.current.add(targetNode.id);
518
- }, autoExpandDelay);
637
+ }, autoExpandDelayRef.current);
519
638
  }
520
639
  } else {
521
640
  // Not hovering inside a collapsed expandable node - cancel timer
@@ -547,7 +666,7 @@ export function useDragDrop<ID>(
547
666
  // then spring to 0 for a smooth "magnetic snap" transition.
548
667
  if (prevLevel !== effectiveLevel) {
549
668
  overlayX.setValue(
550
- (prevLevel - effectiveLevel) * indentationMultiplier
669
+ (prevLevel - effectiveLevel) * indentationMultiplierRef.current
551
670
  );
552
671
  Animated.spring(overlayX, {
553
672
  toValue: 0,
@@ -599,7 +718,7 @@ export function useDragDrop<ID>(
599
718
  return newTarget;
600
719
  });
601
720
  },
602
- [storeId, autoExpandDelay, cancelAutoExpandTimer, indentationMultiplier, overlayX]
721
+ [storeId, cancelAutoExpandTimer, overlayX]
603
722
  );
604
723
 
605
724
  // --- Handle drag end ---
@@ -699,6 +818,9 @@ export function useDragDrop<ID>(
699
818
  viewPosition: 0.5,
700
819
  });
701
820
  }, 100);
821
+ } else if (droppedNodeId !== null) {
822
+ // Drag ended without a valid drop — notify consumer
823
+ onDragCancelRef.current?.({ draggedNodeId: droppedNodeId });
702
824
  }
703
825
 
704
826
  // Collapse auto-expanded nodes that aren't ancestors of the drop target
@@ -791,7 +913,7 @@ export function useDragDrop<ID>(
791
913
 
792
914
  // Update overlay position (with configurable offset)
793
915
  const overlayLocalY =
794
- fingerLocalY - grabOffsetYRef.current + dragOverlayOffset * itemHeightRef.current;
916
+ fingerLocalY - grabOffsetYRef.current + dragOverlayOffsetRef.current * itemHeightRef.current;
795
917
  overlayY.setValue(overlayLocalY);
796
918
 
797
919
  // Calculate drop target (horizontal position used at level cliffs)