react-native-tree-multi-select 3.0.0-beta.2 → 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 +85 -25
- 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 +80 -56
- 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 +44 -61
- package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
- package/lib/module/hooks/useDragDrop.js +141 -34
- 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 +19 -0
- package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
- package/lib/typescript/src/types/treeView.types.d.ts +149 -35
- 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 +90 -58
- 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 +57 -69
- package/src/hooks/useDragDrop.ts +182 -46
- 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 +21 -0
- package/src/types/treeView.types.ts +157 -41
- 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
package/src/TreeView.tsx
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
startTransition,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useRef,
|
|
9
|
+
type ForwardedRef,
|
|
10
|
+
} from "react";
|
|
2
11
|
import type {
|
|
3
12
|
TreeNode,
|
|
4
13
|
TreeViewProps,
|
|
@@ -15,9 +24,11 @@ import {
|
|
|
15
24
|
collapseAll,
|
|
16
25
|
toggleCheckboxes,
|
|
17
26
|
expandNodes,
|
|
18
|
-
collapseNodes
|
|
27
|
+
collapseNodes,
|
|
28
|
+
recalculateCheckedStates,
|
|
29
|
+
moveTreeNode,
|
|
19
30
|
} from "./helpers";
|
|
20
|
-
import { getTreeViewStore, useTreeViewStore } from "./store/treeView.store";
|
|
31
|
+
import { deleteTreeViewStore, getTreeViewStore, useTreeViewStore } from "./store/treeView.store";
|
|
21
32
|
import usePreviousState from "./utils/usePreviousState";
|
|
22
33
|
import { useShallow } from "zustand/react/shallow";
|
|
23
34
|
import useDeepCompareEffect from "./utils/useDeepCompareEffect";
|
|
@@ -25,13 +36,13 @@ import { typedMemo } from "./utils/typedMemo";
|
|
|
25
36
|
import type {
|
|
26
37
|
ScrollToNodeHandlerRef,
|
|
27
38
|
ScrollToNodeParams
|
|
28
|
-
} from "./
|
|
29
|
-
import type { DragEndEvent } from "./types/dragDrop.types";
|
|
39
|
+
} from "./hooks/useScrollToNode";
|
|
40
|
+
import type { DragEndEvent, DropPosition } from "./types/dragDrop.types";
|
|
30
41
|
import { fastIsEqual } from "fast-is-equal";
|
|
31
42
|
|
|
32
43
|
function _innerTreeView<ID>(
|
|
33
44
|
props: TreeViewProps<ID>,
|
|
34
|
-
ref:
|
|
45
|
+
ref: ForwardedRef<TreeViewRef<ID>>
|
|
35
46
|
) {
|
|
36
47
|
const {
|
|
37
48
|
data,
|
|
@@ -57,16 +68,11 @@ function _innerTreeView<ID>(
|
|
|
57
68
|
|
|
58
69
|
CustomNodeRowComponent,
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
onDragEnd,
|
|
62
|
-
longPressDuration,
|
|
63
|
-
autoScrollThreshold,
|
|
64
|
-
autoScrollSpeed,
|
|
65
|
-
dragOverlayOffset,
|
|
66
|
-
autoExpandDelay,
|
|
67
|
-
dragDropCustomizations,
|
|
71
|
+
dragAndDrop,
|
|
68
72
|
} = props;
|
|
69
73
|
|
|
74
|
+
const onDragEnd = dragAndDrop?.onDragEnd;
|
|
75
|
+
|
|
70
76
|
const storeId = useId();
|
|
71
77
|
|
|
72
78
|
const {
|
|
@@ -109,7 +115,7 @@ function _innerTreeView<ID>(
|
|
|
109
115
|
})
|
|
110
116
|
));
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
useImperativeHandle(ref, () => ({
|
|
113
119
|
selectAll: () => selectAll(storeId),
|
|
114
120
|
unselectAll: () => unselectAll(storeId),
|
|
115
121
|
|
|
@@ -129,15 +135,17 @@ function _innerTreeView<ID>(
|
|
|
129
135
|
|
|
130
136
|
scrollToNodeID,
|
|
131
137
|
|
|
132
|
-
getChildToParentMap
|
|
138
|
+
getChildToParentMap,
|
|
139
|
+
|
|
140
|
+
moveNode,
|
|
133
141
|
}));
|
|
134
142
|
|
|
135
|
-
const scrollToNodeHandlerRef =
|
|
143
|
+
const scrollToNodeHandlerRef = useRef<ScrollToNodeHandlerRef<ID>>(null);
|
|
136
144
|
const prevSearchText = usePreviousState(searchText);
|
|
137
|
-
const internalDataRef =
|
|
145
|
+
const internalDataRef = useRef<TreeNode<ID>[] | null>(null);
|
|
138
146
|
|
|
139
147
|
// Wrap onDragEnd to set internalDataRef before calling consumer's callback
|
|
140
|
-
const wrappedOnDragEnd =
|
|
148
|
+
const wrappedOnDragEnd = useCallback((event: DragEndEvent<ID>) => {
|
|
141
149
|
internalDataRef.current = event.newTreeData;
|
|
142
150
|
onDragEnd?.(event);
|
|
143
151
|
}, [onDragEnd]);
|
|
@@ -191,7 +199,24 @@ function _innerTreeView<ID>(
|
|
|
191
199
|
return treeViewStore.getState().childToParentMap;
|
|
192
200
|
}
|
|
193
201
|
|
|
194
|
-
|
|
202
|
+
function moveNode(nodeId: ID, targetNodeId: ID, position: DropPosition) {
|
|
203
|
+
const store = getTreeViewStore<ID>(storeId);
|
|
204
|
+
const currentData = store.getState().initialTreeViewData;
|
|
205
|
+
const newData = moveTreeNode(currentData, nodeId, targetNodeId, position);
|
|
206
|
+
|
|
207
|
+
store.getState().updateInitialTreeViewData(newData);
|
|
208
|
+
initializeNodeMaps(storeId, newData);
|
|
209
|
+
recalculateCheckedStates<ID>(storeId);
|
|
210
|
+
|
|
211
|
+
if (position === "inside") {
|
|
212
|
+
expandNodes(storeId, [targetNodeId]);
|
|
213
|
+
}
|
|
214
|
+
expandNodes(storeId, [nodeId], true);
|
|
215
|
+
|
|
216
|
+
internalDataRef.current = newData;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const getIds = useCallback((node: TreeNode<ID>): ID[] => {
|
|
195
220
|
if (!node.children || node.children.length === 0) {
|
|
196
221
|
return [node.id];
|
|
197
222
|
} else {
|
|
@@ -199,15 +224,15 @@ function _innerTreeView<ID>(
|
|
|
199
224
|
}
|
|
200
225
|
}, []);
|
|
201
226
|
|
|
202
|
-
|
|
227
|
+
useEffect(() => {
|
|
203
228
|
onCheck?.(Array.from(checked), Array.from(indeterminate));
|
|
204
229
|
}, [onCheck, checked, indeterminate]);
|
|
205
230
|
|
|
206
|
-
|
|
231
|
+
useEffect(() => {
|
|
207
232
|
onExpand?.(Array.from(expanded));
|
|
208
233
|
}, [onExpand, expanded]);
|
|
209
234
|
|
|
210
|
-
|
|
235
|
+
useEffect(() => {
|
|
211
236
|
if (searchText) {
|
|
212
237
|
startTransition(() => {
|
|
213
238
|
updateExpanded(new Set(initialTreeViewData.flatMap(
|
|
@@ -230,11 +255,12 @@ function _innerTreeView<ID>(
|
|
|
230
255
|
updateExpanded
|
|
231
256
|
]);
|
|
232
257
|
|
|
233
|
-
|
|
258
|
+
useEffect(() => {
|
|
234
259
|
return () => {
|
|
235
260
|
cleanUpTreeViewStore();
|
|
261
|
+
deleteTreeViewStore(storeId);
|
|
236
262
|
};
|
|
237
|
-
}, [cleanUpTreeViewStore]);
|
|
263
|
+
}, [cleanUpTreeViewStore, storeId]);
|
|
238
264
|
|
|
239
265
|
return (
|
|
240
266
|
<NodeList
|
|
@@ -253,20 +279,16 @@ function _innerTreeView<ID>(
|
|
|
253
279
|
|
|
254
280
|
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
255
281
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
autoScrollSpeed={autoScrollSpeed}
|
|
261
|
-
dragOverlayOffset={dragOverlayOffset}
|
|
262
|
-
autoExpandDelay={autoExpandDelay}
|
|
263
|
-
dragDropCustomizations={dragDropCustomizations}
|
|
282
|
+
dragAndDrop={dragAndDrop && {
|
|
283
|
+
...dragAndDrop,
|
|
284
|
+
onDragEnd: wrappedOnDragEnd,
|
|
285
|
+
}}
|
|
264
286
|
/>
|
|
265
287
|
);
|
|
266
288
|
}
|
|
267
289
|
|
|
268
|
-
const _TreeView =
|
|
269
|
-
props: TreeViewProps<ID> & { ref?:
|
|
290
|
+
const _TreeView = forwardRef(_innerTreeView) as <ID>(
|
|
291
|
+
props: TreeViewProps<ID> & { ref?: ForwardedRef<TreeViewRef<ID>>; }
|
|
270
292
|
) => ReturnType<typeof _innerTreeView>;
|
|
271
293
|
|
|
272
294
|
export const TreeView = typedMemo<typeof _TreeView>(_TreeView);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
4
|
StyleSheet,
|
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
} from "../types/treeView.types";
|
|
14
14
|
import { Checkbox } from "@futurejj/react-native-checkbox";
|
|
15
15
|
|
|
16
|
+
// Intentionally narrow: only re-render when the checkbox value or label text changes.
|
|
17
|
+
// Other props (callbacks, styles) are stable references from parent memoization.
|
|
16
18
|
function arePropsEqual(
|
|
17
19
|
prevProps: BuiltInCheckBoxViewProps,
|
|
18
20
|
nextProps: BuiltInCheckBoxViewProps
|
|
@@ -23,7 +25,7 @@ function arePropsEqual(
|
|
|
23
25
|
);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export const CheckboxView =
|
|
28
|
+
export const CheckboxView = memo(_CheckboxView, arePropsEqual);
|
|
27
29
|
|
|
28
30
|
function _CheckboxView(props: BuiltInCheckBoxViewProps) {
|
|
29
31
|
const {
|
|
@@ -43,7 +45,7 @@ function _CheckboxView(props: BuiltInCheckBoxViewProps) {
|
|
|
43
45
|
},
|
|
44
46
|
} = props;
|
|
45
47
|
|
|
46
|
-
const customCheckboxValToCheckboxValType =
|
|
48
|
+
const customCheckboxValToCheckboxValType = useCallback((
|
|
47
49
|
customCheckboxValueType: CheckboxValueType
|
|
48
50
|
) => {
|
|
49
51
|
return customCheckboxValueType === "indeterminate"
|
|
@@ -59,7 +61,7 @@ function _CheckboxView(props: BuiltInCheckBoxViewProps) {
|
|
|
59
61
|
*
|
|
60
62
|
* @param newValue This represents the updated CheckBox value after it's clicked.
|
|
61
63
|
*/
|
|
62
|
-
const onValueChangeModifier =
|
|
64
|
+
const onValueChangeModifier = useCallback(() => {
|
|
63
65
|
// If the previous state was 'indeterminate', set checked to true
|
|
64
66
|
if (value === "indeterminate") onValueChange(true);
|
|
65
67
|
else onValueChange(!value);
|
|
@@ -106,6 +108,7 @@ export const defaultCheckboxViewStyles = StyleSheet.create({
|
|
|
106
108
|
},
|
|
107
109
|
checkboxTextStyle: {
|
|
108
110
|
color: "black",
|
|
111
|
+
/* istanbul ignore next -- Platform.OS is never "android" in jest */
|
|
109
112
|
marginTop: Platform.OS === "android" ? 2 : undefined,
|
|
110
113
|
},
|
|
111
114
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo } from "react";
|
|
2
2
|
import { type ExpandIconProps } from "../types/treeView.types";
|
|
3
3
|
|
|
4
4
|
// Function to dynamically load FontAwesomeIcon from either Expo or React Native
|
|
@@ -20,7 +20,7 @@ function loadFontAwesomeIcon() {
|
|
|
20
20
|
// Load the FontAwesomeIcon component
|
|
21
21
|
const FontAwesomeIcon = loadFontAwesomeIcon();
|
|
22
22
|
|
|
23
|
-
export const CustomExpandCollapseIcon =
|
|
23
|
+
export const CustomExpandCollapseIcon = memo(
|
|
24
24
|
_CustomExpandCollapseIcon
|
|
25
25
|
);
|
|
26
26
|
|
|
@@ -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
|
),
|
|
@@ -330,39 +349,53 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
330
349
|
// Track when this node was dragged so we can swallow the onPress/onCheck
|
|
331
350
|
// that fires when the user lifts their finger after a long-press-initiated drag.
|
|
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`
|
|
353
|
+
// values surviving FlashList recycling (where refs persist across items).
|
|
354
|
+
const wasDraggedRef = useRef(false);
|
|
334
355
|
if (isDraggingGlobal && isBeingDragged) {
|
|
335
356
|
wasDraggedRef.current = true;
|
|
336
357
|
}
|
|
337
358
|
|
|
338
|
-
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (!isDraggingGlobal) {
|
|
361
|
+
wasDraggedRef.current = false;
|
|
362
|
+
}
|
|
363
|
+
}, [isDraggingGlobal]);
|
|
364
|
+
|
|
365
|
+
const _onToggleExpand = useCallback(() => {
|
|
339
366
|
if (wasDraggedRef.current) return;
|
|
340
367
|
handleToggleExpand(storeId, node.id);
|
|
341
368
|
}, [storeId, node.id]);
|
|
342
369
|
|
|
343
|
-
const _onCheck =
|
|
370
|
+
const _onCheck = useCallback(() => {
|
|
344
371
|
if (wasDraggedRef.current) return;
|
|
345
372
|
toggleCheckboxes(storeId, [node.id]);
|
|
346
373
|
}, [storeId, node.id]);
|
|
347
374
|
|
|
348
|
-
const handleTouchStart =
|
|
375
|
+
const handleTouchStart = useCallback((e: any) => {
|
|
349
376
|
wasDraggedRef.current = false;
|
|
350
377
|
if (!onNodeTouchStart) return;
|
|
351
378
|
const { pageY, locationY } = e.nativeEvent;
|
|
352
379
|
onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
|
|
353
380
|
}, [node.id, nodeIndex, onNodeTouchStart]);
|
|
354
381
|
|
|
355
|
-
const handleTouchEnd =
|
|
382
|
+
const handleTouchEnd = useCallback(() => {
|
|
356
383
|
onNodeTouchEnd?.();
|
|
357
384
|
}, [onNodeTouchEnd]);
|
|
358
385
|
|
|
359
|
-
// Determine opacity for drag state
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
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) => {
|
|
366
399
|
onItemLayout?.(e.nativeEvent.layout.height);
|
|
367
400
|
}, [onItemLayout]);
|
|
368
401
|
|
|
@@ -416,7 +449,6 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
416
449
|
else {
|
|
417
450
|
return (
|
|
418
451
|
<View
|
|
419
|
-
{...touchHandlers}
|
|
420
452
|
onLayout={onItemLayout ? handleLayout : undefined}
|
|
421
453
|
style={[
|
|
422
454
|
{ opacity: nodeOpacity },
|
|
@@ -431,9 +463,12 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
431
463
|
isExpanded={isExpanded}
|
|
432
464
|
onCheck={_onCheck}
|
|
433
465
|
onExpand={_onToggleExpand}
|
|
434
|
-
|
|
466
|
+
isInvalidDropTarget={isDragInvalid}
|
|
467
|
+
isDropTarget={isDropTarget}
|
|
468
|
+
dropPosition={nodeDropPosition ?? undefined}
|
|
435
469
|
isDragging={isDraggingGlobal}
|
|
436
470
|
isDraggedNode={isBeingDragged}
|
|
471
|
+
dragHandleProps={touchHandlers}
|
|
437
472
|
/>
|
|
438
473
|
</View>
|
|
439
474
|
);
|
|
@@ -503,9 +538,6 @@ function NodeDropIndicator({ position, level, indentationMultiplier, styleProps
|
|
|
503
538
|
}
|
|
504
539
|
|
|
505
540
|
const styles = StyleSheet.create({
|
|
506
|
-
defaultHeaderFooter: {
|
|
507
|
-
padding: 5
|
|
508
|
-
},
|
|
509
541
|
nodeExpandableArrowTouchable: {
|
|
510
542
|
flex: 1
|
|
511
543
|
},
|