Files
dpage-editor/src/pages/editor/components/EditorCanvas.tsx
T
hamid zarghami aa6153d289 base mask
2026-01-04 10:18:49 +03:30

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;