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 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);
};

View File

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

View File

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

View File

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