import clsx from 'clsx'; import type { ListChildComponentProps } from 'react-window'; import styles from './TreeView.module.css'; export type NodeInner = { id: string; indent: number; path: string[]; data: T; }; export type BranchNode = NodeInner & { type: 'branch'; collapsed: boolean; }; export type LeafNode = NodeInner & { type: 'leaf'; }; export type Node = BranchNode | LeafNode; export type TreeData = { leafCount: number; nodes: Node[]; highlightedPath: string | null; setHighlightedPath: (id: string | null) => void; }; export type TreeRowProps = ListChildComponentProps> & { rowProps?: React.HTMLProps | null; render: (item: Node) => React.ReactNode; getClasses?: (item: Node) => string[]; onLeafClick?: (item: LeafNode) => void; onHover?: (item: Node) => void; setBranchCollapsed?: (id: string, collapsed: boolean) => void; }; export function TreeRow({ index, style, data: { nodes, highlightedPath, setHighlightedPath }, rowProps, render, getClasses, onLeafClick, onHover, setBranchCollapsed, }: TreeRowProps) { const node = nodes[index]; const classes = [styles.row]; if (node.type === 'branch' && node.collapsed) { classes.push(styles.collapsed); } classes.push(...(getClasses?.(node) ?? [])); const indentItems = []; for (let i = 0; i < node.indent; i++) { indentItems.push( , ); } if (node.type === 'branch') { indentItems.push( , ); } return (
{ if (node.type === 'leaf') { onLeafClick?.(node); } else { const collapsed = !node.collapsed; setBranchCollapsed?.(node.id, collapsed); setHighlightedPath( collapsed ? node.path[node.path.length - 1] : node.id, ); } }} onMouseEnter={() => { onHover?.(node); setHighlightedPath( node.type === 'leaf' || node.collapsed ? node.path[node.path.length - 1] : node.id, ); }} > {indentItems} {render(node)}
); } type NodeWithChildren = BranchNode & { children: (NodeWithChildren | LeafNode)[]; }; export type SimpleTreeNodeData = { label: string }; export type SimpleTreeData = TreeData< SimpleTreeNodeData, L & SimpleTreeNodeData >; // Build a simple tree structure by splitting the path of each item // into components (splitting on '/') and creating a tree node for // each component. export function buildSimpleTree( items: L[], getPath: (item: L) => string, collapsed: Record, highlightedPath: string | null, setHighlightedPath: (id: string | null) => void, ): SimpleTreeData { type SimpleTreeNode = NodeWithChildren< SimpleTreeNodeData, L & SimpleTreeNodeData >; const map = new Map(); const rootNodes = []; for (const item of items) { const path = getPath(item); const split = path.split('/'); let parent: SimpleTreeNode | null = null; for (let i = 0; i < split.length - 1; i++) { const name = split[i]; const dirPath = split.slice(0, i + 1); const key = dirPath.join('/'); let node = map.get(key); if (!node) { node = { type: 'branch', id: key, indent: i, collapsed: !!collapsed[key], children: [], path: buildPath(dirPath), data: { label: name }, }; if (parent) { parent.children.push(node); } else { rootNodes.push(node); } map.set(key, node); } parent = node; } const node: LeafNode = { type: 'leaf', id: path, indent: split.length - 1, path: buildPath(split), data: { ...item, label: split[split.length - 1], }, }; if (parent) { parent.children.push(node); } else { rootNodes.push(node); } } const nodes: SimpleTreeData['nodes'] = []; for (const node of rootNodes) { pushNodes(node, nodes); } return { leafCount: items.length, nodes, highlightedPath, setHighlightedPath, }; } function pushNodes( item: NodeWithChildren | LeafNode, out: Node[], ) { if (item.type === 'branch') { out.push(item); if (!item.collapsed) { for (const child of item.children) { pushNodes(child, out); } } } else if (item.type === 'leaf') { out.push(item); } } function buildPath(split: string[]) { const path = []; for (let i = 0; i < split.length - 1; i++) { path.push(split.slice(0, i + 1).join('/')); } return path; }