ChunDe's picture
feat: Add MCP compatibility with smart collaboration thumbnails
65493c3
import { useEffect, useRef } from 'react';
import { Group, Rect, 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();
}
// Calculate luminance of a color to determine if it's light or dark
// Returns a value between 0 (black) and 255 (white)
function getLuminance(hexColor: string): number {
// Remove # if present
const hex = hexColor.replace('#', '');
// Convert to RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Calculate relative luminance using the formula for perceived brightness
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
return 0.299 * r + 0.587 * g + 0.114 * b;
}
// Get contrasting text color (white or black) based on background color
function getContrastTextColor(backgroundColor: string): string {
const luminance = getLuminance(backgroundColor);
// If luminance > 128, background is light, use black text
// If luminance <= 128, background is dark, use white text
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);
// Auto-enter edit mode when text is first created (empty text)
useEffect(() => {
if (object.text === '' && isSelected && !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.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 the layer to account for its offset
const layer = e.target.getLayer();
const layerX = layer?.x() || 0;
const layerY = layer?.y() || 0;
// Get click position relative to the text object (accounting for layer offset)
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);
}
}
};
// Always use the object's fontSize (no dynamic scaling)
const displayFontSize = object.fontSize;
// Background padding and border radius
const bgPaddingTop = 8;
const bgPaddingRight = 8;
const bgPaddingBottom = 5;
const bgPaddingLeft = 8;
const bgRadius = 8;
// If hasBackground is true, wrap in Group with background Rect
if (object.hasBackground) {
// Get background color (defaults to current fill if not set)
const bgColor = object.backgroundColor || object.fill;
// Calculate contrasting text color
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}
/>
);
}