Extract History into its own module

This commit is contained in:
Gasim Gasimzada 2020-01-06 21:32:07 +04:00
parent 054669cfef
commit 01a922bf20
2 changed files with 71 additions and 46 deletions

53
src/history.ts Normal file
View file

@ -0,0 +1,53 @@
import { ExcalidrawElement } from "./element/types";
import { generateDraw } from "./element";
class SceneHistory {
recording: boolean = true;
stateHistory: string[] = [];
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();
}
isRecording() {
return this.recording;
}
skipRecording() {
this.recording = false;
}
resumeRecording() {
this.recording = true;
}
}
export const createHistory: () => { history: SceneHistory } = () => {
const history = new SceneHistory();
return { history };
};

View file

@ -33,43 +33,15 @@ import { EditableText } from "./components/EditableText";
import { ButtonSelect } from "./components/ButtonSelect"; import { ButtonSelect } from "./components/ButtonSelect";
import { ColorPicker } from "./components/ColorPicker"; import { ColorPicker } from "./components/ColorPicker";
import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes"; import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes";
import { createHistory } from "./history";
import "./styles.scss"; import "./styles.scss";
const { elements } = createScene(); const { elements } = createScene();
const { history } = createHistory();
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`; 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_LEFT = 250;
const CANVAS_WINDOW_OFFSET_TOP = 0; const CANVAS_WINDOW_OFFSET_TOP = 0;
@ -231,24 +203,24 @@ class App extends React.Component<{}, AppState> {
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) { } else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
this.setState({ elementType: findShapeByKey(event.key) }); this.setState({ elementType: findShapeByKey(event.key) });
} else if (event.metaKey && event.code === "KeyZ") { } else if (event.metaKey && event.code === "KeyZ") {
const currentEntry = generateHistoryCurrentEntry(); const currentEntry = history.generateCurrentEntry(elements);
if (event.shiftKey) { if (event.shiftKey) {
// Redo action // Redo action
const entryToRestore = redoStack.pop(); const entryToRestore = history.redoStack.pop();
if (entryToRestore !== undefined) { if (entryToRestore !== undefined) {
restoreHistoryEntry(entryToRestore); history.restoreEntry(elements, entryToRestore);
stateHistory.push(currentEntry); history.stateHistory.push(currentEntry);
} }
} else { } else {
// undo action // undo action
let lastEntry = stateHistory.pop(); let lastEntry = history.stateHistory.pop();
// If nothing was changed since last, take the previous one // If nothing was changed since last, take the previous one
if (currentEntry === lastEntry) { if (currentEntry === lastEntry) {
lastEntry = stateHistory.pop(); lastEntry = history.stateHistory.pop();
} }
if (lastEntry !== undefined) { if (lastEntry !== undefined) {
restoreHistoryEntry(lastEntry); history.restoreEntry(elements, lastEntry);
redoStack.push(currentEntry); history.redoStack.push(currentEntry);
} }
} }
this.forceUpdate(); this.forceUpdate();
@ -798,7 +770,7 @@ class App extends React.Component<{}, AppState> {
lastX = x; lastX = x;
lastY = y; lastY = y;
// We don't want to save history when resizing an element // We don't want to save history when resizing an element
skipHistory = true; history.skipRecording();
this.forceUpdate(); this.forceUpdate();
return; return;
} }
@ -818,7 +790,7 @@ class App extends React.Component<{}, AppState> {
lastX = x; lastX = x;
lastY = y; lastY = y;
// We don't want to save history when dragging an element to initially size it // We don't want to save history when dragging an element to initially size it
skipHistory = true; history.skipRecording();
this.forceUpdate(); this.forceUpdate();
return; return;
} }
@ -850,7 +822,7 @@ class App extends React.Component<{}, AppState> {
setSelection(elements, draggingElement); setSelection(elements, draggingElement);
} }
// We don't want to save history when moving an element // We don't want to save history when moving an element
skipHistory = true; history.skipRecording();
this.forceUpdate(); this.forceUpdate();
}; };
@ -892,7 +864,7 @@ class App extends React.Component<{}, AppState> {
window.addEventListener("mouseup", onMouseUp); window.addEventListener("mouseup", onMouseUp);
// We don't want to save history on mouseDown, only on mouseUp when it's fully configured // We don't want to save history on mouseDown, only on mouseUp when it's fully configured
skipHistory = true; history.skipRecording();
this.forceUpdate(); this.forceUpdate();
}} }}
onDoubleClick={e => { onDoubleClick={e => {
@ -952,11 +924,11 @@ class App extends React.Component<{}, AppState> {
viewBackgroundColor: this.state.viewBackgroundColor viewBackgroundColor: this.state.viewBackgroundColor
}); });
saveToLocalStorage(elements, this.state); saveToLocalStorage(elements, this.state);
if (!skipHistory) { if (history.isRecording()) {
pushHistoryEntry(generateHistoryCurrentEntry()); history.pushEntry(history.generateCurrentEntry(elements));
redoStack.splice(0, redoStack.length); history.redoStack.splice(0, history.redoStack.length);
} }
skipHistory = false; history.resumeRecording();
} }
} }