From beffc290fd1438b61f96d1e000c0b1567d5a62b8 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 8 Mar 2021 16:37:26 +0100 Subject: [PATCH] feat: support importing scene from url (#2726) --- src/data/blob.ts | 7 ++++--- src/data/json.ts | 14 ++++++++++++++ src/excalidraw-app/index.tsx | 35 +++++++++++++++++++++++++++++++---- src/locales/en.json | 1 + 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/data/blob.ts b/src/data/blob.ts index 1ecc9df7f..3e1da4769 100644 --- a/src/data/blob.ts +++ b/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( diff --git a/src/data/json.ts b/src/data/json.ts index 65688c32e..01177c73f 100644 --- a/src/data/json.ts +++ b/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" && diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 926c2635b..ec4c67247 100644 --- a/src/excalidraw-app/index.tsx +++ b/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 => { 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) { diff --git a/src/locales/en.json b/src/locales/en.json index 3bd0c8527..3c82e0c91 100644 --- a/src/locales/en.json +++ b/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": {