import { Folder, FolderOpen } from "lucide-react"; import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react"; import { i18nMessage } from "../i18n/LanguageContext"; export type ExplorerTreeNodeContext = { depth: number; active: boolean; expanded: boolean; hasChildren: boolean; disabled: boolean; }; type ExplorerTreeCommonProps = { nodes: T[]; getNodeId: (node: T) => string; getNodeLabel: (node: T) => string; getNodeChildren: (node: T) => T[]; activeId?: string; onOpen: (node: T, context: ExplorerTreeNodeContext) => void; disabled?: boolean; depth?: number; className?: string; childrenBaseClassName?: string; nodeContainerClassName?: string; nodeWrapBaseClassName?: string; toggleBaseClassName?: string; nodeButtonBaseClassName?: string; renderToggleIcon?: (node: T, context: ExplorerTreeNodeContext) => ReactNode; renderNodeContent?: (node: T, context: ExplorerTreeNodeContext) => ReactNode; renderNodeActions?: (node: T, context: ExplorerTreeNodeContext) => ReactNode; getNodeWrapClassName?: (node: T, context: ExplorerTreeNodeContext) => string | undefined; getNodeWrapStyle?: (node: T, context: ExplorerTreeNodeContext) => CSSProperties | undefined; getNodeButtonClassName?: (node: T, context: ExplorerTreeNodeContext) => string | undefined; getNodeDraggable?: (node: T, context: ExplorerTreeNodeContext) => boolean; onContextMenu?: (event: ReactMouseEvent, node: T, context: ExplorerTreeNodeContext) => void; onDragStart?: (event: ReactDragEvent, node: T, context: ExplorerTreeNodeContext) => void; onDragEnd?: (event: ReactDragEvent, node: T, context: ExplorerTreeNodeContext) => void; onDragOver?: (event: ReactDragEvent, node: T, context: ExplorerTreeNodeContext) => void; onDragLeave?: (event: ReactDragEvent, node: T, context: ExplorerTreeNodeContext) => void; onDrop?: (event: ReactDragEvent, node: T, context: ExplorerTreeNodeContext) => void; }; type CollapsibleExplorerTreeProps = { collapsible?: true; expandedIds: ReadonlySet; onToggle: (node: T, context: ExplorerTreeNodeContext) => void; }; type StaticExplorerTreeProps = { collapsible: false; expandedIds?: never; onToggle?: never; }; export type ExplorerTreeProps = ExplorerTreeCommonProps & ( CollapsibleExplorerTreeProps | StaticExplorerTreeProps ); type SafeNode = { id: string; node: T; }; const EMPTY_EXPANDED_IDS: ReadonlySet = new Set(); export default function ExplorerTree(props: ExplorerTreeProps) { const { nodes, getNodeId, getNodeLabel, getNodeChildren, activeId = "", onOpen, disabled = false, depth = 1, className = "", childrenBaseClassName = "explorer-tree-children file-tree-children", nodeContainerClassName = "", nodeWrapBaseClassName = "explorer-tree-node-wrap file-tree-node-wrap", toggleBaseClassName = "explorer-tree-toggle file-tree-toggle", nodeButtonBaseClassName = "explorer-tree-node file-tree-node", renderToggleIcon, renderNodeContent, renderNodeActions, getNodeWrapClassName, getNodeWrapStyle, getNodeButtonClassName, getNodeDraggable, onContextMenu, onDragStart, onDragEnd, onDragOver, onDragLeave, onDrop } = props; const collapsible = props.collapsible !== false; const expandedIds = props.collapsible === false ? EMPTY_EXPANDED_IDS : props.expandedIds; const onToggle = props.collapsible === false ? undefined : props.onToggle; function pathSafeNodes(levelNodes: T[], ancestorIds: ReadonlySet): SafeNode[] { const siblingIds = new Set(); const safeNodes: SafeNode[] = []; for (const node of levelNodes) { const id = getNodeId(node); if (ancestorIds.has(id) || siblingIds.has(id)) continue; siblingIds.add(id); safeNodes.push({ id, node }); } return safeNodes; } function renderLevel(levelNodes: T[], currentDepth: number, ancestorIds: ReadonlySet, root = false): ReactNode { const safeNodes = pathSafeNodes(levelNodes, ancestorIds); if (safeNodes.length === 0) return null; return (
{safeNodes.map(({ id: nodeId, node }) => { const nextAncestorIds = new Set(ancestorIds); nextAncestorIds.add(nodeId); const children = getNodeChildren(node); const hasChildren = pathSafeNodes(children, nextAncestorIds).length > 0; const expanded = collapsible ? expandedIds.has(nodeId) : hasChildren; const active = activeId === nodeId; const context: ExplorerTreeNodeContext = { depth: currentDepth, active, expanded, hasChildren, disabled }; const draggable = getNodeDraggable?.(node, context) ?? false; const nodeActions = renderNodeActions?.(node, context); const hasNodeActions = nodeActions !== null && nodeActions !== undefined && nodeActions !== false; return (
onContextMenu(event, node, context) : undefined} onDragStart={draggable && onDragStart ? (event) => onDragStart(event, node, context) : undefined} onDragEnd={draggable && onDragEnd ? (event) => onDragEnd(event, node, context) : undefined} onDragOver={onDragOver ? (event) => onDragOver(event, node, context) : undefined} onDragLeave={onDragLeave ? (event) => onDragLeave(event, node, context) : undefined} onDrop={onDrop ? (event) => onDrop(event, node, context) : undefined} > {collapsible ? ( ) : ( )} {hasNodeActions && (
{nodeActions}
)}
{hasChildren && expanded && renderLevel(children, currentDepth + 1, nextAncestorIds)}
); })}
); } return renderLevel(nodes, depth, new Set(), true); }