react-native-tree-multi-select 1.8.0 → 1.9.0-beta.1
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 +52 -24
- package/lib/commonjs/TreeView.js +17 -6
- package/lib/commonjs/TreeView.js.map +1 -1
- package/lib/commonjs/components/NodeList.js +26 -10
- package/lib/commonjs/components/NodeList.js.map +1 -1
- package/lib/commonjs/handlers/ScrollToNodeHandler.js +169 -0
- package/lib/commonjs/handlers/ScrollToNodeHandler.js.map +1 -0
- package/lib/commonjs/helpers/expandCollapse.helper.js +7 -1
- package/lib/commonjs/helpers/expandCollapse.helper.js.map +1 -1
- package/lib/module/TreeView.js +18 -5
- package/lib/module/TreeView.js.map +1 -1
- package/lib/module/components/NodeList.js +27 -11
- package/lib/module/components/NodeList.js.map +1 -1
- package/lib/module/handlers/ScrollToNodeHandler.js +165 -0
- package/lib/module/handlers/ScrollToNodeHandler.js.map +1 -0
- package/lib/module/helpers/expandCollapse.helper.js +7 -1
- package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
- package/lib/typescript/TreeView.d.ts.map +1 -1
- package/lib/typescript/components/NodeList.d.ts.map +1 -1
- package/lib/typescript/handlers/ScrollToNodeHandler.d.ts +58 -0
- package/lib/typescript/handlers/ScrollToNodeHandler.d.ts.map +1 -0
- package/lib/typescript/helpers/expandCollapse.helper.d.ts +3 -1
- package/lib/typescript/helpers/expandCollapse.helper.d.ts.map +1 -1
- package/lib/typescript/types/treeView.types.d.ts +6 -1
- package/lib/typescript/types/treeView.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TreeView.tsx +182 -152
- package/src/components/NodeList.tsx +31 -11
- package/src/handlers/ScrollToNodeHandler.tsx +222 -0
- package/src/helpers/expandCollapse.helper.ts +16 -1
- package/src/types/treeView.types.ts +15 -1
package/src/TreeView.tsx
CHANGED
|
@@ -1,210 +1,240 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { InteractionManager } from 'react-native';
|
|
3
3
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
TreeNode,
|
|
5
|
+
TreeViewProps,
|
|
6
|
+
TreeViewRef
|
|
7
7
|
} from './types/treeView.types';
|
|
8
8
|
import NodeList from './components/NodeList';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
selectAll,
|
|
11
|
+
selectAllFiltered,
|
|
12
|
+
unselectAll,
|
|
13
|
+
unselectAllFiltered,
|
|
14
|
+
initializeNodeMaps,
|
|
15
|
+
expandAll,
|
|
16
|
+
collapseAll,
|
|
17
|
+
toggleCheckboxes,
|
|
18
|
+
expandNodes,
|
|
19
|
+
collapseNodes
|
|
20
20
|
} from './helpers';
|
|
21
|
-
import { useTreeViewStore } from './store/treeView.store';
|
|
21
|
+
import { getTreeViewStore, useTreeViewStore } from './store/treeView.store';
|
|
22
22
|
import usePreviousState from './utils/usePreviousState';
|
|
23
23
|
import { useShallow } from "zustand/react/shallow";
|
|
24
24
|
import uuid from "react-native-uuid";
|
|
25
25
|
import useDeepCompareEffect from "./utils/useDeepCompareEffect";
|
|
26
26
|
import { typedMemo } from './utils/typedMemo';
|
|
27
|
+
import {
|
|
28
|
+
ScrollToNodeHandlerRef,
|
|
29
|
+
ScrollToNodeParams
|
|
30
|
+
} from "./handlers/ScrollToNodeHandler";
|
|
31
|
+
|
|
32
|
+
function _innerTreeView<ID>(
|
|
33
|
+
props: TreeViewProps<ID>,
|
|
34
|
+
ref: React.ForwardedRef<TreeViewRef<ID>>
|
|
35
|
+
) {
|
|
36
|
+
const {
|
|
37
|
+
data,
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
data,
|
|
39
|
+
onCheck,
|
|
40
|
+
onExpand,
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
onExpand,
|
|
42
|
+
selectionPropagation,
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
preselectedIds = [],
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
preExpandedIds = [],
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
initialScrollNodeID,
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
treeFlashListProps,
|
|
51
|
+
checkBoxViewStyleProps,
|
|
52
|
+
indentationMultiplier,
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
CheckboxComponent,
|
|
55
|
+
ExpandCollapseIconComponent,
|
|
56
|
+
ExpandCollapseTouchableComponent,
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
CustomNodeRowComponent,
|
|
59
|
+
} = props;
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
const storeId = React.useMemo(() => uuid.v4(), []);
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const {
|
|
64
|
+
expanded,
|
|
65
|
+
updateExpanded,
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
initialTreeViewData,
|
|
68
|
+
updateInitialTreeViewData,
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
searchText,
|
|
71
|
+
updateSearchText,
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
updateSearchKeys,
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
checked,
|
|
76
|
+
indeterminate,
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
setSelectionPropagation,
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
cleanUpTreeViewStore,
|
|
81
|
+
} = useTreeViewStore<ID>(storeId)(useShallow(
|
|
82
|
+
state => ({
|
|
83
|
+
expanded: state.expanded,
|
|
84
|
+
updateExpanded: state.updateExpanded,
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
initialTreeViewData: state.initialTreeViewData,
|
|
87
|
+
updateInitialTreeViewData: state.updateInitialTreeViewData,
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
searchText: state.searchText,
|
|
90
|
+
updateSearchText: state.updateSearchText,
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
updateSearchKeys: state.updateSearchKeys,
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
checked: state.checked,
|
|
95
|
+
indeterminate: state.indeterminate,
|
|
87
96
|
|
|
88
|
-
|
|
97
|
+
setSelectionPropagation: state.setSelectionPropagation,
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
cleanUpTreeViewStore: state.cleanUpTreeViewStore,
|
|
100
|
+
})
|
|
101
|
+
));
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
React.useImperativeHandle(ref, () => ({
|
|
104
|
+
selectAll: () => selectAll(storeId),
|
|
105
|
+
unselectAll: () => unselectAll(storeId),
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
selectAllFiltered: () => selectAllFiltered(storeId),
|
|
108
|
+
unselectAllFiltered: () => unselectAllFiltered(storeId),
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
expandAll: () => expandAll(storeId),
|
|
111
|
+
collapseAll: () => collapseAll(storeId),
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
expandNodes: (ids: ID[]) => expandNodes(storeId, ids),
|
|
114
|
+
collapseNodes: (ids: ID[]) => collapseNodes(storeId, ids),
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
selectNodes: (ids: ID[]) => selectNodes(ids),
|
|
117
|
+
unselectNodes: (ids: ID[]) => unselectNodes(ids),
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
}));
|
|
119
|
+
setSearchText,
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
scrollToNodeID,
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
getChildToParentMap
|
|
124
|
+
}));
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
const scrollToNodeHandlerRef = React.useRef<ScrollToNodeHandlerRef<ID>>(null);
|
|
127
|
+
const prevSearchText = usePreviousState(searchText);
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
useDeepCompareEffect(() => {
|
|
130
|
+
cleanUpTreeViewStore();
|
|
122
131
|
|
|
123
|
-
|
|
132
|
+
updateInitialTreeViewData(data);
|
|
124
133
|
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
if (selectionPropagation)
|
|
135
|
+
setSelectionPropagation(selectionPropagation);
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
expandNodes(storeId, preExpandedIds);
|
|
130
|
-
}, [data]);
|
|
137
|
+
initializeNodeMaps(storeId, data);
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
139
|
+
// Check any pre-selected nodes
|
|
140
|
+
toggleCheckboxes(storeId, preselectedIds, true);
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// Expand pre-expanded nodes
|
|
143
|
+
expandNodes(storeId, [
|
|
144
|
+
...preExpandedIds,
|
|
145
|
+
...(initialScrollNodeID ? [initialScrollNodeID] : [])
|
|
146
|
+
]);
|
|
147
|
+
}, [data]);
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
149
|
+
function selectNodes(ids: ID[]) {
|
|
150
|
+
toggleCheckboxes(storeId, ids, true);
|
|
151
|
+
}
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} else {
|
|
149
|
-
return [node.id, ...node.children.flatMap((item) => getIds(item))];
|
|
150
|
-
}
|
|
151
|
-
}, []);
|
|
153
|
+
function unselectNodes(ids: ID[]) {
|
|
154
|
+
toggleCheckboxes(storeId, ids, false);
|
|
155
|
+
}
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
function setSearchText(text: string, keys: string[] = ["name"]) {
|
|
158
|
+
updateSearchText(text);
|
|
159
|
+
updateSearchKeys(keys);
|
|
160
|
+
}
|
|
156
161
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
function scrollToNodeID(params: ScrollToNodeParams<ID>) {
|
|
163
|
+
scrollToNodeHandlerRef.current?.scrollToNodeID(params);
|
|
164
|
+
}
|
|
160
165
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
166
|
+
function getChildToParentMap() {
|
|
167
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
168
|
+
return treeViewStore.getState().childToParentMap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const getIds = React.useCallback((node: TreeNode<ID>): ID[] => {
|
|
172
|
+
if (!node.children || node.children.length === 0) {
|
|
173
|
+
return [node.id];
|
|
174
|
+
} else {
|
|
175
|
+
return [node.id, ...node.children.flatMap((item) => getIds(item))];
|
|
176
|
+
}
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
onCheck?.(Array.from(checked), Array.from(indeterminate));
|
|
181
|
+
}, [onCheck, checked, indeterminate]);
|
|
182
|
+
|
|
183
|
+
React.useEffect(() => {
|
|
184
|
+
onExpand?.(Array.from(expanded));
|
|
185
|
+
}, [onExpand, expanded]);
|
|
186
|
+
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
if (searchText) {
|
|
189
|
+
InteractionManager.runAfterInteractions(() => {
|
|
190
|
+
updateExpanded(new Set(initialTreeViewData.flatMap(
|
|
191
|
+
(item) => getIds(item)
|
|
192
|
+
)));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (prevSearchText && prevSearchText !== "") {
|
|
196
|
+
/* Collapse all nodes only if previous search query was non-empty: this is
|
|
197
|
+
done to prevent node collapse on first render if preExpandedIds is provided */
|
|
198
|
+
InteractionManager.runAfterInteractions(() => {
|
|
199
|
+
updateExpanded(new Set());
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}, [
|
|
203
|
+
getIds,
|
|
204
|
+
initialTreeViewData,
|
|
205
|
+
prevSearchText,
|
|
206
|
+
searchText,
|
|
207
|
+
updateExpanded
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
return () => {
|
|
212
|
+
cleanUpTreeViewStore();
|
|
213
|
+
};
|
|
214
|
+
}, [cleanUpTreeViewStore]);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<NodeList
|
|
218
|
+
storeId={storeId}
|
|
219
|
+
|
|
220
|
+
scrollToNodeHandlerRef={scrollToNodeHandlerRef}
|
|
221
|
+
initialScrollNodeID={initialScrollNodeID}
|
|
222
|
+
|
|
223
|
+
treeFlashListProps={treeFlashListProps}
|
|
224
|
+
checkBoxViewStyleProps={checkBoxViewStyleProps}
|
|
225
|
+
indentationMultiplier={indentationMultiplier}
|
|
226
|
+
|
|
227
|
+
CheckboxComponent={CheckboxComponent}
|
|
228
|
+
ExpandCollapseIconComponent={ExpandCollapseIconComponent}
|
|
229
|
+
ExpandCollapseTouchableComponent={ExpandCollapseTouchableComponent}
|
|
230
|
+
|
|
231
|
+
CustomNodeRowComponent={CustomNodeRowComponent}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
205
234
|
}
|
|
235
|
+
|
|
206
236
|
const _TreeView = React.forwardRef(_innerTreeView) as <ID>(
|
|
207
|
-
|
|
237
|
+
props: TreeViewProps<ID> & { ref?: React.ForwardedRef<TreeViewRef<ID>>; }
|
|
208
238
|
) => ReturnType<typeof _innerTreeView>;
|
|
209
239
|
|
|
210
240
|
export const TreeView = typedMemo<typeof _TreeView>(_TreeView);
|
|
@@ -27,6 +27,7 @@ import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
|
|
|
27
27
|
import { defaultIndentationMultiplier } from "../constants/treeView.constants";
|
|
28
28
|
import { useShallow } from 'zustand/react/shallow';
|
|
29
29
|
import { typedMemo } from "../utils/typedMemo";
|
|
30
|
+
import { ScrollToNodeHandler } from "../handlers/ScrollToNodeHandler";
|
|
30
31
|
|
|
31
32
|
const NodeList = typedMemo(_NodeList);
|
|
32
33
|
export default NodeList;
|
|
@@ -35,6 +36,9 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
35
36
|
const {
|
|
36
37
|
storeId,
|
|
37
38
|
|
|
39
|
+
scrollToNodeHandlerRef,
|
|
40
|
+
initialScrollNodeID,
|
|
41
|
+
|
|
38
42
|
treeFlashListProps,
|
|
39
43
|
checkBoxViewStyleProps,
|
|
40
44
|
indentationMultiplier,
|
|
@@ -61,6 +65,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
61
65
|
})
|
|
62
66
|
));
|
|
63
67
|
|
|
68
|
+
const flashListRef = React.useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
|
|
69
|
+
|
|
70
|
+
const [initialScrollIndex, setInitialScrollIndex] = React.useState<number>(-1);
|
|
71
|
+
|
|
64
72
|
// First we filter the tree as per the search term and keys
|
|
65
73
|
const filteredTree = React.useMemo(() => getFilteredTreeData<ID>(
|
|
66
74
|
initialTreeViewData,
|
|
@@ -112,17 +120,29 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
|
|
|
112
120
|
]);
|
|
113
121
|
|
|
114
122
|
return (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
<>
|
|
124
|
+
<ScrollToNodeHandler
|
|
125
|
+
ref={scrollToNodeHandlerRef}
|
|
126
|
+
storeId={storeId}
|
|
127
|
+
flashListRef={flashListRef}
|
|
128
|
+
flattenedFilteredNodes={flattenedFilteredNodes}
|
|
129
|
+
setInitialScrollIndex={setInitialScrollIndex}
|
|
130
|
+
initialScrollNodeID={initialScrollNodeID} />
|
|
131
|
+
|
|
132
|
+
<FlashList
|
|
133
|
+
ref={flashListRef}
|
|
134
|
+
estimatedItemSize={36}
|
|
135
|
+
initialScrollIndex={initialScrollIndex}
|
|
136
|
+
removeClippedSubviews={true}
|
|
137
|
+
keyboardShouldPersistTaps="handled"
|
|
138
|
+
drawDistance={50}
|
|
139
|
+
ListHeaderComponent={<HeaderFooterView />}
|
|
140
|
+
ListFooterComponent={<HeaderFooterView />}
|
|
141
|
+
{...treeFlashListProps}
|
|
142
|
+
data={flattenedFilteredNodes}
|
|
143
|
+
renderItem={nodeRenderer}
|
|
144
|
+
/>
|
|
145
|
+
</>
|
|
126
146
|
);
|
|
127
147
|
};
|
|
128
148
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollToNodeHandler Component
|
|
3
|
+
*
|
|
4
|
+
* This component provides an imperative handle to scroll to a specified node within a tree view.
|
|
5
|
+
* The scrolling action is orchestrated via a two-step "milestone" mechanism that ensures the target
|
|
6
|
+
* node is both expanded in the tree and that the rendered list reflects this expansion before the scroll
|
|
7
|
+
* is performed.
|
|
8
|
+
*
|
|
9
|
+
* The two key milestones tracked by the `expandAndScrollToNodeQueue` state are:
|
|
10
|
+
* 1. EXPANDED: Indicates that the expansion logic for the target node has been initiated.
|
|
11
|
+
* 2. RENDERED: Indicates that the list has re-rendered with the expanded node included.
|
|
12
|
+
*
|
|
13
|
+
* When the `scrollToNodeID` method is called:
|
|
14
|
+
* - The scroll parameters (target node ID, animation preferences, view offset/position) are stored in a ref.
|
|
15
|
+
* - The target node's expansion is triggered via the `expandNodes` helper.
|
|
16
|
+
* - The `expandAndScrollToNodeQueue` state is updated to mark that expansion has begun.
|
|
17
|
+
*
|
|
18
|
+
* As the component re-renders (e.g., after the node expansion changes the rendered list):
|
|
19
|
+
* - A useEffect monitors changes to the list, and once it detects the expansion has occurred,
|
|
20
|
+
* it updates the queue to include the RENDERED milestone.
|
|
21
|
+
*
|
|
22
|
+
* A layout effect then waits for both conditions to be met:
|
|
23
|
+
* - The target node is confirmed to be in the expanded set.
|
|
24
|
+
* - The `expandAndScrollToNodeQueue` exactly matches the expected milestones ([EXPANDED, RENDERED]).
|
|
25
|
+
*
|
|
26
|
+
* Once both conditions are satisfied:
|
|
27
|
+
* - The index of the target node is determined within the latest flattened node list.
|
|
28
|
+
* - The flash list is scrolled to that index.
|
|
29
|
+
* - The queued scroll parameters and milestone queue are reset.
|
|
30
|
+
*
|
|
31
|
+
* This design ensures that the scroll action is performed only after the target node is fully present
|
|
32
|
+
* in the UI, thus preventing issues with attempting to scroll to an element that does not exist yet.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import React from "react";
|
|
36
|
+
import { expandNodes } from "../helpers/expandCollapse.helper";
|
|
37
|
+
import { useTreeViewStore } from "../store/treeView.store";
|
|
38
|
+
import { useShallow } from "zustand/react/shallow";
|
|
39
|
+
import { __FlattenedTreeNode__ } from "../types/treeView.types";
|
|
40
|
+
import { typedMemo } from "../utils/typedMemo";
|
|
41
|
+
import { isEqual } from "lodash";
|
|
42
|
+
|
|
43
|
+
interface Props<ID> {
|
|
44
|
+
storeId: string;
|
|
45
|
+
flashListRef: React.MutableRefObject<any>;
|
|
46
|
+
flattenedFilteredNodes: __FlattenedTreeNode__<ID>[];
|
|
47
|
+
setInitialScrollIndex: React.Dispatch<React.SetStateAction<number>>;
|
|
48
|
+
initialScrollNodeID: ID | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ScrollToNodeParams<ID> {
|
|
52
|
+
nodeId: ID;
|
|
53
|
+
expandScrolledNode?: boolean;
|
|
54
|
+
|
|
55
|
+
animated?: boolean;
|
|
56
|
+
viewOffset?: number;
|
|
57
|
+
viewPosition?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Enum representing the two milestones needed before scrolling
|
|
61
|
+
enum ExpandQueueAction {
|
|
62
|
+
EXPANDED,
|
|
63
|
+
RENDERED,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ScrollToNodeHandlerRef<ID> {
|
|
67
|
+
scrollToNodeID: (params: ScrollToNodeParams<ID>) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _innerScrollToNodeHandler<ID>(
|
|
71
|
+
props: Props<ID>,
|
|
72
|
+
ref: React.ForwardedRef<ScrollToNodeHandlerRef<ID>>
|
|
73
|
+
) {
|
|
74
|
+
const {
|
|
75
|
+
storeId,
|
|
76
|
+
flashListRef,
|
|
77
|
+
flattenedFilteredNodes,
|
|
78
|
+
setInitialScrollIndex,
|
|
79
|
+
initialScrollNodeID
|
|
80
|
+
} = props;
|
|
81
|
+
|
|
82
|
+
const { expanded, childToParentMap } = useTreeViewStore<ID>(storeId)(useShallow(
|
|
83
|
+
state => ({
|
|
84
|
+
expanded: state.expanded,
|
|
85
|
+
childToParentMap: state.childToParentMap
|
|
86
|
+
})
|
|
87
|
+
));
|
|
88
|
+
|
|
89
|
+
React.useImperativeHandle(ref, () => ({
|
|
90
|
+
scrollToNodeID: (params: ScrollToNodeParams<ID>) => {
|
|
91
|
+
queuedScrollToNodeParams.current = params;
|
|
92
|
+
// Mark that expansion is initiated.
|
|
93
|
+
setExpandAndScrollToNodeQueue([ExpandQueueAction.EXPANDED]);
|
|
94
|
+
// Trigger expansion logic (this may update the store and subsequently re-render the list).
|
|
95
|
+
expandNodes(
|
|
96
|
+
storeId,
|
|
97
|
+
[queuedScrollToNodeParams.current.nodeId],
|
|
98
|
+
!queuedScrollToNodeParams.current.expandScrolledNode
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}), [storeId]);
|
|
102
|
+
|
|
103
|
+
// Ref to store the scroll parameters for the queued action.
|
|
104
|
+
const queuedScrollToNodeParams = React.useRef<ScrollToNodeParams<ID> | null>(null);
|
|
105
|
+
|
|
106
|
+
// State to track progression: first the expansion is triggered, then the list is rendered.
|
|
107
|
+
const [expandAndScrollToNodeQueue, setExpandAndScrollToNodeQueue]
|
|
108
|
+
= React.useState<ExpandQueueAction[]>([]);
|
|
109
|
+
|
|
110
|
+
const latestFlattenedFilteredNodesRef = React.useRef(flattenedFilteredNodes);
|
|
111
|
+
|
|
112
|
+
/* When the rendered node list changes, update the ref.
|
|
113
|
+
If an expansion was triggered, mark that the list is now rendered. */
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
setExpandAndScrollToNodeQueue(prevQueue => {
|
|
116
|
+
if (prevQueue.includes(ExpandQueueAction.EXPANDED)) {
|
|
117
|
+
latestFlattenedFilteredNodesRef.current = flattenedFilteredNodes;
|
|
118
|
+
return [
|
|
119
|
+
ExpandQueueAction.EXPANDED,
|
|
120
|
+
ExpandQueueAction.RENDERED
|
|
121
|
+
];
|
|
122
|
+
} else {
|
|
123
|
+
return prevQueue;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}, [flattenedFilteredNodes]);
|
|
127
|
+
|
|
128
|
+
/* Once the target node is expanded and the list is updated (milestones reached),
|
|
129
|
+
perform the scroll using the latest node list. */
|
|
130
|
+
React.useLayoutEffect(() => {
|
|
131
|
+
if (queuedScrollToNodeParams.current === null)
|
|
132
|
+
return;
|
|
133
|
+
|
|
134
|
+
if (!isEqual(
|
|
135
|
+
expandAndScrollToNodeQueue,
|
|
136
|
+
[ExpandQueueAction.EXPANDED, ExpandQueueAction.RENDERED]
|
|
137
|
+
)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If node is set to not expand
|
|
142
|
+
if (!queuedScrollToNodeParams.current.expandScrolledNode) {
|
|
143
|
+
let parentId: ID | undefined;
|
|
144
|
+
// Get the parent's id of the node to scroll to
|
|
145
|
+
if (childToParentMap.has(queuedScrollToNodeParams.current.nodeId)) {
|
|
146
|
+
parentId = childToParentMap.get(queuedScrollToNodeParams.current.nodeId) as ID;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ensure if the parent is expanded before proceeding to scroll to the node
|
|
150
|
+
if (parentId && !expanded.has(parentId))
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// If node is set to expand
|
|
154
|
+
else {
|
|
155
|
+
if (!expanded.has(queuedScrollToNodeParams.current.nodeId))
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const {
|
|
160
|
+
nodeId,
|
|
161
|
+
animated,
|
|
162
|
+
viewOffset,
|
|
163
|
+
viewPosition
|
|
164
|
+
} = queuedScrollToNodeParams.current!;
|
|
165
|
+
|
|
166
|
+
function scrollToItem() {
|
|
167
|
+
const index = latestFlattenedFilteredNodesRef.current.findIndex(
|
|
168
|
+
item => item.id === nodeId
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (index !== -1 && flashListRef.current) {
|
|
172
|
+
// Scroll to the target index.
|
|
173
|
+
flashListRef.current.scrollToIndex({
|
|
174
|
+
index,
|
|
175
|
+
animated,
|
|
176
|
+
viewOffset,
|
|
177
|
+
viewPosition
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
if (__DEV__) {
|
|
181
|
+
console.info("Cannot find the item of the mentioned id to scroll in the rendered tree view list data!");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clear the queued parameters and reset the expansion/render queue.
|
|
186
|
+
queuedScrollToNodeParams.current = null;
|
|
187
|
+
setExpandAndScrollToNodeQueue([]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
scrollToItem();
|
|
191
|
+
}, [childToParentMap, expanded, flashListRef, expandAndScrollToNodeQueue]);
|
|
192
|
+
|
|
193
|
+
////////////////////////////// Handle Initial Scroll /////////////////////////////
|
|
194
|
+
/* On first render, if an initial scroll target is provided, determine its index.
|
|
195
|
+
This is done only once. */
|
|
196
|
+
const initialScrollDone = React.useRef(false);
|
|
197
|
+
React.useLayoutEffect(() => {
|
|
198
|
+
if (initialScrollDone.current) return;
|
|
199
|
+
|
|
200
|
+
const index = flattenedFilteredNodes.findIndex(
|
|
201
|
+
item => item.id === initialScrollNodeID
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
setInitialScrollIndex(index);
|
|
205
|
+
|
|
206
|
+
if (index !== -1) {
|
|
207
|
+
initialScrollDone.current = true;
|
|
208
|
+
}
|
|
209
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
210
|
+
}, [flattenedFilteredNodes, initialScrollNodeID]);
|
|
211
|
+
/////////////////////////////////////////////////////////////////////////////////
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const _ScrollToNodeHandler = React.forwardRef(_innerScrollToNodeHandler) as <ID>(
|
|
217
|
+
props: Props<ID> & { ref?: React.ForwardedRef<ScrollToNodeHandlerRef<ID>>; }
|
|
218
|
+
) => ReturnType<typeof _innerScrollToNodeHandler>;
|
|
219
|
+
|
|
220
|
+
export const ScrollToNodeHandler = typedMemo<
|
|
221
|
+
typeof _ScrollToNodeHandler
|
|
222
|
+
>(_ScrollToNodeHandler);
|