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.

441 lines (388 loc) 19.1 kB
import { ComponentType, RefObject } from "react"; import type { GestureResponderEvent, StyleProp, TextProps, TouchableOpacityProps, ViewStyle } from "react-native"; import type { FlashListProps } from "@shopify/flash-list"; import type { ScrollToNodeHandlerRef, ScrollToNodeParams } from "../hooks/useScrollToNode"; import type { CheckboxProps as _CheckboxProps } from "@futurejj/react-native-checkbox"; import type { DragCancelEvent, DragEndEvent, DragStartEvent, DropPosition, MoveResult } from "./dragDrop.types"; /** The tri-state value of a checkbox: checked, unchecked, or indeterminate */ export type CheckboxValueType = boolean | "indeterminate"; /** Props passed to a custom expand/collapse icon component */ export interface ExpandIconProps { /** Whether the node is currently expanded */ isExpanded: boolean; } /** A single node in the tree data structure */ export interface TreeNode<ID = string> { /** Unique identifier for this node */ id: ID; /** Display name for this node */ name: string; /** Child nodes (omit or empty array for leaf nodes) */ children?: TreeNode<ID>[]; /** Additional custom data fields */ [key: string]: any; } /** Internal flattened representation of a tree node with its nesting level */ export interface __FlattenedTreeNode__<ID = string> extends TreeNode<ID> { /** Nesting depth of this node (0 = root level) */ level?: number; } /** FlashList props available for customization (excludes `data` and `renderItem`) */ export type TreeFlatListProps<ItemT = any> = Omit< FlashListProps<ItemT>, "data" | "renderItem" >; /** Props passed to a custom node row component */ export interface NodeRowProps<ID = string> { /** The tree node to render */ node: TreeNode<ID>; /** Nesting depth of this node (0 = root level) */ level: number; /** Current checkbox state of this node */ checkedValue: CheckboxValueType; /** Whether this node's children are currently visible */ isExpanded: boolean; /** Callback to toggle this node's checked state */ onCheck: () => void; /** Callback to toggle this node's expanded/collapsed state */ onExpand: () => void; /** Whether this node is an invalid drop target during a drag operation */ isInvalidDropTarget?: boolean; /** Whether this node is the current valid drop target */ isDropTarget?: boolean; /** The drop position if this node is the current drop target */ dropPosition?: DropPosition; /** Whether a drag operation is currently in progress */ isDragging?: boolean; /** Whether this node is the one being dragged */ isDraggedNode?: boolean; /** Props to spread on a drag handle element. Attach to a specific View to * make only that area initiate drag, or spread on the root for whole-row drag. * Only present when drag-and-drop is enabled. */ dragHandleProps?: DragHandleProps; } /** Touch handlers to spread on a drag handle element within a custom node row */ export interface DragHandleProps { /** Arms the long-press timer that initiates a drag. Fires on finger down. */ onTouchStart: (e: GestureResponderEvent) => void; /** Cancels a pending long-press, or ends a drag the PanResponder never * captured (finger lifted without movement). Fires on finger up. */ onTouchEnd: () => void; /** Cancels a pending long-press when the system interrupts the touch. */ onTouchCancel: () => void; } /** Customization options for tree item appearance and behavior */ export interface TreeItemCustomizations<ID = string> { /** Style props for the built-in checkbox view */ checkBoxViewStyleProps?: BuiltInCheckBoxViewStyleProps; /** Pixels of indentation per nesting level. Default: 15 */ indentationMultiplier?: number; /** Custom checkbox component replacing the built-in checkbox */ CheckboxComponent?: ComponentType<CheckBoxViewProps>; /** Custom expand/collapse icon component */ ExpandCollapseIconComponent?: ComponentType<ExpandIconProps>; /** Custom touchable component wrapping the expand/collapse icon */ ExpandCollapseTouchableComponent?: ComponentType<TouchableOpacityProps>; /** Fully custom node row component replacing the entire built-in row */ CustomNodeRowComponent?: ComponentType<NodeRowProps<ID>>; } /** Internal props for a single node in the list (extends TreeItemCustomizations) */ export interface NodeProps<ID = string> extends TreeItemCustomizations<ID> { /** The flattened tree node data */ node: __FlattenedTreeNode__<ID>; /** Nesting depth of this node */ level: number; /** Internal store identifier */ storeId: string; /** Index of this node in the flattened list */ nodeIndex?: number; /** Whether drag-and-drop is enabled */ dragEnabled?: boolean; /** Whether a drag operation is currently in progress */ isDragging?: boolean; /** Callback when a touch starts on this node (used for drag initiation) */ onNodeTouchStart?: ( nodeId: ID, pageY: number, locationY: number, nodeIndex: number ) => void; /** Callback when a touch ends on this node */ onNodeTouchEnd?: () => void; /** Callback reporting this node's measured height, keyed by its stable node id * (used for accurate variable-height drop targeting) */ onItemLayout?: (id: ID, height: number) => void; /** Customizations for drag-and-drop visuals */ dragDropCustomizations?: DragDropCustomizations<ID>; } /** Options for auto-scrolling to the dropped node after a successful drop. * Uses the same scroll parameters as `scrollToNodeID` (minus internally managed fields). */ export interface DropAutoScrollOptions extends Omit<ScrollToNodeParams<never>, "nodeId" | "expandScrolledNode"> { /** Enable auto-scroll to the dropped node. Default: true */ enabled?: boolean; } /** Configuration options for drag-and-drop reordering */ export interface DragAndDropOptions<ID = string> { /** Enable drag-and-drop reordering. Default: true (when dragAndDrop is provided) */ enabled?: boolean; /** Callback fired when a drag operation begins */ onDragStart?: (event: DragStartEvent<ID>) => void; /** Callback fired after a node is successfully dropped at a new position */ onDragEnd?: (event: DragEndEvent<ID>) => void; /** Callback fired when a drag is cancelled without a successful drop */ onDragCancel?: (event: DragCancelEvent<ID>) => void; /** Long press duration in ms to start drag. Default: 400 */ longPressDuration?: number; /** Distance from edge (px) to trigger auto-scroll during drag. Default: 60 */ autoScrollThreshold?: number; /** Speed multiplier for auto-scroll during drag. Default: 1.0 */ autoScrollSpeed?: number; /** Offset of the dragged overlay from the finger, in item-height units. Default: -2 (two items above finger) */ dragOverlayOffset?: number; /** Advanced: extra vertical correction for the drag overlay, in item-height units, * added on top of `dragOverlayOffset`. Compensates for Android reporting touch * `locationY` differently from iOS. Default: -2 on Android, 0 on other platforms. * Override only if the overlay sits noticeably off from the finger on a device. */ overlayYCorrection?: number; /** Delay in ms before auto-expanding a collapsed node during drag hover. Default: 800 */ autoExpandDelay?: number; /** Auto-expand a collapsed node while hovering "inside" it during a drag. * Default: true. Set false to disable hover-to-expand entirely. */ autoExpand?: boolean; /** Animate the drag overlay with a magnetic "snap" spring when the effective drop * level changes. Default: true. Set false to keep the overlay tracking the level * without the spring animation (e.g. for reduced-motion preferences). */ magneticSnap?: boolean; /** Customizations for drag-and-drop visuals (overlay, indicator, opacity) */ customizations?: DragDropCustomizations<ID>; /** Callback to determine if a node can be dropped on a specific target. * Return false to grey out the target and suppress the drop indicator. */ canDrop?: (draggedNode: TreeNode<ID>, targetNode: TreeNode<ID>, position: DropPosition) => boolean; /** Maximum nesting depth allowed. Drops that would exceed this depth are suppressed. */ maxDepth?: number; /** Callback to determine if a node can accept children. * Return false to suppress the "inside" drop zone for that node. */ canNodeHaveChildren?: (node: TreeNode<ID>) => boolean; /** Callback to determine if a node can be dragged. * Return false to prevent dragging this node. Default: all nodes are draggable. */ canDrag?: (node: TreeNode<ID>) => boolean; /** Auto-scroll to the dropped node after a successful drop, if it ended up * outside the viewport (no scroll when the node is already visible). * Pass `false` to disable, `true` for defaults, or an object to customize. * Default: `{ enabled: true, animated: true, viewPosition: 0.5 }` */ autoScrollToDroppedNode?: boolean | DropAutoScrollOptions; } /** Props for the NodeList component that renders the flattened tree */ export interface NodeListProps<ID = string> extends TreeItemCustomizations<ID> { /** Additional props passed to the underlying FlashList */ treeFlashListProps?: TreeFlatListProps; /** Ref for programmatic scroll-to-node functionality */ scrollToNodeHandlerRef: RefObject<ScrollToNodeHandlerRef<ID>>; /** Node ID to scroll to on initial render */ initialScrollNodeID?: ID; /** Internal store identifier */ storeId: string; /** Drag-and-drop configuration */ dragAndDrop?: DragAndDropOptions<ID>; } /** Props for the TreeView component */ export interface TreeViewProps<ID = string> extends Omit< NodeListProps<ID>, "storeId" | "scrollToNodeHandlerRef" > { /** The tree data to render */ data: TreeNode<ID>[]; /** Callback fired when checked nodes change. Receives checked and indeterminate node IDs. */ onCheck?: (checkedIds: ID[], indeterminateIds: ID[]) => void; /** Callback fired when expanded nodes change. Receives all currently expanded node IDs. */ onExpand?: (expandedIds: ID[]) => void; /** Node IDs that should be checked on initial render */ preselectedIds?: ID[]; /** Node IDs that should be expanded on initial render */ preExpandedIds?: ID[]; /** Controls whether checking a node propagates to its children and/or parents */ selectionPropagation?: SelectionPropagation; /** Drag-and-drop configuration */ dragAndDrop?: DragAndDropOptions<ID>; } type CheckboxProps = Omit<_CheckboxProps, "onPress" | "status">; /** Props for the checkbox view component */ export interface CheckBoxViewProps { /** Current checkbox state */ value: CheckboxValueType; /** Callback when the checkbox value changes */ onValueChange: (value: boolean) => void; /** Label text displayed next to the checkbox */ text: string; /** Test ID for testing frameworks */ testID?: string; } /** Style props for customizing the built-in checkbox view */ export interface BuiltInCheckBoxViewStyleProps { /** Style for the outermost container wrapping the checkbox and text */ outermostParentViewStyle?: StyleProp<ViewStyle>; /** Style for the view wrapping the checkbox itself */ checkboxParentViewStyle?: StyleProp<ViewStyle>; /** Style for the touchable area wrapping the label text */ textTouchableStyle?: StyleProp<ViewStyle>; /** Additional props passed to the underlying Checkbox component */ checkboxProps?: CheckboxProps; /** Props passed to the label Text component */ textProps?: TextProps; } /** Combined props for the built-in checkbox view (CheckBoxViewProps + style props) */ export type BuiltInCheckBoxViewProps = CheckBoxViewProps & BuiltInCheckBoxViewStyleProps; /** Ref handle exposed by the TreeView component for imperative operations */ export interface TreeViewRef<ID = string> { /** Select (check) all nodes in the tree */ selectAll: () => void; /** Unselect (uncheck) all nodes in the tree */ unselectAll: () => void; /** Select all nodes that match the current search filter */ selectAllFiltered: () => void; /** Unselect all nodes that match the current search filter */ unselectAllFiltered: () => void; /** Expand all nodes in the tree */ expandAll: () => void; /** Collapse all nodes in the tree */ collapseAll: () => void; /** Expand specific nodes by their IDs */ expandNodes: (ids: ID[]) => void; /** Collapse specific nodes by their IDs */ collapseNodes: (ids: ID[]) => void; /** Select (check) specific nodes by their IDs */ selectNodes: (ids: ID[]) => void; /** Unselect (uncheck) specific nodes by their IDs */ unselectNodes: (ids: ID[]) => void; /** Set the search text and optionally specify which node fields to search */ setSearchText: (searchText: string, searchKeys?: string[]) => void; /** Programmatically scroll to a specific node by its ID */ scrollToNodeID: (scrollToNodeParams: ScrollToNodeParams<ID>) => void; /** Get a map of child node IDs to their parent node IDs */ getChildToParentMap: () => Map<ID, ID>; /** Get the current tree data held by the component (the original `data` prop * before any move, or the reordered structure after a drag-and-drop / `moveNode`). * Useful for reading the full tree without pushing it through the `onDragEnd` / * `moveNode` move delta. * * NOTE: this returns a LIVE internal reference - treat it as read-only and do * NOT mutate it in place. Mutating it desyncs the internal node maps and the * reinit-skip diff. Clone it (e.g. with the exported `moveTreeNode`, or * `structuredClone`) before modifying. */ getTreeData: () => TreeNode<ID>[]; /** Programmatically move a node to a new position in the tree. * Works like a drag-and-drop but without user interaction. * Returns a lightweight {@link MoveResult} describing the move, or `null` if it * was a no-op / invalid (e.g. moving a node onto itself or into its own * descendant, or - with `{ validate: true }` - blocked by `canDrop`, * `maxDepth`, or `canNodeHaveChildren`). * * `{ validate: true }` enforces the `canDrop` / `maxDepth` / `canNodeHaveChildren` * rules - but those rules live on the `dragAndDrop` prop, so validation only runs * when a `dragAndDrop` prop is configured. Without it, `validate` is ignored (in * dev a warning is logged) and the move proceeds subject only to the structural * no-op checks. * * `{ scrollToNode: true }` scrolls the moved node into view after the move (the * interactive drag does this automatically; programmatic moves do not by default). * Pass a {@link DropAutoScrollOptions} object to customize the scroll. */ moveNode: ( nodeId: ID, targetNodeId: ID, position: DropPosition, options?: { validate?: boolean; scrollToNode?: boolean | DropAutoScrollOptions; } ) => MoveResult<ID> | null; } /** Controls how checkbox selection propagates through the tree hierarchy */ export interface SelectionPropagation { /** Whether checking a parent node automatically checks all its children. Default: true */ toChildren?: boolean; /** Whether checking all children automatically checks their parent. Default: true */ toParents?: boolean; } // --- Drag-and-drop customization types --- /** Props for the drop indicator rendered on the target node during drag */ export interface DropIndicatorComponentProps { /** Whether the indicator is above, below, or inside the target node */ position: DropPosition; /** The nesting level of the target node (useful for indenting the indicator) */ level: number; /** The indentation multiplier used for each level (pixels per level) */ indentationMultiplier: number; } /** Style props for customizing the built-in drop indicator appearance */ export interface DropIndicatorStyleProps { /** Color of the line indicator (above/below). Default: "#0078FF" */ lineColor?: string; /** Thickness of the line indicator. Default: 3 */ lineThickness?: number; /** Diameter of the circle at the line's start. Default: 10 */ circleSize?: number; /** Background color of the "inside" highlight. Default: "rgba(0, 120, 255, 0.15)" */ highlightColor?: string; /** Border color of the "inside" highlight. Default: "rgba(0, 120, 255, 0.5)" */ highlightBorderColor?: string; /** Border width of the "inside" highlight box. Default: 2 */ highlightBorderWidth?: number; /** Corner radius of the "inside" highlight box. Default: 4 */ highlightBorderRadius?: number; } /** Style props for customizing the drag overlay (the "lifted" node ghost) */ export interface DragOverlayStyleProps { /** Background color of the overlay. Default: "rgba(255, 255, 255, 0.95)" */ backgroundColor?: string; /** Shadow color. Default: "#000" */ shadowColor?: string; /** Shadow offset (iOS). Default: { width: 0, height: 2 } */ shadowOffset?: { width: number; height: number; }; /** Shadow opacity. Default: 0.25 */ shadowOpacity?: number; /** Shadow radius. Default: 4 */ shadowRadius?: number; /** Android elevation. Default: 10 */ elevation?: number; /** Stacking order of the overlay. Default: 9999 */ zIndex?: number; /** Custom style applied to the overlay container */ style?: StyleProp<ViewStyle>; } /** Combined drag-and-drop customization props */ export interface DragDropCustomizations<ID = string> { /** Opacity applied to the node being dragged. Default: 0.3 */ draggedNodeOpacity?: number; /** Opacity applied to invalid drop targets during drag. Default: 0.3 */ invalidTargetOpacity?: number; /** Style props for the built-in drop indicator */ dropIndicatorStyleProps?: DropIndicatorStyleProps; /** Style props for the drag overlay (lifted node ghost) */ dragOverlayStyleProps?: DragOverlayStyleProps; /** Fully custom drop indicator component - replaces the built-in line/highlight */ CustomDropIndicatorComponent?: ComponentType<DropIndicatorComponentProps>; /** Fully custom drag overlay component - replaces the built-in ghost node */ CustomDragOverlayComponent?: ComponentType<DragOverlayComponentProps<ID>>; } /** Props passed to a custom drag overlay component */ export interface DragOverlayComponentProps<ID = string> { /** The node being dragged */ node: TreeNode<ID>; /** The nesting level of the dragged node */ level: number; /** The current checkbox value of the dragged node */ checkedValue: CheckboxValueType; }