diff --git a/src/components/CellEditor.tsx b/src/components/CellEditor.tsx new file mode 100644 index 0000000..26f88a8 --- /dev/null +++ b/src/components/CellEditor.tsx @@ -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(null); + + useEffect(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + 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 ( +
+ +
+ ); +}; + +export default CellEditor; + diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 0000000..489b6ba --- /dev/null +++ b/src/components/Table.tsx @@ -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(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) => { + e.cancelBubble = true; + if (groupRef.current) { + onCellClick(obj.id, cellId, groupRef.current); + } + }; + + const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject) => { + 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 ( + + {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 ( + + handleCellClick(cellId, e)} + onDblClick={(e) => handleCellDblClick(cellId, e)} + /> + {cell.text && ( + handleCellClick(cellId, e)} + onDblClick={(e) => handleCellDblClick(cellId, e)} + /> + )} + + ); + }) + )} + + ); +}; + +export default Table; + diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx index 8ab273f..f096963 100644 --- a/src/pages/editor/components/EditorCanvas.tsx +++ b/src/pages/editor/components/EditorCanvas.tsx @@ -6,6 +6,7 @@ import { clx } from "@/helpers/utils"; import { useDrawingHandlers } from "./canvas/useDrawingHandlers"; import { useStageSize } from "./canvas/useStageSize"; import ObjectRenderer from "./canvas/ObjectRenderer"; +import CellEditor from "@/components/CellEditor"; const EditorCanvas = () => { const stageRef = useRef(null); @@ -17,6 +18,13 @@ const EditorCanvas = () => { updateObject, selectedObjectId, setSelectedObjectId, + setSelectedTableId, + selectedCellId, + setSelectedCellId, + editingCell, + setEditingCell, + updateCellValue, + applyTableResize, setStageRef, setLayerRef, } = useEditorStore(); @@ -34,13 +42,83 @@ const EditorCanvas = () => { }, [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, text: string) => { + setEditingCell({ tableId, cellId, x, y, value: text }); + }; + + const handleCellEditorSave = (text: string) => { + if (editingCell) { + updateCellValue(editingCell.tableId, editingCell.cellId, text); + setEditingCell(null); + } + }; + + const handleCellEditorCancel = () => { + setEditingCell(null); + }; + + 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]); + return (
{ onSelect={handleSelect} onUpdate={updateObject} draggable={tool === "select"} + onCellClick={handleCellClick} + onCellDblClick={handleCellDblClick} + selectedCellId={selectedCellId} /> ))} {selectedObjectId && (() => { @@ -101,6 +182,23 @@ const EditorCanvas = () => { })()} + {editingCell && (() => { + const obj = objects.find((o) => o.id === editingCell.tableId); + if (!obj || !obj.tableData) return null; + const cell = obj.tableData.cells[editingCell.cellId]; + if (!cell) return null; + return ( + + ); + })()}
); }; diff --git a/src/pages/editor/components/canvas/ObjectRenderer.tsx b/src/pages/editor/components/canvas/ObjectRenderer.tsx index da95711..b4e78f2 100644 --- a/src/pages/editor/components/canvas/ObjectRenderer.tsx +++ b/src/pages/editor/components/canvas/ObjectRenderer.tsx @@ -12,6 +12,7 @@ import { LinkShape, VideoShape, } from "../tools"; +import Table from "@/components/Table"; type ObjectRendererProps = { obj: EditorObject; @@ -21,6 +22,9 @@ type ObjectRendererProps = { onSelect: (id: string, node: Konva.Node) => void; onUpdate: (id: string, updates: Partial) => void; draggable: boolean; + onCellClick?: (tableId: string, cellId: string) => void; + onCellDblClick?: (tableId: string, cellId: string, x: number, y: number, text: string) => void; + selectedCellId?: string | null; }; const ObjectRenderer = ({ @@ -31,6 +35,9 @@ const ObjectRenderer = ({ onSelect, onUpdate, draggable, + onCellClick, + onCellDblClick, + selectedCellId, }: ObjectRendererProps) => { const commonProps = { obj, @@ -67,6 +74,25 @@ const ObjectRenderer = ({ return ; case "document": return ; + case "grid": + return ( + { + onSelect(tableId, node); + onCellClick?.(tableId, cellId); + }} + onCellDblClick={(tableId, cellId, x, y, text) => { + onCellDblClick?.(tableId, cellId, x, y, text); + }} + onDragEnd={(tableId, x, y) => { + onUpdate(tableId, { x, y }); + }} + draggable={draggable} + /> + ); default: return null; } diff --git a/src/pages/editor/components/canvas/useDrawingHandlers.ts b/src/pages/editor/components/canvas/useDrawingHandlers.ts index eec0ad9..15efd2b 100644 --- a/src/pages/editor/components/canvas/useDrawingHandlers.ts +++ b/src/pages/editor/components/canvas/useDrawingHandlers.ts @@ -10,7 +10,7 @@ export const useDrawingHandlers = () => { const [tempObject, setTempObject] = useState(null); const transformerRef = useRef(null); - const { tool, addObject, updateObject, setSelectedObjectId, selectedObjectId, setTool } = useEditorStore(); + const { tool, addObject, updateObject, setSelectedObjectId, selectedObjectId, setTool, addTable, setSelectedTableId } = useEditorStore(); const { defaults } = useTextDefaultsStore(); useEffect(() => { @@ -73,6 +73,12 @@ export const useDrawingHandlers = () => { setSelectedObjectId(newText.id); setTool("select"); } + + if (tool === "grid") { + addTable(pointerPos.x, pointerPos.y, 4, 5); + setSelectedTableId(null); + setTool("select"); + } }; const handleStageMouseMove = (e: Konva.KonvaEventObject) => { diff --git a/src/pages/editor/components/sidebar/instructions/GridInstruction.tsx b/src/pages/editor/components/sidebar/instructions/GridInstruction.tsx index cde23d4..02e1206 100644 --- a/src/pages/editor/components/sidebar/instructions/GridInstruction.tsx +++ b/src/pages/editor/components/sidebar/instructions/GridInstruction.tsx @@ -1,6 +1,9 @@ const GridInstruction = () => ( -
-

برای افزودن گرید، روی کانوس کلیک کنید

+
+

برای افزودن جدول، روی کانوس کلیک کنید

+

+ جدول به صورت پیش‌فرض با 3 ردیف و 3 ستون ایجاد می‌شود +

); diff --git a/src/pages/editor/components/sidebar/settings/GridSettings.tsx b/src/pages/editor/components/sidebar/settings/GridSettings.tsx index ca1f212..a3392b1 100644 --- a/src/pages/editor/components/sidebar/settings/GridSettings.tsx +++ b/src/pages/editor/components/sidebar/settings/GridSettings.tsx @@ -1,35 +1,127 @@ +import { Add, Minus } from "iconsax-react"; import type { EditorObject } from "../../../store/editorStore"; -import Input from "@/components/Input"; +import { useEditorStore } from "../../../store/editorStore"; +import ColorPicker from "@/components/ColorPicker"; type GridSettingsProps = { selectedObject: EditorObject; onUpdate: (id: string, updates: Partial) => void; }; -const GridSettings = ({ selectedObject, onUpdate }: GridSettingsProps) => { +const GridSettings = ({ selectedObject }: GridSettingsProps) => { + const { + addRow, + addColumn, + removeRow, + removeColumn, + changeCellBackground, + selectedCellId, + } = useEditorStore(); + + const tableDataObj = selectedObject.tableData; + + const handleAddRow = () => { + if (tableDataObj) { + addRow(selectedObject.id); + } + }; + + const handleAddColumn = () => { + if (tableDataObj) { + addColumn(selectedObject.id); + } + }; + + const handleRemoveRow = () => { + if (tableDataObj) { + removeRow(selectedObject.id); + } + }; + + const handleRemoveColumn = () => { + if (tableDataObj) { + removeColumn(selectedObject.id); + } + }; + + const handleCellBackgroundChange = (color: string) => { + if (selectedCellId && tableDataObj) { + changeCellBackground(selectedObject.id, selectedCellId, color); + } + }; + + const selectedCell = selectedCellId && tableDataObj ? tableDataObj.cells[selectedCellId] : null; + return ( - <> - - onUpdate(selectedObject.id, { - width: parseInt(e.target.value) || 200, - }) - } - /> - - onUpdate(selectedObject.id, { - height: parseInt(e.target.value) || 200, - }) - } - /> - +
+
+
+ ردیف‌ها +
+ + +
+
+
+ تعداد ردیف‌ها: {tableDataObj?.rows || 0} +
+
+ +
+
+ ستون‌ها +
+ + +
+
+
+ تعداد ستون‌ها: {tableDataObj?.cols || 0} +
+
+ + {selectedCell && ( +
+ +
+ )} + + {!selectedCell && ( +
+ برای تغییر رنگ سلول، ابتدا روی یک سلول کلیک کنید +
+ )} +
); }; diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 6a0ac2d..cb35a20 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -15,6 +15,25 @@ export type ToolType = | "link" | "grid"; +export type TableCell = { + id: string; + text: string; + background: string; + width: number; + height: number; +}; + +export type TableData = { + id: string; + rows: number; + cols: number; + cellWidth: number; + cellHeight: number; + cells: Record; + stroke?: string; + strokeWidth?: number; +}; + export type EditorObject = { id: string; type: ToolType; @@ -39,6 +58,7 @@ export type EditorObject = { scaleX?: number; scaleY?: number; shapeType?: ShapeType; + tableData?: TableData; }; type EditorStoreType = { @@ -50,13 +70,46 @@ type EditorStoreType = { deleteObject: (id: string) => void; selectedObjectId: string | null; setSelectedObjectId: (id: string | null) => void; + selectedTableId: string | null; + setSelectedTableId: (id: string | null) => void; + selectedCellId: string | null; + setSelectedCellId: (id: string | null) => void; + editingCell: { tableId: string; cellId: string; x: number; y: number; value: string } | null; + setEditingCell: (cell: { tableId: string; cellId: string; x: number; y: number; value: string } | null) => void; + addTable: (x: number, y: number, rows?: number, cols?: number) => void; + addRow: (tableId: string) => void; + addColumn: (tableId: string) => void; + removeRow: (tableId: string) => void; + removeColumn: (tableId: string) => void; + updateCellValue: (tableId: string, cellId: string, text: string) => void; + changeCellBackground: (tableId: string, cellId: string, color: string) => void; + applyTableResize: (tableId: string, newWidth: number, newHeight: number) => void; stageRef: React.RefObject | null; setStageRef: (ref: React.RefObject) => void; layerRef: React.RefObject | null; setLayerRef: (ref: React.RefObject) => void; }; -export const useEditorStore = create((set) => ({ +const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`; + +const createInitialCells = (rows: number, cols: number, cellWidth: number, cellHeight: number): Record => { + const cells: Record = {}; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const cellId = createTableCellId(row, col); + cells[cellId] = { + id: cellId, + text: "", + background: "#f5f5f5", + width: cellWidth, + height: cellHeight, + }; + } + } + return cells; +}; + +export const useEditorStore = create((set, get) => ({ tool: "select", setTool: (tool) => set((state) => ({ @@ -77,9 +130,222 @@ export const useEditorStore = create((set) => ({ objects: state.objects.filter((obj) => obj.id !== id), selectedObjectId: state.selectedObjectId === id ? null : state.selectedObjectId, + selectedTableId: state.selectedTableId === id ? null : state.selectedTableId, })), selectedObjectId: null, setSelectedObjectId: (id) => set({ selectedObjectId: id }), + selectedTableId: null, + setSelectedTableId: (id) => set({ selectedTableId: id }), + selectedCellId: null, + setSelectedCellId: (id) => set({ selectedCellId: id }), + editingCell: null, + setEditingCell: (cell) => set({ editingCell: cell }), + addTable: (x, y, rows = 4, cols = 5) => { + const tableId = `table-${crypto.randomUUID()}`; + const cellWidth = 100; + const cellHeight = 50; + const tableData: TableData = { + id: tableId, + rows, + cols, + cellWidth, + cellHeight, + cells: createInitialCells(rows, cols, cellWidth, cellHeight), + stroke: "#808080", + strokeWidth: 1, + }; + const tableObject: EditorObject = { + id: tableId, + type: "grid", + x, + y, + width: cols * cellWidth, + height: rows * cellHeight, + tableData, + }; + get().addObject(tableObject); + get().setSelectedObjectId(tableId); + get().setSelectedTableId(tableId); + }, + addRow: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const newRow = tableData.rows; + const newCells = { ...tableData.cells }; + + for (let col = 0; col < tableData.cols; col++) { + const cellId = createTableCellId(newRow, col); + newCells[cellId] = { + id: cellId, + text: "", + background: "#f5f5f5", + width: tableData.cellWidth, + height: tableData.cellHeight, + }; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + rows: tableData.rows + 1, + cells: newCells, + }, + height: (tableData.rows + 1) * tableData.cellHeight, + }); + }, + addColumn: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const newCol = tableData.cols; + const newCells = { ...tableData.cells }; + + for (let row = 0; row < tableData.rows; row++) { + const cellId = createTableCellId(row, newCol); + newCells[cellId] = { + id: cellId, + text: "", + background: "#f5f5f5", + width: tableData.cellWidth, + height: tableData.cellHeight, + }; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + cols: tableData.cols + 1, + cells: newCells, + }, + width: (tableData.cols + 1) * tableData.cellWidth, + }); + }, + removeRow: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + if (tableData.rows <= 1) return; + + const newCells = { ...tableData.cells }; + for (let col = 0; col < tableData.cols; col++) { + const cellId = createTableCellId(tableData.rows - 1, col); + delete newCells[cellId]; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + rows: tableData.rows - 1, + cells: newCells, + }, + height: (tableData.rows - 1) * tableData.cellHeight, + }); + }, + removeColumn: (tableId) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + if (tableData.cols <= 1) return; + + const newCells = { ...tableData.cells }; + for (let row = 0; row < tableData.rows; row++) { + const cellId = createTableCellId(row, tableData.cols - 1); + delete newCells[cellId]; + } + + state.updateObject(tableId, { + tableData: { + ...tableData, + cols: tableData.cols - 1, + cells: newCells, + }, + width: (tableData.cols - 1) * tableData.cellWidth, + }); + }, + updateCellValue: (tableId, cellId, text) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const updatedCells = { + ...tableData.cells, + [cellId]: { + ...tableData.cells[cellId], + text, + }, + }; + + state.updateObject(tableId, { + tableData: { + ...tableData, + cells: updatedCells, + }, + }); + }, + changeCellBackground: (tableId, cellId, color) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const updatedCells = { + ...tableData.cells, + [cellId]: { + ...tableData.cells[cellId], + background: color, + }, + }; + + state.updateObject(tableId, { + tableData: { + ...tableData, + cells: updatedCells, + }, + }); + }, + applyTableResize: (tableId, newWidth, newHeight) => { + const state = get(); + const obj = state.objects.find((o) => o.id === tableId); + if (!obj || !obj.tableData) return; + + const { tableData } = obj; + const newCellWidth = newWidth / tableData.cols; + const newCellHeight = newHeight / tableData.rows; + + const updatedCells: Record = {}; + for (let row = 0; row < tableData.rows; row++) { + for (let col = 0; col < tableData.cols; col++) { + const cellId = createTableCellId(row, col); + const oldCell = tableData.cells[cellId]; + updatedCells[cellId] = { + ...oldCell, + width: newCellWidth, + height: newCellHeight, + }; + } + } + + state.updateObject(tableId, { + width: newWidth, + height: newHeight, + tableData: { + ...tableData, + cellWidth: newCellWidth, + cellHeight: newCellHeight, + cells: updatedCells, + }, + }); + }, stageRef: null, setStageRef: (ref) => set({ stageRef: ref }), layerRef: null,