Procházet zdrojové kódy

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
Gasim Gasimzada před 5 roky
rodič
revize
d0365933a9
3 změnil soubory, kde provedl 92 přidání a 90 odebrání
  1. 80 0
      src/history.ts
  2. 12 53
      src/index.tsx
  3. 0 37
      src/roundRect.ts

+ 80 - 0
src/history.ts

@@ -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 };
+};

+ 12 - 53
src/index.tsx

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

+ 0 - 37
src/roundRect.ts

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