UNPKG

react-native-tree-multi-select

Version:

Super-fast, customizable tree view component for React Native with drag-and-drop reordering, multi-selection, and search filtering.

256 lines (242 loc) 8.03 kB
"use strict"; import { getTreeViewStore } from "../store/treeView.store.js"; import { initializeNodeMaps } from "./treeNode.helper.js"; import { recalculateCheckedStates } from "./toggleCheckbox.helper.js"; import { expandNodes } from "./expandCollapse.helper.js"; /** * Move a node within a tree structure. Returns a new tree (no mutation). * * @param data - The current tree data * @param draggedNodeId - The ID of the node to move * @param targetNodeId - The ID of the target node * @param position - Where to place relative to target: "above", "below", or "inside" * @returns New tree data with the node moved, or the original data if the move is invalid */ export function moveTreeNode(data, draggedNodeId, targetNodeId, position) { if (draggedNodeId === targetNodeId) return data; // Prevent moving a node into its own descendant (would create a cycle) if (isDescendant(data, draggedNodeId, targetNodeId)) return data; // Step 1: Deep clone the tree const cloned = deepCloneTree(data); // Step 2: Remove the dragged node const removedNode = removeNodeById(cloned, draggedNodeId); if (!removedNode) return data; // Step 3: Insert at the new position const inserted = insertNode(cloned, removedNode, targetNodeId, position); if (!inserted) return data; return cloned; } /** * Commit the result of a `moveTreeNode` to the store: swap in the new tree, * rebuild the node maps, recalculate parent checked/indeterminate states, and * expand whatever is needed to make the moved node visible ("inside" drops * expand the target; ancestors of the moved node are always expanded). * * Shared by the interactive drag commit (useDragDrop.handleDragEnd) and the * programmatic `TreeViewRef.moveNode` so the two paths cannot drift. */ export function applyMoveToStore(storeId, newData, movedNodeId, targetNodeId, position) { const store = getTreeViewStore(storeId); store.getState().updateInitialTreeViewData(newData); initializeNodeMaps(storeId, newData); recalculateCheckedStates(storeId); if (position === "inside") { expandNodes(storeId, [targetNodeId]); } expandNodes(storeId, [movedNodeId], true); } /** * Locate a node within a tree, returning its parent id (null at root) and its * index within that parent's children (or the root array). Returns null if the * node is not found. Iterative (stack-based) DFS. * * Used to build the lightweight `MoveResult` delta (previous/new parent + index) * without exposing a full tree copy. */ export function findNodePosition(data, nodeId) { const stack = [{ nodes: data, parentId: null }]; while (stack.length > 0) { const { nodes, parentId } = stack.pop(); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.id === nodeId) return { parentId, index: i }; if (node.children?.length) { stack.push({ nodes: node.children, parentId: node.id }); } } } return null; } /** * `findNodePosition` for a node that is still in the store's CURRENT tree: * uses the already-built childToParentMap/nodeMap for O(depth + siblings) * instead of a full-tree DFS. Only valid while the maps match `data` (i.e. * before the move mutates the tree). */ export function findNodePositionFromMaps(data, nodeMap, childToParentMap, nodeId) { const parentId = childToParentMap.get(nodeId); const siblings = parentId !== undefined ? nodeMap.get(parentId)?.children : data; if (!siblings) return null; const index = siblings.findIndex(n => n.id === nodeId); return index === -1 ? null : { parentId: parentId ?? null, index }; } /** * Check if `candidateDescendantId` is a descendant of `ancestorId` in the tree. * Iterative (stack-based) DFS to avoid call-stack limits on deep trees. */ function isDescendant(nodes, ancestorId, candidateDescendantId) { const stack = [...nodes]; while (stack.length > 0) { const node = stack.pop(); if (node.id === ancestorId) { // Found the ancestor - search its subtree for the candidate. // IDs are unique, so the first match is the only ancestor to check. return containsNode(node.children ?? [], candidateDescendantId); } if (node.children) { for (const child of node.children) stack.push(child); } } return false; } /** * Check if a node with the given ID exists anywhere in the subtree. * Iterative (stack-based) DFS to avoid call-stack limits on deep trees. */ function containsNode(nodes, nodeId) { const stack = [...nodes]; while (stack.length > 0) { const node = stack.pop(); if (node.id === nodeId) return true; if (node.children) { for (const child of node.children) stack.push(child); } } return false; } /** * Deep clone a tree structure so mutations don't affect the original. * Iterative (stack-based) clone to avoid call-stack limits on deep trees. * Preserves the original shape: every node carries a `children` key * (`undefined` for leaves, a cloned array - possibly empty - otherwise). */ function deepCloneTree(nodes) { const root = nodes.map(node => ({ ...node, children: undefined })); const stack = [{ src: nodes, dst: root }]; while (stack.length > 0) { const { src, dst } = stack.pop(); for (let i = 0; i < src.length; i++) { const children = src[i].children; if (children) { const clonedChildren = children.map(child => ({ ...child, children: undefined })); dst[i].children = clonedChildren; stack.push({ src: children, dst: clonedChildren }); } } } return root; } /** * Remove a node by ID from the tree. Mutates the cloned tree in-place. * Returns the removed node, or null if not found. * Iterative (stack-based) DFS to avoid call-stack limits on deep trees. */ function removeNodeById(nodes, nodeId) { // Each frame carries the array being scanned plus the node that owns it // (null at the root) so an emptied children array can be detached. const stack = [{ nodes, parent: null }]; while (stack.length > 0) { const { nodes: level, parent } = stack.pop(); for (let i = 0; i < level.length; i++) { const node = level[i]; if (node.id === nodeId) { const [removed] = level.splice(i, 1); // Clean up an emptied children array on the owning parent // (matches the original shape: leaves carry children: undefined). if (parent && level.length === 0) { parent.children = undefined; } /* istanbul ignore next -- splice at a found index always yields the element; noUncheckedIndexedAccess guard */ return removed ?? null; } if (node.children?.length) { stack.push({ nodes: node.children, parent: node }); } } } return null; } /** * Insert a node relative to a target node. Mutates the cloned tree in-place. * Returns true if insertion was successful. * Iterative (stack-based) DFS to avoid call-stack limits on deep trees. */ function insertNode(nodes, nodeToInsert, targetId, position) { const stack = [nodes]; while (stack.length > 0) { const level = stack.pop(); for (let i = 0; i < level.length; i++) { const node = level[i]; if (node.id === targetId) { if (position === "above") { level.splice(i, 0, nodeToInsert); } else if (position === "below") { level.splice(i + 1, 0, nodeToInsert); } else { // "inside" - add as first child if (node.children) { node.children.unshift(nodeToInsert); } else { node.children = [nodeToInsert]; } } return true; } if (node.children?.length) { stack.push(node.children); } } } return false; } //# sourceMappingURL=moveTreeNode.helper.js.map