Undo and redo for moving and adding objects

This commit is contained in:
1ilit 2023-09-19 15:49:44 +03:00
parent 59609b1cd8
commit c79999d773
4 changed files with 243 additions and 59 deletions

View File

@ -1,6 +1,6 @@
import React, { useContext, useRef, useState, useEffect } from "react"; import React, { useContext, useRef, useState, useEffect } from "react";
import Table from "./table"; import Table from "./table";
import { Cardinality, Constraint, ObjectType } from "../data/data"; import { Action, Cardinality, Constraint, ObjectType } from "../data/data";
import Area from "./area"; import Area from "./area";
import Relationship from "./relationship"; import Relationship from "./relationship";
import { import {
@ -8,6 +8,7 @@ import {
NoteContext, NoteContext,
SettingsContext, SettingsContext,
TableContext, TableContext,
UndoRedoContext,
} from "../pages/editor"; } from "../pages/editor";
import Note from "./note"; import Note from "./note";
@ -17,7 +18,13 @@ export default function Canvas(props) {
const { areas, setAreas } = useContext(AreaContext); const { areas, setAreas } = useContext(AreaContext);
const { notes, setNotes } = useContext(NoteContext); const { notes, setNotes } = useContext(NoteContext);
const { settings, setSettings } = useContext(SettingsContext); 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 [linking, setLinking] = useState(false);
const [line, setLine] = useState({ const [line, setLine] = useState({
startTableId: -1, startTableId: -1,
@ -62,21 +69,36 @@ export default function Canvas(props) {
x: clientX / settings.zoom - table.x, x: clientX / settings.zoom - table.x,
y: clientY / settings.zoom - table.y, 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) { } else if (type === ObjectType.AREA) {
const area = areas.find((t) => t.id === id); const area = areas.find((t) => t.id === id);
setOffset({ setOffset({
x: clientX / settings.zoom - area.x, x: clientX / settings.zoom - area.x,
y: clientY / settings.zoom - area.y, 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) { } else if (type === ObjectType.NOTE) {
const note = notes.find((t) => t.id === id); const note = notes.find((t) => t.id === id);
setOffset({ setOffset({
x: clientX / settings.zoom - note.x, x: clientX / settings.zoom - note.x,
y: clientY / settings.zoom - note.y, 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 ( } else if (
panning && panning &&
dragging[0] === ObjectType.NONE && dragging.element === ObjectType.NONE &&
areaResize.id === -1 areaResize.id === -1
) { ) {
const dx = (e.clientX - panOffset.x) / settings.zoom; 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 }))); 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 }))); 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) => { const updatedTables = tables.map((t) => {
if (t.id === dragging[1]) { if (t.id === dragging.id) {
return { return {
...t, ...t,
x: e.clientX / settings.zoom - offset.x, x: e.clientX / settings.zoom - offset.x,
@ -135,13 +157,13 @@ export default function Canvas(props) {
setTables(updatedTables); setTables(updatedTables);
setRelationships((prev) => setRelationships((prev) =>
prev.map((r) => { prev.map((r) => {
if (r.startTableId === dragging[1]) { if (r.startTableId === dragging.id) {
return { return {
...r, ...r,
startX: tables[r.startTableId].x + 15, startX: tables[r.startTableId].x + 15,
startY: tables[r.startTableId].y + r.startFieldId * 36 + 50 + 19, startY: tables[r.startTableId].y + r.startFieldId * 36 + 50 + 19,
}; };
} else if (r.endTableId === dragging[1]) { } else if (r.endTableId === dragging.id) {
return { return {
...r, ...r,
endX: tables[r.endTableId].x + 15, endX: tables[r.endTableId].x + 15,
@ -152,13 +174,13 @@ export default function Canvas(props) {
}) })
); );
} else if ( } else if (
dragging[0] === ObjectType.AREA && dragging.element === ObjectType.AREA &&
dragging[1] >= 0 && dragging.id >= 0 &&
areaResize.id === -1 areaResize.id === -1
) { ) {
setAreas((prev) => setAreas((prev) =>
prev.map((t) => { prev.map((t) => {
if (t.id === dragging[1]) { if (t.id === dragging.id) {
const updatedArea = { const updatedArea = {
...t, ...t,
x: e.clientX / settings.zoom - offset.x, x: e.clientX / settings.zoom - offset.x,
@ -169,10 +191,10 @@ export default function Canvas(props) {
return t; return t;
}) })
); );
} else if (dragging[0] === ObjectType.NOTE && dragging[1] >= 0) { } else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
setNotes((prev) => setNotes((prev) =>
prev.map((t) => { prev.map((t) => {
if (t.id === dragging[1]) { if (t.id === dragging.id) {
return { return {
...t, ...t,
x: e.clientX / settings.zoom - offset.x, x: e.clientX / settings.zoom - offset.x,
@ -233,8 +255,28 @@ export default function Canvas(props) {
setCursor("grabbing"); setCursor("grabbing");
}; };
const handleMouseUp = () => { const coordsDidUpdate = () => {
setDragging([ObjectType.NONE, -1]); 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); setPanning(false);
setCursor("default"); setCursor("default");
if (linking) handleLinking(); if (linking) handleLinking();
@ -252,7 +294,7 @@ export default function Canvas(props) {
const handleGripField = (id) => { const handleGripField = (id) => {
setPanning(false); setPanning(false);
setDragging([ObjectType.NONE, -1]); setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
setLinking(true); setLinking(true);
}; };

View File

@ -44,8 +44,13 @@ import {
TableContext, TableContext,
UndoRedoContext, UndoRedoContext,
} from "../pages/editor"; } from "../pages/editor";
import { AddTable, AddArea, AddNote } from "./custom_icons"; import { IconAddTable, IconAddArea, IconAddNote } from "./custom_icons";
import { defaultTableTheme, defaultNoteTheme } from "../data/data"; import {
defaultTableTheme,
defaultNoteTheme,
ObjectType,
Action,
} from "../data/data";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import { json } from "@codemirror/lang-json"; import { json } from "@codemirror/lang-json";
import jsPDF from "jspdf"; 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 = () => { const undo = () => {
if (undoStack.length === 0) return; if (undoStack.length === 0) return;
const a = undoStack.pop(); const a = undoStack.pop();
if (a.action === "add") { if (a.action === Action.ADD) {
if (a.element === "table") { if (a.element === ObjectType.TABLE) {
setTables((prev) => setTables((prev) =>
prev prev
.filter((e) => e.id !== prev.length - 1) .filter((e) => e.id !== prev.length - 1)
.map((e, i) => ({ ...e, id: i })) .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 = () => { const redo = () => {
if (redoStack.length === 0) return; if (redoStack.length === 0) return;
const a = redoStack.pop(); const a = redoStack.pop();
if (a.action === "add") { if (a.action === Action.ADD) {
if (a.element === "table") { if (a.element === ObjectType.TABLE) {
addTable(); 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 ( return (
@ -545,14 +681,20 @@ export default function ControlPanel(props) {
title="Undo" title="Undo"
onClick={undo} onClick={undo}
> >
<IconUndo size="large" /> <IconUndo
size="large"
style={{ color: undoStack.length === 0 ? "#9598a6" : "" }}
/>
</button> </button>
<button <button
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center" className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
title="Redo" title="Redo"
onClick={redo} onClick={redo}
> >
<IconRedo size="large" /> <IconRedo
size="large"
style={{ color: redoStack.length === 0 ? "#9598a6" : "" }}
/>
</button> </button>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<button <button
@ -563,54 +705,48 @@ export default function ControlPanel(props) {
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
action: "add", action: Action.ADD,
element: "table", element: ObjectType.TABLE,
}, },
]); ]);
if (redoStack.length > 0) setRedoStack([]); setRedoStack([]);
}} }}
> >
<AddTable /> <IconAddTable />
</button> </button>
<button <button
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center" className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
title="Add subject area" title="Add subject area"
onClick={() => onClick={() => {
setAreas((prev) => [ addArea();
setUndoStack((prev) => [
...prev, ...prev,
{ {
id: prev.length, action: Action.ADD,
name: `area_${prev.length}`, element: ObjectType.AREA,
x: 0,
y: 0,
width: 200,
height: 200,
color: defaultTableTheme,
}, },
]) ]);
} setRedoStack([]);
}}
> >
<AddArea /> <IconAddArea />
</button> </button>
<button <button
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center" className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
title="Add new note" title="Add new note"
onClick={() => onClick={() => {
setNotes((prev) => [ addNote();
setUndoStack((prev) => [
...prev, ...prev,
{ {
id: prev.length, action: Action.ADD,
x: 0, element: ObjectType.NOTE,
y: 0,
title: `note_${prev.length}`,
content: "",
color: defaultNoteTheme,
height: 88,
}, },
]) ]);
} setRedoStack([]);
}}
> >
<AddNote /> <IconAddNote />
</button> </button>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<button <button

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
export function AddTable() { export function IconAddTable() {
return ( return (
<svg height="26" width="26"> <svg height="26" width="26">
<path <path
@ -13,7 +13,7 @@ export function AddTable() {
); );
} }
export function AddArea() { export function IconAddArea() {
return ( return (
<svg height="26" width="26"> <svg height="26" width="26">
<path <path
@ -26,7 +26,7 @@ export function AddArea() {
); );
} }
export function AddNote() { export function IconAddNote() {
return ( return (
<svg height="26" width="26"> <svg height="26" width="26">
<path <path

View File

@ -75,6 +75,11 @@ const ObjectType = {
NOTE: 3, NOTE: 3,
}; };
const Action = {
ADD: 0,
MOVE: 1,
}
export { export {
bgBlue, bgBlue,
sqlDataTypes, sqlDataTypes,
@ -86,4 +91,5 @@ export {
Constraint, Constraint,
Tab, Tab,
ObjectType, ObjectType,
Action
}; };