| | 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<Konva.Transformer>; |
| | stageRef: React.RefObject<Konva.Stage>; |
| | } |
| |
|
| | export default function LayerContainer({ |
| | objects, |
| | selectedIds, |
| | onSelect, |
| | onObjectsChange, |
| | transformerRef, |
| | stageRef |
| | }: LayerContainerProps) { |
| | const containerRef = useRef<HTMLDivElement>(null); |
| | const [position, setPosition] = useState<{ top: number; left: number } | null>(null); |
| | const [draggedId, setDraggedId] = useState<string | null>(null); |
| | const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null); |
| |
|
| | |
| | useEffect(() => { |
| | if (selectedIds.length === 0 || !transformerRef.current || !stageRef.current) { |
| | setPosition(null); |
| | return; |
| | } |
| |
|
| | let animationFrameId: number | null = null; |
| | let lastUpdateTime = 0; |
| | const UPDATE_THROTTLE = 50; |
| |
|
| | const updatePosition = (timestamp: number = performance.now()) => { |
| | const transformer = transformerRef.current; |
| | const stage = stageRef.current; |
| |
|
| | if (!transformer || !stage) return; |
| |
|
| | |
| | if (timestamp - lastUpdateTime < UPDATE_THROTTLE) { |
| | animationFrameId = requestAnimationFrame(updatePosition); |
| | return; |
| | } |
| |
|
| | lastUpdateTime = timestamp; |
| |
|
| | const box = transformer.getClientRect(); |
| | const container = stage.container(); |
| | const containerRect = container.getBoundingClientRect(); |
| |
|
| | |
| | let left = containerRect.left + box.x + box.width + 20; |
| | let top = containerRect.top + box.y; |
| |
|
| | |
| | if (containerRef.current) { |
| | const layerWidth = containerRef.current.offsetWidth || 80; |
| | if (left + layerWidth > window.innerWidth - 20) { |
| | left = containerRect.left + box.x - layerWidth - 20; |
| | } |
| | } |
| |
|
| | |
| | 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]); |
| |
|
| | |
| | const handleLayerReorder = (draggedId: string, targetIndex: number) => { |
| | const sortedByZ = sortByZIndex(objects); |
| | const draggedObj = objects.find(obj => obj.id === draggedId); |
| |
|
| | if (!draggedObj) return; |
| |
|
| | |
| | const filtered = sortedByZ.filter(obj => obj.id !== draggedId); |
| |
|
| | |
| | filtered.splice(targetIndex, 0, draggedObj); |
| |
|
| | |
| | const updated = filtered.map((obj, index) => ({ |
| | ...obj, |
| | zIndex: index |
| | })); |
| |
|
| | onObjectsChange(updated); |
| | setDraggedId(null); |
| | setDropTargetIndex(null); |
| | }; |
| |
|
| | |
| | const handleLayerClick = (id: string, shiftKey: boolean) => { |
| | if (shiftKey) { |
| | |
| | if (selectedIds.includes(id)) { |
| | onSelect(selectedIds.filter(selectedId => selectedId !== id)); |
| | } else { |
| | onSelect([...selectedIds, id]); |
| | } |
| | } else { |
| | |
| | onSelect([id]); |
| | } |
| | }; |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | const sortedObjects = sortByZIndex(objects).reverse(); |
| |
|
| | return ( |
| | <div |
| | ref={containerRef} |
| | className="layer-container" |
| | style={{ |
| | position: 'fixed', |
| | top: `${position.top}px`, |
| | left: `${position.left}px`, |
| | width: '80px', |
| | maxHeight: '320px', |
| | background: 'rgba(43, 45, 49, 0.95)', |
| | borderRadius: '8px', |
| | padding: '4px', |
| | boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', |
| | overflowY: 'auto', |
| | overflowX: 'hidden', |
| | zIndex: 1000, |
| | // Custom scrollbar |
| | scrollbarWidth: 'thin', |
| | scrollbarColor: '#b8b8b8 transparent', |
| | // Smooth animations |
| | animation: 'layerFadeIn 0.2s ease-out', |
| | transition: 'top 0.15s ease-out, left 0.15s ease-out' |
| | }} |
| | > |
| | {sortedObjects.map((obj, index) => ( |
| | <LayerItem |
| | key={obj.id} |
| | object={obj} |
| | index={index} |
| | isSelected={selectedIds.includes(obj.id)} |
| | isDragging={draggedId === obj.id} |
| | isDropTarget={dropTargetIndex === index} |
| | onSelect={handleLayerClick} |
| | onDragStart={(id) => setDraggedId(id)} |
| | onDragOver={(index) => setDropTargetIndex(index)} |
| | onDragEnd={handleLayerReorder} |
| | onNameChange={handleNameChange} |
| | /> |
| | ))} |
| | </div> |
| | ); |
| | } |
| |
|