|
|
|
@@ -14,7 +14,7 @@ const selectedTreeVariants = cva( |
|
|
|
'before:opacity-100 before:bg-accent/70 text-accent-foreground', |
|
|
|
); |
|
|
|
|
|
|
|
interface TreeDataItem { |
|
|
|
export interface TreeDataItem { |
|
|
|
id: string; |
|
|
|
name: string; |
|
|
|
icon?: any; |
|
|
|
@@ -25,84 +25,147 @@ interface TreeDataItem { |
|
|
|
onClick?: () => void; |
|
|
|
} |
|
|
|
|
|
|
|
const AccordionTrigger = React.forwardRef< |
|
|
|
React.ElementRef<typeof AccordionPrimitive.Trigger>, |
|
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> |
|
|
|
>(({ className, children, ...props }, ref) => ( |
|
|
|
<AccordionPrimitive.Header> |
|
|
|
<AccordionPrimitive.Trigger |
|
|
|
ref={ref} |
|
|
|
className={cn( |
|
|
|
'flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90', |
|
|
|
className, |
|
|
|
)} |
|
|
|
{...props} |
|
|
|
> |
|
|
|
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 mr-1" /> |
|
|
|
{children} |
|
|
|
</AccordionPrimitive.Trigger> |
|
|
|
</AccordionPrimitive.Header> |
|
|
|
)); |
|
|
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; |
|
|
|
type TreeProps = React.HTMLAttributes<HTMLDivElement> & { |
|
|
|
data: TreeDataItem[] | TreeDataItem; |
|
|
|
initialSelectedItemId?: string; |
|
|
|
onSelectChange?: (item: TreeDataItem | undefined) => void; |
|
|
|
expandAll?: boolean; |
|
|
|
defaultNodeIcon?: any; |
|
|
|
defaultLeafIcon?: any; |
|
|
|
}; |
|
|
|
|
|
|
|
const AccordionContent = React.forwardRef< |
|
|
|
React.ElementRef<typeof AccordionPrimitive.Content>, |
|
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> |
|
|
|
>(({ className, children, ...props }, ref) => ( |
|
|
|
<AccordionPrimitive.Content |
|
|
|
ref={ref} |
|
|
|
className={cn( |
|
|
|
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down', |
|
|
|
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>( |
|
|
|
( |
|
|
|
{ |
|
|
|
data, |
|
|
|
initialSelectedItemId, |
|
|
|
onSelectChange, |
|
|
|
expandAll, |
|
|
|
defaultLeafIcon, |
|
|
|
defaultNodeIcon, |
|
|
|
className, |
|
|
|
)} |
|
|
|
{...props} |
|
|
|
> |
|
|
|
<div className="pb-1 pt-0">{children}</div> |
|
|
|
</AccordionPrimitive.Content> |
|
|
|
)); |
|
|
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName; |
|
|
|
...props |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => { |
|
|
|
const [selectedItemId, setSelectedItemId] = React.useState< |
|
|
|
string | undefined |
|
|
|
>(initialSelectedItemId); |
|
|
|
|
|
|
|
const TreeIcon = ({ |
|
|
|
item, |
|
|
|
isOpen, |
|
|
|
isSelected, |
|
|
|
default: defaultIcon, |
|
|
|
}: { |
|
|
|
item: TreeDataItem; |
|
|
|
isOpen?: boolean; |
|
|
|
isSelected?: boolean; |
|
|
|
default?: any; |
|
|
|
}) => { |
|
|
|
let Icon = defaultIcon; |
|
|
|
if (isSelected && item.selectedIcon) { |
|
|
|
Icon = item.selectedIcon; |
|
|
|
} else if (isOpen && item.openIcon) { |
|
|
|
Icon = item.openIcon; |
|
|
|
} else if (item.icon) { |
|
|
|
Icon = item.icon; |
|
|
|
} |
|
|
|
return Icon ? <Icon className="h-4 w-4 shrink-0 mr-2" /> : <></>; |
|
|
|
}; |
|
|
|
const handleSelectChange = React.useCallback( |
|
|
|
(item: TreeDataItem | undefined) => { |
|
|
|
setSelectedItemId(item?.id); |
|
|
|
if (onSelectChange) { |
|
|
|
onSelectChange(item); |
|
|
|
} |
|
|
|
}, |
|
|
|
[onSelectChange], |
|
|
|
); |
|
|
|
|
|
|
|
const TreeActions = ({ |
|
|
|
children, |
|
|
|
isSelected, |
|
|
|
}: { |
|
|
|
children: React.ReactNode; |
|
|
|
isSelected: boolean; |
|
|
|
}) => { |
|
|
|
return ( |
|
|
|
<div |
|
|
|
className={cn( |
|
|
|
isSelected ? 'block' : 'hidden', |
|
|
|
'absolute right-3 group-hover:block', |
|
|
|
)} |
|
|
|
> |
|
|
|
{children} |
|
|
|
</div> |
|
|
|
); |
|
|
|
const expandedItemIds = React.useMemo(() => { |
|
|
|
if (!initialSelectedItemId) { |
|
|
|
return [] as string[]; |
|
|
|
} |
|
|
|
|
|
|
|
const ids: string[] = []; |
|
|
|
|
|
|
|
function walkTreeItems( |
|
|
|
items: TreeDataItem[] | TreeDataItem, |
|
|
|
targetId: string, |
|
|
|
) { |
|
|
|
if (items instanceof Array) { |
|
|
|
for (let i = 0; i < items.length; i++) { |
|
|
|
ids.push(items[i]!.id); |
|
|
|
if (walkTreeItems(items[i]!, targetId) && !expandAll) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (!expandAll) ids.pop(); |
|
|
|
} |
|
|
|
} else if (!expandAll && items.id === targetId) { |
|
|
|
return true; |
|
|
|
} else if (items.children) { |
|
|
|
return walkTreeItems(items.children, targetId); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
walkTreeItems(data, initialSelectedItemId); |
|
|
|
return ids; |
|
|
|
}, [data, expandAll, initialSelectedItemId]); |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className={cn('overflow-hidden relative p-2', className)}> |
|
|
|
<TreeItem |
|
|
|
data={data} |
|
|
|
ref={ref} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
expandedItemIds={expandedItemIds} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
defaultNodeIcon={defaultNodeIcon} |
|
|
|
{...props} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
); |
|
|
|
}, |
|
|
|
); |
|
|
|
TreeView.displayName = 'TreeView'; |
|
|
|
|
|
|
|
type TreeItemProps = TreeProps & { |
|
|
|
selectedItemId?: string; |
|
|
|
handleSelectChange: (item: TreeDataItem | undefined) => void; |
|
|
|
expandedItemIds: string[]; |
|
|
|
defaultNodeIcon?: any; |
|
|
|
defaultLeafIcon?: any; |
|
|
|
}; |
|
|
|
|
|
|
|
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>( |
|
|
|
( |
|
|
|
{ |
|
|
|
className, |
|
|
|
data, |
|
|
|
selectedItemId, |
|
|
|
handleSelectChange, |
|
|
|
expandedItemIds, |
|
|
|
defaultNodeIcon, |
|
|
|
defaultLeafIcon, |
|
|
|
...props |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => { |
|
|
|
if (!(data instanceof Array)) { |
|
|
|
data = [data]; |
|
|
|
} |
|
|
|
return ( |
|
|
|
<div ref={ref} role="tree" className={className} {...props}> |
|
|
|
<ul> |
|
|
|
{data.map((item) => ( |
|
|
|
<li key={item.id}> |
|
|
|
{item.children ? ( |
|
|
|
<TreeNode |
|
|
|
item={item} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
expandedItemIds={expandedItemIds} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
defaultNodeIcon={defaultNodeIcon} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
/> |
|
|
|
) : ( |
|
|
|
<TreeLeaf |
|
|
|
item={item} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
/> |
|
|
|
)} |
|
|
|
</li> |
|
|
|
))} |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
); |
|
|
|
}, |
|
|
|
); |
|
|
|
TreeItem.displayName = 'TreeItem'; |
|
|
|
|
|
|
|
const TreeNode = ({ |
|
|
|
item, |
|
|
|
handleSelectChange, |
|
|
|
@@ -164,62 +227,6 @@ const TreeNode = ({ |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
type TreeItemProps = TreeProps & { |
|
|
|
selectedItemId?: string; |
|
|
|
handleSelectChange: (item: TreeDataItem | undefined) => void; |
|
|
|
expandedItemIds: string[]; |
|
|
|
defaultNodeIcon?: any; |
|
|
|
defaultLeafIcon?: any; |
|
|
|
}; |
|
|
|
|
|
|
|
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>( |
|
|
|
( |
|
|
|
{ |
|
|
|
className, |
|
|
|
data, |
|
|
|
selectedItemId, |
|
|
|
handleSelectChange, |
|
|
|
expandedItemIds, |
|
|
|
defaultNodeIcon, |
|
|
|
defaultLeafIcon, |
|
|
|
...props |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => { |
|
|
|
if (!(data instanceof Array)) { |
|
|
|
data = [data]; |
|
|
|
} |
|
|
|
return ( |
|
|
|
<div ref={ref} role="tree" className={className} {...props}> |
|
|
|
<ul> |
|
|
|
{data.map((item) => ( |
|
|
|
<li key={item.id}> |
|
|
|
{item.children ? ( |
|
|
|
<TreeNode |
|
|
|
item={item} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
expandedItemIds={expandedItemIds} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
defaultNodeIcon={defaultNodeIcon} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
/> |
|
|
|
) : ( |
|
|
|
<TreeLeaf |
|
|
|
item={item} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
/> |
|
|
|
)} |
|
|
|
</li> |
|
|
|
))} |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
); |
|
|
|
}, |
|
|
|
); |
|
|
|
TreeItem.displayName = 'TreeItem'; |
|
|
|
|
|
|
|
const TreeLeaf = React.forwardRef< |
|
|
|
HTMLDivElement, |
|
|
|
React.HTMLAttributes<HTMLDivElement> & { |
|
|
|
@@ -270,89 +277,82 @@ const TreeLeaf = React.forwardRef< |
|
|
|
); |
|
|
|
TreeLeaf.displayName = 'TreeLeaf'; |
|
|
|
|
|
|
|
type TreeProps = React.HTMLAttributes<HTMLDivElement> & { |
|
|
|
data: TreeDataItem[] | TreeDataItem; |
|
|
|
initialSelectedItemId?: string; |
|
|
|
onSelectChange?: (item: TreeDataItem | undefined) => void; |
|
|
|
expandAll?: boolean; |
|
|
|
defaultNodeIcon?: any; |
|
|
|
defaultLeafIcon?: any; |
|
|
|
}; |
|
|
|
const AccordionTrigger = React.forwardRef< |
|
|
|
React.ElementRef<typeof AccordionPrimitive.Trigger>, |
|
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> |
|
|
|
>(({ className, children, ...props }, ref) => ( |
|
|
|
<AccordionPrimitive.Header> |
|
|
|
<AccordionPrimitive.Trigger |
|
|
|
ref={ref} |
|
|
|
className={cn( |
|
|
|
'flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90', |
|
|
|
className, |
|
|
|
)} |
|
|
|
{...props} |
|
|
|
> |
|
|
|
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 mr-1" /> |
|
|
|
{children} |
|
|
|
</AccordionPrimitive.Trigger> |
|
|
|
</AccordionPrimitive.Header> |
|
|
|
)); |
|
|
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; |
|
|
|
|
|
|
|
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>( |
|
|
|
( |
|
|
|
{ |
|
|
|
data, |
|
|
|
initialSelectedItemId, |
|
|
|
onSelectChange, |
|
|
|
expandAll, |
|
|
|
defaultLeafIcon, |
|
|
|
defaultNodeIcon, |
|
|
|
const AccordionContent = React.forwardRef< |
|
|
|
React.ElementRef<typeof AccordionPrimitive.Content>, |
|
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> |
|
|
|
>(({ className, children, ...props }, ref) => ( |
|
|
|
<AccordionPrimitive.Content |
|
|
|
ref={ref} |
|
|
|
className={cn( |
|
|
|
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down', |
|
|
|
className, |
|
|
|
...props |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => { |
|
|
|
const [selectedItemId, setSelectedItemId] = React.useState< |
|
|
|
string | undefined |
|
|
|
>(initialSelectedItemId); |
|
|
|
|
|
|
|
const handleSelectChange = React.useCallback( |
|
|
|
(item: TreeDataItem | undefined) => { |
|
|
|
setSelectedItemId(item?.id); |
|
|
|
if (onSelectChange) { |
|
|
|
onSelectChange(item); |
|
|
|
} |
|
|
|
}, |
|
|
|
[onSelectChange], |
|
|
|
); |
|
|
|
|
|
|
|
const expandedItemIds = React.useMemo(() => { |
|
|
|
if (!initialSelectedItemId) { |
|
|
|
return [] as string[]; |
|
|
|
} |
|
|
|
|
|
|
|
const ids: string[] = []; |
|
|
|
|
|
|
|
function walkTreeItems( |
|
|
|
items: TreeDataItem[] | TreeDataItem, |
|
|
|
targetId: string, |
|
|
|
) { |
|
|
|
if (items instanceof Array) { |
|
|
|
for (let i = 0; i < items.length; i++) { |
|
|
|
ids.push(items[i]!.id); |
|
|
|
if (walkTreeItems(items[i]!, targetId) && !expandAll) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (!expandAll) ids.pop(); |
|
|
|
} |
|
|
|
} else if (!expandAll && items.id === targetId) { |
|
|
|
return true; |
|
|
|
} else if (items.children) { |
|
|
|
return walkTreeItems(items.children, targetId); |
|
|
|
} |
|
|
|
} |
|
|
|
)} |
|
|
|
{...props} |
|
|
|
> |
|
|
|
<div className="pb-1 pt-0">{children}</div> |
|
|
|
</AccordionPrimitive.Content> |
|
|
|
)); |
|
|
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName; |
|
|
|
|
|
|
|
walkTreeItems(data, initialSelectedItemId); |
|
|
|
return ids; |
|
|
|
}, [data, expandAll, initialSelectedItemId]); |
|
|
|
const TreeIcon = ({ |
|
|
|
item, |
|
|
|
isOpen, |
|
|
|
isSelected, |
|
|
|
default: defaultIcon, |
|
|
|
}: { |
|
|
|
item: TreeDataItem; |
|
|
|
isOpen?: boolean; |
|
|
|
isSelected?: boolean; |
|
|
|
default?: any; |
|
|
|
}) => { |
|
|
|
let Icon = defaultIcon; |
|
|
|
if (isSelected && item.selectedIcon) { |
|
|
|
Icon = item.selectedIcon; |
|
|
|
} else if (isOpen && item.openIcon) { |
|
|
|
Icon = item.openIcon; |
|
|
|
} else if (item.icon) { |
|
|
|
Icon = item.icon; |
|
|
|
} |
|
|
|
return Icon ? <Icon className="h-4 w-4 shrink-0 mr-2" /> : <></>; |
|
|
|
}; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className={cn('overflow-hidden relative p-2', className)}> |
|
|
|
<TreeItem |
|
|
|
data={data} |
|
|
|
ref={ref} |
|
|
|
selectedItemId={selectedItemId} |
|
|
|
handleSelectChange={handleSelectChange} |
|
|
|
expandedItemIds={expandedItemIds} |
|
|
|
defaultLeafIcon={defaultLeafIcon} |
|
|
|
defaultNodeIcon={defaultNodeIcon} |
|
|
|
{...props} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
); |
|
|
|
}, |
|
|
|
); |
|
|
|
TreeView.displayName = 'TreeView'; |
|
|
|
const TreeActions = ({ |
|
|
|
children, |
|
|
|
isSelected, |
|
|
|
}: { |
|
|
|
children: React.ReactNode; |
|
|
|
isSelected: boolean; |
|
|
|
}) => { |
|
|
|
return ( |
|
|
|
<div |
|
|
|
className={cn( |
|
|
|
isSelected ? 'block' : 'hidden', |
|
|
|
'absolute right-3 group-hover:block', |
|
|
|
)} |
|
|
|
> |
|
|
|
{children} |
|
|
|
</div> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
export { TreeView, type TreeDataItem }; |