mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 04:44:31 +01:00 
			
		
		
		
	feat: support importing scene from url (#2726)
This commit is contained in:
		@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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" &&
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user