'use client'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react'; import React, { createContext, forwardRef, useCallback, useContext, useEffect, useState, } from 'react'; import { cn } from '../../lib/utils'; import { Button } from './button'; import { ScrollArea } from './scroll-area'; type TreeViewElement = { id: string; name: string; isSelectable?: boolean; children?: TreeViewElement[]; }; type TreeContextProps = { selectedId: string | undefined; expandedItems: string[] | undefined; indicator: boolean; handleExpand: (id: string) => void; selectItem: (id: string) => void; setExpandedItems?: React.Dispatch>; openIcon?: React.ReactNode; closeIcon?: React.ReactNode; direction: 'rtl' | 'ltr'; }; const TreeContext = createContext(null); const useTree = () => { const context = useContext(TreeContext); if (!context) { throw new Error('useTree must be used within a TreeProvider'); } return context; }; interface TreeViewComponentProps extends React.HTMLAttributes {} type Direction = 'rtl' | 'ltr' | undefined; type TreeViewProps = { initialSelectedId?: string; indicator?: boolean; elements?: TreeViewElement[]; initialExpandedItems?: string[]; openIcon?: React.ReactNode; closeIcon?: React.ReactNode; } & TreeViewComponentProps; const Tree = forwardRef( ( { className, elements, initialSelectedId, initialExpandedItems, children, indicator = true, openIcon, closeIcon, dir, ...props }, ref, ) => { const [selectedId, setSelectedId] = useState(initialSelectedId); const [expandedItems, setExpandedItems] = useState(initialExpandedItems); const selectItem = useCallback((id: string) => { setSelectedId(id); }, []); const handleExpand = useCallback((id: string) => { setExpandedItems((prev) => { if (prev?.includes(id)) { return prev.filter((item) => item !== id); } return [...(prev ?? []), id]; }); }, []); const expandSpecificTargetedElements = useCallback( (elements?: TreeViewElement[], selectId?: string) => { if (!elements || !selectId) return; const findParent = (currentElement: TreeViewElement, currentPath: string[] = []) => { const isSelectable = currentElement.isSelectable ?? true; const newPath = [...currentPath, currentElement.id]; if (currentElement.id === selectId) { if (isSelectable) { setExpandedItems((prev) => [...(prev ?? []), ...newPath]); } else { if (newPath.includes(currentElement.id)) { newPath.pop(); setExpandedItems((prev) => [...(prev ?? []), ...newPath]); } } return; } if (isSelectable && currentElement.children && currentElement.children.length > 0) { currentElement.children.forEach((child) => { findParent(child, newPath); }); } }; elements.forEach((element) => { findParent(element); }); }, [], ); useEffect(() => { if (initialSelectedId) { expandSpecificTargetedElements(elements, initialSelectedId); } }, [initialSelectedId, elements]); const direction = dir === 'rtl' ? 'rtl' : 'ltr'; return (
setExpandedItems((prev) => [...(prev ?? []), value[0]])} dir={dir as Direction} > {children}
); }, ); Tree.displayName = 'Tree'; const TreeIndicator = forwardRef>( ({ className, ...props }, ref) => { const { direction } = useTree(); return (
); }, ); TreeIndicator.displayName = 'TreeIndicator'; interface FolderComponentProps extends React.ComponentPropsWithoutRef {} type FolderProps = { expandedItems?: string[]; element: string; isSelectable?: boolean; isSelect?: boolean; } & FolderComponentProps; const Folder = forwardRef>( ({ className, element, value, isSelectable = true, isSelect, children, ...props }, ref) => { const { direction, handleExpand, expandedItems, indicator, setExpandedItems, openIcon, closeIcon, } = useTree(); return ( handleExpand(value)} > {expandedItems?.includes(value) ? (openIcon ?? ) : (closeIcon ?? )} {element} {element && indicator && ); }, ); Folder.displayName = 'Folder'; const File = forwardRef< HTMLButtonElement, { value: string; handleSelect?: (id: string) => void; isSelectable?: boolean; isSelect?: boolean; fileIcon?: React.ReactNode; } & React.ComponentPropsWithoutRef >( ( { value, className, handleSelect, isSelectable = true, isSelect, fileIcon, children, ...props }, ref, ) => { const { direction, selectedId, selectItem } = useTree(); const isSelected = isSelect ?? selectedId === value; return ( selectItem(value)} > {fileIcon ?? } {children} ); }, ); File.displayName = 'File'; const CollapseButton = forwardRef< HTMLButtonElement, { elements: TreeViewElement[]; expandAll?: boolean; } & React.HTMLAttributes >(({ className, elements, expandAll = false, children, ...props }, ref) => { const { expandedItems, setExpandedItems } = useTree(); const expendAllTree = useCallback((elements: TreeViewElement[]) => { const expandTree = (element: TreeViewElement) => { const isSelectable = element.isSelectable ?? true; if (isSelectable && element.children && element.children.length > 0) { setExpandedItems?.((prev) => [...(prev ?? []), element.id]); element.children.forEach(expandTree); } }; elements.forEach(expandTree); }, []); const closeAll = useCallback(() => { setExpandedItems?.([]); }, []); useEffect(() => { console.log(expandAll); if (expandAll) { expendAllTree(elements); } }, [expandAll]); return ( ); }); CollapseButton.displayName = 'CollapseButton'; export { CollapseButton, File, Folder, Tree, type TreeViewElement };