import React, { useState } from 'react'; import type { TreeNode } from '../types/ui.types'; interface TreeViewProps { nodes: TreeNode[]; selectedId?: string | null; selectedIds?: string[]; onSelect: (id: string) => void; onToggle?: (id: string) => void; onReorder?: (draggedId: string, targetId: string, position: 'before' | 'after' | 'inside') => void; renderIcon?: (node: TreeNode) => React.ReactNode; renderActions?: (node: TreeNode) => React.ReactNode; renderDetail?: (node: TreeNode) => React.ReactNode; draggable?: boolean; } const TreeView: React.FC = ({ nodes, selectedId, selectedIds, onSelect, onToggle, onReorder, renderIcon, renderActions, renderDetail, draggable = false, }) => { const [expandedSet, setExpandedSet] = useState>(new Set()); const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const [dragPosition, setDragPosition] = useState<'before' | 'after' | 'inside'>('before'); const toggleExpand = (id: string) => { setExpandedSet((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); onToggle?.(id); }; const handleDragStart = (e: React.DragEvent, id: string) => { if (!draggable || !onReorder) return; setDraggedId(id); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', id); }; const handleDragOver = (e: React.DragEvent, id: string) => { if (!draggable || !onReorder || !draggedId || draggedId === id) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const rect = e.currentTarget.getBoundingClientRect(); const offset = e.clientY - rect.top; const third = rect.height / 3; if (offset < third) { setDragPosition('before'); } else if (offset > third * 2) { setDragPosition('after'); } else { setDragPosition('inside'); } setDragOverId(id); }; const handleDragLeave = (_e: React.DragEvent, id: string) => { if (!draggable || !onReorder) return; if (dragOverId === id) { setDragOverId(null); } }; const handleDrop = (e: React.DragEvent, id: string) => { if (!draggable || !onReorder || !draggedId || draggedId === id) return; e.preventDefault(); e.stopPropagation(); onReorder(draggedId, id, dragPosition); setDraggedId(null); setDragOverId(null); }; const handleDragEnd = () => { setDraggedId(null); setDragOverId(null); }; const renderNode = (node: TreeNode, level: number): React.ReactNode => { const hasChildren = node.children && node.children.length > 0; const isExpanded = expandedSet.has(node.id) || node.expanded; const isSelected = selectedId === node.id || (selectedIds !== undefined && selectedIds.includes(node.id)); const isDragOver = dragOverId === node.id; const isDragging = draggedId === node.id; let dragClass = ''; if (isDragOver && dragPosition === 'before') dragClass = ' tree-drag-before'; if (isDragOver && dragPosition === 'after') dragClass = ' tree-drag-after'; if (isDragOver && dragPosition === 'inside') dragClass = ' tree-drag-inside'; return (
onSelect(node.id)} draggable={draggable} onDragStart={(e) => handleDragStart(e, node.id)} onDragOver={(e) => handleDragOver(e, node.id)} onDragLeave={(e) => handleDragLeave(e, node.id)} onDrop={(e) => handleDrop(e, node.id)} onDragEnd={handleDragEnd} > {hasChildren && ( )} {!hasChildren && } {renderIcon && {renderIcon(node)}} {node.name} {node.count !== undefined && ({node.count})} {renderActions && e.stopPropagation()}>{renderActions(node)}}
{renderDetail && isSelected && (
{renderDetail(node)}
)} {hasChildren && isExpanded && (
{node.children!.map((child) => renderNode(child, level + 1))}
)}
); }; return
{nodes.map((node) => renderNode(node, 0))}
; }; export default TreeView;