|
|
import { useEffect, useRef } from 'react'; |
|
|
import { Group, Rect, Text as KonvaText } from 'react-konva'; |
|
|
import { TextObject } from '../../types/canvas.types'; |
|
|
import Konva from 'konva'; |
|
|
|
|
|
|
|
|
function getFontStyle(bold: boolean, italic: boolean, fontWeight?: 'normal' | 'bold' | 'black'): string { |
|
|
const weight = fontWeight === 'black' ? '900' : bold ? 'bold' : 'normal'; |
|
|
const style = italic ? 'italic' : ''; |
|
|
return `${weight} ${style}`.trim(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getLuminance(hexColor: string): number { |
|
|
|
|
|
const hex = hexColor.replace('#', ''); |
|
|
|
|
|
|
|
|
const r = parseInt(hex.substring(0, 2), 16); |
|
|
const g = parseInt(hex.substring(2, 4), 16); |
|
|
const b = parseInt(hex.substring(4, 6), 16); |
|
|
|
|
|
|
|
|
|
|
|
return 0.299 * r + 0.587 * g + 0.114 * b; |
|
|
} |
|
|
|
|
|
|
|
|
function getContrastTextColor(backgroundColor: string): string { |
|
|
const luminance = getLuminance(backgroundColor); |
|
|
|
|
|
|
|
|
return luminance > 128 ? '#000000' : '#FFFFFF'; |
|
|
} |
|
|
|
|
|
interface TextEditableProps { |
|
|
object: TextObject; |
|
|
isSelected: boolean; |
|
|
onSelect: (e?: Konva.KonvaEventObject<MouseEvent>) => void; |
|
|
onDragStart?: (e: Konva.KonvaEventObject<DragEvent>) => void; |
|
|
onDragMove?: (e: Konva.KonvaEventObject<DragEvent>) => void; |
|
|
onDragEnd?: (e: Konva.KonvaEventObject<DragEvent>) => void; |
|
|
onTransformEnd?: (e: Konva.KonvaEventObject<Event>) => void; |
|
|
onEditingChange: (id: string, isEditing: boolean, clickX?: number, clickY?: number) => void; |
|
|
onMouseEnter?: () => void; |
|
|
onMouseLeave?: () => void; |
|
|
shapeRef?: ((node: Konva.Text | null) => void) | React.RefObject<Konva.Text>; |
|
|
} |
|
|
|
|
|
export default function TextEditable({ |
|
|
object, |
|
|
isSelected, |
|
|
onSelect, |
|
|
onDragStart, |
|
|
onDragMove, |
|
|
onDragEnd, |
|
|
onTransformEnd, |
|
|
onEditingChange, |
|
|
onMouseEnter, |
|
|
onMouseLeave, |
|
|
shapeRef |
|
|
}: TextEditableProps) { |
|
|
const textNodeRef = useRef<Konva.Text | null>(null); |
|
|
const groupRef = useRef<Konva.Group | null>(null); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (object.text === '' && isSelected && !object.isEditing) { |
|
|
|
|
|
const timer = setTimeout(() => { |
|
|
onEditingChange(object.id, true); |
|
|
}, 100); |
|
|
return () => clearTimeout(timer); |
|
|
} |
|
|
}, [object.text, isSelected, object.isEditing, object.id, onEditingChange]); |
|
|
|
|
|
|
|
|
const handleDoubleClick = (e: Konva.KonvaEventObject<MouseEvent>) => { |
|
|
if (!object.isEditing) { |
|
|
const stage = e.target.getStage(); |
|
|
const pointerPos = stage?.getPointerPosition(); |
|
|
if (pointerPos) { |
|
|
|
|
|
const layer = e.target.getLayer(); |
|
|
const layerX = layer?.x() || 0; |
|
|
const layerY = layer?.y() || 0; |
|
|
|
|
|
|
|
|
const clickX = pointerPos.x - layerX - object.x; |
|
|
const clickY = pointerPos.y - layerY - object.y; |
|
|
onEditingChange(object.id, true, clickX, clickY); |
|
|
} else { |
|
|
onEditingChange(object.id, true); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const displayFontSize = object.fontSize; |
|
|
|
|
|
|
|
|
const bgPaddingTop = 8; |
|
|
const bgPaddingRight = 8; |
|
|
const bgPaddingBottom = 5; |
|
|
const bgPaddingLeft = 8; |
|
|
const bgRadius = 8; |
|
|
|
|
|
|
|
|
if (object.hasBackground) { |
|
|
|
|
|
const bgColor = object.backgroundColor || object.fill; |
|
|
|
|
|
|
|
|
const textColor = getContrastTextColor(bgColor); |
|
|
|
|
|
return ( |
|
|
<Group |
|
|
ref={(node) => { |
|
|
groupRef.current = node; |
|
|
// Attach the Group to the transformer ref, not the inner text |
|
|
// Cast to any to handle the type mismatch between Group and Text |
|
|
if (typeof shapeRef === 'function') { |
|
|
(shapeRef as any)(node); |
|
|
} else if (shapeRef) { |
|
|
(shapeRef as any).current = node; |
|
|
} |
|
|
}} |
|
|
// Position the group so the VISUAL top-left (background rect) is at object.x, object.y |
|
|
x={object.x + bgPaddingLeft} |
|
|
y={object.y + bgPaddingTop} |
|
|
rotation={object.rotation} |
|
|
draggable={true} |
|
|
onClick={(e) => onSelect(e as Konva.KonvaEventObject<MouseEvent>)} |
|
|
onTap={(e) => onSelect(e as Konva.KonvaEventObject<MouseEvent>)} |
|
|
onDblClick={handleDoubleClick} |
|
|
onDblTap={handleDoubleClick} |
|
|
onDragStart={onDragStart} |
|
|
onDragMove={onDragMove} |
|
|
onDragEnd={onDragEnd} |
|
|
onTransformEnd={onTransformEnd} |
|
|
onMouseEnter={onMouseEnter} |
|
|
onMouseLeave={onMouseLeave} |
|
|
opacity={object.isEditing ? 0 : 1} |
|
|
listening={!object.isEditing} |
|
|
> |
|
|
{/* Background Rectangle */} |
|
|
<Rect |
|
|
x={-bgPaddingLeft} |
|
|
y={-bgPaddingTop} |
|
|
width={object.width + bgPaddingLeft + bgPaddingRight} |
|
|
height={object.height + bgPaddingTop + bgPaddingBottom} |
|
|
fill={bgColor} |
|
|
cornerRadius={bgRadius} |
|
|
/> |
|
|
|
|
|
{/* Text */} |
|
|
<KonvaText |
|
|
id={object.id} |
|
|
ref={(node) => { |
|
|
// Store text node internally for measurements |
|
|
textNodeRef.current = node; |
|
|
}} |
|
|
x={0} |
|
|
y={0} |
|
|
width={object.width} |
|
|
height={object.height} |
|
|
text={object.text} |
|
|
fontSize={displayFontSize} |
|
|
fontFamily={object.fontFamily} |
|
|
fill={textColor} |
|
|
fontStyle={getFontStyle(object.bold, object.italic, object.fontWeight)} |
|
|
align={object.align || 'left'} |
|
|
verticalAlign="top" |
|
|
padding={0} |
|
|
lineHeight={1} |
|
|
draggable={false} |
|
|
/> |
|
|
</Group> |
|
|
); |
|
|
} |
|
|
|
|
|
// No background - render text only |
|
|
return ( |
|
|
<KonvaText |
|
|
id={object.id} |
|
|
ref={(node) => { |
|
|
// Call the callback ref if it's a function |
|
|
if (typeof shapeRef === 'function') { |
|
|
shapeRef(node); |
|
|
} else if (shapeRef) { |
|
|
// Set the RefObject if it's an object |
|
|
(shapeRef as React.MutableRefObject<Konva.Text | null>).current = node; |
|
|
} |
|
|
// Also store internally for our own use |
|
|
textNodeRef.current = node; |
|
|
}} |
|
|
x={object.x} |
|
|
y={object.y} |
|
|
width={object.width} |
|
|
height={object.height} |
|
|
text={object.text} |
|
|
fontSize={displayFontSize} |
|
|
fontFamily={object.fontFamily} |
|
|
fill={object.fill} |
|
|
fontStyle={getFontStyle(object.bold, object.italic, object.fontWeight)} |
|
|
align={object.align || 'left'} |
|
|
verticalAlign="top" |
|
|
rotation={object.rotation} |
|
|
padding={0} |
|
|
lineHeight={1} |
|
|
draggable={true} |
|
|
onClick={(e) => onSelect(e as Konva.KonvaEventObject<MouseEvent>)} |
|
|
onTap={(e) => onSelect(e as Konva.KonvaEventObject<MouseEvent>)} |
|
|
onDblClick={handleDoubleClick} |
|
|
onDblTap={handleDoubleClick} |
|
|
onDragStart={onDragStart} |
|
|
onDragMove={onDragMove} |
|
|
onDragEnd={onDragEnd} |
|
|
onTransformEnd={onTransformEnd} |
|
|
onMouseEnter={onMouseEnter} |
|
|
onMouseLeave={onMouseLeave} |
|
|
opacity={object.isEditing ? 0 : 1} |
|
|
listening={!object.isEditing} |
|
|
/> |
|
|
); |
|
|
} |
|
|
|