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.
170 lines (136 loc) • 6.42 kB
text/typescript
import { create, type StoreApi, type UseBoundStore } from "zustand";
import type { SelectionPropagation, TreeNode } from "../types/treeView.types";
import type { DropPosition } from "../types/dragDrop.types";
export type TreeViewState<ID> = {
// Store ids of checked tree nodes
checked: Set<ID>;
updateChecked: (checked: Set<ID>) => void;
// Store ids of indeterminate state nodes
indeterminate: Set<ID>;
updateIndeterminate: (indeterminate: Set<ID>) => void;
// Store ids of expanded parent nodes
expanded: Set<ID>;
updateExpanded: (expanded: Set<ID>) => void;
// Store initial tree view data exactly as passed by the consumer
initialTreeViewData: TreeNode<ID>[];
updateInitialTreeViewData: (initialTreeViewData: TreeNode<ID>[]) => void;
// Map to store the id to the tree node map
nodeMap: Map<ID, TreeNode<ID>>;
updateNodeMap: (nodeMap: Map<ID, TreeNode<ID>>) => void;
// Map to store child id to parent id map
childToParentMap: Map<ID, ID>;
updateChildToParentMap: (childToParentMap: Map<ID, ID>) => void;
// Search text state
searchText: string;
updateSearchText: (searchText: string) => void;
// Search keys state
searchKeys: string[];
updateSearchKeys: (searchKeys: string[]) => void;
// To store inner most children ids - required to un/select all filtered-only nodes
innerMostChildrenIds: ID[];
updateInnerMostChildrenIds: (innerMostChildrenIds: ID[]) => void;
selectionPropagation: SelectionPropagation;
setSelectionPropagation: (
selectionPropagation: SelectionPropagation
) => void;
// Drag-and-drop state
draggedNodeId: ID | null;
updateDraggedNodeId: (draggedNodeId: ID | null) => void;
invalidDragTargetIds: Set<ID>;
updateInvalidDragTargetIds: (invalidDragTargetIds: Set<ID>) => void;
// Drop target state (used by nodes to render their own indicator)
dropTargetNodeId: ID | null;
dropPosition: DropPosition | null;
dropLevel: number | null;
updateDropTarget: (nodeId: ID | null, position: DropPosition | null, level?: number | null) => void;
// Cleanup all states in this store
cleanUpTreeViewStore: () => void;
};
// Map to store individual tree view stores by id
const treeViewStores = new Map<string, UseBoundStore<StoreApi<TreeViewState<unknown>>>>();
// a function that returns a strongly typed version of `treeViewStores`
const typedStore: <ID>() => Map<string, UseBoundStore<StoreApi<TreeViewState<ID>>>> = <ID>() => treeViewStores as Map<string, UseBoundStore<StoreApi<TreeViewState<ID>>>>;
export function getTreeViewStore<ID>(id: string): UseBoundStore<StoreApi<TreeViewState<ID>>> {
if (!typedStore<ID>().has(id)) {
const store = create<TreeViewState<ID>>((set) => ({
checked: new Set(),
updateChecked: (checked: Set<ID>) => set({ checked }),
indeterminate: new Set(),
updateIndeterminate: (indeterminate: Set<ID>) => set({ indeterminate }),
expanded: new Set<ID>(),
updateExpanded: (expanded: Set<ID>) => set({ expanded }),
initialTreeViewData: [],
updateInitialTreeViewData: (initialTreeViewData: TreeNode<ID>[]) => set({
initialTreeViewData
}),
nodeMap: new Map<ID, TreeNode<ID>>(),
updateNodeMap: (nodeMap: Map<ID, TreeNode<ID>>) => set({ nodeMap }),
childToParentMap: new Map<ID, ID>(),
updateChildToParentMap: (childToParentMap: Map<ID, ID>) => set({
childToParentMap
}),
searchText: "",
updateSearchText: (searchText: string) => set({ searchText }),
searchKeys: [""],
updateSearchKeys: (searchKeys: string[]) => set({ searchKeys }),
innerMostChildrenIds: [],
updateInnerMostChildrenIds: (innerMostChildrenIds: ID[]) => set({
innerMostChildrenIds
}),
selectionPropagation: { toChildren: true, toParents: true },
setSelectionPropagation: (selectionPropagation) => set({
selectionPropagation: {
// Default selection propagation for parent and children to true if not specified
toChildren: selectionPropagation.toChildren ?? true,
toParents: selectionPropagation.toParents ?? true
}
}),
draggedNodeId: null,
updateDraggedNodeId: (draggedNodeId) => set({ draggedNodeId }),
invalidDragTargetIds: new Set<ID>(),
updateInvalidDragTargetIds: (invalidDragTargetIds) => set({
invalidDragTargetIds
}),
dropTargetNodeId: null,
dropPosition: null,
dropLevel: null,
updateDropTarget: (nodeId, position, level) => set({
dropTargetNodeId: nodeId,
dropPosition: position,
dropLevel: level ?? null,
}),
cleanUpTreeViewStore: () =>
set({
checked: new Set(),
indeterminate: new Set(),
expanded: new Set<ID>(),
initialTreeViewData: [],
nodeMap: new Map<ID, TreeNode<ID>>(),
childToParentMap: new Map<ID, ID>(),
searchText: "",
searchKeys: [""],
innerMostChildrenIds: [],
selectionPropagation: { toChildren: true, toParents: true },
draggedNodeId: null,
invalidDragTargetIds: new Set<ID>(),
dropTargetNodeId: null,
dropPosition: null,
dropLevel: null,
}),
}));
typedStore<ID>().set(id, store);
}
return typedStore<ID>().get(id)!;
}
export function deleteTreeViewStore(id: string) {
treeViewStores.delete(id);
}
/**
* Returns the per-instance bound Zustand store for the given id. The returned
* store is itself callable as a hook with a selector - e.g.
* `useTreeViewStore(id)(useShallow(state => ...))` - which is why it keeps the
* `use` prefix. Internal helper; not part of the public API.
*/
export function useTreeViewStore<ID = string>(id: string) {
return getTreeViewStore<ID>(id);
}