diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index dfe26f408..9f4481580 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -14,20 +14,16 @@ export class ActionManager implements ActionsManagerInterface { updater: UpdaterFn; - resumeHistoryRecording: () => void; - getAppState: () => AppState; getElements: () => readonly ExcalidrawElement[]; constructor( updater: UpdaterFn, - resumeHistoryRecording: () => void, getAppState: () => AppState, getElements: () => readonly ExcalidrawElement[], ) { this.updater = updater; - this.resumeHistoryRecording = resumeHistoryRecording; this.getAppState = getAppState; this.getElements = getElements; } @@ -46,17 +42,18 @@ export class ActionManager implements ActionsManagerInterface { ); if (data.length === 0) { - return null; + return false; } event.preventDefault(); - if ( + const commitToHistory = data[0].commitToHistory && - data[0].commitToHistory(this.getAppState(), this.getElements()) - ) { - this.resumeHistoryRecording(); - } - return data[0].perform(this.getElements(), this.getAppState(), null); + data[0].commitToHistory(this.getAppState(), this.getElements()); + this.updater( + data[0].perform(this.getElements(), this.getAppState(), null), + commitToHistory, + ); + return true; } getContextMenuItems(actionFilter: ActionFilterFn = action => action) { @@ -71,14 +68,12 @@ export class ActionManager implements ActionsManagerInterface { .map(action => ({ label: action.contextItemLabel ? t(action.contextItemLabel) : "", action: () => { - if ( + const commitToHistory = action.commitToHistory && - action.commitToHistory(this.getAppState(), this.getElements()) - ) { - this.resumeHistoryRecording(); - } + action.commitToHistory(this.getAppState(), this.getElements()); this.updater( action.perform(this.getElements(), this.getAppState(), null), + commitToHistory, ); }, })); @@ -89,15 +84,12 @@ export class ActionManager implements ActionsManagerInterface { const action = this.actions[name]; const PanelComponent = action.PanelComponent!; const updateData = (formState: any) => { - if ( + const commitToHistory = action.commitToHistory && - action.commitToHistory(this.getAppState(), this.getElements()) === - true - ) { - this.resumeHistoryRecording(); - } + action.commitToHistory(this.getAppState(), this.getElements()); this.updater( action.perform(this.getElements(), this.getAppState(), formState), + commitToHistory, ); }; diff --git a/src/actions/types.ts b/src/actions/types.ts index 2ce35fdef..5e57d7ab6 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -3,8 +3,8 @@ import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; export type ActionResult = { - elements?: readonly ExcalidrawElement[]; - appState?: AppState; + elements?: readonly ExcalidrawElement[] | null; + appState?: AppState | null; }; type ActionFn = ( @@ -13,7 +13,7 @@ type ActionFn = ( formData: any, ) => ActionResult; -export type UpdaterFn = (res: ActionResult) => void; +export type UpdaterFn = (res: ActionResult, commitToHistory?: boolean) => void; export type ActionFilterFn = (action: Action) => void; export interface Action { @@ -43,7 +43,7 @@ export interface ActionsManagerInterface { [keyProp: string]: Action; }; registerAction: (action: Action) => void; - handleKeyDown: (event: KeyboardEvent) => ActionResult | null; + handleKeyDown: (event: KeyboardEvent) => boolean; getContextMenuItems: ( actionFilter: ActionFilterFn, ) => { label: string; action: () => void }[]; diff --git a/src/appState.ts b/src/appState.ts index 3ca929ead..f28e15dde 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -1,6 +1,5 @@ import { AppState } from "./types"; import { getDateTime } from "./utils"; -import { getLanguage } from "./i18n"; const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`; @@ -29,7 +28,6 @@ export function getDefaultAppState(): AppState { name: DEFAULT_PROJECT_NAME, isResizing: false, selectionElement: null, - lng: getLanguage(), }; } diff --git a/src/index.tsx b/src/index.tsx index 3b4613628..7026209fa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,7 +23,6 @@ import { deleteSelectedElements, getElementsWithinSelection, isOverScrollBars, - restoreFromLocalStorage, saveToLocalStorage, getElementAtPosition, createScene, @@ -32,9 +31,8 @@ import { hasStroke, hasText, exportCanvas, - importFromBackend, - addToLoadedScenes, loadedScenes, + loadScene, calculateScrollCenter, loadFromBlob, } from "./scene"; @@ -163,6 +161,7 @@ interface LayerUIProps { canvas: HTMLCanvasElement | null; setAppState: any; elements: readonly ExcalidrawElement[]; + language: string; setElements: (elements: readonly ExcalidrawElement[]) => void; } @@ -173,6 +172,7 @@ const LayerUI = React.memo( setAppState, canvas, elements, + language, setElements, }: LayerUIProps) => { function renderCanvasActions() { @@ -318,56 +318,101 @@ const LayerUI = React.memo( ); } + function renderIdsDropdown() { + const scenes = loadedScenes(); + if (scenes.length === 0) { + return; + } + return ( + + actionManager.updater(await loadScene(id, k)) + } + /> + ); + } + return ( - -
- -
-

- {t("headings.canvasActions")} -

- {renderCanvasActions()} -
-
-

- {t("headings.selectedShapeActions")} -

- {renderSelectedShapeActions(elements)} -
-
-
- - - -

- {t("headings.shapes")} -

- {renderShapesSwitcher()} -
- { - setAppState({ - elementLocked: !appState.elementLocked, - elementType: appState.elementLocked - ? "selection" - : appState.elementType, - }); - }} - title={t("toolBar.lock")} - /> -
+ <> + +
+ +
+

+ {t("headings.canvasActions")} +

+ {renderCanvasActions()} +
+
+

+ {t("headings.selectedShapeActions")} +

+ {renderSelectedShapeActions(elements)} +
-
-
-
- +
+ + + +

+ {t("headings.shapes")} +

+ {renderShapesSwitcher()} +
+ { + setAppState({ + elementLocked: !appState.elementLocked, + elementType: appState.elementLocked + ? "selection" + : appState.elementType, + }); + }} + title={t("toolBar.lock")} + /> +
+
+
+
+
+ +
+ + { + setLanguage(lng); + setAppState({}); + }} + languages={languages} + currentLanguage={language} + /> + {renderIdsDropdown()} + {appState.scrolledOutside && ( + + )} +
+ ); }, (prev, next) => { @@ -390,6 +435,7 @@ const LayerUI = React.memo( const keys = Object.keys(prevAppState) as (keyof Partial)[]; return ( + prev.language === next.language && prev.elements === next.elements && keys.every(k => prevAppState[k] === nextAppState[k]) ); @@ -406,9 +452,6 @@ export class App extends React.Component { super(props); this.actionManager = new ActionManager( this.syncActionResult, - () => { - history.resumeRecording(); - }, () => this.state, () => elements, ); @@ -443,13 +486,22 @@ export class App extends React.Component { this.canvasOnlyActions = [actionSelectAll]; } - private syncActionResult = (res: ActionResult) => { - if (res.elements !== undefined) { + private syncActionResult = ( + res: ActionResult, + commitToHistory: boolean = true, + ) => { + if (res.elements) { elements = res.elements; + if (commitToHistory) { + history.resumeRecording(); + } this.setState({}); } - if (res.appState !== undefined) { + if (res.appState) { + if (commitToHistory) { + history.resumeRecording(); + } this.setState({ ...res.appState }); } }; @@ -478,32 +530,6 @@ export class App extends React.Component { this.saveDebounced.flush(); }; - private async loadScene(id: string | null, k: string | undefined) { - let data; - let selectedId; - if (id != null) { - // k is the private key used to decrypt the content from the server, take - // extra care not to leak it - data = await importFromBackend(id, k); - addToLoadedScenes(id, k); - selectedId = id; - window.history.replaceState({}, "Excalidraw", window.location.origin); - } else { - data = restoreFromLocalStorage(); - } - - if (data.elements) { - elements = data.elements; - } - - if (data.appState) { - history.resumeRecording(); - this.setState({ ...data.appState, selectedId }); - } else { - this.setState({}); - } - } - public async componentDidMount() { document.addEventListener("copy", this.onCopy); document.addEventListener("paste", this.pasteFromClipboard); @@ -523,15 +549,15 @@ export class App extends React.Component { if (id) { // Backwards compatibility with legacy url format - this.loadScene(id, undefined); + this.syncActionResult(await loadScene(id)); } else { const match = window.location.hash.match( /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/, ); if (match) { - this.loadScene(match[1], match[2]); + this.syncActionResult(await loadScene(match[1], match[2])); } else { - this.loadScene(null, undefined); + this.syncActionResult(await loadScene(null)); } } } @@ -572,13 +598,8 @@ export class App extends React.Component { return; } - const actionResult = this.actionManager.handleKeyDown(event); - - if (actionResult) { - this.syncActionResult(actionResult); - if (actionResult) { - return; - } + if (this.actionManager.handleKeyDown(event)) { + return; } const shape = findShapeByKey(event.key); @@ -750,6 +771,7 @@ export class App extends React.Component { actionManager={this.actionManager} elements={elements} setElements={this.setElements} + language={getLanguage()} />
{ if (file?.type === "application/json") { loadFromBlob(file) .then(({ elements, appState }) => - this.syncActionResult({ - elements, - appState, - } as ActionResult), + this.syncActionResult({ elements, appState }), ) .catch(err => console.error(err)); } @@ -1809,52 +1828,10 @@ export class App extends React.Component { {t("labels.drawingCanvas")}
-
- - - { - setLanguage(lng); - this.setState({ lng }); - }} - languages={languages} - currentLanguage={getLanguage()} - /> - {this.renderIdsDropdown()} - {this.state.scrolledOutside && ( - - )} -
); } - private renderIdsDropdown() { - const scenes = loadedScenes(); - if (scenes.length === 0) { - return; - } - return ( - this.loadScene(id, k)} - /> - ); - } - private handleWheel = (e: WheelEvent) => { e.preventDefault(); const { deltaX, deltaY } = e; diff --git a/src/scene/data.ts b/src/scene/data.ts index 8dde96a63..2f31cc94e 100644 --- a/src/scene/data.ts +++ b/src/scene/data.ts @@ -123,17 +123,15 @@ export async function loadFromBlob(blob: any) { if ("text" in Blob) { contents = await blob.text(); } else { - contents = await (async () => { - return new Promise(resolve => { - const reader = new FileReader(); - reader.readAsText(blob, "utf8"); - reader.onloadend = () => { - if (reader.readyState === FileReader.DONE) { - resolve(reader.result as string); - } - }; - }); - })(); + contents = await new Promise(resolve => { + const reader = new FileReader(); + reader.readAsText(blob, "utf8"); + reader.onloadend = () => { + if (reader.readyState === FileReader.DONE) { + resolve(reader.result as string); + } + }; + }); } const { elements, appState } = updateAppState(contents); if (!elements.length) { @@ -488,3 +486,23 @@ export function addToLoadedScenes(id: string, k: string | undefined): void { JSON.stringify(scenes), ); } + +export async function loadScene(id: string | null, k?: string) { + let data; + let selectedId; + if (id != null) { + // k is the private key used to decrypt the content from the server, take + // extra care not to leak it + data = await importFromBackend(id, k); + addToLoadedScenes(id, k); + selectedId = id; + window.history.replaceState({}, "Excalidraw", window.location.origin); + } else { + data = restoreFromLocalStorage(); + } + + return { + elements: data.elements, + appState: data.appState && { ...data.appState, selectedId }, + }; +} diff --git a/src/scene/index.ts b/src/scene/index.ts index 8f34215d1..fa6f81394 100644 --- a/src/scene/index.ts +++ b/src/scene/index.ts @@ -18,6 +18,7 @@ export { importFromBackend, addToLoadedScenes, loadedScenes, + loadScene, calculateScrollCenter, } from "./data"; export { diff --git a/src/types.ts b/src/types.ts index 7862b0e03..db10bfc32 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,5 +28,4 @@ export type AppState = { name: string; selectedId?: string; isResizing: boolean; - lng: string; };