소스 검색

move footer into layerUI & refactor ActionManager (#729)

David Luzar 5 년 전
부모
커밋
d79293de06
7개의 변경된 파일163개의 추가작업 그리고 178개의 파일을 삭제
  1. 14 22
      src/actions/manager.tsx
  2. 4 4
      src/actions/types.ts
  3. 0 2
      src/appState.ts
  4. 115 138
      src/index.tsx
  5. 29 11
      src/scene/data.ts
  6. 1 0
      src/scene/index.ts
  7. 0 1
      src/types.ts

+ 14 - 22
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,
         );
       };
 

+ 4 - 4
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 }[];

+ 0 - 2
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(),
   };
 }
 

+ 115 - 138
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 (
+        <StoredScenesList
+          scenes={scenes}
+          currentId={appState.selectedId}
+          onChange={async (id, k) =>
+            actionManager.updater(await loadScene(id, k))
+          }
+        />
+      );
+    }
+
     return (
-      <FixedSideContainer side="top">
-        <div className="App-menu App-menu_top">
-          <Stack.Col gap={4} align="end">
-            <section
-              className="App-right-menu"
-              aria-labelledby="canvas-actions-title"
-            >
-              <h2 className="visually-hidden" id="canvas-actions-title">
-                {t("headings.canvasActions")}
-              </h2>
-              <Island padding={4}>{renderCanvasActions()}</Island>
+      <>
+        <FixedSideContainer side="top">
+          <div className="App-menu App-menu_top">
+            <Stack.Col gap={4} align="end">
+              <section
+                className="App-right-menu"
+                aria-labelledby="canvas-actions-title"
+              >
+                <h2 className="visually-hidden" id="canvas-actions-title">
+                  {t("headings.canvasActions")}
+                </h2>
+                <Island padding={4}>{renderCanvasActions()}</Island>
+              </section>
+              <section
+                className="App-right-menu"
+                aria-labelledby="selected-shape-title"
+              >
+                <h2 className="visually-hidden" id="selected-shape-title">
+                  {t("headings.selectedShapeActions")}
+                </h2>
+                {renderSelectedShapeActions(elements)}
+              </section>
+            </Stack.Col>
+            <section aria-labelledby="shapes-title">
+              <Stack.Col gap={4} align="start">
+                <Stack.Row gap={1}>
+                  <Island padding={1}>
+                    <h2 className="visually-hidden" id="shapes-title">
+                      {t("headings.shapes")}
+                    </h2>
+                    <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
+                  </Island>
+                  <LockIcon
+                    checked={appState.elementLocked}
+                    onChange={() => {
+                      setAppState({
+                        elementLocked: !appState.elementLocked,
+                        elementType: appState.elementLocked
+                          ? "selection"
+                          : appState.elementType,
+                      });
+                    }}
+                    title={t("toolBar.lock")}
+                  />
+                </Stack.Row>
+              </Stack.Col>
             </section>
-            <section
-              className="App-right-menu"
-              aria-labelledby="selected-shape-title"
+            <div />
+          </div>
+        </FixedSideContainer>
+        <footer role="contentinfo">
+          <HintViewer
+            elementType={appState.elementType}
+            multiMode={appState.multiElement !== null}
+            isResizing={appState.isResizing}
+            elements={elements}
+          />
+          <LanguageList
+            onChange={lng => {
+              setLanguage(lng);
+              setAppState({});
+            }}
+            languages={languages}
+            currentLanguage={language}
+          />
+          {renderIdsDropdown()}
+          {appState.scrolledOutside && (
+            <button
+              className="scroll-back-to-content"
+              onClick={() => {
+                setAppState({ ...calculateScrollCenter(elements) });
+              }}
             >
-              <h2 className="visually-hidden" id="selected-shape-title">
-                {t("headings.selectedShapeActions")}
-              </h2>
-              {renderSelectedShapeActions(elements)}
-            </section>
-          </Stack.Col>
-          <section aria-labelledby="shapes-title">
-            <Stack.Col gap={4} align="start">
-              <Stack.Row gap={1}>
-                <Island padding={1}>
-                  <h2 className="visually-hidden" id="shapes-title">
-                    {t("headings.shapes")}
-                  </h2>
-                  <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
-                </Island>
-                <LockIcon
-                  checked={appState.elementLocked}
-                  onChange={() => {
-                    setAppState({
-                      elementLocked: !appState.elementLocked,
-                      elementType: appState.elementLocked
-                        ? "selection"
-                        : appState.elementType,
-                    });
-                  }}
-                  title={t("toolBar.lock")}
-                />
-              </Stack.Row>
-            </Stack.Col>
-          </section>
-          <div />
-        </div>
-      </FixedSideContainer>
+              {t("buttons.scrollBackToContent")}
+            </button>
+          )}
+        </footer>
+      </>
     );
   },
   (prev, next) => {
@@ -390,6 +435,7 @@ const LayerUI = React.memo(
     const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
 
     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<any, AppState> {
     super(props);
     this.actionManager = new ActionManager(
       this.syncActionResult,
-      () => {
-        history.resumeRecording();
-      },
       () => this.state,
       () => elements,
     );
@@ -443,13 +486,22 @@ export class App extends React.Component<any, AppState> {
     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<any, AppState> {
     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<any, AppState> {
 
     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<any, AppState> {
       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<any, AppState> {
           actionManager={this.actionManager}
           elements={elements}
           setElements={this.setElements}
+          language={getLanguage()}
         />
         <main>
           <canvas
@@ -1797,10 +1819,7 @@ export class App extends React.Component<any, AppState> {
               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<any, AppState> {
             {t("labels.drawingCanvas")}
           </canvas>
         </main>
-        <footer role="contentinfo">
-          <HintViewer
-            elementType={this.state.elementType}
-            multiMode={this.state.multiElement !== null}
-            isResizing={this.state.isResizing}
-            elements={elements}
-          />
-
-          <LanguageList
-            onChange={lng => {
-              setLanguage(lng);
-              this.setState({ lng });
-            }}
-            languages={languages}
-            currentLanguage={getLanguage()}
-          />
-          {this.renderIdsDropdown()}
-          {this.state.scrolledOutside && (
-            <button
-              className="scroll-back-to-content"
-              onClick={() => {
-                this.setState({ ...calculateScrollCenter(elements) });
-              }}
-            >
-              {t("buttons.scrollBackToContent")}
-            </button>
-          )}
-        </footer>
       </div>
     );
   }
 
-  private renderIdsDropdown() {
-    const scenes = loadedScenes();
-    if (scenes.length === 0) {
-      return;
-    }
-    return (
-      <StoredScenesList
-        scenes={scenes}
-        currentId={this.state.selectedId}
-        onChange={(id, k) => this.loadScene(id, k)}
-      />
-    );
-  }
-
   private handleWheel = (e: WheelEvent) => {
     e.preventDefault();
     const { deltaX, deltaY } = e;

+ 29 - 11
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 },
+  };
+}

+ 1 - 0
src/scene/index.ts

@@ -18,6 +18,7 @@ export {
   importFromBackend,
   addToLoadedScenes,
   loadedScenes,
+  loadScene,
   calculateScrollCenter,
 } from "./data";
 export {

+ 0 - 1
src/types.ts

@@ -28,5 +28,4 @@ export type AppState = {
   name: string;
   selectedId?: string;
   isResizing: boolean;
-  lng: string;
 };