From c79999d7737d9e9b94325e3f278228c170fb9a88 Mon Sep 17 00:00:00 2001 From: 1ilit Date: Tue, 19 Sep 2023 15:49:44 +0300 Subject: [PATCH] Undo and redo for moving and adding objects --- src/components/canvas.jsx | 78 +++++++++--- src/components/control_panel.jsx | 212 +++++++++++++++++++++++++------ src/components/custom_icons.jsx | 6 +- src/data/data.js | 6 + 4 files changed, 243 insertions(+), 59 deletions(-) diff --git a/src/components/canvas.jsx b/src/components/canvas.jsx index 0dbcd7c..82ca911 100644 --- a/src/components/canvas.jsx +++ b/src/components/canvas.jsx @@ -1,6 +1,6 @@ import React, { useContext, useRef, useState, useEffect } from "react"; import Table from "./table"; -import { Cardinality, Constraint, ObjectType } from "../data/data"; +import { Action, Cardinality, Constraint, ObjectType } from "../data/data"; import Area from "./area"; import Relationship from "./relationship"; import { @@ -8,6 +8,7 @@ import { NoteContext, SettingsContext, TableContext, + UndoRedoContext, } from "../pages/editor"; import Note from "./note"; @@ -17,7 +18,13 @@ export default function Canvas(props) { const { areas, setAreas } = useContext(AreaContext); const { notes, setNotes } = useContext(NoteContext); const { settings, setSettings } = useContext(SettingsContext); - const [dragging, setDragging] = useState([ObjectType.NONE, -1]); + const { redoStack, setUndoStack, setRedoStack } = useContext(UndoRedoContext); + const [dragging, setDragging] = useState({ + element: ObjectType.NONE, + id: -1, + prevX: 0, + prevY: 0, + }); const [linking, setLinking] = useState(false); const [line, setLine] = useState({ startTableId: -1, @@ -62,21 +69,36 @@ export default function Canvas(props) { x: clientX / settings.zoom - table.x, y: clientY / settings.zoom - table.y, }); - setDragging([ObjectType.TABLE, id]); + setDragging({ + element: ObjectType.TABLE, + id: id, + prevX: table.x, + prevY: table.y, + }); } else if (type === ObjectType.AREA) { const area = areas.find((t) => t.id === id); setOffset({ x: clientX / settings.zoom - area.x, y: clientY / settings.zoom - area.y, }); - setDragging([ObjectType.AREA, id]); + setDragging({ + element: ObjectType.AREA, + id: id, + prevX: area.x, + prevY: area.y, + }); } else if (type === ObjectType.NOTE) { const note = notes.find((t) => t.id === id); setOffset({ x: clientX / settings.zoom - note.x, y: clientY / settings.zoom - note.y, }); - setDragging([ObjectType.NOTE, id]); + setDragging({ + element: ObjectType.NOTE, + id: id, + prevX: note.x, + prevY: note.y, + }); } }; @@ -93,7 +115,7 @@ export default function Canvas(props) { }); } else if ( panning && - dragging[0] === ObjectType.NONE && + dragging.element === ObjectType.NONE && areaResize.id === -1 ) { const dx = (e.clientX - panOffset.x) / settings.zoom; @@ -121,9 +143,9 @@ export default function Canvas(props) { setAreas((prev) => prev.map((t) => ({ ...t, x: t.x + dx, y: t.y + dy }))); setNotes((prev) => prev.map((n) => ({ ...n, x: n.x + dx, y: n.y + dy }))); - } else if (dragging[0] === ObjectType.TABLE && dragging[1] >= 0) { + } else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) { const updatedTables = tables.map((t) => { - if (t.id === dragging[1]) { + if (t.id === dragging.id) { return { ...t, x: e.clientX / settings.zoom - offset.x, @@ -135,13 +157,13 @@ export default function Canvas(props) { setTables(updatedTables); setRelationships((prev) => prev.map((r) => { - if (r.startTableId === dragging[1]) { + if (r.startTableId === dragging.id) { return { ...r, startX: tables[r.startTableId].x + 15, startY: tables[r.startTableId].y + r.startFieldId * 36 + 50 + 19, }; - } else if (r.endTableId === dragging[1]) { + } else if (r.endTableId === dragging.id) { return { ...r, endX: tables[r.endTableId].x + 15, @@ -152,13 +174,13 @@ export default function Canvas(props) { }) ); } else if ( - dragging[0] === ObjectType.AREA && - dragging[1] >= 0 && + dragging.element === ObjectType.AREA && + dragging.id >= 0 && areaResize.id === -1 ) { setAreas((prev) => prev.map((t) => { - if (t.id === dragging[1]) { + if (t.id === dragging.id) { const updatedArea = { ...t, x: e.clientX / settings.zoom - offset.x, @@ -169,10 +191,10 @@ export default function Canvas(props) { return t; }) ); - } else if (dragging[0] === ObjectType.NOTE && dragging[1] >= 0) { + } else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) { setNotes((prev) => prev.map((t) => { - if (t.id === dragging[1]) { + if (t.id === dragging.id) { return { ...t, x: e.clientX / settings.zoom - offset.x, @@ -233,8 +255,28 @@ export default function Canvas(props) { setCursor("grabbing"); }; - const handleMouseUp = () => { - setDragging([ObjectType.NONE, -1]); + const coordsDidUpdate = () => { + return !( + dragging.prevX === tables[dragging.id].x && + dragging.prevY === tables[dragging.id].y + ); + }; + + const handleMouseUp = (e) => { + if (dragging.element !== ObjectType.NONE && coordsDidUpdate()) { + setUndoStack((prev) => [ + ...prev, + { + action: Action.MOVE, + element: dragging.element, + x: dragging.prevX, + y: dragging.prevY, + id: dragging.id, + }, + ]); + if (redoStack.length > 0) setRedoStack([]); + } + setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 }); setPanning(false); setCursor("default"); if (linking) handleLinking(); @@ -252,7 +294,7 @@ export default function Canvas(props) { const handleGripField = (id) => { setPanning(false); - setDragging([ObjectType.NONE, -1]); + setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 }); setLinking(true); }; diff --git a/src/components/control_panel.jsx b/src/components/control_panel.jsx index 7e94965..0fbc26e 100644 --- a/src/components/control_panel.jsx +++ b/src/components/control_panel.jsx @@ -44,8 +44,13 @@ import { TableContext, UndoRedoContext, } from "../pages/editor"; -import { AddTable, AddArea, AddNote } from "./custom_icons"; -import { defaultTableTheme, defaultNoteTheme } from "../data/data"; +import { IconAddTable, IconAddArea, IconAddNote } from "./custom_icons"; +import { + defaultTableTheme, + defaultNoteTheme, + ObjectType, + Action, +} from "../data/data"; import CodeMirror from "@uiw/react-codemirror"; import { json } from "@codemirror/lang-json"; import jsPDF from "jspdf"; @@ -445,30 +450,161 @@ export default function ControlPanel(props) { ]); }; + const addArea = () => { + setAreas((prev) => [ + ...prev, + { + id: prev.length, + name: `area_${prev.length}`, + x: 0, + y: 0, + width: 200, + height: 200, + color: defaultTableTheme, + }, + ]); + }; + + const addNote = () => { + setNotes((prev) => [ + ...prev, + { + id: prev.length, + x: 0, + y: 0, + title: `note_${prev.length}`, + content: "", + color: defaultNoteTheme, + height: 88, + }, + ]); + }; + + const moveTable = (id, x, y) => { + setTables((prev) => + prev.map((t) => { + if (t.id === id) { + return { + ...t, + x: x, + y: y, + }; + } + return t; + }) + ); + }; + + const moveArea = (id, x, y) => { + setAreas((prev) => + prev.map((t) => { + if (t.id === id) { + return { + ...t, + x: x, + y: y, + }; + } + return t; + }) + ); + }; + + const moveNote = (id, x, y) => { + setNotes((prev) => + prev.map((t) => { + if (t.id === id) { + return { + ...t, + x: x, + y: y, + }; + } + return t; + }) + ); + }; + const undo = () => { if (undoStack.length === 0) return; const a = undoStack.pop(); - if (a.action === "add") { - if (a.element === "table") { + if (a.action === Action.ADD) { + if (a.element === ObjectType.TABLE) { setTables((prev) => prev .filter((e) => e.id !== prev.length - 1) .map((e, i) => ({ ...e, id: i })) ); + } else if (a.element === ObjectType.AREA) { + setAreas((prev) => + prev + .filter((e) => e.id !== prev.length - 1) + .map((e, i) => ({ ...e, id: i })) + ); + } else if (a.element === ObjectType.NOTE) { + setNotes((prev) => + prev + .filter((e) => e.id !== prev.length - 1) + .map((e, i) => ({ ...e, id: i })) + ); + } + setRedoStack((prev) => [...prev, a]); + } else if (a.action === Action.MOVE) { + if (a.element === ObjectType.TABLE) { + setRedoStack((prev) => [ + ...prev, + { ...a, x: tables[a.id].x, y: tables[a.id].y }, + ]); + moveTable(a.id, a.x, a.y); + } else if (a.element === ObjectType.AREA) { + setRedoStack((prev) => [ + ...prev, + { ...a, x: areas[a.id].x, y: areas[a.id].y }, + ]); + moveArea(a.id, a.x, a.y); + } else if (a.element === ObjectType.NOTE) { + setRedoStack((prev) => [ + ...prev, + { ...a, x: notes[a.id].x, y: notes[a.id].y }, + ]); + moveNote(a.id, a.x, a.y); } } - setRedoStack((prev) => [...prev, a]); }; const redo = () => { if (redoStack.length === 0) return; const a = redoStack.pop(); - if (a.action === "add") { - if (a.element === "table") { + if (a.action === Action.ADD) { + if (a.element === ObjectType.TABLE) { addTable(); + } else if (a.element === ObjectType.AREA) { + addArea(); + } else if (a.element === ObjectType.NOTE) { + addNote(); + } + setUndoStack((prev) => [...prev, a]); + } else if (a.action === Action.MOVE) { + if (a.element === ObjectType.TABLE) { + setUndoStack((prev) => [ + ...prev, + { ...a, x: tables[a.id].x, y: tables[a.id].y }, + ]); + moveTable(a.id, a.x, a.y); + }else if (a.element === ObjectType.AREA) { + setUndoStack((prev) => [ + ...prev, + { ...a, x: areas[a.id].x, y: areas[a.id].y }, + ]); + moveArea(a.id, a.x, a.y); + } else if (a.element === ObjectType.NOTE) { + setUndoStack((prev) => [ + ...prev, + { ...a, x: notes[a.id].x, y: notes[a.id].y }, + ]); + moveNote(a.id, a.x, a.y); } } - setUndoStack((prev) => [...prev, a]); }; return ( @@ -545,14 +681,20 @@ export default function ControlPanel(props) { title="Undo" onClick={undo} > - +