grid editor
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type CellEditorProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
value: string;
|
||||
onSave: (text: string) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const CellEditor = ({ x, y, width, height, value, onSave, onCancel }: CellEditorProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onSave(inputRef.current?.value || "");
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onSave(inputRef.current?.value || "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
zIndex: 1000,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "4px 8px",
|
||||
border: "2px solid #3b82f6",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px",
|
||||
fontFamily: "Arial",
|
||||
textAlign: "center",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CellEditor;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import React from "react";
|
||||
import { Group, Rect, Text } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||
|
||||
type TableProps = {
|
||||
obj: EditorObject;
|
||||
selectedCellId: string | null;
|
||||
onCellClick: (tableId: string, cellId: string, node: Konva.Node) => void;
|
||||
onCellDblClick: (tableId: string, cellId: string, x: number, y: number, text: string) => void;
|
||||
onDragEnd: (tableId: string, x: number, y: number) => void;
|
||||
draggable: boolean;
|
||||
};
|
||||
|
||||
const Table = ({
|
||||
obj,
|
||||
selectedCellId,
|
||||
onCellClick,
|
||||
onCellDblClick,
|
||||
onDragEnd,
|
||||
draggable,
|
||||
}: TableProps) => {
|
||||
const groupRef = React.useRef<Konva.Group>(null);
|
||||
const tableData = obj.tableData;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (groupRef.current) {
|
||||
groupRef.current.setAttr("id", obj.id);
|
||||
}
|
||||
}, [obj.id]);
|
||||
|
||||
if (!tableData) return null;
|
||||
|
||||
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = tableData;
|
||||
|
||||
const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
if (groupRef.current) {
|
||||
onCellClick(obj.id, cellId, groupRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
const stage = e.target.getStage();
|
||||
if (!stage || !groupRef.current) return;
|
||||
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return;
|
||||
|
||||
const row = parseInt(cellId.split("-")[1]);
|
||||
const col = parseInt(cellId.split("-")[2]);
|
||||
const cellX = col * cellWidth;
|
||||
const cellY = row * cellHeight;
|
||||
|
||||
const groupPos = groupRef.current.getAbsolutePosition();
|
||||
const stageBox = stage.container().getBoundingClientRect();
|
||||
const x = stageBox.left + groupPos.x + cellX;
|
||||
const y = stageBox.top + groupPos.y + cellY;
|
||||
|
||||
onCellDblClick(obj.id, cellId, x, y, cell.text);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (groupRef.current) {
|
||||
const pos = groupRef.current.position();
|
||||
onDragEnd(obj.id, pos.x, pos.y);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
ref={groupRef}
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
draggable={draggable}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{Array.from({ length: rows }).map((_, row) =>
|
||||
Array.from({ length: cols }).map((_, col) => {
|
||||
const cellId = `cell-${row}-${col}`;
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return null;
|
||||
|
||||
const x = col * cellWidth;
|
||||
const y = row * cellHeight;
|
||||
const isCellSelected = selectedCellId === cellId;
|
||||
|
||||
const isTopLeft = row === 0 && col === 0;
|
||||
const isTopRight = row === 0 && col === cols - 1;
|
||||
|
||||
return (
|
||||
<Group key={cellId}>
|
||||
<Rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
fill={cell.background}
|
||||
stroke={isCellSelected ? "#3b82f6" : stroke || "#808080"}
|
||||
strokeWidth={isCellSelected ? 3 : strokeWidth || 1}
|
||||
cornerRadius={isTopLeft ? [8, 0, 0, 0] : isTopRight ? [0, 8, 0, 0] : 0}
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
{cell.text && (
|
||||
<Text
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
text={cell.text}
|
||||
fontSize={14}
|
||||
fontFamily="Arial"
|
||||
fill="#000000"
|
||||
align="center"
|
||||
verticalAlign="middle"
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
Reference in New Issue
Block a user