react-native-tree-multi-select 1.5.0-beta.2 → 1.5.0-beta.3

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.
@@ -111,11 +111,11 @@ function _NodeList(props: NodeListProps) {
111
111
  removeClippedSubviews={true}
112
112
  keyboardShouldPersistTaps="handled"
113
113
  drawDistance={50}
114
- data={flattenedFilteredNodes}
115
- renderItem={nodeRenderer}
116
114
  ListHeaderComponent={<HeaderFooterView />}
117
115
  ListFooterComponent={<HeaderFooterView />}
118
116
  {...treeFlashListProps}
117
+ data={flattenedFilteredNodes}
118
+ renderItem={nodeRenderer}
119
119
  />
120
120
  );
121
121
  };
@@ -1,4 +1,3 @@
1
- import { TreeNode } from "../types/treeView.types";
2
1
  import { useTreeViewStore } from "../store/treeView.store";
3
2
 
4
3
  /**
@@ -11,52 +10,40 @@ import { useTreeViewStore } from "../store/treeView.store";
11
10
  */
12
11
  export function handleToggleExpand(id: string) {
13
12
  const {
14
- initialTreeViewData,
15
13
  expanded,
16
- updateExpanded
14
+ updateExpanded,
15
+ nodeMap
17
16
  } = useTreeViewStore.getState();
18
17
 
19
18
  // Create a new Set based on the current expanded state
20
19
  const newExpanded = new Set(expanded);
21
20
 
22
- /**
23
- * Recursively deletes a node and its descendants from the expanded set.
24
- *
25
- * @param node - The tree node to start deleting from.
26
- */
27
- function deleteChildrenFromExpanded(node: TreeNode) {
28
- if (node.children) {
29
- for (let child of node.children) {
30
- newExpanded.delete(child.id);
31
- deleteChildrenFromExpanded(child);
32
- }
33
- }
34
- }
35
-
36
- // Find the node to expand or collapse
37
- const node = findNode(initialTreeViewData, id);
38
-
39
21
  if (expanded.has(id)) {
40
22
  // If the node is currently expanded, collapse it and its descendants
41
23
  newExpanded.delete(id);
42
- /*
43
- istanbul ignore next:
44
-
45
- ignore because this condition is just added to satisfy
46
- typescript type check but the node will never be undefined if it is already in
47
- expanded Set
48
- */
49
- if (node) {
50
- deleteChildrenFromExpanded(node);
24
+
25
+ // Use an iterative approach to remove all descendants from the expanded set
26
+ const stack = [id];
27
+
28
+ while (stack.length > 0) {
29
+ const currentId = stack.pop()!;
30
+ const node = nodeMap.get(currentId);
31
+
32
+ if (node && node.children) {
33
+ for (const child of node.children) {
34
+ newExpanded.delete(child.id);
35
+ stack.push(child.id);
36
+ }
37
+ }
51
38
  }
52
39
  } else {
53
40
  // If the node is currently collapsed, expand it
54
41
  newExpanded.add(id);
55
42
  }
56
43
 
57
- // Set the new expanded state
44
+ // Update the expanded state
58
45
  updateExpanded(newExpanded);
59
- };
46
+ }
60
47
 
61
48
  /**
62
49
  * Expand all nodes in the tree.
@@ -66,51 +53,33 @@ export function expandAll() {
66
53
  // Create a new Set containing the IDs of all nodes
67
54
  const newExpanded = new Set(nodeMap.keys());
68
55
  updateExpanded(newExpanded);
69
- };
56
+ }
70
57
 
71
58
  /**
72
59
  * Collapse all nodes in the tree.
73
60
  */
74
61
  export function collapseAll() {
75
62
  const { updateExpanded } = useTreeViewStore.getState();
76
- // Create an empty Set
77
- const newExpanded = new Set<string>();
78
- updateExpanded(newExpanded);
79
- };
63
+ // Clear the expanded state
64
+ updateExpanded(new Set<string>());
65
+ }
80
66
 
81
67
  /**
82
68
  * Expand tree nodes of given ids. If the id is of a child, it also expands
83
- * the parent which it belongs to.
84
- * @param ids Ids of nodes to expand.
69
+ * its ancestors up to the root.
70
+ * @param ids - Ids of nodes to expand.
85
71
  */
86
72
  export function expandNodes(ids: string[]) {
87
73
  const { expanded, updateExpanded, childToParentMap } = useTreeViewStore.getState();
88
74
  const newExpanded = new Set(expanded);
89
- const processedParents = new Set(); // To track already processed parents
90
-
91
- ids.forEach(id => {
92
- newExpanded.add(id); // Start by adding the node ID to the set
93
- let currentId = id;
94
-
95
- while (currentId && childToParentMap.has(currentId) && !processedParents.has(currentId)) {
96
- const parentId = childToParentMap.get(currentId);
97
- /*
98
- istanbul ignore else:
99
-
100
- ignore because this condition is just added to satisfy
101
- typescript type check as parentId will never be undefined if it is already in
102
- childToParentMap Map
103
- */
104
- if (parentId) {
105
- /* istanbul ignore else: nothing to be done in else block */
106
- if (!newExpanded.has(parentId)) {
107
- newExpanded.add(parentId); // Add the parent ID only if not already processed
108
- processedParents.add(parentId);
109
- }
110
- currentId = parentId; // Move up to the next parent
111
- } else {
112
- break; // Break the loop if there's no further parent
113
- }
75
+ const processedIds = new Set<string>();
76
+
77
+ ids.forEach((id) => {
78
+ let currentId: string | undefined = id;
79
+ while (currentId && !processedIds.has(currentId)) {
80
+ newExpanded.add(currentId);
81
+ processedIds.add(currentId);
82
+ currentId = childToParentMap.get(currentId);
114
83
  }
115
84
  });
116
85
 
@@ -119,55 +88,35 @@ export function expandNodes(ids: string[]) {
119
88
 
120
89
  /**
121
90
  * Collapse tree nodes of given ids. If the id is of a parent, it also collapses
122
- * the children inside it.
123
- * @param ids Ids of nodes to collapse.
91
+ * its descendants.
92
+ * @param ids - Ids of nodes to collapse.
124
93
  */
125
94
  export function collapseNodes(ids: string[]) {
126
95
  const { expanded, updateExpanded, nodeMap } = useTreeViewStore.getState();
127
96
  const newExpanded = new Set(expanded);
128
97
 
129
- // Function to recursively remove child nodes from the expanded set
130
- const deleteChildrenFromExpanded = (nodeId: string, visited = new Set()) => {
131
- /*
132
- istanbul ignore next:
133
-
134
- ignore because this is a redundancy check.
135
- */
136
- if (visited.has(nodeId)) return; // Prevent redundant processing
137
- visited.add(nodeId);
138
-
139
- const node = nodeMap.get(nodeId);
140
- node?.children?.forEach(child => {
141
- newExpanded.delete(child.id);
142
- deleteChildrenFromExpanded(child.id, visited);
143
- });
98
+ // Use an iterative approach to remove all descendants from the expanded set
99
+ const deleteChildrenFromExpanded = (nodeId: string) => {
100
+ const stack = [nodeId];
101
+
102
+ while (stack.length > 0) {
103
+ const currentId = stack.pop()!;
104
+ const node = nodeMap.get(currentId);
105
+
106
+ if (node && node.children) {
107
+ for (const child of node.children) {
108
+ newExpanded.delete(child.id);
109
+ stack.push(child.id);
110
+ }
111
+ }
112
+ }
144
113
  };
145
114
 
146
- ids.forEach(id => {
147
- // Remove the node ID from the set and all its children
115
+ ids.forEach((id) => {
116
+ // Remove the node ID from the set and all its descendants
148
117
  newExpanded.delete(id);
149
118
  deleteChildrenFromExpanded(id);
150
119
  });
151
120
 
152
121
  updateExpanded(newExpanded);
153
122
  }
154
-
155
- /**
156
- * Finds a node in the tree by its ID.
157
- *
158
- * @param nodes - The array of tree nodes to search through.
159
- * @returns The found tree node, or undefined if not found.
160
- */
161
- function findNode(nodes: TreeNode[], parentId: string): TreeNode | undefined {
162
- for (let node of nodes) {
163
- if (node.id === parentId) {
164
- return node;
165
- } else if (node.children) {
166
- const found = findNode(node.children, parentId);
167
- if (found) {
168
- return found;
169
- }
170
- }
171
- }
172
- return undefined;
173
- }
@@ -4,7 +4,7 @@ import { useTreeViewStore } from "../store/treeView.store";
4
4
  * Function to toggle checkbox state for a tree structure.
5
5
  * It sets the checked and indeterminate state for all affected nodes in the tree after an action to check/uncheck is made.
6
6
  * @param {string[]} ids - The ids of nodes that need to be checked or unchecked.
7
- * @param {boolean} [forceCheck] - Optional. If provided, will force the check state of the nodes to be this value.
7
+ * @param {boolean} [forceCheck] - Optional. If provided, will force the check state of the nodes to be this value.
8
8
  * If not provided, the check state will be toggled based on the current state.
9
9
  */
10
10
  export function toggleCheckboxes(ids: string[], forceCheck?: boolean) {
@@ -26,145 +26,168 @@ export function toggleCheckboxes(ids: string[], forceCheck?: boolean) {
26
26
  const tempChecked = new Set(checked);
27
27
  const tempIndeterminate = new Set(indeterminate);
28
28
 
29
- // Maps for memoization of the recursive functions areAllDescendantsChecked and areAnyDescendantsChecked.
30
- const memoAllDescendantsChecked = new Map<string, boolean>();
31
- const memoAnyDescendantsChecked = new Map<string, boolean>();
29
+ // Keep track of nodes that have been toggled or affected.
30
+ const affectedNodes = new Set<string>();
32
31
 
33
- /**
34
- * Recursive function to check if all descendants of a node are checked.
35
- * It uses memoization to avoid redundant calculations.
36
- * @param {string} nodeId - The id of the node to be checked.
37
- * @returns {boolean} - Whether all descendants of the node are checked.
38
- */
39
- const areAllDescendantsChecked = (nodeId: string): boolean => {
40
- // If the result for this node is already in the map, return it.
41
- if (memoAllDescendantsChecked.has(nodeId)) {
42
- return memoAllDescendantsChecked.get(nodeId)!;
43
- }
44
-
45
- const node = nodeMap.get(nodeId);
46
- let allChecked = true;
47
- if (node?.children) {
48
- // If the node has children, recursively check all children.
49
- for (const childNode of node.children) {
50
- allChecked = allChecked && areAllDescendantsChecked(childNode.id);
51
- }
52
- } else {
53
- // If the node has no children, its state is equal to whether it is in the checked set.
54
- allChecked = tempChecked.has(nodeId);
55
- }
32
+ // Memoization maps for node depths.
33
+ const nodeDepths = new Map<string, number>();
56
34
 
57
- // Store the result in the map and return it.
58
- memoAllDescendantsChecked.set(nodeId, allChecked);
59
- return allChecked;
60
- };
61
-
62
- /**
63
- * Updated function to check if any descendants of a node are checked.
64
- * It uses memoization to avoid redundant calculations and avoids unnecessarily deep recursion.
65
- * @param {string} nodeId - The id of the node to be checked.
66
- * @returns {boolean} - Whether any descendants of the node are checked.
67
- */
68
- const areAnyDescendantsChecked = (nodeId: string): boolean => {
69
- // If the result for this node is already in the map, return it.
70
- if (memoAnyDescendantsChecked.has(nodeId)) {
71
- return memoAnyDescendantsChecked.get(nodeId)!;
72
- }
73
-
74
- const node = nodeMap.get(nodeId);
75
- let anyChecked = false;
76
- if (node?.children) {
77
- // Check if any direct child is checked, without requiring all descendants.
78
- for (const childNode of node.children) {
79
- if (tempChecked.has(childNode.id) || areAnyDescendantsChecked(childNode.id)) {
80
- anyChecked = true;
81
- break;
82
- }
83
- }
84
- } else {
85
- // If the node has no children, its state is equal to whether it is in the checked set.
86
- anyChecked = tempChecked.has(nodeId);
35
+ // Step 1: Toggle the clicked nodes and their children without updating parents yet.
36
+ ids.forEach((id) => {
37
+ const node = nodeMap.get(id);
38
+ if (!node) {
39
+ // Node does not exist; skip processing this ID
40
+ return;
87
41
  }
88
42
 
89
- // Store the result in the map and return it.
90
- memoAnyDescendantsChecked.set(nodeId, anyChecked);
91
- return anyChecked;
92
- };
93
-
94
- // Toggle the clicked nodes and their children.
95
- ids.forEach((id) => {
96
43
  const isChecked = tempChecked.has(id);
97
44
  const newCheckedState = forceCheck === undefined ? !isChecked : forceCheck;
98
45
 
99
46
  if (newCheckedState) {
100
47
  tempChecked.add(id);
101
48
  tempIndeterminate.delete(id);
49
+ affectedNodes.add(id);
102
50
  if (toChildren) {
103
- recursivelyUpdateChildren(id, true);
51
+ updateChildrenIteratively(id, true);
104
52
  }
105
53
  } else {
106
54
  tempChecked.delete(id);
107
55
  tempIndeterminate.delete(id);
56
+ affectedNodes.add(id);
108
57
  if (toChildren) {
109
- recursivelyUpdateChildren(id, false);
58
+ updateChildrenIteratively(id, false);
110
59
  }
111
60
  }
112
-
113
- // Skip updating parent nodes if toParents is false
114
- if (toParents) {
115
- updateParentNodes(id);
116
- }
117
61
  });
118
62
 
119
- // Function to recursively update children nodes as per childrenChecked value
120
- function recursivelyUpdateChildren(nodeId: string, childrenChecked: boolean) {
121
- const node = nodeMap.get(nodeId);
122
- if (node && node.children) {
123
- node.children.forEach((childNode) => {
124
- if (childrenChecked) {
125
- tempChecked.add(childNode.id);
126
- tempIndeterminate.delete(childNode.id);
63
+ // Step 2: Collect all affected parent nodes.
64
+ const nodesToUpdate = new Set<string>();
65
+
66
+ if (toParents) {
67
+ affectedNodes.forEach((id) => {
68
+ let currentNodeId: string | undefined = id;
69
+ while (currentNodeId) {
70
+ const parentNodeId = childToParentMap.get(currentNodeId);
71
+ if (parentNodeId) {
72
+ nodesToUpdate.add(parentNodeId);
73
+ currentNodeId = parentNodeId;
127
74
  } else {
128
- tempChecked.delete(childNode.id);
129
- tempIndeterminate.delete(childNode.id);
75
+ break;
76
+ }
77
+ }
78
+ });
79
+ }
80
+
81
+ // Step 3: Update parent nodes in bottom-up order.
82
+ if (toParents && nodesToUpdate.size > 0) {
83
+ // Convert the set to an array and sort nodes by depth (deepest first).
84
+ const sortedNodes = Array.from(nodesToUpdate).sort((a, b) => {
85
+ return getNodeDepth(b) - getNodeDepth(a);
86
+ });
87
+
88
+ sortedNodes.forEach((nodeId) => {
89
+ updateNodeState(nodeId);
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Function to iteratively update children nodes as per childrenChecked value.
95
+ * @param rootId - The ID of the root node to start updating from.
96
+ * @param childrenChecked - The desired checked state for children.
97
+ */
98
+ function updateChildrenIteratively(rootId: string, childrenChecked: boolean) {
99
+ const stack = [rootId];
100
+
101
+ while (stack.length > 0) {
102
+ const nodeId = stack.pop()!;
103
+ const node = nodeMap.get(nodeId);
104
+ if (!node) continue; // Node does not exist; skip
105
+
106
+ if (childrenChecked) {
107
+ tempChecked.add(nodeId);
108
+ tempIndeterminate.delete(nodeId);
109
+ } else {
110
+ tempChecked.delete(nodeId);
111
+ tempIndeterminate.delete(nodeId);
112
+ }
113
+ affectedNodes.add(nodeId);
114
+
115
+ if (node.children && node.children.length > 0) {
116
+ for (const childNode of node.children) {
117
+ stack.push(childNode.id);
130
118
  }
131
- recursivelyUpdateChildren(childNode.id, childrenChecked);
132
- });
119
+ }
133
120
  }
134
121
  }
135
122
 
136
- // Function to update parent nodes
137
- function updateParentNodes(nodeId: string) {
123
+ /**
124
+ * Function to get the depth of a node for sorting purposes, with memoization.
125
+ * @param nodeId - The ID of the node to get the depth for.
126
+ * @returns The depth of the node.
127
+ */
128
+ function getNodeDepth(nodeId: string): number {
129
+ if (nodeDepths.has(nodeId)) {
130
+ return nodeDepths.get(nodeId)!;
131
+ }
132
+
133
+ let depth = 0;
138
134
  let currentNodeId: string | undefined = nodeId;
139
135
  while (currentNodeId) {
140
136
  const parentNodeId = childToParentMap.get(currentNodeId);
141
137
  if (parentNodeId) {
142
- if (tempChecked.has(parentNodeId)) {
143
- // If the parent node is currently checked, but not all child nodes are checked,
144
- // move the parent node to an indeterminate state
145
- if (!areAllDescendantsChecked(parentNodeId)) {
146
- tempChecked.delete(parentNodeId);
147
- tempIndeterminate.add(parentNodeId);
148
- }
149
- } else if (tempIndeterminate.has(parentNodeId)) {
150
- // If the parent node is currently in an indeterminate state,
151
- // then check if all descendants are checked
152
- if (areAllDescendantsChecked(parentNodeId)) {
153
- tempIndeterminate.delete(parentNodeId);
154
- tempChecked.add(parentNodeId);
155
- } else if (!areAnyDescendantsChecked(parentNodeId)) {
156
- // If no descendants are checked, remove from indeterminate set
157
- tempIndeterminate.delete(parentNodeId);
158
- }
159
- } else {
160
- // If the parent node is not checked or indeterminate,
161
- // check if any descendants are checked and update appropriately
162
- if (areAnyDescendantsChecked(parentNodeId)) {
163
- tempIndeterminate.add(parentNodeId);
164
- }
165
- }
138
+ depth++;
139
+ currentNodeId = parentNodeId;
140
+ } else {
141
+ break;
142
+ }
143
+ }
144
+
145
+ nodeDepths.set(nodeId, depth);
146
+ return depth;
147
+ }
148
+
149
+ /**
150
+ * Function to update the state of a node based on its children's states.
151
+ * @param nodeId - The ID of the node to update.
152
+ */
153
+ function updateNodeState(nodeId: string) {
154
+ const node = nodeMap.get(nodeId);
155
+ if (!node || !node.children || node.children.length === 0) {
156
+ // Leaf nodes are already updated.
157
+ return;
158
+ }
159
+
160
+ let allChildrenChecked = true;
161
+ let anyChildCheckedOrIndeterminate = false;
162
+
163
+ for (const child of node.children) {
164
+ const isChecked = tempChecked.has(child.id);
165
+ const isIndeterminate = tempIndeterminate.has(child.id);
166
+
167
+ if (isChecked) {
168
+ anyChildCheckedOrIndeterminate = true;
169
+ } else if (isIndeterminate) {
170
+ anyChildCheckedOrIndeterminate = true;
171
+ allChildrenChecked = false;
172
+ } else {
173
+ allChildrenChecked = false;
174
+ }
175
+
176
+ // If both conditions are met, we can break early.
177
+ if (!allChildrenChecked && anyChildCheckedOrIndeterminate) {
178
+ break;
166
179
  }
167
- currentNodeId = parentNodeId;
180
+ }
181
+
182
+ if (allChildrenChecked) {
183
+ tempChecked.add(nodeId);
184
+ tempIndeterminate.delete(nodeId);
185
+ } else if (anyChildCheckedOrIndeterminate) {
186
+ tempChecked.delete(nodeId);
187
+ tempIndeterminate.add(nodeId);
188
+ } else {
189
+ tempChecked.delete(nodeId);
190
+ tempIndeterminate.delete(nodeId);
168
191
  }
169
192
  }
170
193