UNPKG

react-native-tree-multi-select

Version:

A super-fast, customizable tree view component for React Native with multi-selection, checkboxes, and search filtering capabilities.

165 lines (154 loc) 7.07 kB
"use strict"; /** * ScrollToNodeHandler Component * * This component provides an imperative handle to scroll to a specified node within a tree view. * The scrolling action is orchestrated via a two-step "milestone" mechanism that ensures the target * node is both expanded in the tree and that the rendered list reflects this expansion before the scroll * is performed. * * The two key milestones tracked by the `expandAndScrollToNodeQueue` state are: * 1. EXPANDED: Indicates that the expansion logic for the target node has been initiated. * 2. RENDERED: Indicates that the list has re-rendered with the expanded node included. * * When the `scrollToNodeID` method is called: * - The scroll parameters (target node ID, animation preferences, view offset/position) are stored in a ref. * - The target node's expansion is triggered via the `expandNodes` helper. * - The `expandAndScrollToNodeQueue` state is updated to mark that expansion has begun. * * As the component re-renders (e.g., after the node expansion changes the rendered list): * - A useEffect monitors changes to the list, and once it detects the expansion has occurred, * it updates the queue to include the RENDERED milestone. * * A layout effect then waits for both conditions to be met: * - The target node is confirmed to be in the expanded set. * - The `expandAndScrollToNodeQueue` exactly matches the expected milestones ([EXPANDED, RENDERED]). * * Once both conditions are satisfied: * - The index of the target node is determined within the latest flattened node list. * - The flash list is scrolled to that index. * - The queued scroll parameters and milestone queue are reset. * * This design ensures that the scroll action is performed only after the target node is fully present * in the UI, thus preventing issues with attempting to scroll to an element that does not exist yet. */ import React from "react"; import { expandNodes } from "../helpers/expandCollapse.helper"; import { useTreeViewStore } from "../store/treeView.store"; import { useShallow } from "zustand/react/shallow"; import { typedMemo } from "../utils/typedMemo"; import { fastIsEqual } from "fast-is-equal"; // Enum representing the two milestones needed before scrolling var ExpandQueueAction = /*#__PURE__*/function (ExpandQueueAction) { ExpandQueueAction[ExpandQueueAction["EXPANDED"] = 0] = "EXPANDED"; ExpandQueueAction[ExpandQueueAction["RENDERED"] = 1] = "RENDERED"; return ExpandQueueAction; }(ExpandQueueAction || {}); function _innerScrollToNodeHandler(props, ref) { const { storeId, flashListRef, flattenedFilteredNodes, setInitialScrollIndex, initialScrollNodeID } = props; const { expanded, childToParentMap } = useTreeViewStore(storeId)(useShallow(state => ({ expanded: state.expanded, childToParentMap: state.childToParentMap }))); React.useImperativeHandle(ref, () => ({ scrollToNodeID: params => { queuedScrollToNodeParams.current = params; // Mark that expansion is initiated. setExpandAndScrollToNodeQueue([ExpandQueueAction.EXPANDED]); // Trigger expansion logic (this may update the store and subsequently re-render the list). expandNodes(storeId, [queuedScrollToNodeParams.current.nodeId], !queuedScrollToNodeParams.current.expandScrolledNode); } }), [storeId]); // Ref to store the scroll parameters for the queued action. const queuedScrollToNodeParams = React.useRef(null); // State to track progression: first the expansion is triggered, then the list is rendered. const [expandAndScrollToNodeQueue, setExpandAndScrollToNodeQueue] = React.useState([]); const latestFlattenedFilteredNodesRef = React.useRef(flattenedFilteredNodes); /* When the rendered node list changes, update the ref. If an expansion was triggered, mark that the list is now rendered. */ React.useEffect(() => { setExpandAndScrollToNodeQueue(prevQueue => { if (prevQueue.includes(ExpandQueueAction.EXPANDED)) { latestFlattenedFilteredNodesRef.current = flattenedFilteredNodes; return [ExpandQueueAction.EXPANDED, ExpandQueueAction.RENDERED]; } else { return prevQueue; } }); }, [flattenedFilteredNodes]); /* Once the target node is expanded and the list is updated (milestones reached), perform the scroll using the latest node list. */ React.useLayoutEffect(() => { if (queuedScrollToNodeParams.current === null) return; if (!fastIsEqual(expandAndScrollToNodeQueue, [ExpandQueueAction.EXPANDED, ExpandQueueAction.RENDERED])) { return; } // If node is set to not expand if (!queuedScrollToNodeParams.current.expandScrolledNode) { let parentId; // Get the parent's id of the node to scroll to if (childToParentMap.has(queuedScrollToNodeParams.current.nodeId)) { parentId = childToParentMap.get(queuedScrollToNodeParams.current.nodeId); } // Ensure if the parent is expanded before proceeding to scroll to the node if (parentId && !expanded.has(parentId)) return; } // If node is set to expand else { if (!expanded.has(queuedScrollToNodeParams.current.nodeId)) return; } const { nodeId, animated, viewOffset, viewPosition } = queuedScrollToNodeParams.current; function scrollToItem() { const index = latestFlattenedFilteredNodesRef.current.findIndex(item => item.id === nodeId); if (index !== -1 && flashListRef.current) { // Scroll to the target index. flashListRef.current.scrollToIndex({ index, animated, viewOffset, viewPosition }); } else { if (__DEV__) { console.info("Cannot find the item of the mentioned id to scroll in the rendered tree view list data!"); } } // Clear the queued parameters and reset the expansion/render queue. queuedScrollToNodeParams.current = null; setExpandAndScrollToNodeQueue([]); } scrollToItem(); }, [childToParentMap, expanded, flashListRef, expandAndScrollToNodeQueue]); ////////////////////////////// Handle Initial Scroll ///////////////////////////// /* On first render, if an initial scroll target is provided, determine its index. This is done only once. */ const initialScrollDone = React.useRef(false); React.useLayoutEffect(() => { if (initialScrollDone.current) return; const index = flattenedFilteredNodes.findIndex(item => item.id === initialScrollNodeID); setInitialScrollIndex(index); if (index !== -1) { initialScrollDone.current = true; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [flattenedFilteredNodes, initialScrollNodeID]); ///////////////////////////////////////////////////////////////////////////////// return null; } const _ScrollToNodeHandler = /*#__PURE__*/React.forwardRef(_innerScrollToNodeHandler); export const ScrollToNodeHandler = typedMemo(_ScrollToNodeHandler); //# sourceMappingURL=ScrollToNodeHandler.js.map