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.
- package/README.md +84 -24
- package/lib/module/TreeView.js +36 -31
- package/lib/module/TreeView.js.map +1 -1
- package/lib/module/components/CheckboxView.js +8 -4
- package/lib/module/components/CheckboxView.js.map +1 -1
- package/lib/module/components/CustomExpandCollapseIcon.js +2 -2
- package/lib/module/components/CustomExpandCollapseIcon.js.map +1 -1
- package/lib/module/components/DragOverlay.js +17 -5
- package/lib/module/components/DragOverlay.js.map +1 -1
- package/lib/module/components/DropIndicator.js +2 -2
- package/lib/module/components/DropIndicator.js.map +1 -1
- package/lib/module/components/NodeList.js +74 -57
- package/lib/module/components/NodeList.js.map +1 -1
- package/lib/module/constants/treeView.constants.js +3 -0
- package/lib/module/constants/treeView.constants.js.map +1 -1
- package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
- package/lib/module/helpers/moveTreeNode.helper.js +30 -0
- package/lib/module/helpers/moveTreeNode.helper.js.map +1 -1
- package/lib/module/helpers/selectAll.helper.js.map +1 -1
- package/lib/module/helpers/toggleCheckbox.helper.js +43 -60
- package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
- package/lib/module/hooks/useDragDrop.js +114 -19
- package/lib/module/hooks/useDragDrop.js.map +1 -1
- package/lib/module/{handlers/ScrollToNodeHandler.js → hooks/useScrollToNode.js} +27 -26
- package/lib/module/hooks/useScrollToNode.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest.setup.js +14 -1
- package/lib/module/jest.setup.js.map +1 -1
- package/lib/module/store/treeView.store.js +3 -0
- package/lib/module/store/treeView.store.js.map +1 -1
- package/lib/module/utils/typedMemo.js +3 -3
- package/lib/module/utils/typedMemo.js.map +1 -1
- package/lib/module/utils/useDeepCompareEffect.js +5 -5
- package/lib/module/utils/useDeepCompareEffect.js.map +1 -1
- package/lib/typescript/src/TreeView.d.ts +3 -3
- package/lib/typescript/src/TreeView.d.ts.map +1 -1
- package/lib/typescript/src/components/CheckboxView.d.ts +1 -2
- package/lib/typescript/src/components/CheckboxView.d.ts.map +1 -1
- package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts +1 -2
- package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts.map +1 -1
- package/lib/typescript/src/components/DragOverlay.d.ts +1 -0
- package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -1
- package/lib/typescript/src/components/DropIndicator.d.ts +1 -2
- package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -1
- package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
- package/lib/typescript/src/constants/treeView.constants.d.ts +2 -0
- package/lib/typescript/src/constants/treeView.constants.d.ts.map +1 -1
- package/lib/typescript/src/helpers/expandCollapse.helper.d.ts +2 -2
- package/lib/typescript/src/helpers/expandCollapse.helper.d.ts.map +1 -1
- package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -1
- package/lib/typescript/src/helpers/selectAll.helper.d.ts +4 -4
- package/lib/typescript/src/helpers/selectAll.helper.d.ts.map +1 -1
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +3 -0
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useDragDrop.d.ts +18 -7
- package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -1
- package/lib/typescript/src/{handlers/ScrollToNodeHandler.d.ts → hooks/useScrollToNode.d.ts} +13 -15
- package/lib/typescript/src/hooks/useScrollToNode.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/jest.setup.d.ts +1 -1
- package/lib/typescript/src/jest.setup.d.ts.map +1 -1
- package/lib/typescript/src/store/treeView.store.d.ts +2 -1
- package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
- package/lib/typescript/src/types/dragDrop.types.d.ts +10 -0
- package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
- package/lib/typescript/src/types/treeView.types.d.ts +69 -41
- package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
- package/lib/typescript/src/utils/typedMemo.d.ts +1 -1
- package/lib/typescript/src/utils/typedMemo.d.ts.map +1 -1
- package/lib/typescript/src/utils/useDeepCompareEffect.d.ts +2 -2
- package/lib/typescript/src/utils/useDeepCompareEffect.d.ts.map +1 -1
- package/package.json +32 -15
- package/src/TreeView.tsx +57 -35
- package/src/components/CheckboxView.tsx +7 -4
- package/src/components/CustomExpandCollapseIcon.tsx +2 -2
- package/src/components/DragOverlay.tsx +19 -6
- package/src/components/DropIndicator.tsx +2 -2
- package/src/components/NodeList.tsx +83 -59
- package/src/constants/treeView.constants.ts +4 -1
- package/src/helpers/expandCollapse.helper.ts +5 -5
- package/src/helpers/moveTreeNode.helper.ts +33 -0
- package/src/helpers/selectAll.helper.ts +10 -10
- package/src/helpers/toggleCheckbox.helper.ts +56 -68
- package/src/hooks/useDragDrop.ts +152 -30
- package/src/{handlers/ScrollToNodeHandler.tsx → hooks/useScrollToNode.ts} +48 -45
- package/src/index.tsx +9 -0
- package/src/jest.setup.ts +14 -1
- package/src/store/treeView.store.ts +6 -1
- package/src/types/dragDrop.types.ts +12 -0
- package/src/types/treeView.types.ts +76 -43
- package/src/utils/typedMemo.ts +3 -3
- package/src/utils/useDeepCompareEffect.ts +13 -7
- package/lib/module/handlers/ScrollToNodeHandler.js.map +0 -1
- 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;
|
|
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
|
|
161
|
-
|
|
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);
|
package/src/hooks/useDragDrop.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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:
|
|
19
|
-
containerRef:
|
|
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:
|
|
26
|
-
measuredItemHeightRef:
|
|
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:
|
|
50
|
-
headerOffsetRef:
|
|
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
|
|
265
|
+
// Use measured item height if available, fall back to default
|
|
192
266
|
const measured = measuredItemHeightRef.current;
|
|
193
|
-
|
|
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 =
|
|
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 +
|
|
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
|
-
|
|
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 =
|
|
306
|
-
const maxSpeed = 8 *
|
|
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
|
-
[
|
|
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) *
|
|
403
|
-
+
|
|
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) *
|
|
440
|
-
+
|
|
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
|
-
|
|
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
|
-
},
|
|
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) *
|
|
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,
|
|
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 +
|
|
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)
|