Extract history (#213)
* Extract History into its own module * Encapsulate undo and redo actions within history * Encapsulate clearing redo stack within History * Add private access modifiers to scene history class member variables * Remove duplicate files
This commit is contained in:
parent
054669cfef
commit
d0365933a9
80
src/history.ts
Normal file
80
src/history.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { generateDraw } from "./element";
|
||||
|
||||
class SceneHistory {
|
||||
private recording: boolean = true;
|
||||
private stateHistory: string[] = [];
|
||||
private redoStack: string[] = [];
|
||||
|
||||
generateCurrentEntry(elements: ExcalidrawElement[]) {
|
||||
return JSON.stringify(
|
||||
elements.map(element => ({ ...element, isSelected: false }))
|
||||
);
|
||||
}
|
||||
|
||||
pushEntry(newEntry: string) {
|
||||
if (
|
||||
this.stateHistory.length > 0 &&
|
||||
this.stateHistory[this.stateHistory.length - 1] === newEntry
|
||||
) {
|
||||
// If the last entry is the same as this one, ignore it
|
||||
return;
|
||||
}
|
||||
this.stateHistory.push(newEntry);
|
||||
}
|
||||
|
||||
restoreEntry(elements: ExcalidrawElement[], entry: string) {
|
||||
const newElements = JSON.parse(entry);
|
||||
elements.splice(0, elements.length);
|
||||
newElements.forEach((newElement: ExcalidrawElement) => {
|
||||
generateDraw(newElement);
|
||||
elements.push(newElement);
|
||||
});
|
||||
// When restoring, we shouldn't add an history entry otherwise we'll be stuck with it and can't go back
|
||||
this.skipRecording();
|
||||
}
|
||||
|
||||
clearRedoStack() {
|
||||
this.redoStack.splice(0, this.redoStack.length);
|
||||
}
|
||||
|
||||
redoOnce(elements: ExcalidrawElement[]) {
|
||||
const currentEntry = this.generateCurrentEntry(elements);
|
||||
const entryToRestore = this.redoStack.pop();
|
||||
if (entryToRestore !== undefined) {
|
||||
this.restoreEntry(elements, entryToRestore);
|
||||
this.stateHistory.push(currentEntry);
|
||||
}
|
||||
}
|
||||
|
||||
undoOnce(elements: ExcalidrawElement[]) {
|
||||
const currentEntry = this.generateCurrentEntry(elements);
|
||||
let entryToRestore = this.stateHistory.pop();
|
||||
|
||||
// If nothing was changed since last, take the previous one
|
||||
if (currentEntry === entryToRestore) {
|
||||
entryToRestore = this.stateHistory.pop();
|
||||
}
|
||||
if (entryToRestore !== undefined) {
|
||||
this.restoreEntry(elements, entryToRestore);
|
||||
this.redoStack.push(currentEntry);
|
||||
}
|
||||
}
|
||||
|
||||
isRecording() {
|
||||
return this.recording;
|
||||
}
|
||||
|
||||
skipRecording() {
|
||||
this.recording = false;
|
||||
}
|
||||
|
||||
resumeRecording() {
|
||||
this.recording = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const createHistory: () => { history: SceneHistory } = () => {
|
||||
const history = new SceneHistory();
|
||||
return { history };
|
||||
};
|
@ -33,43 +33,15 @@ import { EditableText } from "./components/EditableText";
|
||||
import { ButtonSelect } from "./components/ButtonSelect";
|
||||
import { ColorPicker } from "./components/ColorPicker";
|
||||
import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes";
|
||||
import { createHistory } from "./history";
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
const { elements } = createScene();
|
||||
const { history } = createHistory();
|
||||
|
||||
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
||||
|
||||
let skipHistory = false;
|
||||
const stateHistory: string[] = [];
|
||||
const redoStack: string[] = [];
|
||||
|
||||
function generateHistoryCurrentEntry() {
|
||||
return JSON.stringify(
|
||||
elements.map(element => ({ ...element, isSelected: false }))
|
||||
);
|
||||
}
|
||||
function pushHistoryEntry(newEntry: string) {
|
||||
if (
|
||||
stateHistory.length > 0 &&
|
||||
stateHistory[stateHistory.length - 1] === newEntry
|
||||
) {
|
||||
// If the last entry is the same as this one, ignore it
|
||||
return;
|
||||
}
|
||||
stateHistory.push(newEntry);
|
||||
}
|
||||
function restoreHistoryEntry(entry: string) {
|
||||
const newElements = JSON.parse(entry);
|
||||
elements.splice(0, elements.length);
|
||||
newElements.forEach((newElement: ExcalidrawElement) => {
|
||||
generateDraw(newElement);
|
||||
elements.push(newElement);
|
||||
});
|
||||
// When restoring, we shouldn't add an history entry otherwise we'll be stuck with it and can't go back
|
||||
skipHistory = true;
|
||||
}
|
||||
|
||||
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
||||
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
||||
|
||||
@ -231,25 +203,12 @@ class App extends React.Component<{}, AppState> {
|
||||
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
||||
this.setState({ elementType: findShapeByKey(event.key) });
|
||||
} else if (event.metaKey && event.code === "KeyZ") {
|
||||
const currentEntry = generateHistoryCurrentEntry();
|
||||
if (event.shiftKey) {
|
||||
// Redo action
|
||||
const entryToRestore = redoStack.pop();
|
||||
if (entryToRestore !== undefined) {
|
||||
restoreHistoryEntry(entryToRestore);
|
||||
stateHistory.push(currentEntry);
|
||||
}
|
||||
history.redoOnce(elements);
|
||||
} else {
|
||||
// undo action
|
||||
let lastEntry = stateHistory.pop();
|
||||
// If nothing was changed since last, take the previous one
|
||||
if (currentEntry === lastEntry) {
|
||||
lastEntry = stateHistory.pop();
|
||||
}
|
||||
if (lastEntry !== undefined) {
|
||||
restoreHistoryEntry(lastEntry);
|
||||
redoStack.push(currentEntry);
|
||||
}
|
||||
history.undoOnce(elements);
|
||||
}
|
||||
this.forceUpdate();
|
||||
event.preventDefault();
|
||||
@ -798,7 +757,7 @@ class App extends React.Component<{}, AppState> {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
// We don't want to save history when resizing an element
|
||||
skipHistory = true;
|
||||
history.skipRecording();
|
||||
this.forceUpdate();
|
||||
return;
|
||||
}
|
||||
@ -818,7 +777,7 @@ class App extends React.Component<{}, AppState> {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
// We don't want to save history when dragging an element to initially size it
|
||||
skipHistory = true;
|
||||
history.skipRecording();
|
||||
this.forceUpdate();
|
||||
return;
|
||||
}
|
||||
@ -850,7 +809,7 @@ class App extends React.Component<{}, AppState> {
|
||||
setSelection(elements, draggingElement);
|
||||
}
|
||||
// We don't want to save history when moving an element
|
||||
skipHistory = true;
|
||||
history.skipRecording();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
@ -892,7 +851,7 @@ class App extends React.Component<{}, AppState> {
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
// We don't want to save history on mouseDown, only on mouseUp when it's fully configured
|
||||
skipHistory = true;
|
||||
history.skipRecording();
|
||||
this.forceUpdate();
|
||||
}}
|
||||
onDoubleClick={e => {
|
||||
@ -952,11 +911,11 @@ class App extends React.Component<{}, AppState> {
|
||||
viewBackgroundColor: this.state.viewBackgroundColor
|
||||
});
|
||||
saveToLocalStorage(elements, this.state);
|
||||
if (!skipHistory) {
|
||||
pushHistoryEntry(generateHistoryCurrentEntry());
|
||||
redoStack.splice(0, redoStack.length);
|
||||
if (history.isRecording()) {
|
||||
history.pushEntry(history.generateCurrentEntry(elements));
|
||||
history.clearRedoStack();
|
||||
}
|
||||
skipHistory = false;
|
||||
history.resumeRecording();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* https://stackoverflow.com/a/3368118
|
||||
* Draws a rounded rectangle using the current state of the canvas.
|
||||
* @param {CanvasRenderingContext2D} context
|
||||
* @param {Number} x The top left x coordinate
|
||||
* @param {Number} y The top left y coordinate
|
||||
* @param {Number} width The width of the rectangle
|
||||
* @param {Number} height The height of the rectangle
|
||||
* @param {Number} radius The corner radius
|
||||
*/
|
||||
export function roundRect(
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number
|
||||
) {
|
||||
context.beginPath();
|
||||
context.moveTo(x + radius, y);
|
||||
context.lineTo(x + width - radius, y);
|
||||
context.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
context.lineTo(x + width, y + height - radius);
|
||||
context.quadraticCurveTo(
|
||||
x + width,
|
||||
y + height,
|
||||
x + width - radius,
|
||||
y + height
|
||||
);
|
||||
context.lineTo(x + radius, y + height);
|
||||
context.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
context.lineTo(x, y + radius);
|
||||
context.quadraticCurveTo(x, y, x + radius, y);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user