react-native-tree-multi-select 3.0.0-beta.3 → 3.0.0-beta.5
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 +100 -30
- 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 +78 -58
- 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 +146 -65
- 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 +24 -8
- 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 +79 -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 +87 -60
- 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 +190 -80
- package/src/{handlers/ScrollToNodeHandler.tsx → hooks/useScrollToNode.ts} +48 -45
- package/src/index.tsx +11 -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 +87 -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 { 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,30 @@ 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
|
-
dragOverlayOffset = -
|
|
77
|
+
dragOverlayOffset = -2,
|
|
63
78
|
autoExpandDelay = 800,
|
|
64
|
-
dragDropCustomizations,
|
|
65
|
-
|
|
79
|
+
customizations: dragDropCustomizations,
|
|
80
|
+
canDrop: canDropCallback,
|
|
81
|
+
maxDepth,
|
|
82
|
+
canNodeHaveChildren,
|
|
83
|
+
canDrag,
|
|
84
|
+
autoScrollToDroppedNode,
|
|
85
|
+
} = dragAndDrop ?? {};
|
|
86
|
+
|
|
87
|
+
// When the dragAndDrop prop is provided, drag is enabled by default.
|
|
88
|
+
// Users can still toggle it off with enabled: false at runtime.
|
|
89
|
+
const dragEnabled = dragAndDrop ? (_dragEnabled ?? true) : false;
|
|
66
90
|
|
|
67
91
|
const {
|
|
68
92
|
expanded,
|
|
@@ -80,34 +104,43 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
80
104
|
})
|
|
81
105
|
));
|
|
82
106
|
|
|
83
|
-
const flashListRef =
|
|
84
|
-
const containerRef =
|
|
85
|
-
const internalDataRef =
|
|
86
|
-
const measuredItemHeightRef =
|
|
107
|
+
const flashListRef = useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
|
|
108
|
+
const containerRef = useRef<View>(null);
|
|
109
|
+
const internalDataRef = useRef<TreeNode<ID>[] | null>(null);
|
|
110
|
+
const measuredItemHeightRef = useRef(0);
|
|
87
111
|
|
|
88
|
-
const handleItemLayout =
|
|
112
|
+
const handleItemLayout = useCallback((height: number) => {
|
|
89
113
|
if (measuredItemHeightRef.current === 0 && height > 0) {
|
|
90
114
|
measuredItemHeightRef.current = height;
|
|
91
115
|
}
|
|
92
116
|
}, []);
|
|
93
117
|
|
|
94
|
-
const [initialScrollIndex, setInitialScrollIndex] =
|
|
118
|
+
const [initialScrollIndex, setInitialScrollIndex] = useState<number>(-1);
|
|
95
119
|
|
|
96
120
|
// First we filter the tree as per the search term and keys
|
|
97
|
-
const filteredTree =
|
|
121
|
+
const filteredTree = useMemo(() => getFilteredTreeData<ID>(
|
|
98
122
|
initialTreeViewData,
|
|
99
123
|
searchText.trim().toLowerCase(),
|
|
100
124
|
searchKeys
|
|
101
125
|
), [initialTreeViewData, searchText, searchKeys]);
|
|
102
126
|
|
|
103
127
|
// Then we flatten the tree to make it "render-compatible" in a "flat" list
|
|
104
|
-
const flattenedFilteredNodes =
|
|
128
|
+
const flattenedFilteredNodes = useMemo(() => getFlattenedTreeData<ID>(
|
|
105
129
|
filteredTree,
|
|
106
130
|
expanded,
|
|
107
131
|
), [filteredTree, expanded]);
|
|
108
132
|
|
|
133
|
+
useScrollToNode<ID>({
|
|
134
|
+
storeId,
|
|
135
|
+
scrollToNodeHandlerRef,
|
|
136
|
+
flashListRef,
|
|
137
|
+
flattenedFilteredNodes,
|
|
138
|
+
setInitialScrollIndex,
|
|
139
|
+
initialScrollNodeID,
|
|
140
|
+
});
|
|
141
|
+
|
|
109
142
|
// And update the innermost children id -> required to un/select filtered tree
|
|
110
|
-
|
|
143
|
+
useEffect(() => {
|
|
111
144
|
const updatedInnerMostChildrenIds = getInnerMostChildrenIdsInTree<ID>(
|
|
112
145
|
filteredTree
|
|
113
146
|
);
|
|
@@ -133,8 +166,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
133
166
|
flattenedNodes: flattenedFilteredNodes,
|
|
134
167
|
flashListRef,
|
|
135
168
|
containerRef,
|
|
136
|
-
dragEnabled
|
|
169
|
+
dragEnabled,
|
|
170
|
+
onDragStart,
|
|
137
171
|
onDragEnd,
|
|
172
|
+
onDragCancel,
|
|
138
173
|
longPressDuration,
|
|
139
174
|
autoScrollThreshold,
|
|
140
175
|
autoScrollSpeed,
|
|
@@ -143,10 +178,16 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
143
178
|
dragOverlayOffset,
|
|
144
179
|
autoExpandDelay,
|
|
145
180
|
indentationMultiplier: effectiveIndentationMultiplier,
|
|
181
|
+
canDrop: canDropCallback,
|
|
182
|
+
maxDepth,
|
|
183
|
+
canNodeHaveChildren,
|
|
184
|
+
canDrag,
|
|
185
|
+
scrollToNodeHandlerRef,
|
|
186
|
+
autoScrollToDroppedNode,
|
|
146
187
|
});
|
|
147
188
|
|
|
148
189
|
// Combined onScroll handler
|
|
149
|
-
const handleScroll =
|
|
190
|
+
const handleScroll = useCallback((
|
|
150
191
|
event: NativeSyntheticEvent<NativeScrollEvent>
|
|
151
192
|
) => {
|
|
152
193
|
scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
|
@@ -156,7 +197,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
156
197
|
treeFlashListProps?.onScroll?.(event as any);
|
|
157
198
|
}, [scrollOffsetRef, cancelLongPressTimer, treeFlashListProps]);
|
|
158
199
|
|
|
159
|
-
const nodeRenderer =
|
|
200
|
+
const nodeRenderer = useCallback((
|
|
160
201
|
{ item, index }: { item: __FlattenedTreeNode__<ID>; index: number; }
|
|
161
202
|
) => {
|
|
162
203
|
return (
|
|
@@ -223,14 +264,6 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
223
264
|
|
|
224
265
|
return (
|
|
225
266
|
<>
|
|
226
|
-
<ScrollToNodeHandler
|
|
227
|
-
ref={scrollToNodeHandlerRef}
|
|
228
|
-
storeId={storeId}
|
|
229
|
-
flashListRef={flashListRef}
|
|
230
|
-
flattenedFilteredNodes={flattenedFilteredNodes}
|
|
231
|
-
setInitialScrollIndex={setInitialScrollIndex}
|
|
232
|
-
initialScrollNodeID={initialScrollNodeID} />
|
|
233
|
-
|
|
234
267
|
{dragEnabled ? (
|
|
235
268
|
<View
|
|
236
269
|
ref={containerRef}
|
|
@@ -240,6 +273,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
240
273
|
{flashListElement}
|
|
241
274
|
{isDragging && draggedNode && (
|
|
242
275
|
<DragOverlay<ID>
|
|
276
|
+
storeId={storeId}
|
|
243
277
|
overlayY={overlayY}
|
|
244
278
|
overlayX={overlayX}
|
|
245
279
|
node={draggedNode}
|
|
@@ -262,22 +296,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
262
296
|
|
|
263
297
|
function HeaderFooterView() {
|
|
264
298
|
return (
|
|
265
|
-
<View style={
|
|
299
|
+
<View style={{ padding: listHeaderFooterPadding }} />
|
|
266
300
|
);
|
|
267
301
|
}
|
|
268
302
|
|
|
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
303
|
|
|
282
304
|
const Node = typedMemo(_Node);
|
|
283
305
|
function _Node<ID>(props: NodeProps<ID>) {
|
|
@@ -315,7 +337,7 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
315
337
|
} = useTreeViewStore<ID>(storeId)(useShallow(
|
|
316
338
|
state => ({
|
|
317
339
|
isExpanded: state.expanded.has(node.id),
|
|
318
|
-
value:
|
|
340
|
+
value: getCheckboxValue(
|
|
319
341
|
state.checked.has(node.id),
|
|
320
342
|
state.indeterminate.has(node.id)
|
|
321
343
|
),
|
|
@@ -332,45 +354,51 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
332
354
|
// The flag is set during render (synchronous) and cleared on the next touch start.
|
|
333
355
|
// It is also cleared via effect when dragging ends, to prevent stale `true`
|
|
334
356
|
// values surviving FlashList recycling (where refs persist across items).
|
|
335
|
-
const wasDraggedRef =
|
|
357
|
+
const wasDraggedRef = useRef(false);
|
|
336
358
|
if (isDraggingGlobal && isBeingDragged) {
|
|
337
359
|
wasDraggedRef.current = true;
|
|
338
360
|
}
|
|
339
361
|
|
|
340
|
-
|
|
362
|
+
useEffect(() => {
|
|
341
363
|
if (!isDraggingGlobal) {
|
|
342
364
|
wasDraggedRef.current = false;
|
|
343
365
|
}
|
|
344
366
|
}, [isDraggingGlobal]);
|
|
345
367
|
|
|
346
|
-
const _onToggleExpand =
|
|
368
|
+
const _onToggleExpand = useCallback(() => {
|
|
347
369
|
if (wasDraggedRef.current) return;
|
|
348
370
|
handleToggleExpand(storeId, node.id);
|
|
349
371
|
}, [storeId, node.id]);
|
|
350
372
|
|
|
351
|
-
const _onCheck =
|
|
373
|
+
const _onCheck = useCallback(() => {
|
|
352
374
|
if (wasDraggedRef.current) return;
|
|
353
375
|
toggleCheckboxes(storeId, [node.id]);
|
|
354
376
|
}, [storeId, node.id]);
|
|
355
377
|
|
|
356
|
-
const handleTouchStart =
|
|
378
|
+
const handleTouchStart = useCallback((e: any) => {
|
|
357
379
|
wasDraggedRef.current = false;
|
|
358
380
|
if (!onNodeTouchStart) return;
|
|
359
381
|
const { pageY, locationY } = e.nativeEvent;
|
|
360
382
|
onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
|
|
361
383
|
}, [node.id, nodeIndex, onNodeTouchStart]);
|
|
362
384
|
|
|
363
|
-
const handleTouchEnd =
|
|
385
|
+
const handleTouchEnd = useCallback(() => {
|
|
364
386
|
onNodeTouchEnd?.();
|
|
365
387
|
}, [onNodeTouchEnd]);
|
|
366
388
|
|
|
367
|
-
// Determine opacity for drag state
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
389
|
+
// Determine opacity for drag state (separate values for dragged node vs invalid targets).
|
|
390
|
+
// When CustomNodeRowComponent is used, hand off all visual control
|
|
391
|
+
// (including drag opacity) to the custom component - it receives
|
|
392
|
+
// isDraggedNode / isInvalidDropTarget / isDragging props.
|
|
393
|
+
const draggedOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
|
|
394
|
+
const invalidOpacity = dragDropCustomizations?.invalidTargetOpacity ?? 0.3;
|
|
395
|
+
const nodeOpacity = CustomNodeRowComponent
|
|
396
|
+
? 1.0
|
|
397
|
+
: isDraggingGlobal
|
|
398
|
+
? (isBeingDragged ? draggedOpacity : isDragInvalid ? invalidOpacity : 1.0)
|
|
399
|
+
: 1.0;
|
|
400
|
+
|
|
401
|
+
const handleLayout = useCallback((e: any) => {
|
|
374
402
|
onItemLayout?.(e.nativeEvent.layout.height);
|
|
375
403
|
}, [onItemLayout]);
|
|
376
404
|
|
|
@@ -424,7 +452,6 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
424
452
|
else {
|
|
425
453
|
return (
|
|
426
454
|
<View
|
|
427
|
-
{...touchHandlers}
|
|
428
455
|
onLayout={onItemLayout ? handleLayout : undefined}
|
|
429
456
|
style={[
|
|
430
457
|
{ opacity: nodeOpacity },
|
|
@@ -439,9 +466,12 @@ function _Node<ID>(props: NodeProps<ID>) {
|
|
|
439
466
|
isExpanded={isExpanded}
|
|
440
467
|
onCheck={_onCheck}
|
|
441
468
|
onExpand={_onToggleExpand}
|
|
442
|
-
|
|
469
|
+
isInvalidDropTarget={isDragInvalid}
|
|
470
|
+
isDropTarget={isDropTarget}
|
|
471
|
+
dropPosition={nodeDropPosition ?? undefined}
|
|
443
472
|
isDragging={isDraggingGlobal}
|
|
444
473
|
isDraggedNode={isBeingDragged}
|
|
474
|
+
dragHandleProps={touchHandlers}
|
|
445
475
|
/>
|
|
446
476
|
</View>
|
|
447
477
|
);
|
|
@@ -511,9 +541,6 @@ function NodeDropIndicator({ position, level, indentationMultiplier, styleProps
|
|
|
511
541
|
}
|
|
512
542
|
|
|
513
543
|
const styles = StyleSheet.create({
|
|
514
|
-
defaultHeaderFooter: {
|
|
515
|
-
padding: 5
|
|
516
|
-
},
|
|
517
544
|
nodeExpandableArrowTouchable: {
|
|
518
545
|
flex: 1
|
|
519
546
|
},
|
|
@@ -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
|
|