diff --git a/src/appState.ts b/src/appState.ts index 04c1eaf9c..e10d8813d 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -62,30 +62,95 @@ export const getDefaultAppState = (): AppState => { }; }; -export const clearAppStateForLocalStorage = (appState: AppState) => { - const { - draggingElement, - resizingElement, - multiElement, - editingElement, - selectionElement, - isResizing, - isRotating, - collaborators, - isCollaborating, - isLoading, - errorMessage, - showShortcutsDialog, - editingLinearElement, - isLibraryOpen, - ...exportedState - } = appState; - return exportedState; +/** + * Config containing all AppState keys. Used to determine whether given state + * prop should be stripped when exporting to given storage type. + */ +const APP_STATE_STORAGE_CONF = (< + Values extends { + /** whether to keep when storing to browser storage (localStorage/IDB) */ + browser: boolean; + /** whether to keep when exporting to file/database */ + export: boolean; + }, + T extends Record +>( + config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }, +) => config)({ + collaborators: { browser: false, export: false }, + currentItemBackgroundColor: { browser: true, export: false }, + currentItemFillStyle: { browser: true, export: false }, + currentItemFontFamily: { browser: true, export: false }, + currentItemFontSize: { browser: true, export: false }, + currentItemOpacity: { browser: true, export: false }, + currentItemRoughness: { browser: true, export: false }, + currentItemStrokeColor: { browser: true, export: false }, + currentItemStrokeStyle: { browser: true, export: false }, + currentItemStrokeWidth: { browser: true, export: false }, + currentItemTextAlign: { browser: true, export: false }, + cursorButton: { browser: true, export: false }, + cursorX: { browser: true, export: false }, + cursorY: { browser: true, export: false }, + draggingElement: { browser: false, export: false }, + editingElement: { browser: false, export: false }, + editingGroupId: { browser: true, export: false }, + editingLinearElement: { browser: false, export: false }, + elementLocked: { browser: true, export: false }, + elementType: { browser: true, export: false }, + errorMessage: { browser: false, export: false }, + exportBackground: { browser: true, export: false }, + gridSize: { browser: true, export: true }, + height: { browser: false, export: false }, + isCollaborating: { browser: false, export: false }, + isLibraryOpen: { browser: false, export: false }, + isLoading: { browser: false, export: false }, + isResizing: { browser: false, export: false }, + isRotating: { browser: false, export: false }, + lastPointerDownWith: { browser: true, export: false }, + multiElement: { browser: false, export: false }, + name: { browser: true, export: false }, + openMenu: { browser: true, export: false }, + previousSelectedElementIds: { browser: true, export: false }, + resizingElement: { browser: false, export: false }, + scrolledOutside: { browser: true, export: false }, + scrollX: { browser: true, export: false }, + scrollY: { browser: true, export: false }, + selectedElementIds: { browser: true, export: false }, + selectedGroupIds: { browser: true, export: false }, + selectionElement: { browser: false, export: false }, + shouldAddWatermark: { browser: true, export: false }, + shouldCacheIgnoreZoom: { browser: true, export: false }, + showShortcutsDialog: { browser: false, export: false }, + username: { browser: true, export: false }, + viewBackgroundColor: { browser: true, export: true }, + width: { browser: false, export: false }, + zenModeEnabled: { browser: true, export: false }, + zoom: { browser: true, export: false }, +}); + +const _clearAppStateForStorage = ( + appState: Partial, + exportType: ExportType, +) => { + type ExportableKeys = { + [K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true + ? K + : never; + }[keyof typeof APP_STATE_STORAGE_CONF]; + const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] }; + for (const key of Object.keys(appState) as (keyof typeof appState)[]) { + if (APP_STATE_STORAGE_CONF[key][exportType]) { + // @ts-ignore see https://github.com/microsoft/TypeScript/issues/31445 + stateForExport[key] = appState[key]; + } + } + return stateForExport; }; -export const cleanAppStateForExport = (appState: AppState) => { - return { - viewBackgroundColor: appState.viewBackgroundColor, - gridSize: appState.gridSize, - }; +export const clearAppStateForLocalStorage = (appState: Partial) => { + return _clearAppStateForStorage(appState, "browser"); +}; + +export const cleanAppStateForExport = (appState: Partial) => { + return _clearAppStateForStorage(appState, "export"); }; diff --git a/src/data/blob.ts b/src/data/blob.ts index 4d770e8cd..e7e63de35 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,6 +1,7 @@ -import { getDefaultAppState } from "../appState"; +import { getDefaultAppState, cleanAppStateForExport } from "../appState"; import { restore } from "./restore"; import { t } from "../i18n"; +import { AppState } from "../types"; export const loadFromBlob = async (blob: any) => { const updateAppState = (contents: string) => { @@ -13,7 +14,10 @@ export const loadFromBlob = async (blob: any) => { throw new Error(t("alerts.couldNotLoadInvalidFile")); } elements = data.elements || []; - appState = { ...defaultAppState, ...data.appState }; + appState = { + ...defaultAppState, + ...cleanAppStateForExport(data.appState as Partial), + }; } catch { throw new Error(t("alerts.couldNotLoadInvalidFile")); } diff --git a/src/data/index.ts b/src/data/index.ts index 74a017850..d50a1730a 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -374,7 +374,7 @@ export const loadScene = async (id: string | null, privateKey?: string) => { return { elements: data.elements, - appState: data.appState && { ...data.appState }, + appState: data.appState, commitToHistory: false, }; }; diff --git a/src/data/localStorage.ts b/src/data/localStorage.ts index d395a0e14..ec8aa08f0 100644 --- a/src/data/localStorage.ts +++ b/src/data/localStorage.ts @@ -1,6 +1,6 @@ import { ExcalidrawElement } from "../element/types"; import { AppState, LibraryItems } from "../types"; -import { clearAppStateForLocalStorage } from "../appState"; +import { clearAppStateForLocalStorage, getDefaultAppState } from "../appState"; import { restore } from "./restore"; const LOCAL_STORAGE_KEY = "excalidraw"; @@ -111,7 +111,8 @@ export const restoreFromLocalStorage = () => { if (savedElements) { try { elements = JSON.parse(savedElements); - } catch { + } catch (error) { + console.error(error); // Do nothing because elements array is already empty } } @@ -119,13 +120,14 @@ export const restoreFromLocalStorage = () => { let appState = null; if (savedState) { try { - appState = JSON.parse(savedState) as AppState; - // If we're retrieving from local storage, we should not be collaborating - appState.isCollaborating = false; - appState.collaborators = new Map(); - delete appState.width; - delete appState.height; - } catch { + appState = { + ...getDefaultAppState(), + ...clearAppStateForLocalStorage( + JSON.parse(savedState) as Partial, + ), + }; + } catch (error) { + console.error(error); // Do nothing because appState is already null } }