Undo and redo for moving and adding objects
This commit is contained in:
parent
59609b1cd8
commit
c79999d773
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user