import React, { useRef, useEffect, useState, useCallback } from 'react'; import { Group, Rect, Text, Transformer } from 'react-konva'; import { Html } from 'react-konva-utils'; import type Konva from 'konva'; import type { TextElement } from '../../types'; interface TextBlockProps { element: TextElement; isSelected: boolean; onSelect: () => void; onChange: (updates: Partial) => void; } const TextBlock: React.FC = ({ element, isSelected, onSelect, onChange }) => { const groupRef = useRef(null); const textRef = useRef(null); const trRef = useRef(null); const textareaRef = useRef(null); const [textDimensions, setTextDimensions] = useState({ width: 200, height: 30 }); const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(element.props.text); const [textareaStyle, setTextareaStyle] = useState({ display: 'none' }); const { x, y, rotation, props } = element; useEffect(() => { if (isSelected && trRef.current && groupRef.current) { trRef.current.nodes([groupRef.current]); trRef.current.getLayer()?.batchDraw(); } }, [isSelected]); useEffect(() => { if (textRef.current) { setTextDimensions({ width: textRef.current.width(), height: textRef.current.height(), }); } }, [props.text, props.fontSize, props.fontFamily, props.bold, props.italic]); // Sync editValue when text changes externally useEffect(() => { setEditValue(props.text); }, [props.text]); // Focus textarea when editing starts useEffect(() => { if (isEditing && textareaRef.current) { textareaRef.current.focus(); textareaRef.current.select(); } }, [isEditing]); const handleDragEnd = (e: Konva.KonvaEventObject) => { onChange({ x: e.target.x(), y: e.target.y(), }); }; const handleTransformEnd = () => { const node = groupRef.current; if (!node) return; node.scaleX(1); node.scaleY(1); onChange({ x: node.x(), y: node.y(), rotation: node.rotation(), }); }; const handleSaveEdit = useCallback(() => { setIsEditing(false); setTextareaStyle({ display: 'none' }); onChange({ props: { ...props, text: editValue }, }); }, [editValue, onChange, props]); // Close editing when deselecting useEffect(() => { if (!isSelected && isEditing) { handleSaveEdit(); } }, [isSelected, isEditing, handleSaveEdit]); const handleDoubleClick = useCallback((e: Konva.KonvaEventObject) => { e.cancelBubble = true; onSelect(); if (element.locked || !groupRef.current) return; const group = groupRef.current; const stage = group.getStage(); if (!stage) return; const scale = stage.scaleX(); setTextareaStyle({ position: 'absolute', left: 0, top: 0, width: Math.max(40, textDimensions.width) * scale, height: Math.max(24, textDimensions.height) * scale, fontSize: props.fontSize * scale, fontFamily: props.fontFamily, fontWeight: props.bold ? 'bold' : 'normal', fontStyle: props.italic ? 'italic' : 'normal', lineHeight: '1.2', background: 'transparent', color: props.color, border: 'none', outline: 'none', padding: '0', margin: '0', resize: 'none', overflow: 'hidden', whiteSpace: 'pre', textAlign: props.align, zIndex: 1000, transformOrigin: 'top left', caretColor: props.color, }); setIsEditing(true); setEditValue(props.text); }, [element.locked, onSelect, props, textDimensions.height, textDimensions.width]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); handleSaveEdit(); } if ((e.key === 'Enter' && (e.metaKey || e.ctrlKey))) { e.preventDefault(); handleSaveEdit(); } }, [handleSaveEdit]); const totalWidth = textDimensions.width + props.padding * 2; const totalHeight = textDimensions.height + props.padding * 2; const fontStyle = [ props.bold ? 'bold' : '', props.italic ? 'italic' : '', ].filter(Boolean).join(' ') || 'normal'; return ( <> {/* Background */} {props.background && ( )} {/* Text */} {/* Inline text editor */} {isEditing && (