feat: support importing scene from url (#2726)

This commit is contained in:
David Luzar 2021-03-08 16:37:26 +01:00 committed by GitHub
parent 9b58efd363
commit beffc290fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 7 deletions

View File

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

View File

@ -6,6 +6,7 @@ import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { loadFromBlob } from "./blob"; import { loadFromBlob } from "./blob";
import { Library } from "./library"; import { Library } from "./library";
import { ImportedDataState } from "./types";
export const serializeAsJSON = ( export const serializeAsJSON = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@ -53,6 +54,19 @@ export const loadFromJSON = async (localAppState: AppState) => {
return loadFromBlob(blob, localAppState); 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) => { export const isValidLibrary = (json: any) => {
return ( return (
typeof json === "object" && typeof json === "object" &&

View File

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

View File

@ -142,6 +142,7 @@
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?", "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?", "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", "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?" "resetLibrary": "This will clear your library. Are you sure?"
}, },
"toolBar": { "toolBar": {