import { useState, useEffect, useRef } from 'react'; import { CanvasObject } from '../../types/canvas.types'; import { sortByZIndex } from '../../utils/canvas.utils'; import Konva from 'konva'; import LayerItem from './LayerItem'; interface LayerContainerProps { objects: CanvasObject[]; selectedIds: string[]; onSelect: (ids: string[]) => void; onObjectsChange: (objects: CanvasObject[]) => void; transformerRef: React.RefObject; stageRef: React.RefObject; } export default function LayerContainer({ objects, selectedIds, onSelect, onObjectsChange, transformerRef, stageRef }: LayerContainerProps) { const containerRef = useRef(null); const [position, setPosition] = useState<{ top: number; left: number } | null>(null); const [draggedId, setDraggedId] = useState(null); const [dropTargetIndex, setDropTargetIndex] = useState(null); // Calculate position near selected object useEffect(() => { if (selectedIds.length === 0 || !transformerRef.current || !stageRef.current) { setPosition(null); return; } let animationFrameId: number | null = null; let lastUpdateTime = 0; const UPDATE_THROTTLE = 50; // Update at most every 50ms const updatePosition = (timestamp: number = performance.now()) => { const transformer = transformerRef.current; const stage = stageRef.current; if (!transformer || !stage) return; // Throttle updates if (timestamp - lastUpdateTime < UPDATE_THROTTLE) { animationFrameId = requestAnimationFrame(updatePosition); return; } lastUpdateTime = timestamp; const box = transformer.getClientRect(); const container = stage.container(); const containerRect = container.getBoundingClientRect(); // Position to the right of the selection, with some gap let left = containerRect.left + box.x + box.width + 20; let top = containerRect.top + box.y; // If would go off-screen right, position to the left instead if (containerRef.current) { const layerWidth = containerRef.current.offsetWidth || 80; if (left + layerWidth > window.innerWidth - 20) { left = containerRect.left + box.x - layerWidth - 20; } } // Ensure it doesn't go off-screen top or bottom if (containerRef.current) { const layerHeight = containerRef.current.offsetHeight || 300; if (top + layerHeight > window.innerHeight - 20) { top = window.innerHeight - layerHeight - 20; } if (top < 20) { top = 20; } } setPosition({ top, left }); animationFrameId = requestAnimationFrame(updatePosition); }; updatePosition(); return () => { if (animationFrameId !== null) { cancelAnimationFrame(animationFrameId); } }; }, [selectedIds, objects, transformerRef, stageRef]); // Handle layer reorder const handleLayerReorder = (draggedId: string, targetIndex: number) => { const sortedByZ = sortByZIndex(objects); const draggedObj = objects.find(obj => obj.id === draggedId); if (!draggedObj) return; // Remove dragged object from array const filtered = sortedByZ.filter(obj => obj.id !== draggedId); // Insert at target position filtered.splice(targetIndex, 0, draggedObj); // Reassign z-index values (0 to length-1) const updated = filtered.map((obj, index) => ({ ...obj, zIndex: index })); onObjectsChange(updated); setDraggedId(null); setDropTargetIndex(null); }; // Handle layer click to select const handleLayerClick = (id: string, shiftKey: boolean) => { if (shiftKey) { // Add/remove from selection if (selectedIds.includes(id)) { onSelect(selectedIds.filter(selectedId => selectedId !== id)); } else { onSelect([...selectedIds, id]); } } else { // Select only this layer onSelect([id]); } }; // Handle layer name change const handleNameChange = (id: string, name: string) => { const updatedObjects = objects.map(obj => obj.id === id ? { ...obj, name } : obj ); onObjectsChange(updatedObjects); }; if (selectedIds.length === 0 || !position) { return null; } // Sort objects by z-index (highest first = top of list) const sortedObjects = sortByZIndex(objects).reverse(); return (
{sortedObjects.map((obj, index) => ( setDraggedId(id)} onDragOver={(index) => setDropTargetIndex(index)} onDragEnd={handleLayerReorder} onNameChange={handleNameChange} /> ))}
); }