import type { createStore } from 'jotai'; import type { ReactNode } from 'react'; import type { CollapsedAtom } from './context'; import { selectedIdsFamily } from './atoms'; export type JotaiStore = ReturnType; export type ContextMenuRenderer = (props: { items: unknown[]; position: { x: number; y: number }; onClose: () => void; }) => ReactNode; export interface TreeNode { children?: TreeNode[]; item: T; hidden?: boolean; parent: TreeNode | null; depth: number; draggable?: boolean; localDrag?: boolean; } export interface SelectableTreeNode { node: TreeNode; depth: number; index: number; } export function getSelectedItems( store: JotaiStore, treeId: string, selectableItems: SelectableTreeNode[], ) { const selectedItemIds = store.get(selectedIdsFamily(treeId)); return selectableItems .filter((i) => selectedItemIds.includes(i.node.item.id)) .map((i) => i.node.item); } export function equalSubtree( a: TreeNode, b: TreeNode, getItemKey: (t: T) => string, ): boolean { if (getNodeKey(a, getItemKey) !== getNodeKey(b, getItemKey)) return false; const ak = a.children ?? []; const bk = b.children ?? []; if (ak.length !== bk.length) { return false; } for (let i = 0; i < ak.length; i++) { // biome-ignore lint/style/noNonNullAssertion: none if (!equalSubtree(ak[i]!, bk[i]!, getItemKey)) return false; } return true; } export function getNodeKey(a: TreeNode, getItemKey: (i: T) => string) { return getItemKey(a.item) + a.hidden; } export function hasAncestor(node: TreeNode, ancestorId: string) { if (node.parent == null) return false; if (node.parent.item.id === ancestorId) return true; // Check parents recursively return hasAncestor(node.parent, ancestorId); } export function isVisibleNode(store: JotaiStore, collapsedAtom: CollapsedAtom, node: TreeNode) { const collapsed = store.get(collapsedAtom); let p = node.parent; while (p) { if (collapsed[p.item.id]) return false; // any collapsed ancestor hides this node p = p.parent; } return true; } export function closestVisibleNode( store: JotaiStore, collapsedAtom: CollapsedAtom, node: TreeNode, ): TreeNode | null { let n: TreeNode | null = node; while (n) { if (isVisibleNode(store, collapsedAtom, n) && !n.hidden) return n; if (n.parent == null) return null; n = n.parent; } return null; }