react-native-tree-multi-select 2.0.13 → 3.0.0-beta.2
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 +151 -0
- package/lib/module/TreeView.js +32 -2
- package/lib/module/TreeView.js.map +1 -1
- package/lib/module/components/DragOverlay.js +104 -0
- package/lib/module/components/DragOverlay.js.map +1 -0
- package/lib/module/components/DropIndicator.js +79 -0
- package/lib/module/components/DropIndicator.js.map +1 -0
- package/lib/module/components/NodeList.js +288 -33
- package/lib/module/components/NodeList.js.map +1 -1
- package/lib/module/helpers/index.js +1 -0
- package/lib/module/helpers/index.js.map +1 -1
- package/lib/module/helpers/moveTreeNode.helper.js +96 -0
- package/lib/module/helpers/moveTreeNode.helper.js.map +1 -0
- package/lib/module/helpers/toggleCheckbox.helper.js +88 -0
- package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
- package/lib/module/hooks/useDragDrop.js +683 -0
- package/lib/module/hooks/useDragDrop.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/store/treeView.store.js +22 -1
- package/lib/module/store/treeView.store.js.map +1 -1
- package/lib/module/types/dragDrop.types.js +4 -0
- package/lib/module/types/dragDrop.types.js.map +1 -0
- package/lib/typescript/src/TreeView.d.ts.map +1 -1
- package/lib/typescript/src/components/DragOverlay.d.ts +13 -0
- package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/DropIndicator.d.ts +13 -0
- package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -0
- package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
- package/lib/typescript/src/helpers/index.d.ts +1 -0
- package/lib/typescript/src/helpers/index.d.ts.map +1 -1
- package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts +13 -0
- package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -0
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +6 -0
- package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useDragDrop.d.ts +40 -0
- package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/store/treeView.store.d.ts +9 -0
- package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
- package/lib/typescript/src/types/dragDrop.types.d.ts +21 -0
- package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -0
- package/lib/typescript/src/types/treeView.types.d.ts +94 -0
- package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TreeView.tsx +34 -0
- package/src/components/DragOverlay.tsx +114 -0
- package/src/components/DropIndicator.tsx +95 -0
- package/src/components/NodeList.tsx +327 -30
- package/src/helpers/index.ts +2 -1
- package/src/helpers/moveTreeNode.helper.ts +105 -0
- package/src/helpers/toggleCheckbox.helper.ts +96 -0
- package/src/hooks/useDragDrop.ts +835 -0
- package/src/index.tsx +19 -2
- package/src/store/treeView.store.ts +36 -0
- package/src/types/dragDrop.types.ts +23 -0
- package/src/types/treeView.types.ts +110 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { TreeNode } from "../types/treeView.types";
|
|
2
|
+
import type { DropPosition } from "../types/dragDrop.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Move a node within a tree structure. Returns a new tree (no mutation).
|
|
6
|
+
*
|
|
7
|
+
* @param data - The current tree data
|
|
8
|
+
* @param draggedNodeId - The ID of the node to move
|
|
9
|
+
* @param targetNodeId - The ID of the target node
|
|
10
|
+
* @param position - Where to place relative to target: "above", "below", or "inside"
|
|
11
|
+
* @returns New tree data with the node moved, or the original data if the move is invalid
|
|
12
|
+
*/
|
|
13
|
+
export function moveTreeNode<ID>(
|
|
14
|
+
data: TreeNode<ID>[],
|
|
15
|
+
draggedNodeId: ID,
|
|
16
|
+
targetNodeId: ID,
|
|
17
|
+
position: DropPosition
|
|
18
|
+
): TreeNode<ID>[] {
|
|
19
|
+
if (draggedNodeId === targetNodeId) return data;
|
|
20
|
+
|
|
21
|
+
// Step 1: Deep clone the tree
|
|
22
|
+
const cloned = deepCloneTree(data);
|
|
23
|
+
|
|
24
|
+
// Step 2: Remove the dragged node
|
|
25
|
+
const removedNode = removeNodeById(cloned, draggedNodeId);
|
|
26
|
+
if (!removedNode) return data;
|
|
27
|
+
|
|
28
|
+
// Step 3: Insert at the new position
|
|
29
|
+
const inserted = insertNode(cloned, removedNode, targetNodeId, position);
|
|
30
|
+
if (!inserted) return data;
|
|
31
|
+
|
|
32
|
+
return cloned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deepCloneTree<ID>(nodes: TreeNode<ID>[]): TreeNode<ID>[] {
|
|
36
|
+
return nodes.map(node => ({
|
|
37
|
+
...node,
|
|
38
|
+
children: node.children ? deepCloneTree(node.children) : undefined,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove a node by ID from the tree. Mutates the cloned tree in-place.
|
|
44
|
+
* Returns the removed node, or null if not found.
|
|
45
|
+
*/
|
|
46
|
+
function removeNodeById<ID>(
|
|
47
|
+
nodes: TreeNode<ID>[],
|
|
48
|
+
nodeId: ID,
|
|
49
|
+
): TreeNode<ID> | null {
|
|
50
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
51
|
+
if (nodes[i]!.id === nodeId) {
|
|
52
|
+
const [removed] = nodes.splice(i, 1);
|
|
53
|
+
return removed!;
|
|
54
|
+
}
|
|
55
|
+
const children = nodes[i]!.children;
|
|
56
|
+
if (children) {
|
|
57
|
+
const removed = removeNodeById(children, nodeId);
|
|
58
|
+
if (removed) {
|
|
59
|
+
// Clean up empty children arrays
|
|
60
|
+
if (children.length === 0) {
|
|
61
|
+
nodes[i] = { ...nodes[i]!, children: undefined };
|
|
62
|
+
}
|
|
63
|
+
return removed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert a node relative to a target node. Mutates the cloned tree in-place.
|
|
72
|
+
* Returns true if insertion was successful.
|
|
73
|
+
*/
|
|
74
|
+
function insertNode<ID>(
|
|
75
|
+
nodes: TreeNode<ID>[],
|
|
76
|
+
nodeToInsert: TreeNode<ID>,
|
|
77
|
+
targetId: ID,
|
|
78
|
+
position: DropPosition,
|
|
79
|
+
): boolean {
|
|
80
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
81
|
+
if (nodes[i]!.id === targetId) {
|
|
82
|
+
if (position === "above") {
|
|
83
|
+
nodes.splice(i, 0, nodeToInsert);
|
|
84
|
+
} else if (position === "below") {
|
|
85
|
+
nodes.splice(i + 1, 0, nodeToInsert);
|
|
86
|
+
} else {
|
|
87
|
+
// "inside" - add as first child
|
|
88
|
+
const target = nodes[i]!;
|
|
89
|
+
if (target.children) {
|
|
90
|
+
target.children.unshift(nodeToInsert);
|
|
91
|
+
} else {
|
|
92
|
+
nodes[i] = { ...target, children: [nodeToInsert] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const children = nodes[i]!.children;
|
|
98
|
+
if (children) {
|
|
99
|
+
if (insertNode(children, nodeToInsert, targetId, position)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -200,3 +200,99 @@ export function toggleCheckboxes<ID>(
|
|
|
200
200
|
updateChecked(tempChecked);
|
|
201
201
|
updateIndeterminate(tempIndeterminate);
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Recalculates checked/indeterminate state for all parent nodes bottom-up.
|
|
206
|
+
* Should be called after tree structure changes (e.g., drag-drop moves) to ensure
|
|
207
|
+
* parent states correctly reflect their children's checked states.
|
|
208
|
+
*/
|
|
209
|
+
export function recalculateCheckedStates<ID>(storeId: string) {
|
|
210
|
+
const treeViewStore = getTreeViewStore<ID>(storeId);
|
|
211
|
+
const {
|
|
212
|
+
checked,
|
|
213
|
+
updateChecked,
|
|
214
|
+
indeterminate,
|
|
215
|
+
updateIndeterminate,
|
|
216
|
+
nodeMap,
|
|
217
|
+
childToParentMap,
|
|
218
|
+
selectionPropagation,
|
|
219
|
+
} = treeViewStore.getState();
|
|
220
|
+
|
|
221
|
+
// Only recalculate if parent propagation is enabled
|
|
222
|
+
if (!selectionPropagation.toParents) return;
|
|
223
|
+
|
|
224
|
+
const tempChecked = new Set(checked);
|
|
225
|
+
const tempIndeterminate = new Set(indeterminate);
|
|
226
|
+
|
|
227
|
+
// 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
|
|
229
|
+
// all children of a formerly-indeterminate parent are dragged away.
|
|
230
|
+
const parentNodes: ID[] = [];
|
|
231
|
+
for (const [id, node] of nodeMap) {
|
|
232
|
+
if (node.children && node.children.length > 0) {
|
|
233
|
+
parentNodes.push(id);
|
|
234
|
+
} else {
|
|
235
|
+
tempIndeterminate.delete(id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort by depth descending (deepest first) for correct bottom-up propagation
|
|
240
|
+
const nodeDepths = new Map<ID, number>();
|
|
241
|
+
function getDepth(nodeId: ID): number {
|
|
242
|
+
if (nodeDepths.has(nodeId)) return nodeDepths.get(nodeId)!;
|
|
243
|
+
let depth = 0;
|
|
244
|
+
let currentId: ID | undefined = nodeId;
|
|
245
|
+
while (currentId) {
|
|
246
|
+
const parentId = childToParentMap.get(currentId);
|
|
247
|
+
if (parentId) {
|
|
248
|
+
depth++;
|
|
249
|
+
currentId = parentId;
|
|
250
|
+
} else {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
nodeDepths.set(nodeId, depth);
|
|
255
|
+
return depth;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
parentNodes.sort((a, b) => getDepth(b) - getDepth(a));
|
|
259
|
+
|
|
260
|
+
// Update each parent based on its children's current state
|
|
261
|
+
for (const parentId of parentNodes) {
|
|
262
|
+
const node = nodeMap.get(parentId);
|
|
263
|
+
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
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
updateChecked(tempChecked);
|
|
297
|
+
updateIndeterminate(tempIndeterminate);
|
|
298
|
+
}
|