ChunDe's picture
refactor: Remove offsetX/offsetY from text objects for more reliable positioning
3c80b47
raw
history blame
5 kB
import { useEffect, useRef } from 'react';
import { Text as KonvaText } from 'react-konva';
import { TextObject } from '../../types/canvas.types';
import Konva from 'konva';
// Helper function to get fontStyle string based on weight and italic
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();
}
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);
// Auto-enter edit mode when text is first created (empty text)
useEffect(() => {
if (object.text === '' && isSelected && !object.isFixedSize && !object.isEditing) {
// Delay to ensure Konva node is fully rendered
const timer = setTimeout(() => {
onEditingChange(object.id, true);
}, 100);
return () => clearTimeout(timer);
}
}, [object.text, isSelected, object.isFixedSize, object.isEditing, object.id, onEditingChange]);
// Handle double-click to edit
const handleDoubleClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
if (!object.isEditing) {
const stage = e.target.getStage();
const pointerPos = stage?.getPointerPosition();
if (pointerPos) {
// Get click position relative to the text object
const clickX = pointerPos.x - object.x;
const clickY = pointerPos.y - object.y;
onEditingChange(object.id, true, clickX, clickY);
} else {
onEditingChange(object.id, true);
}
}
};
// Calculate font size to fit text in fixed box (Stage 2)
const calculateFitFontSize = (): number => {
if (!object.isFixedSize) return object.fontSize;
if (!object.text) return object.fontSize;
try {
// Create temporary text to measure
const tempText = new Konva.Text({
text: object.text || 'M',
fontSize: object.fontSize,
fontFamily: object.fontFamily,
fontStyle: getFontStyle(object.bold, object.italic, object.fontWeight),
width: object.width,
height: object.height
});
let fontSize = object.fontSize;
const maxWidth = object.width;
const maxHeight = object.height;
// Scale down font if text doesn't fit
while (fontSize > 10) {
tempText.fontSize(fontSize);
const size = tempText.measureSize(object.text || 'M');
if (size.width <= maxWidth && size.height <= maxHeight) {
break;
}
fontSize -= 1;
}
tempText.destroy();
return fontSize;
} catch (error) {
console.error('Error calculating fit font size:', error);
return object.fontSize;
}
};
const displayFontSize = object.isFixedSize ? calculateFitFontSize() : object.fontSize;
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)}
onTap={(e) => onSelect(e)}
onDblClick={handleDoubleClick}
onDblTap={handleDoubleClick}
onDragStart={onDragStart}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
onTransformEnd={onTransformEnd}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
opacity={object.isEditing ? 0 : 1}
listening={!object.isEditing}
/>
);
}