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.
Files changed (96) hide show
  1. package/README.md +85 -25
  2. package/lib/module/TreeView.js +36 -31
  3. package/lib/module/TreeView.js.map +1 -1
  4. package/lib/module/components/CheckboxView.js +8 -4
  5. package/lib/module/components/CheckboxView.js.map +1 -1
  6. package/lib/module/components/CustomExpandCollapseIcon.js +2 -2
  7. package/lib/module/components/CustomExpandCollapseIcon.js.map +1 -1
  8. package/lib/module/components/DragOverlay.js +17 -5
  9. package/lib/module/components/DragOverlay.js.map +1 -1
  10. package/lib/module/components/DropIndicator.js +2 -2
  11. package/lib/module/components/DropIndicator.js.map +1 -1
  12. package/lib/module/components/NodeList.js +80 -56
  13. package/lib/module/components/NodeList.js.map +1 -1
  14. package/lib/module/constants/treeView.constants.js +3 -0
  15. package/lib/module/constants/treeView.constants.js.map +1 -1
  16. package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
  17. package/lib/module/helpers/moveTreeNode.helper.js +30 -0
  18. package/lib/module/helpers/moveTreeNode.helper.js.map +1 -1
  19. package/lib/module/helpers/selectAll.helper.js.map +1 -1
  20. package/lib/module/helpers/toggleCheckbox.helper.js +44 -61
  21. package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
  22. package/lib/module/hooks/useDragDrop.js +141 -34
  23. package/lib/module/hooks/useDragDrop.js.map +1 -1
  24. package/lib/module/{handlers/ScrollToNodeHandler.js → hooks/useScrollToNode.js} +27 -26
  25. package/lib/module/hooks/useScrollToNode.js.map +1 -0
  26. package/lib/module/index.js +1 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/jest.setup.js +14 -1
  29. package/lib/module/jest.setup.js.map +1 -1
  30. package/lib/module/store/treeView.store.js +3 -0
  31. package/lib/module/store/treeView.store.js.map +1 -1
  32. package/lib/module/utils/typedMemo.js +3 -3
  33. package/lib/module/utils/typedMemo.js.map +1 -1
  34. package/lib/module/utils/useDeepCompareEffect.js +5 -5
  35. package/lib/module/utils/useDeepCompareEffect.js.map +1 -1
  36. package/lib/typescript/src/TreeView.d.ts +3 -3
  37. package/lib/typescript/src/TreeView.d.ts.map +1 -1
  38. package/lib/typescript/src/components/CheckboxView.d.ts +1 -2
  39. package/lib/typescript/src/components/CheckboxView.d.ts.map +1 -1
  40. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts +1 -2
  41. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts.map +1 -1
  42. package/lib/typescript/src/components/DragOverlay.d.ts +1 -0
  43. package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -1
  44. package/lib/typescript/src/components/DropIndicator.d.ts +1 -2
  45. package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -1
  46. package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
  47. package/lib/typescript/src/constants/treeView.constants.d.ts +2 -0
  48. package/lib/typescript/src/constants/treeView.constants.d.ts.map +1 -1
  49. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts +2 -2
  50. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts.map +1 -1
  51. package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -1
  52. package/lib/typescript/src/helpers/selectAll.helper.d.ts +4 -4
  53. package/lib/typescript/src/helpers/selectAll.helper.d.ts.map +1 -1
  54. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +3 -0
  55. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks/useDragDrop.d.ts +18 -7
  57. package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -1
  58. package/lib/typescript/src/{handlers/ScrollToNodeHandler.d.ts → hooks/useScrollToNode.d.ts} +13 -15
  59. package/lib/typescript/src/hooks/useScrollToNode.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +4 -3
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/jest.setup.d.ts +1 -1
  63. package/lib/typescript/src/jest.setup.d.ts.map +1 -1
  64. package/lib/typescript/src/store/treeView.store.d.ts +2 -1
  65. package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
  66. package/lib/typescript/src/types/dragDrop.types.d.ts +19 -0
  67. package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
  68. package/lib/typescript/src/types/treeView.types.d.ts +149 -35
  69. package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/typedMemo.d.ts +1 -1
  71. package/lib/typescript/src/utils/typedMemo.d.ts.map +1 -1
  72. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts +2 -2
  73. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts.map +1 -1
  74. package/package.json +32 -15
  75. package/src/TreeView.tsx +57 -35
  76. package/src/components/CheckboxView.tsx +7 -4
  77. package/src/components/CustomExpandCollapseIcon.tsx +2 -2
  78. package/src/components/DragOverlay.tsx +19 -6
  79. package/src/components/DropIndicator.tsx +2 -2
  80. package/src/components/NodeList.tsx +90 -58
  81. package/src/constants/treeView.constants.ts +4 -1
  82. package/src/helpers/expandCollapse.helper.ts +5 -5
  83. package/src/helpers/moveTreeNode.helper.ts +33 -0
  84. package/src/helpers/selectAll.helper.ts +10 -10
  85. package/src/helpers/toggleCheckbox.helper.ts +57 -69
  86. package/src/hooks/useDragDrop.ts +182 -46
  87. package/src/{handlers/ScrollToNodeHandler.tsx → hooks/useScrollToNode.ts} +48 -45
  88. package/src/index.tsx +9 -0
  89. package/src/jest.setup.ts +14 -1
  90. package/src/store/treeView.store.ts +6 -1
  91. package/src/types/dragDrop.types.ts +21 -0
  92. package/src/types/treeView.types.ts +157 -41
  93. package/src/utils/typedMemo.ts +3 -3
  94. package/src/utils/useDeepCompareEffect.ts +13 -7
  95. package/lib/module/handlers/ScrollToNodeHandler.js.map +0 -1
  96. package/lib/typescript/src/handlers/ScrollToNodeHandler.d.ts.map +0 -1
@@ -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<string>());
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
 
@@ -1,5 +1,56 @@
1
+ import type { CheckboxValueType, TreeNode } from "../types/treeView.types";
1
2
  import { getTreeViewStore } from "../store/treeView.store";
2
3
 
4
+ /** Derive the tri-state checkbox value from checked and indeterminate booleans. */
5
+ export function getCheckboxValue(
6
+ isChecked: boolean,
7
+ isIndeterminate: boolean
8
+ ): CheckboxValueType {
9
+ if (isIndeterminate) return "indeterminate";
10
+ return isChecked;
11
+ }
12
+
13
+ /**
14
+ * Update a parent node's checked/indeterminate state based on its children.
15
+ * Mutates tempChecked and tempIndeterminate in-place.
16
+ */
17
+ function updateParentCheckState<ID>(
18
+ parentId: ID,
19
+ children: TreeNode<ID>[],
20
+ tempChecked: Set<ID>,
21
+ tempIndeterminate: Set<ID>,
22
+ ): void {
23
+ let allChecked = true;
24
+ let anyCheckedOrIndeterminate = false;
25
+
26
+ for (const child of children) {
27
+ const isChecked = tempChecked.has(child.id);
28
+ const isIndeterminate = tempIndeterminate.has(child.id);
29
+
30
+ if (isChecked) {
31
+ anyCheckedOrIndeterminate = true;
32
+ } else if (isIndeterminate) {
33
+ anyCheckedOrIndeterminate = true;
34
+ allChecked = false;
35
+ } else {
36
+ allChecked = false;
37
+ }
38
+
39
+ if (!allChecked && anyCheckedOrIndeterminate) break;
40
+ }
41
+
42
+ if (allChecked) {
43
+ tempChecked.add(parentId);
44
+ tempIndeterminate.delete(parentId);
45
+ } else if (anyCheckedOrIndeterminate) {
46
+ tempChecked.delete(parentId);
47
+ tempIndeterminate.add(parentId);
48
+ } else {
49
+ tempChecked.delete(parentId);
50
+ tempIndeterminate.delete(parentId);
51
+ }
52
+ }
53
+
3
54
  /**
4
55
  * Function to toggle checkbox state for a tree structure.
5
56
  * It sets the checked and indeterminate state for all affected nodes in the tree after an action to check/uncheck is made.
@@ -106,7 +157,7 @@ export function toggleCheckboxes<ID>(
106
157
  while (stack.length > 0) {
107
158
  const nodeId = stack.pop()!;
108
159
  const node = nodeMap.get(nodeId);
109
- if (!node) continue; // Node does not exist; skip
160
+ if (!node) continue;
110
161
 
111
162
  if (childrenChecked) {
112
163
  tempChecked.add(nodeId);
@@ -157,43 +208,8 @@ export function toggleCheckboxes<ID>(
157
208
  */
158
209
  function updateNodeState(nodeId: ID) {
159
210
  const node = nodeMap.get(nodeId);
160
- if (!node || !node.children || node.children.length === 0) {
161
- // Leaf nodes are already updated.
162
- return;
163
- }
164
-
165
- let allChildrenChecked = true;
166
- let anyChildCheckedOrIndeterminate = false;
167
-
168
- for (const child of node.children) {
169
- const isChecked = tempChecked.has(child.id);
170
- const isIndeterminate = tempIndeterminate.has(child.id);
171
-
172
- if (isChecked) {
173
- anyChildCheckedOrIndeterminate = true;
174
- } else if (isIndeterminate) {
175
- anyChildCheckedOrIndeterminate = true;
176
- allChildrenChecked = false;
177
- } else {
178
- allChildrenChecked = false;
179
- }
180
-
181
- // If both conditions are met, we can break early.
182
- if (!allChildrenChecked && anyChildCheckedOrIndeterminate) {
183
- break;
184
- }
185
- }
186
-
187
- if (allChildrenChecked) {
188
- tempChecked.add(nodeId);
189
- tempIndeterminate.delete(nodeId);
190
- } else if (anyChildCheckedOrIndeterminate) {
191
- tempChecked.delete(nodeId);
192
- tempIndeterminate.add(nodeId);
193
- } else {
194
- tempChecked.delete(nodeId);
195
- tempIndeterminate.delete(nodeId);
196
- }
211
+ if (!node?.children?.length) return;
212
+ updateParentCheckState(nodeId, node.children, tempChecked, tempIndeterminate);
197
213
  }
198
214
 
199
215
  // Update the state object with the new checked and indeterminate sets.
@@ -225,7 +241,7 @@ export function recalculateCheckedStates<ID>(storeId: string) {
225
241
  const tempIndeterminate = new Set(indeterminate);
226
242
 
227
243
  // Collect parent nodes and clean up leaf nodes that shouldn't be indeterminate.
228
- // A leaf node (no children) can never be indeterminate this can happen when
244
+ // A leaf node (no children) can never be indeterminate - this can happen when
229
245
  // all children of a formerly-indeterminate parent are dragged away.
230
246
  const parentNodes: ID[] = [];
231
247
  for (const [id, node] of nodeMap) {
@@ -260,37 +276,9 @@ export function recalculateCheckedStates<ID>(storeId: string) {
260
276
  // Update each parent based on its children's current state
261
277
  for (const parentId of parentNodes) {
262
278
  const node = nodeMap.get(parentId);
279
+ /* istanbul ignore next -- parentNodes is built from nodeMap entries with children above; same nodeMap, same iteration — unreachable unless nodeMap mutates between loops */
263
280
  if (!node?.children?.length) continue;
264
-
265
- let allChecked = true;
266
- let anyCheckedOrIndeterminate = false;
267
-
268
- for (const child of node.children) {
269
- const isChecked = tempChecked.has(child.id);
270
- const isIndeterminate = tempIndeterminate.has(child.id);
271
-
272
- if (isChecked) {
273
- anyCheckedOrIndeterminate = true;
274
- } else if (isIndeterminate) {
275
- anyCheckedOrIndeterminate = true;
276
- allChecked = false;
277
- } else {
278
- allChecked = false;
279
- }
280
-
281
- if (!allChecked && anyCheckedOrIndeterminate) break;
282
- }
283
-
284
- if (allChecked) {
285
- tempChecked.add(parentId);
286
- tempIndeterminate.delete(parentId);
287
- } else if (anyCheckedOrIndeterminate) {
288
- tempChecked.delete(parentId);
289
- tempIndeterminate.add(parentId);
290
- } else {
291
- tempChecked.delete(parentId);
292
- tempIndeterminate.delete(parentId);
293
- }
281
+ updateParentCheckState(parentId, node.children, tempChecked, tempIndeterminate);
294
282
  }
295
283
 
296
284
  updateChecked(tempChecked);