Переглянути джерело

feat: support importing scene from url (#2726)

David Luzar 4 роки тому
батько
коміт
beffc290fd
4 змінених файлів з 50 додано та 7 видалено
  1. 4 3
      src/data/blob.ts
  2. 14 0
      src/data/json.ts
  3. 31 4
      src/excalidraw-app/index.tsx
  4. 1 0
      src/locales/en.json

+ 4 - 3
src/data/blob.ts

@@ -5,8 +5,9 @@ import { CanvasError } from "../errors";
 import { t } from "../i18n";
 import { calculateScrollCenter } from "../scene";
 import { AppState } from "../types";
+import { isValidExcalidrawData } from "./json";
 import { restore } from "./restore";
-import { ImportedDataState, LibraryData } from "./types";
+import { LibraryData } from "./types";
 
 const parseFileContents = async (blob: Blob | File) => {
   let contents: string;
@@ -85,8 +86,8 @@ export const loadFromBlob = async (
 ) => {
   const contents = await parseFileContents(blob);
   try {
-    const data: ImportedDataState = JSON.parse(contents);
-    if (data.type !== "excalidraw") {
+    const data = JSON.parse(contents);
+    if (!isValidExcalidrawData(data)) {
       throw new Error(t("alerts.couldNotLoadInvalidFile"));
     }
     const result = restore(

+ 14 - 0
src/data/json.ts

@@ -6,6 +6,7 @@ import { ExcalidrawElement } from "../element/types";
 import { AppState } from "../types";
 import { loadFromBlob } from "./blob";
 import { Library } from "./library";
+import { ImportedDataState } from "./types";
 
 export const serializeAsJSON = (
   elements: readonly ExcalidrawElement[],
@@ -53,6 +54,19 @@ export const loadFromJSON = async (localAppState: AppState) => {
   return loadFromBlob(blob, localAppState);
 };
 
+export const isValidExcalidrawData = (data?: {
+  type?: any;
+  elements?: any;
+  appState?: any;
+}): data is ImportedDataState => {
+  return (
+    data?.type === "excalidraw" &&
+    (!data.elements ||
+      (Array.isArray(data.elements) &&
+        (!data.appState || typeof data.appState === "object")))
+  );
+};
+
 export const isValidLibrary = (json: any) => {
   return (
     typeof json === "object" &&

+ 31 - 4
src/excalidraw-app/index.tsx

@@ -13,6 +13,7 @@ import { ExcalidrawImperativeAPI } from "../components/App";
 import { ErrorDialog } from "../components/ErrorDialog";
 import { TopErrorBoundary } from "../components/TopErrorBoundary";
 import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
+import { loadFromBlob } from "../data/blob";
 import { DataState, ImportedDataState } from "../data/types";
 import {
   ExcalidrawElement,
@@ -69,9 +70,10 @@ const initializeScene = async (opts: {
 }): Promise<ImportedDataState | null> => {
   const searchParams = new URLSearchParams(window.location.search);
   const id = searchParams.get("id");
-  const jsonMatch = window.location.hash.match(
+  const jsonBackendMatch = window.location.hash.match(
     /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
   );
+  const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
 
   const initialData = importFromLocalStorage();
 
@@ -82,7 +84,7 @@ const initializeScene = async (opts: {
   );
 
   let roomLinkData = getCollaborationLinkData(window.location.href);
-  const isExternalScene = !!(id || jsonMatch || roomLinkData);
+  const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
   if (isExternalScene) {
     if (
       // don't prompt if scene is empty
@@ -95,8 +97,12 @@ const initializeScene = async (opts: {
       // Backwards compatibility with legacy url format
       if (id) {
         scene = await loadScene(id, null, initialData);
-      } else if (jsonMatch) {
-        scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
+      } else if (jsonBackendMatch) {
+        scene = await loadScene(
+          jsonBackendMatch[1],
+          jsonBackendMatch[2],
+          initialData,
+        );
       }
       scene.scrollToCenter = true;
       if (!roomLinkData) {
@@ -119,7 +125,28 @@ const initializeScene = async (opts: {
       roomLinkData = null;
       window.history.replaceState({}, APP_NAME, window.location.origin);
     }
+  } else if (externalUrlMatch) {
+    window.history.replaceState({}, APP_NAME, window.location.origin);
+
+    const url = externalUrlMatch[1];
+    try {
+      const request = await fetch(window.decodeURIComponent(url));
+      const data = await loadFromBlob(await request.blob(), null);
+      if (
+        !scene.elements.length ||
+        window.confirm(t("alerts.loadSceneOverridePrompt"))
+      ) {
+        return data;
+      }
+    } catch (error) {
+      return {
+        appState: {
+          errorMessage: t("alerts.invalidSceneUrl"),
+        },
+      };
+    }
   }
+
   if (roomLinkData) {
     return opts.collabAPI.initializeSocketClient(roomLinkData);
   } else if (scene) {

+ 1 - 0
src/locales/en.json

@@ -142,6 +142,7 @@
     "confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
     "imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
     "cannotRestoreFromImage": "Scene couldn't be restored from this image file",
+    "invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
     "resetLibrary": "This will clear your library. Are you sure?"
   },
   "toolBar": {