Browse Source

fix restoring appState (#2182)

David Luzar 4 years ago
parent
commit
adb1ac5788
8 changed files with 74 additions and 49 deletions
  1. 1 1
      src/components/App.tsx
  2. 13 15
      src/data/blob.ts
  3. 5 9
      src/data/index.ts
  4. 1 1
      src/data/localStorage.ts
  5. 41 18
      src/data/restore.ts
  6. 9 1
      src/data/types.ts
  7. 2 2
      src/index.tsx
  8. 2 2
      src/types.ts

+ 1 - 1
src/components/App.tsx

@@ -3927,7 +3927,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     this.setState({ shouldCacheIgnoreZoom: false });
   }, 300);
 
-  private getCanvasOffsets() {
+  private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
     if (this.excalidrawRef?.current) {
       const parentElement = this.excalidrawRef.current.parentElement;
       const { left, top } = parentElement.getBoundingClientRect();

+ 13 - 15
src/data/blob.ts

@@ -1,8 +1,8 @@
-import { getDefaultAppState, cleanAppStateForExport } from "../appState";
+import { cleanAppStateForExport } from "../appState";
 import { restore } from "./restore";
 import { t } from "../i18n";
 import { AppState } from "../types";
-import { LibraryData } from "./types";
+import { LibraryData, ImportedDataState } from "./types";
 import { calculateScrollCenter } from "../scene";
 
 const loadFileContents = async (blob: any) => {
@@ -34,26 +34,24 @@ export const loadFromBlob = async (blob: any, appState?: AppState) => {
   }
 
   const contents = await loadFileContents(blob);
-  const defaultAppState = getDefaultAppState();
-  let elements = [];
-  let _appState = appState || defaultAppState;
   try {
-    const data = JSON.parse(contents);
+    const data: ImportedDataState = JSON.parse(contents);
     if (data.type !== "excalidraw") {
       throw new Error(t("alerts.couldNotLoadInvalidFile"));
     }
-    elements = data.elements || [];
-    _appState = {
-      ...defaultAppState,
-      appearance: _appState.appearance,
-      ...cleanAppStateForExport(data.appState as Partial<AppState>),
-      ...(appState ? calculateScrollCenter(elements, appState, null) : {}),
-    };
+    return restore({
+      elements: data.elements,
+      appState: {
+        appearance: appState?.appearance,
+        ...cleanAppStateForExport(data.appState || {}),
+        ...(appState
+          ? calculateScrollCenter(data.elements || [], appState, null)
+          : {}),
+      },
+    });
   } catch {
     throw new Error(t("alerts.couldNotLoadInvalidFile"));
   }
-
-  return restore(elements, _appState);
 };
 
 export const loadLibraryFromBlob = async (blob: any) => {

+ 5 - 9
src/data/index.ts

@@ -18,7 +18,7 @@ import { serializeAsJSON } from "./json";
 
 import { ExportType } from "../scene/types";
 import { restore } from "./restore";
-import { DataState } from "./types";
+import { ImportedDataState } from "./types";
 
 export { loadFromBlob } from "./blob";
 export { saveAsJSON, loadFromJSON } from "./json";
@@ -232,7 +232,7 @@ export const exportToBackend = async (
 const importFromBackend = async (
   id: string | null,
   privateKey?: string | null,
-) => {
+): Promise<ImportedDataState> => {
   let elements: readonly ExcalidrawElement[] = [];
   let appState = getDefaultAppState();
 
@@ -364,19 +364,15 @@ export const exportCanvas = async (
 export const loadScene = async (
   id: string | null,
   privateKey?: string | null,
-  initialData?: DataState,
+  initialData?: ImportedDataState,
 ) => {
   let data;
   if (id != null) {
     // the private key is used to decrypt the content from the server, take
     // extra care not to leak it
-    const { elements, appState } = await importFromBackend(id, privateKey);
-    data = restore(elements, appState);
+    data = restore(await importFromBackend(id, privateKey));
   } else {
-    data = restore(
-      initialData?.elements ?? [],
-      initialData?.appState ?? getDefaultAppState(),
-    );
+    data = restore(initialData || {});
   }
 
   return {

+ 1 - 1
src/data/localStorage.ts

@@ -22,7 +22,7 @@ export const loadLibrary = (): Promise<LibraryItems> => {
       }
 
       const items = (JSON.parse(data) as LibraryItems).map(
-        (elements) => restore(elements, null).elements,
+        (elements) => restore({ elements, appState: null }).elements,
       ) as Mutable<LibraryItems>;
 
       // clone to ensure we don't mutate the cached library elements in the app

+ 41 - 18
src/data/restore.ts

@@ -4,7 +4,7 @@ import {
   ExcalidrawSelectionElement,
 } from "../element/types";
 import { AppState } from "../types";
-import { DataState } from "./types";
+import { DataState, ImportedDataState } from "./types";
 import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
 import { isLinearElementType } from "../element/typeChecks";
 import { randomId } from "../random";
@@ -14,6 +14,7 @@ import {
   DEFAULT_TEXT_ALIGN,
   DEFAULT_VERTICAL_ALIGN,
 } from "../constants";
+import { getDefaultAppState } from "../appState";
 
 const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
   for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
@@ -24,10 +25,10 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
   return DEFAULT_FONT_FAMILY;
 };
 
-function migrateElementWithProperties<T extends ExcalidrawElement>(
+const restoreElementWithProperties = <T extends ExcalidrawElement>(
   element: Required<T>,
   extra: Omit<Required<T>, keyof ExcalidrawElement>,
-): T {
+): T => {
   const base: Pick<T, keyof ExcalidrawElement> = {
     type: element.type,
     // all elements must have version > 0 so getDrawingVersion() will pick up
@@ -61,9 +62,9 @@ function migrateElementWithProperties<T extends ExcalidrawElement>(
     ...getNormalizedDimensions(base),
     ...extra,
   } as T;
-}
+};
 
-const migrateElement = (
+const restoreElement = (
   element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
 ): typeof element => {
   switch (element.type) {
@@ -78,7 +79,7 @@ const migrateElement = (
         fontSize = parseInt(fontPx, 10);
         fontFamily = getFontFamilyByName(_fontFamily);
       }
-      return migrateElementWithProperties(element, {
+      return restoreElementWithProperties(element, {
         fontSize,
         fontFamily,
         text: element.text ?? "",
@@ -89,7 +90,7 @@ const migrateElement = (
     case "draw":
     case "line":
     case "arrow": {
-      return migrateElementWithProperties(element, {
+      return restoreElementWithProperties(element, {
         startBinding: element.startBinding,
         endBinding: element.endBinding,
         points:
@@ -105,11 +106,11 @@ const migrateElement = (
     }
     // generic elements
     case "ellipse":
-      return migrateElementWithProperties(element, {});
+      return restoreElementWithProperties(element, {});
     case "rectangle":
-      return migrateElementWithProperties(element, {});
+      return restoreElementWithProperties(element, {});
     case "diamond":
-      return migrateElementWithProperties(element, {});
+      return restoreElementWithProperties(element, {});
 
     // don't use default case so as to catch a missing an element type case
     //  (we also don't want to throw, but instead return void so we
@@ -117,24 +118,46 @@ const migrateElement = (
   }
 };
 
-export const restore = (
-  savedElements: readonly ExcalidrawElement[],
-  savedState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null,
-): DataState => {
-  const elements = savedElements.reduce((elements, element) => {
+const restoreElements = (
+  elements: ImportedDataState["elements"],
+): ExcalidrawElement[] => {
+  return (elements || []).reduce((elements, element) => {
     // filtering out selection, which is legacy, no longer kept in elements,
     //  and causing issues if retained
     if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
-      const migratedElement = migrateElement(element);
+      const migratedElement = restoreElement(element);
       if (migratedElement) {
         elements.push(migratedElement);
       }
     }
     return elements;
   }, [] as ExcalidrawElement[]);
+};
+
+const restoreAppState = (appState: ImportedDataState["appState"]): AppState => {
+  appState = appState || {};
+
+  const defaultAppState = getDefaultAppState();
+  const nextAppState = {} as typeof defaultAppState;
+
+  for (const [key, val] of Object.entries(defaultAppState)) {
+    if ((appState as any)[key] !== undefined) {
+      (nextAppState as any)[key] = (appState as any)[key];
+    } else {
+      (nextAppState as any)[key] = val;
+    }
+  }
+
+  return {
+    ...nextAppState,
+    offsetLeft: appState.offsetLeft || 0,
+    offsetTop: appState.offsetTop || 0,
+  };
+};
 
+export const restore = (data: ImportedDataState): DataState => {
   return {
-    elements: elements,
-    appState: savedState,
+    elements: restoreElements(data.elements),
+    appState: restoreAppState(data.appState),
   };
 };

+ 9 - 1
src/data/types.ts

@@ -6,7 +6,15 @@ export interface DataState {
   version?: string;
   source?: string;
   elements: readonly ExcalidrawElement[];
-  appState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
+  appState: MarkOptional<AppState, "offsetTop" | "offsetLeft">;
+}
+
+export interface ImportedDataState {
+  type?: string;
+  version?: string;
+  source?: string;
+  elements?: DataState["elements"] | null;
+  appState?: Partial<DataState["appState"]> | null;
 }
 
 export interface LibraryData {

+ 2 - 2
src/index.tsx

@@ -17,7 +17,7 @@ import {
 } from "./data/localStorage";
 
 import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./time_constants";
-import { DataState } from "./data/types";
+import { ImportedDataState } from "./data/types";
 import { LoadingMessage } from "./components/LoadingMessage";
 import { ExcalidrawElement } from "./element/types";
 import { AppState } from "./types";
@@ -112,7 +112,7 @@ function ExcalidrawApp() {
   // ---------------------------------------------------------------------------
 
   const [initialState, setInitialState] = useState<{
-    data: DataState;
+    data: ImportedDataState;
     user: {
       name: string | null;
     };

+ 2 - 2
src/types.ts

@@ -14,7 +14,7 @@ import { Point as RoughPoint } from "roughjs/bin/geometry";
 import { SocketUpdateDataSource } from "./data";
 import { LinearElementEditor } from "./element/linearElementEditor";
 import { SuggestedBinding } from "./element/binding";
-import { DataState } from "./data/types";
+import { ImportedDataState } from "./data/types";
 
 export type FlooredNumber = number & { _brand: "FlooredNumber" };
 export type Point = Readonly<RoughPoint>;
@@ -127,7 +127,7 @@ export interface ExcalidrawProps {
     elements: readonly ExcalidrawElement[],
     appState: AppState,
   ) => void;
-  initialData?: DataState;
+  initialData?: ImportedDataState;
   user?: {
     name?: string | null;
   };