diff --git a/src/index.tsx b/src/index.tsx index 69bccb67..9930e264 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,6 +20,34 @@ const LOCAL_STORAGE_KEY_STATE = "excalidraw-state"; const elements = Array.of(); +let skipHistory = false; +const stateHistory: 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; +} + // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316 const LCG = (seed: number) => () => ((2 ** 31 - 1) & (seed = Math.imul(48271, seed))) / 2 ** 31; @@ -901,6 +929,17 @@ class App extends React.Component<{}, AppState> { event.preventDefault(); } else if (shapesShortcutKeys.includes(event.key.toLowerCase())) { this.setState({ elementType: findElementByKey(event.key) }); + } else if (event.metaKey && event.code === "KeyZ") { + let lastEntry = stateHistory.pop(); + // If nothing was changed since last, take the previous one + if (generateHistoryCurrentEntry() === lastEntry) { + lastEntry = stateHistory.pop(); + } + if (lastEntry !== undefined) { + restoreHistoryEntry(lastEntry); + } + this.forceUpdate(); + event.preventDefault(); } }; @@ -1301,6 +1340,8 @@ class App extends React.Component<{}, AppState> { }); lastX = x; lastY = y; + // We don't want to save history when resizing an element + skipHistory = true; this.forceUpdate(); return; } @@ -1319,6 +1360,8 @@ 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; this.forceUpdate(); return; } @@ -1347,6 +1390,8 @@ class App extends React.Component<{}, AppState> { if (this.state.elementType === "selection") { setSelection(draggingElement); } + // We don't want to save history when moving an element + skipHistory = true; this.forceUpdate(); }; @@ -1384,6 +1429,8 @@ class App extends React.Component<{}, AppState> { window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); + // We don't want to save history on mouseDown, only on mouseUp when it's fully configured + skipHistory = true; this.forceUpdate(); }} /> @@ -1407,6 +1454,10 @@ class App extends React.Component<{}, AppState> { viewBackgroundColor: this.state.viewBackgroundColor }); save(this.state); + if (!skipHistory) { + pushHistoryEntry(generateHistoryCurrentEntry()); + } + skipHistory = false; } }