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,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ComponentType } from "react";
|
|
2
2
|
import { Animated, StyleSheet, View } from "react-native";
|
|
3
3
|
|
|
4
4
|
import type {
|
|
@@ -10,8 +10,12 @@ import type {
|
|
|
10
10
|
import { CheckboxView } from "./CheckboxView";
|
|
11
11
|
import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
|
|
12
12
|
import { defaultIndentationMultiplier } from "../constants/treeView.constants";
|
|
13
|
+
import { getTreeViewStore } from "../store/treeView.store";
|
|
14
|
+
import { getCheckboxValue } from "../helpers";
|
|
15
|
+
import { typedMemo } from "../utils/typedMemo";
|
|
13
16
|
|
|
14
17
|
interface DragOverlayProps<ID> extends TreeItemCustomizations<ID> {
|
|
18
|
+
storeId: string;
|
|
15
19
|
overlayY: Animated.Value;
|
|
16
20
|
overlayX: Animated.Value;
|
|
17
21
|
node: __FlattenedTreeNode__<ID>;
|
|
@@ -21,18 +25,24 @@ interface DragOverlayProps<ID> extends TreeItemCustomizations<ID> {
|
|
|
21
25
|
|
|
22
26
|
function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
|
|
23
27
|
const {
|
|
28
|
+
storeId,
|
|
24
29
|
overlayY,
|
|
25
30
|
overlayX,
|
|
26
31
|
node,
|
|
27
32
|
level,
|
|
28
33
|
indentationMultiplier = defaultIndentationMultiplier,
|
|
29
|
-
CheckboxComponent = CheckboxView as
|
|
34
|
+
CheckboxComponent = CheckboxView as ComponentType<CheckBoxViewProps>,
|
|
30
35
|
ExpandCollapseIconComponent = CustomExpandCollapseIcon,
|
|
31
36
|
CustomNodeRowComponent,
|
|
32
37
|
checkBoxViewStyleProps,
|
|
33
38
|
dragDropCustomizations,
|
|
34
39
|
} = props;
|
|
35
40
|
|
|
41
|
+
// Read the actual checked state for the dragged node
|
|
42
|
+
const store = getTreeViewStore<ID>(storeId);
|
|
43
|
+
const { checked, indeterminate } = store.getState();
|
|
44
|
+
const checkedValue = getCheckboxValue(checked.has(node.id), indeterminate.has(node.id));
|
|
45
|
+
|
|
36
46
|
const overlayStyleProps = dragDropCustomizations?.dragOverlayStyleProps;
|
|
37
47
|
const CustomOverlay = dragDropCustomizations?.CustomDragOverlayComponent;
|
|
38
48
|
|
|
@@ -52,13 +62,16 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
|
|
|
52
62
|
{ transform: [{ translateX: overlayX }, { translateY: overlayY }] },
|
|
53
63
|
]}
|
|
54
64
|
>
|
|
65
|
+
{/* Render priority: CustomDragOverlayComponent > CustomNodeRowComponent > built-in.
|
|
66
|
+
The overlay is display-only (pointerEvents="none" on parent), so handlers are no-ops.
|
|
67
|
+
isExpanded is always false because useDragDrop collapses the node at drag start. */}
|
|
55
68
|
{CustomOverlay ? (
|
|
56
|
-
<CustomOverlay node={node} level={level} />
|
|
69
|
+
<CustomOverlay node={node} level={level} checkedValue={checkedValue} />
|
|
57
70
|
) : CustomNodeRowComponent ? (
|
|
58
71
|
<CustomNodeRowComponent
|
|
59
72
|
node={node}
|
|
60
73
|
level={level}
|
|
61
|
-
checkedValue={
|
|
74
|
+
checkedValue={checkedValue}
|
|
62
75
|
isExpanded={false}
|
|
63
76
|
onCheck={() => {}}
|
|
64
77
|
onExpand={() => {}}
|
|
@@ -73,7 +86,7 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
|
|
|
73
86
|
<CheckboxComponent
|
|
74
87
|
text={node.name}
|
|
75
88
|
onValueChange={() => {}}
|
|
76
|
-
value={
|
|
89
|
+
value={checkedValue}
|
|
77
90
|
{...checkBoxViewStyleProps}
|
|
78
91
|
/>
|
|
79
92
|
{node.children?.length ? (
|
|
@@ -87,7 +100,7 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
|
|
|
87
100
|
);
|
|
88
101
|
}
|
|
89
102
|
|
|
90
|
-
export const DragOverlay =
|
|
103
|
+
export const DragOverlay = typedMemo(_DragOverlay);
|
|
91
104
|
|
|
92
105
|
const styles = StyleSheet.create({
|
|
93
106
|
overlay: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo } from "react";
|
|
2
2
|
import { Animated, View, StyleSheet } from "react-native";
|
|
3
3
|
import type { DropPosition } from "../types/dragDrop.types";
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ interface DropIndicatorProps {
|
|
|
10
10
|
indentationMultiplier: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export const DropIndicator =
|
|
13
|
+
export const DropIndicator = memo(function DropIndicator(
|
|
14
14
|
props: DropIndicatorProps
|
|
15
15
|
) {
|
|
16
16
|
const {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
2
8
|
import {
|
|
3
9
|
View,
|
|
4
10
|
StyleSheet,
|
|
@@ -9,7 +15,6 @@ import {
|
|
|
9
15
|
import { FlashList } from "@shopify/flash-list";
|
|
10
16
|
|
|
11
17
|
import type {
|
|
12
|
-
CheckboxValueType,
|
|
13
18
|
__FlattenedTreeNode__,
|
|
14
19
|
DropIndicatorStyleProps,
|
|
15
20
|
NodeListProps,
|
|
@@ -21,6 +26,7 @@ import { useTreeViewStore } from "../store/treeView.store";
|
|
|
21
26
|
import {
|
|
22
27
|
getFilteredTreeData,
|
|
23
28
|
getFlattenedTreeData,
|
|
29
|
+
getCheckboxValue,
|
|
24
30
|
getInnerMostChildrenIdsInTree,
|
|
25
31
|
handleToggleExpand,
|
|
26
32
|
toggleCheckboxes
|
|
@@ -29,10 +35,13 @@ import { CheckboxView } from "./CheckboxView";
|
|
|
29
35
|
import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
|
|
30
36
|
import { DragOverlay } from "./DragOverlay";
|
|
31
37
|
import type { DropPosition } from "../types/dragDrop.types";
|
|
32
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
defaultIndentationMultiplier,
|
|
40
|
+
listHeaderFooterPadding
|
|
41
|
+
} from "../constants/treeView.constants";
|
|
33
42
|
import { useShallow } from "zustand/react/shallow";
|
|
34
43
|
import { typedMemo } from "../utils/typedMemo";
|
|
35
|
-
import {
|
|
44
|
+
import { useScrollToNode } from "../hooks/useScrollToNode";
|
|
36
45
|
import { useDragDrop } from "../hooks/useDragDrop";
|
|
37
46
|
|
|
38
47
|
const NodeList = typedMemo(_NodeList);
|
|
@@ -54,15 +63,29 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
54
63
|
ExpandCollapseTouchableComponent,
|
|
55
64
|
CustomNodeRowComponent,
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
dragAndDrop,
|
|
67
|
+
} = props;
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
enabled: _dragEnabled,
|
|
71
|
+
onDragStart,
|
|
58
72
|
onDragEnd,
|
|
73
|
+
onDragCancel,
|
|
59
74
|
longPressDuration = 400,
|
|
60
75
|
autoScrollThreshold = 60,
|
|
61
76
|
autoScrollSpeed = 1.0,
|
|
62
77
|
dragOverlayOffset = -4,
|
|
63
78
|
autoExpandDelay = 800,
|
|
64
|
-
dragDropCustomizations,
|
|
65
|
-
|
|
79
|
+
customizations: dragDropCustomizations,
|
|
80
|
+
canDrop: canDropCallback,
|
|
81
|
+
maxDepth,
|
|
82
|
+
canNodeHaveChildren,
|
|
83
|
+
canDrag,
|
|
84
|
+
} = dragAndDrop ?? {};
|
|
85
|
+
|
|
86
|
+
// When the dragAndDrop prop is provided, drag is enabled by default.
|
|
87
|
+
// Users can still toggle it off with enabled: false at runtime.
|
|
88
|
+
const dragEnabled = dragAndDrop ? (_dragEnabled ?? true) : false;
|
|
66
89
|
|
|
67
90
|
const {
|
|
68
91
|
expanded,
|
|
@@ -80,34 +103,43 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
80
103
|
})
|
|
81
104
|
));
|
|
82
105
|
|
|
83
|
-
const flashListRef =
|
|
84
|
-
const containerRef =
|
|
85
|
-
const internalDataRef =
|
|
86
|
-
const measuredItemHeightRef =
|
|
106
|
+
const flashListRef = useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
|
|
107
|
+
const containerRef = useRef<View>(null);
|
|
108
|
+
const internalDataRef = useRef<TreeNode<ID>[] | null>(null);
|
|
109
|
+
const measuredItemHeightRef = useRef(0);
|
|
87
110
|
|
|
88
|
-
const handleItemLayout =
|
|
111
|
+
const handleItemLayout = useCallback((height: number) => {
|
|
89
112
|
if (measuredItemHeightRef.current === 0 && height > 0) {
|
|
90
113
|
measuredItemHeightRef.current = height;
|
|
91
114
|
}
|
|
92
115
|
}, []);
|
|
93
116
|
|
|
94
|
-
const [initialScrollIndex, setInitialScrollIndex] =
|
|
117
|
+
const [initialScrollIndex, setInitialScrollIndex] = useState<number>(-1);
|
|
95
118
|
|
|
96
119
|
// First we filter the tree as per the search term and keys
|
|
97
|
-
const filteredTree =
|
|
120
|
+
const filteredTree = useMemo(() => getFilteredTreeData<ID>(
|
|
98
121
|
initialTreeViewData,
|
|
99
122
|
searchText.trim().toLowerCase(),
|
|
100
123
|
searchKeys
|
|
101
124
|
), [initialTreeViewData, searchText, searchKeys]);
|
|
102
125
|
|
|
103
126
|
// Then we flatten the tree to make it "render-compatible" in a "flat" list
|
|
104
|
-
const flattenedFilteredNodes =
|
|
127
|
+
const flattenedFilteredNodes = useMemo(() => getFlattenedTreeData<ID>(
|
|
105
128
|
filteredTree,
|
|
106
129
|
expanded,
|
|
107
130
|
), [filteredTree, expanded]);
|
|
108
131
|
|
|
132
|
+
useScrollToNode<ID>({
|
|
133
|
+
storeId,
|
|
134
|
+
scrollToNodeHandlerRef,
|
|
135
|
+
flashListRef,
|
|
136
|
+
flattenedFilteredNodes,
|
|
137
|
+
setInitialScrollIndex,
|
|
138
|
+
initialScrollNodeID,
|
|
139
|
+
});
|
|
140
|
+
|
|
109
141
|
// And update the innermost children id -> required to un/select filtered tree
|
|
110
|
-
|
|
142
|
+
useEffect(() => {
|
|
111
143
|
const updatedInnerMostChildrenIds = getInnerMostChildrenIdsInTree<ID>(
|
|
112
144
|
filteredTree
|
|
113
145
|
);
|
|
@@ -133,8 +165,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
133
165
|
flattenedNodes: flattenedFilteredNodes,
|
|
134
166
|
flashListRef,
|
|
135
167
|
containerRef,
|
|
136
|
-
dragEnabled
|
|
168
|
+
dragEnabled,
|
|
169
|
+
onDragStart,
|
|
137
170
|
onDragEnd,
|
|
171
|
+
onDragCancel,
|
|
138
172
|
longPressDuration,
|
|
139
173
|
autoScrollThreshold,
|
|
140
174
|
autoScrollSpeed,
|
|
@@ -143,10 +177,14 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
143
177
|
dragOverlayOffset,
|
|
144
178
|
autoExpandDelay,
|
|
145
179
|
indentationMultiplier: effectiveIndentationMultiplier,
|
|
180
|
+
canDrop: canDropCallback,
|
|
181
|
+
maxDepth,
|
|
182
|
+
canNodeHaveChildren,
|
|
183
|
+
canDrag,
|
|
146
184
|
});
|
|
147
185
|
|
|
148
186
|
// Combined onScroll handler
|
|
149
|
-
const handleScroll =
|
|
187
|
+
const handleScroll = useCallback((
|
|
150
188
|
event: NativeSyntheticEvent<NativeScrollEvent>
|
|
151
189
|
) => {
|
|
152
190
|
scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
|
@@ -156,7 +194,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
156
194
|
treeFlashListProps?.onScroll?.(event as any);
|
|
157
195
|
}, [scrollOffsetRef, cancelLongPressTimer, treeFlashListProps]);
|
|
158
196
|
|
|
159
|
-
const nodeRenderer =
|
|
197
|
+
const nodeRenderer = useCallback((
|
|
160
198
|
{ item, index }: { item: __FlattenedTreeNode__<ID>; index: number; }
|
|
161
199
|
) => {
|
|
162
200
|
return (
|
|
@@ -223,14 +261,6 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
223
261
|
|
|
224
262
|
return (
|
|
225
263
|
<>
|
|
226
|
-
<ScrollToNodeHandler
|
|
227
|
-
ref={scrollToNodeHandlerRef}
|
|
228
|
-
storeId={storeId}
|
|
229
|
-
flashListRef={flashListRef}
|
|
230
|
-
flattenedFilteredNodes={flattenedFilteredNodes}
|
|
231
|
-
setInitialScrollIndex={setInitialScrollIndex}
|
|
232
|
-
initialScrollNodeID={initialScrollNodeID} />
|
|
233
|
-
|
|
234
264
|
{dragEnabled ? (
|
|
235
265
|
<View
|
|
236
266
|
ref={containerRef}
|
|
@@ -240,6 +270,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
240
270
|
{flashListElement}
|
|
241
271
|
{isDragging && draggedNode && (
|
|
242
272
|
<DragOverlay<ID>
|
|
273
|
+
storeId={storeId}
|
|
243
274
|
overlayY={overlayY}
|
|
244
275
|
overlayX={overlayX}
|
|
245
276
|
node={draggedNode}
|
|
@@ -262,22 +293,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
262
293
|
|
|
263
294
|
function HeaderFooterView() {
|
|
264
295
|
return (
|
|
265
|
-
<View style={
|
|
296
|
+
<View style={{ padding: listHeaderFooterPadding }} />
|
|
266
297
|
);
|
|
267
298
|
}
|
|
268
299
|
|
|
269
|
-
function getValue(
|
|
270
|
-
isChecked: boolean,
|
|
271
|
-
isIndeterminate: boolean
|
|
272
|
-
): CheckboxValueType {
|
|
273
|
-
if (isIndeterminate) {
|
|
274
|
-
return "indeterminate";
|
|
275
|
-
} else if (isChecked) {
|
|
276
|
-
return true;
|
|
277
|
-
} else {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
300
|
|
|
282
301
|
const Node = typedMemo(_Node);
|
|
283
302
|
function _Node<ID>(props: NodeProps<ID>) {
|
|
@@ -315,7 +334,7 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
315
334
|
} = useTreeViewStore<ID>(storeId)(useShallow(
|
|
316
335
|
state => ({
|
|
317
336
|
isExpanded: state.expanded.has(node.id),
|
|
318
|
-
value:
|
|
337
|
+
value: getCheckboxValue(
|
|
319
338
|
state.checked.has(node.id),
|
|
320
339
|
state.indeterminate.has(node.id)
|
|
321
340
|
),
|
|
@@ -332,45 +351,51 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
332
351
|
// The flag is set during render (synchronous) and cleared on the next touch start.
|
|
333
352
|
// It is also cleared via effect when dragging ends, to prevent stale `true`
|
|
334
353
|
// values surviving FlashList recycling (where refs persist across items).
|
|
335
|
-
const wasDraggedRef =
|
|
354
|
+
const wasDraggedRef = useRef(false);
|
|
336
355
|
if (isDraggingGlobal && isBeingDragged) {
|
|
337
356
|
wasDraggedRef.current = true;
|
|
338
357
|
}
|
|
339
358
|
|
|
340
|
-
|
|
359
|
+
useEffect(() => {
|
|
341
360
|
if (!isDraggingGlobal) {
|
|
342
361
|
wasDraggedRef.current = false;
|
|
343
362
|
}
|
|
344
363
|
}, [isDraggingGlobal]);
|
|
345
364
|
|
|
346
|
-
const _onToggleExpand =
|
|
365
|
+
const _onToggleExpand = useCallback(() => {
|
|
347
366
|
if (wasDraggedRef.current) return;
|
|
348
367
|
handleToggleExpand(storeId, node.id);
|
|
349
368
|
}, [storeId, node.id]);
|
|
350
369
|
|
|
351
|
-
const _onCheck =
|
|
370
|
+
const _onCheck = useCallback(() => {
|
|
352
371
|
if (wasDraggedRef.current) return;
|
|
353
372
|
toggleCheckboxes(storeId, [node.id]);
|
|
354
373
|
}, [storeId, node.id]);
|
|
355
374
|
|
|
356
|
-
const handleTouchStart =
|
|
375
|
+
const handleTouchStart = useCallback((e: any) => {
|
|
357
376
|
wasDraggedRef.current = false;
|
|
358
377
|
if (!onNodeTouchStart) return;
|
|
359
378
|
const { pageY, locationY } = e.nativeEvent;
|
|
360
379
|
onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
|
|
361
380
|
}, [node.id, nodeIndex, onNodeTouchStart]);
|
|
362
381
|
|
|
363
|
-
const handleTouchEnd =
|
|
382
|
+
const handleTouchEnd = useCallback(() => {
|
|
364
383
|
onNodeTouchEnd?.();
|
|
365
384
|
}, [onNodeTouchEnd]);
|
|
366
385
|
|
|
367
|
-
// Determine opacity for drag state
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
386
|
+
// Determine opacity for drag state (separate values for dragged node vs invalid targets).
|
|
387
|
+
// When CustomNodeRowComponent is used, hand off all visual control
|
|
388
|
+
// (including drag opacity) to the custom component — it receives
|
|
389
|
+
// isDraggedNode / isInvalidDropTarget / isDragging props.
|
|
390
|
+
const draggedOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
|
|
391
|
+
const invalidOpacity = dragDropCustomizations?.invalidTargetOpacity ?? 0.3;
|
|
392
|
+
const nodeOpacity = CustomNodeRowComponent
|
|
393
|
+
? 1.0
|
|
394
|
+
: isDraggingGlobal
|
|
395
|
+
? (isBeingDragged ? draggedOpacity : isDragInvalid ? invalidOpacity : 1.0)
|
|
396
|
+
: 1.0;
|
|
397
|
+
|
|
398
|
+
const handleLayout = useCallback((e: any) => {
|
|
374
399
|
onItemLayout?.(e.nativeEvent.layout.height);
|
|
375
400
|
}, [onItemLayout]);
|
|
376
401
|
|
|
@@ -424,7 +449,6 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
424
449
|
else {
|
|
425
450
|
return (
|
|
426
451
|
<View
|
|
427
|
-
{...touchHandlers}
|
|
428
452
|
onLayout={onItemLayout ? handleLayout : undefined}
|
|
429
453
|
style={[
|
|
430
454
|
{ opacity: nodeOpacity },
|
|
@@ -439,9 +463,12 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
439
463
|
isExpanded={isExpanded}
|
|
440
464
|
onCheck={_onCheck}
|
|
441
465
|
onExpand={_onToggleExpand}
|
|
442
|
-
|
|
466
|
+
isInvalidDropTarget={isDragInvalid}
|
|
467
|
+
isDropTarget={isDropTarget}
|
|
468
|
+
dropPosition={nodeDropPosition ?? undefined}
|
|
443
469
|
isDragging={isDraggingGlobal}
|
|
444
470
|
isDraggedNode={isBeingDragged}
|
|
471
|
+
dragHandleProps={touchHandlers}
|
|
445
472
|
/>
|
|
446
473
|
</View>
|
|
447
474
|
);
|
|
@@ -511,9 +538,6 @@ function NodeDropIndicator({ position, level, indentationMultiplier, styleProps
|
|
|
511
538
|
}
|
|
512
539
|
|
|
513
540
|
const styles = StyleSheet.create({
|
|
514
|
-
defaultHeaderFooter: {
|
|
515
|
-
padding: 5
|
|
516
|
-
},
|
|
517
541
|
nodeExpandableArrowTouchable: {
|
|
518
542
|
flex: 1
|
|
519
543
|
},
|
|
@@ -49,8 +49,8 @@ export function handleToggleExpand<ID>(storeId: string, id: ID) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Expand all nodes in the tree.
|
|
51
51
|
*/
|
|
52
|
-
export function expandAll(storeId: string) {
|
|
53
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
52
|
+
export function expandAll<ID>(storeId: string) {
|
|
53
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
54
54
|
const { nodeMap, updateExpanded } = treeViewStore.getState();
|
|
55
55
|
// Create a new Set containing the IDs of all nodes
|
|
56
56
|
const newExpanded = new Set(nodeMap.keys());
|
|
@@ -60,11 +60,11 @@ export function expandAll(storeId: string) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Collapse all nodes in the tree.
|
|
62
62
|
*/
|
|
63
|
-
export function collapseAll(storeId: string) {
|
|
64
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
63
|
+
export function collapseAll<ID>(storeId: string) {
|
|
64
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
65
65
|
const { updateExpanded } = treeViewStore.getState();
|
|
66
66
|
// Clear the expanded state
|
|
67
|
-
updateExpanded(new Set
|
|
67
|
+
updateExpanded(new Set());
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -18,6 +18,9 @@ export function moveTreeNode<ID>(
|
|
|
18
18
|
): TreeNode<ID>[] {
|
|
19
19
|
if (draggedNodeId === targetNodeId) return data;
|
|
20
20
|
|
|
21
|
+
// Prevent moving a node into its own descendant (would create a cycle)
|
|
22
|
+
if (isDescendant(data, draggedNodeId, targetNodeId)) return data;
|
|
23
|
+
|
|
21
24
|
// Step 1: Deep clone the tree
|
|
22
25
|
const cloned = deepCloneTree(data);
|
|
23
26
|
|
|
@@ -32,6 +35,36 @@ export function moveTreeNode<ID>(
|
|
|
32
35
|
return cloned;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Check if `candidateDescendantId` is a descendant of `ancestorId` in the tree.
|
|
40
|
+
*/
|
|
41
|
+
function isDescendant<ID>(
|
|
42
|
+
nodes: TreeNode<ID>[],
|
|
43
|
+
ancestorId: ID,
|
|
44
|
+
candidateDescendantId: ID,
|
|
45
|
+
): boolean {
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
if (node.id === ancestorId) {
|
|
48
|
+
// Found the ancestor — search its subtree for the candidate
|
|
49
|
+
return containsNode(node.children ?? [], candidateDescendantId);
|
|
50
|
+
}
|
|
51
|
+
if (node.children && isDescendant(node.children, ancestorId, candidateDescendantId)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Check if a node with the given ID exists anywhere in the subtree. */
|
|
59
|
+
function containsNode<ID>(nodes: TreeNode<ID>[], nodeId: ID): boolean {
|
|
60
|
+
for (const node of nodes) {
|
|
61
|
+
if (node.id === nodeId) return true;
|
|
62
|
+
if (node.children && containsNode(node.children, nodeId)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Deep clone a tree structure so mutations don't affect the original. */
|
|
35
68
|
function deepCloneTree<ID>(nodes: TreeNode<ID>[]): TreeNode<ID>[] {
|
|
36
69
|
return nodes.map(node => ({
|
|
37
70
|
...node,
|
|
@@ -7,13 +7,13 @@ import { toggleCheckboxes } from "./toggleCheckbox.helper";
|
|
|
7
7
|
*
|
|
8
8
|
* If there is no search text, then it selects all nodes; otherwise, it selects all visible nodes.
|
|
9
9
|
*/
|
|
10
|
-
export function selectAllFiltered(storeId: string) {
|
|
11
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
10
|
+
export function selectAllFiltered<ID>(storeId: string) {
|
|
11
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
12
12
|
const { searchText, innerMostChildrenIds } = treeViewStore.getState();
|
|
13
13
|
|
|
14
14
|
// If there's no search text, select all nodes
|
|
15
15
|
if (!searchText) {
|
|
16
|
-
selectAll(storeId);
|
|
16
|
+
selectAll<ID>(storeId);
|
|
17
17
|
} else {
|
|
18
18
|
// If there's search text, only select the visible nodes
|
|
19
19
|
toggleCheckboxes(storeId, innerMostChildrenIds, true);
|
|
@@ -25,13 +25,13 @@ export function selectAllFiltered(storeId: string) {
|
|
|
25
25
|
*
|
|
26
26
|
* If there is no search text, then it unselects all nodes; otherwise, it unselects all visible nodes.
|
|
27
27
|
*/
|
|
28
|
-
export function unselectAllFiltered(storeId: string) {
|
|
29
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
28
|
+
export function unselectAllFiltered<ID>(storeId: string) {
|
|
29
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
30
30
|
const { searchText, innerMostChildrenIds } = treeViewStore.getState();
|
|
31
31
|
|
|
32
32
|
// If there's no search text, unselect all nodes
|
|
33
33
|
if (!searchText) {
|
|
34
|
-
unselectAll(storeId);
|
|
34
|
+
unselectAll<ID>(storeId);
|
|
35
35
|
} else {
|
|
36
36
|
// If there's search text, only unselect the visible nodes
|
|
37
37
|
toggleCheckboxes(storeId, innerMostChildrenIds, false);
|
|
@@ -43,8 +43,8 @@ export function unselectAllFiltered(storeId: string) {
|
|
|
43
43
|
*
|
|
44
44
|
* This function selects all nodes by adding all node ids to the checked set and clearing the indeterminate set.
|
|
45
45
|
*/
|
|
46
|
-
export function selectAll(storeId: string) {
|
|
47
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
46
|
+
export function selectAll<ID>(storeId: string) {
|
|
47
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
48
48
|
const {
|
|
49
49
|
nodeMap,
|
|
50
50
|
updateChecked,
|
|
@@ -64,8 +64,8 @@ export function selectAll(storeId: string) {
|
|
|
64
64
|
*
|
|
65
65
|
* This function unselects all nodes by clearing both the checked and indeterminate sets.
|
|
66
66
|
*/
|
|
67
|
-
export function unselectAll(storeId: string) {
|
|
68
|
-
const treeViewStore = getTreeViewStore(storeId);
|
|
67
|
+
export function unselectAll<ID>(storeId: string) {
|
|
68
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
69
69
|
const { updateChecked, updateIndeterminate } = treeViewStore.getState();
|
|
70
70
|
// Update the state to mark all nodes as unchecked
|
|
71
71
|
|