269 lines
8.4 KiB
TypeScript
269 lines
8.4 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { Stage, Layer, Transformer, Rect } from "react-konva";
|
|
import Konva from "konva";
|
|
import { useEditorStore } from "../store/editorStore";
|
|
import { useDrawingHandlers } from "./canvas/useDrawingHandlers";
|
|
import { useStageSize } from "./canvas/useStageSize";
|
|
import { useKeyboardMovement } from "./canvas/useKeyboardMovement";
|
|
import ObjectRenderer from "./canvas/ObjectRenderer";
|
|
import { getMaskForObject } from "./canvas/maskUtils";
|
|
import CellEditor from "@/components/CellEditor";
|
|
import ZoomControls from "./ZoomControls";
|
|
|
|
const EditorCanvas = () => {
|
|
const stageRef = useRef<Konva.Stage>(null);
|
|
const layerRef = useRef<Konva.Layer>(null);
|
|
|
|
const {
|
|
tool,
|
|
objects,
|
|
updateObject,
|
|
selectedObjectId,
|
|
setSelectedObjectId,
|
|
setSelectedTableId,
|
|
selectedCellId,
|
|
setSelectedCellId,
|
|
editingCell,
|
|
setEditingCell,
|
|
updateCellValue,
|
|
applyTableResize,
|
|
setStageRef,
|
|
setLayerRef,
|
|
zoom,
|
|
} = useEditorStore();
|
|
|
|
const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers();
|
|
const stageSize = useStageSize();
|
|
useKeyboardMovement();
|
|
|
|
useEffect(() => {
|
|
if (stageRef.current) {
|
|
setStageRef(stageRef);
|
|
}
|
|
if (layerRef.current) {
|
|
setLayerRef(layerRef);
|
|
}
|
|
}, [setStageRef, setLayerRef]);
|
|
|
|
const handleSelect = (id: string, node: Konva.Node) => {
|
|
const obj = objects.find((o) => o.id === id);
|
|
setSelectedObjectId(id);
|
|
if (obj?.type === "grid") {
|
|
setSelectedTableId(id);
|
|
} else {
|
|
setSelectedTableId(null);
|
|
}
|
|
setSelectedCellId(null);
|
|
if (transformerRef.current) {
|
|
transformerRef.current.nodes([node]);
|
|
transformerRef.current.getLayer()?.batchDraw();
|
|
}
|
|
};
|
|
|
|
const handleCellClick = (tableId: string, cellId: string) => {
|
|
setSelectedTableId(tableId);
|
|
setSelectedObjectId(tableId);
|
|
setSelectedCellId(cellId);
|
|
const obj = objects.find((o) => o.id === tableId);
|
|
if (obj && transformerRef.current && layerRef.current) {
|
|
const tableNode = layerRef.current.findOne(`#${tableId}`);
|
|
if (tableNode) {
|
|
transformerRef.current.nodes([tableNode]);
|
|
transformerRef.current.getLayer()?.batchDraw();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCellDblClick = (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => {
|
|
setEditingCell({ tableId, cellId, x, y, width, height, value: text });
|
|
};
|
|
|
|
const handleCellEditorSave = (text: string) => {
|
|
if (editingCell) {
|
|
updateCellValue(editingCell.tableId, editingCell.cellId, text);
|
|
setEditingCell(null);
|
|
}
|
|
};
|
|
|
|
const handleCellEditorCancel = () => {
|
|
setEditingCell(null);
|
|
};
|
|
|
|
// Attach transformer to selected object when it changes
|
|
useEffect(() => {
|
|
if (!transformerRef.current || !selectedObjectId || !layerRef.current) return;
|
|
|
|
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
|
if (!selectedObject) return;
|
|
|
|
// For grid objects, handle separately
|
|
if (selectedObject.type === "grid") {
|
|
const tableNode = layerRef.current.findOne(`#${selectedObjectId}`);
|
|
if (tableNode && transformerRef.current) {
|
|
transformerRef.current.nodes([tableNode]);
|
|
transformerRef.current.getLayer()?.batchDraw();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For other objects (like images), find the node by id and attach transformer
|
|
// Use a small delay to ensure the node is mounted (especially for images that need to load)
|
|
const timeoutId = setTimeout(() => {
|
|
if (!layerRef.current || !transformerRef.current) return;
|
|
|
|
// Check if object is masked - if so, find parent Group
|
|
const maskShape = getMaskForObject(selectedObject, objects);
|
|
let node: Konva.Node | undefined;
|
|
|
|
if (maskShape) {
|
|
node = layerRef.current.findOne(`.masked-group-${selectedObjectId}`);
|
|
} else {
|
|
node = layerRef.current.findOne(`#${selectedObjectId}`);
|
|
}
|
|
|
|
if (node && transformerRef.current) {
|
|
transformerRef.current.nodes([node]);
|
|
transformerRef.current.getLayer()?.batchDraw();
|
|
}
|
|
}, 100);
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
}, [selectedObjectId, objects, transformerRef, layerRef]);
|
|
|
|
useEffect(() => {
|
|
if (!transformerRef.current || !selectedObjectId) return;
|
|
|
|
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
|
if (selectedObject?.type !== "grid") return;
|
|
|
|
const transformer = transformerRef.current;
|
|
const handleTransformEnd = () => {
|
|
const node = transformer.nodes()[0];
|
|
if (!node || !selectedObject) return;
|
|
|
|
const scaleX = node.scaleX();
|
|
const scaleY = node.scaleY();
|
|
|
|
node.scaleX(1);
|
|
node.scaleY(1);
|
|
|
|
const newWidth = (selectedObject.width || 300) * scaleX;
|
|
const newHeight = (selectedObject.height || 200) * scaleY;
|
|
|
|
applyTableResize(selectedObject.id, newWidth, newHeight);
|
|
updateObject(selectedObject.id, {
|
|
x: node.x(),
|
|
y: node.y(),
|
|
});
|
|
};
|
|
|
|
transformer.on("transformend", handleTransformEnd);
|
|
|
|
return () => {
|
|
transformer.off("transformend", handleTransformEnd);
|
|
};
|
|
}, [selectedObjectId, objects, transformerRef, applyTableResize, updateObject]);
|
|
|
|
const finalScale = stageSize.scale * zoom;
|
|
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center pb-8 overflow-auto relative">
|
|
<div className="shadow-lg mt-8">
|
|
<Stage
|
|
ref={stageRef}
|
|
width={stageSize.width * finalScale}
|
|
height={stageSize.height * finalScale}
|
|
scaleX={finalScale}
|
|
scaleY={finalScale}
|
|
onMouseDown={handleStageMouseDown}
|
|
onMouseMove={handleStageMouseMove}
|
|
onMouseUp={handleStageMouseUp}
|
|
>
|
|
<Layer listening={false}>
|
|
<Rect
|
|
x={0}
|
|
y={0}
|
|
width={stageSize.width}
|
|
height={stageSize.height}
|
|
fill="#ffffff"
|
|
listening={false}
|
|
/>
|
|
</Layer>
|
|
|
|
<Layer ref={layerRef}>
|
|
{objects.map((obj) => {
|
|
const maskShape = getMaskForObject(obj, objects);
|
|
|
|
return (
|
|
<ObjectRenderer
|
|
key={obj.id}
|
|
obj={obj}
|
|
isSelected={selectedObjectId === obj.id}
|
|
transformerRef={transformerRef}
|
|
layerRef={layerRef}
|
|
onSelect={handleSelect}
|
|
onUpdate={updateObject}
|
|
draggable={tool === "select"}
|
|
onCellClick={handleCellClick}
|
|
onCellDblClick={handleCellDblClick}
|
|
selectedCellId={selectedCellId}
|
|
maskShape={maskShape}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{selectedObjectId && (() => {
|
|
const selectedObject = objects.find(obj => obj.id === selectedObjectId);
|
|
const isText = selectedObject?.type === "text";
|
|
|
|
return (
|
|
<Transformer
|
|
ref={transformerRef}
|
|
boundBoxFunc={(oldBox, newBox) => {
|
|
if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
|
|
return oldBox;
|
|
}
|
|
return newBox;
|
|
}}
|
|
ignoreStroke={isText}
|
|
perfectDrawEnabled={false}
|
|
anchorFill="#3b82f6"
|
|
anchorStroke="#ffffff"
|
|
borderStroke="#3b82f6"
|
|
borderStrokeWidth={2}
|
|
anchorSize={8}
|
|
rotateEnabled={true}
|
|
enabledAnchors={[
|
|
"top-left",
|
|
"top-right",
|
|
"bottom-left",
|
|
"bottom-right",
|
|
"top-center",
|
|
"bottom-center",
|
|
"middle-left",
|
|
"middle-right",
|
|
]}
|
|
/>
|
|
);
|
|
})()}
|
|
</Layer>
|
|
</Stage>
|
|
</div>
|
|
{editingCell && (
|
|
<CellEditor
|
|
x={editingCell.x}
|
|
y={editingCell.y}
|
|
width={editingCell.width}
|
|
height={editingCell.height}
|
|
value={editingCell.value}
|
|
onSave={handleCellEditorSave}
|
|
onCancel={handleCellEditorCancel}
|
|
/>
|
|
)}
|
|
<ZoomControls />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EditorCanvas;
|