import React, { useState, useEffect, useRef } from 'react'; import { Handle, Position, useUpdateNodeInternals, useReactFlow } from '@xyflow/react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '../../../../context/ThemeContext'; const TextNode = ({ id, data, selected, width }) => { const { t } = useTranslation(); const { isDarkMode } = useTheme(); const updateNodeInternals = useUpdateNodeInternals(); const { getZoom, getNode, setNodes } = useReactFlow(); const [localText, setLocalText] = useState(data.label || ''); const [isResizing, setIsResizing] = useState(false); const [isEditing, setIsEditing] = useState(false); const textareaRef = useRef(null); // Auto-resize textarea height useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; const newHeight = textareaRef.current.scrollHeight; textareaRef.current.style.height = newHeight + 'px'; // Notify React Flow that height (and handles) have shifted updateNodeInternals(id); } }, [localText, width, data.width, id, updateNodeInternals]); // Sync if data changes from remote useEffect(() => { if (document.activeElement !== textareaRef.current) { setLocalText(data.label || ''); } }, [data.label]); // Sync node's draggable state with editing state useEffect(() => { setNodes((nds) => nds.map((n) => { if (n.id === id) { return { ...n, draggable: isEditing ? false : !data.isPinned }; } return n; }) ); }, [isEditing, id, data.isPinned, setNodes]); const handleTextChange = (e) => { const newText = e.target.value; setLocalText(newText); if (data.updateNodeData) { data.updateNodeData(id, { label: newText }); } }; const handleDoubleClick = (e) => { e.stopPropagation(); setIsEditing(true); setTimeout(() => { if (textareaRef.current) { textareaRef.current.focus(); textareaRef.current.select(); } }, 0); }; const handleBlur = () => { setIsEditing(false); if (data.updateNodeData) { data.updateNodeData(id, { label: localText }); } }; const handleResizeStart = (e, direction) => { e.stopPropagation(); e.preventDefault(); const node = getNode(id); if (!node) return; setIsResizing(true); const startX = e.clientX; const startY = e.clientY; const startPos = { ...node.position }; const startSize = { width: node.width || node.style?.width || data.width || 250, height: node.height || node.style?.height || 100 }; const zoom = getZoom(); let finalWidth = startSize.width; let finalX = startPos.x; const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`); const handlePointerMove = (moveEvent) => { const dx = (moveEvent.clientX - startX) / zoom; const dy = (moveEvent.clientY - startY) / zoom; // For TextNode, we only resize width if (['right', 'top-right', 'bottom-right', 'top', 'bottom'].includes(direction)) { // Dragging right-side / vertical handles: expand width to the right const delta = ['top', 'bottom'].includes(direction) ? dy : dx; finalWidth = Math.max(50, startSize.width + delta); finalX = startPos.x; } else if (['left', 'top-left', 'bottom-left'].includes(direction)) { // Dragging left-side handles: expand width to the left, shifting position x finalWidth = Math.max(50, startSize.width - dx); finalX = startPos.x + (startSize.width - finalWidth); } if (nodeElement) { nodeElement.style.width = `${finalWidth}px`; nodeElement.style.transform = `translate(${finalX}px, ${startPos.y}px)`; } }; const handlePointerUp = () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); setNodes((nds) => nds.map((n) => { if (n.id === id) { return { ...n, position: { x: finalX, y: startPos.y }, width: finalWidth, style: { ...n.style, width: finalWidth } }; } return n; }) ); if (data.updateNodeData) { data.updateNodeData(id, { width: finalWidth, x: finalX, y: startPos.y }); } setIsResizing(false); }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); }; const isCustomSized = width !== undefined || isResizing; const styleWidth = isCustomSized ? '100%' : (data.width || 250); const fontSize = data.fontSize || 18; const color = data.color || (isDarkMode ? '#ffffff' : '#31363F'); // Common handle style & class (Removed transition-all to prevent scale animation lag) const handleClass = "absolute w-3.5 h-3.5 bg-surface-container-lowest border-2 border-primary rounded-md shadow-[2px_2px_0px_0px_var(--color-primary)] z-50 hover:scale-110 active:scale-95 transition-transform select-none"; return (