feat: factor out url library init & switch to updateLibrary
API (#5115)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
parent
2537b225ac
commit
cad6097d60
@ -70,14 +70,12 @@ import {
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
THEME,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
URL_HASH_KEYS,
|
||||
URL_QUERY_KEYS,
|
||||
VERTICAL_ALIGN,
|
||||
ZOOM_STEP,
|
||||
} from "../constants";
|
||||
import { loadFromBlob } from "../data";
|
||||
import Library from "../data/library";
|
||||
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
|
||||
import { restore, restoreElements } from "../data/restore";
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
@ -234,7 +232,6 @@ import {
|
||||
isSupportedImageFile,
|
||||
loadSceneOrLibraryFromBlob,
|
||||
normalizeFile,
|
||||
loadLibraryFromBlob,
|
||||
resizeImageFile,
|
||||
SVGStringToFile,
|
||||
} from "../data/blob";
|
||||
@ -261,7 +258,6 @@ import {
|
||||
isPointHittingLinkIcon,
|
||||
isLocalLink,
|
||||
} from "../element/Hyperlink";
|
||||
import { AbortError } from "../errors";
|
||||
|
||||
const defaultDeviceTypeContext: DeviceType = {
|
||||
isMobile: false,
|
||||
@ -361,6 +357,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
this.id = nanoid();
|
||||
|
||||
this.library = new Library(this);
|
||||
|
||||
if (excalidrawRef) {
|
||||
const readyPromise =
|
||||
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
||||
@ -370,6 +368,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
ready: true,
|
||||
readyPromise,
|
||||
updateScene: this.updateScene,
|
||||
updateLibrary: this.library.updateLibrary,
|
||||
addFiles: this.addFiles,
|
||||
resetScene: this.resetScene,
|
||||
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
|
||||
@ -381,7 +380,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
getAppState: () => this.state,
|
||||
getFiles: () => this.files,
|
||||
refresh: this.refresh,
|
||||
importLibrary: this.importLibraryFromUrl,
|
||||
setToastMessage: this.setToastMessage,
|
||||
id: this.id,
|
||||
setActiveTool: this.setActiveTool,
|
||||
@ -400,7 +398,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
this.scene = new Scene();
|
||||
this.library = new Library(this);
|
||||
this.history = new History();
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
@ -698,54 +695,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.onSceneUpdated();
|
||||
};
|
||||
|
||||
private importLibraryFromUrl = async (url: string, token?: string | null) => {
|
||||
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
hash.delete(URL_HASH_KEYS.addLibrary);
|
||||
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
|
||||
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
query.delete(URL_QUERY_KEYS.addLibrary);
|
||||
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
|
||||
}
|
||||
|
||||
const defaultStatus = "published";
|
||||
|
||||
this.setState({ isLibraryOpen: true });
|
||||
|
||||
try {
|
||||
await this.library.importLibrary(
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
const request = await fetch(decodeURIComponent(url));
|
||||
const blob = await request.blob();
|
||||
const libraryItems = await loadLibraryFromBlob(blob, defaultStatus);
|
||||
|
||||
if (
|
||||
token === this.id ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", {
|
||||
numShapes: libraryItems.length,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
resolve(libraryItems);
|
||||
} else {
|
||||
reject(new AbortError());
|
||||
}
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: t("errors.importLibraryError") });
|
||||
} finally {
|
||||
this.focusContainer();
|
||||
}
|
||||
};
|
||||
|
||||
private resetHistory = () => {
|
||||
this.history.clear();
|
||||
};
|
||||
@ -790,7 +739,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
try {
|
||||
initialData = (await this.props.initialData) || null;
|
||||
if (initialData?.libraryItems) {
|
||||
this.library.importLibrary(initialData.libraryItems, "unpublished");
|
||||
this.library
|
||||
.updateLibrary({
|
||||
libraryItems: initialData.libraryItems,
|
||||
merge: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@ -802,10 +758,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const scene = restore(initialData, null, null);
|
||||
scene.appState = {
|
||||
...scene.appState,
|
||||
isLibraryOpen: this.state.isLibraryOpen,
|
||||
activeTool:
|
||||
scene.appState.activeTool.type === "image"
|
||||
? { ...scene.appState.activeTool, type: "selection" }
|
||||
@ -834,20 +790,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...scene,
|
||||
commitToHistory: true,
|
||||
});
|
||||
|
||||
const libraryUrl =
|
||||
// current
|
||||
new URLSearchParams(window.location.hash.slice(1)).get(
|
||||
URL_HASH_KEYS.addLibrary,
|
||||
) ||
|
||||
// legacy, kept for compat reasons
|
||||
new URLSearchParams(window.location.search).get(
|
||||
URL_QUERY_KEYS.addLibrary,
|
||||
);
|
||||
|
||||
if (libraryUrl) {
|
||||
await this.importLibraryFromUrl(libraryUrl);
|
||||
}
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
@ -1691,14 +1633,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
appState?: Pick<AppState, K> | null;
|
||||
collaborators?: SceneData["collaborators"];
|
||||
commitToHistory?: SceneData["commitToHistory"];
|
||||
libraryItems?:
|
||||
| ((
|
||||
currentLibraryItems: LibraryItems,
|
||||
) =>
|
||||
| Required<SceneData>["libraryItems"]
|
||||
| Promise<Required<SceneData>["libraryItems"]>)
|
||||
| Required<SceneData>["libraryItems"]
|
||||
| Promise<Required<SceneData>["libraryItems"]>;
|
||||
}) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
this.history.resumeRecording();
|
||||
@ -1715,23 +1649,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (sceneData.collaborators) {
|
||||
this.setState({ collaborators: sceneData.collaborators });
|
||||
}
|
||||
|
||||
if (sceneData.libraryItems) {
|
||||
this.library.setLibrary((currentLibraryItems) => {
|
||||
const nextItems =
|
||||
typeof sceneData.libraryItems === "function"
|
||||
? sceneData.libraryItems(currentLibraryItems)
|
||||
: sceneData.libraryItems;
|
||||
|
||||
return new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
resolve(restoreLibraryItems(await nextItems, "unpublished"));
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -5335,19 +5252,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
commitToHistory: true,
|
||||
});
|
||||
} else if (ret.type === MIME_TYPES.excalidrawlib) {
|
||||
this.library
|
||||
.importLibrary(file)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
await this.library
|
||||
.updateLibrary({
|
||||
libraryItems: file,
|
||||
merge: true,
|
||||
openLibraryMenu: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMessage: t("errors.importLibraryError"),
|
||||
});
|
||||
this.setState({ errorMessage: t("errors.importLibraryError") });
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { chunk } from "lodash";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||
import { saveLibraryAsJSON } from "../data/json";
|
||||
import Library from "../data/library";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
@ -23,6 +23,7 @@ import { Tooltip } from "./Tooltip";
|
||||
import "./LibraryMenuItems.scss";
|
||||
import { VERSIONS } from "../constants";
|
||||
import Spinner from "./Spinner";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
|
||||
const LibraryMenuItems = ({
|
||||
isLoading,
|
||||
@ -107,13 +108,23 @@ const LibraryMenuItems = ({
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
icon={load}
|
||||
onClick={() => {
|
||||
importLibraryFromJSON(library)
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||
onClick={async () => {
|
||||
try {
|
||||
await fileOpen({
|
||||
description: "Excalidraw library files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
*/
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.name === "AbortError") {
|
||||
console.warn(error);
|
||||
return;
|
||||
}
|
||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||
}
|
||||
}}
|
||||
className="library-actions--load"
|
||||
/>
|
||||
|
@ -17,7 +17,6 @@ import {
|
||||
ExportedLibraryData,
|
||||
ImportedLibraryData,
|
||||
} from "./types";
|
||||
import Library from "./library";
|
||||
|
||||
/**
|
||||
* Strips out files which are only referenced by deleted elements
|
||||
@ -147,15 +146,3 @@ export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const importLibraryFromJSON = async (library: Library) => {
|
||||
const blob = await fileOpen({
|
||||
description: "Excalidraw library files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
*/
|
||||
});
|
||||
await library.importLibrary(blob);
|
||||
};
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { loadLibraryFromBlob } from "./blob";
|
||||
import { LibraryItems, LibraryItem } from "../types";
|
||||
import {
|
||||
LibraryItems,
|
||||
LibraryItem,
|
||||
ExcalidrawImperativeAPI,
|
||||
LibraryItemsSource,
|
||||
} from "../types";
|
||||
import { restoreLibraryItems } from "./restore";
|
||||
import type App from "../components/App";
|
||||
import { ImportedDataState } from "./types";
|
||||
import { atom } from "jotai";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { AbortError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT } from "../constants";
|
||||
|
||||
export const libraryItemsAtom = atom<{
|
||||
status: "loading" | "loaded";
|
||||
@ -102,36 +110,6 @@ class Library {
|
||||
return this.setLibrary([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* imports library (from blob or libraryItems), merging with current library
|
||||
* (attempting to remove duplicates)
|
||||
*/
|
||||
importLibrary(
|
||||
library:
|
||||
| Blob
|
||||
| Required<ImportedDataState>["libraryItems"]
|
||||
| Promise<Required<ImportedDataState>["libraryItems"]>,
|
||||
defaultStatus: LibraryItem["status"] = "unpublished",
|
||||
): Promise<LibraryItems> {
|
||||
return this.setLibrary(
|
||||
() =>
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
let libraryItems: LibraryItems;
|
||||
if (library instanceof Blob) {
|
||||
libraryItems = await loadLibraryFromBlob(library, defaultStatus);
|
||||
} else {
|
||||
libraryItems = restoreLibraryItems(await library, defaultStatus);
|
||||
}
|
||||
|
||||
resolve(mergeLibraryItems(this.lastLibraryItems, libraryItems));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns latest cloned libraryItems. Awaits all in-progress updates first.
|
||||
*/
|
||||
@ -151,6 +129,65 @@ class Library {
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE this is a high-level public API (exposed on ExcalidrawAPI) with
|
||||
// a slight overhead (always restoring library items). For internal use
|
||||
// where merging isn't needed, use `library.setLibrary()` directly.
|
||||
updateLibrary = async ({
|
||||
libraryItems,
|
||||
prompt = false,
|
||||
merge = false,
|
||||
openLibraryMenu = false,
|
||||
defaultStatus = "unpublished",
|
||||
}: {
|
||||
libraryItems: LibraryItemsSource;
|
||||
merge?: boolean;
|
||||
prompt?: boolean;
|
||||
openLibraryMenu?: boolean;
|
||||
defaultStatus?: "unpublished" | "published";
|
||||
}): Promise<LibraryItems> => {
|
||||
if (openLibraryMenu) {
|
||||
this.app.setState({ isLibraryOpen: true });
|
||||
}
|
||||
|
||||
return this.setLibrary(() => {
|
||||
return new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
const source = await (typeof libraryItems === "function"
|
||||
? libraryItems(this.lastLibraryItems)
|
||||
: libraryItems);
|
||||
|
||||
let nextItems;
|
||||
|
||||
if (source instanceof Blob) {
|
||||
nextItems = await loadLibraryFromBlob(source, defaultStatus);
|
||||
} else {
|
||||
nextItems = restoreLibraryItems(source, defaultStatus);
|
||||
}
|
||||
if (
|
||||
!prompt ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", {
|
||||
numShapes: nextItems.length,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
if (merge) {
|
||||
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
|
||||
} else {
|
||||
resolve(nextItems);
|
||||
}
|
||||
} else {
|
||||
reject(new AbortError());
|
||||
}
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}).finally(() => {
|
||||
this.app.focusContainer();
|
||||
});
|
||||
};
|
||||
|
||||
setLibrary = (
|
||||
/**
|
||||
* LibraryItems that will replace current items. Can be a function which
|
||||
@ -204,3 +241,102 @@ class Library {
|
||||
}
|
||||
|
||||
export default Library;
|
||||
|
||||
export const parseLibraryTokensFromUrl = () => {
|
||||
const libraryUrl =
|
||||
// current
|
||||
new URLSearchParams(window.location.hash.slice(1)).get(
|
||||
URL_HASH_KEYS.addLibrary,
|
||||
) ||
|
||||
// legacy, kept for compat reasons
|
||||
new URLSearchParams(window.location.search).get(URL_QUERY_KEYS.addLibrary);
|
||||
const idToken = libraryUrl
|
||||
? new URLSearchParams(window.location.hash.slice(1)).get("token")
|
||||
: null;
|
||||
|
||||
return libraryUrl ? { libraryUrl, idToken } : null;
|
||||
};
|
||||
|
||||
export const useHandleLibrary = ({
|
||||
excalidrawAPI,
|
||||
getInitialLibraryItems,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
||||
getInitialLibraryItems?: () => LibraryItemsSource;
|
||||
}) => {
|
||||
const getInitialLibraryRef = useRef(getInitialLibraryItems);
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importLibraryFromURL = ({
|
||||
libraryUrl,
|
||||
idToken,
|
||||
}: {
|
||||
libraryUrl: string;
|
||||
idToken: string | null;
|
||||
}) => {
|
||||
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
hash.delete(URL_HASH_KEYS.addLibrary);
|
||||
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
|
||||
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
query.delete(URL_QUERY_KEYS.addLibrary);
|
||||
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
|
||||
}
|
||||
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: new Promise<Blob>(async (resolve, reject) => {
|
||||
try {
|
||||
const request = await fetch(decodeURIComponent(libraryUrl));
|
||||
const blob = await request.blob();
|
||||
resolve(blob);
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
}),
|
||||
prompt: idToken !== excalidrawAPI.id,
|
||||
merge: true,
|
||||
defaultStatus: "published",
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
};
|
||||
const onHashChange = (event: HashChangeEvent) => {
|
||||
event.preventDefault();
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
if (libraryUrlTokens) {
|
||||
event.stopImmediatePropagation();
|
||||
// If hash changed and it contains library url, import it and replace
|
||||
// the url to its previous state (important in case of collaboration
|
||||
// and similar).
|
||||
// Using history API won't trigger another hashchange.
|
||||
window.history.replaceState({}, "", event.oldURL);
|
||||
|
||||
importLibraryFromURL(libraryUrlTokens);
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ------ init load --------------------------------------------------------
|
||||
if (getInitialLibraryRef.current) {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: getInitialLibraryRef.current(),
|
||||
});
|
||||
}
|
||||
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
|
||||
if (libraryUrlTokens) {
|
||||
importLibraryFromURL(libraryUrlTokens);
|
||||
}
|
||||
// --------------------------------------------------------- init load -----
|
||||
|
||||
window.addEventListener(EVENT.HASHCHANGE, onHashChange);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.HASHCHANGE, onHashChange);
|
||||
};
|
||||
}, [excalidrawAPI]);
|
||||
};
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFiles,
|
||||
LibraryItems,
|
||||
LibraryItems_anyVersion,
|
||||
} from "../types";
|
||||
import type { cleanAppStateForExport } from "../appState";
|
||||
import { VERSIONS } from "../constants";
|
||||
|
||||
@ -19,7 +24,7 @@ export interface ImportedDataState {
|
||||
elements?: readonly ExcalidrawElement[] | null;
|
||||
appState?: Readonly<Partial<AppState>> | null;
|
||||
scrollToContent?: boolean;
|
||||
libraryItems?: LibraryItems | LibraryItems_v1;
|
||||
libraryItems?: LibraryItems_anyVersion;
|
||||
files?: BinaryFiles;
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,7 @@ import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
TITLE_TIMEOUT,
|
||||
URL_HASH_KEYS,
|
||||
VERSION_TIMEOUT,
|
||||
} from "../constants";
|
||||
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
||||
import { loadFromBlob } from "../data/blob";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
@ -72,6 +66,7 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
@ -232,6 +227,11 @@ const ExcalidrawWrapper = () => {
|
||||
|
||||
const collabAPI = useContext(CollabContext)?.api;
|
||||
|
||||
useHandleLibrary({
|
||||
excalidrawAPI,
|
||||
getInitialLibraryItems: getLibraryItemsFromStorage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!collabAPI || !excalidrawAPI) {
|
||||
return;
|
||||
@ -301,8 +301,6 @@ const ExcalidrawWrapper = () => {
|
||||
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
|
||||
}
|
||||
}
|
||||
|
||||
data.scene.libraryItems = getLibraryItemsFromStorage();
|
||||
};
|
||||
|
||||
initializeScene({ collabAPI }).then((data) => {
|
||||
@ -310,18 +308,10 @@ const ExcalidrawWrapper = () => {
|
||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||
});
|
||||
|
||||
const onHashChange = (event: HashChangeEvent) => {
|
||||
const onHashChange = async (event: HashChangeEvent) => {
|
||||
event.preventDefault();
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
const libraryUrl = hash.get(URL_HASH_KEYS.addLibrary);
|
||||
if (libraryUrl) {
|
||||
// If hash changed and it contains library url, import it and replace
|
||||
// the url to its previous state (important in case of collaboration
|
||||
// and similar).
|
||||
// Using history API won't trigger another hashchange.
|
||||
window.history.replaceState({}, "", event.oldURL);
|
||||
excalidrawAPI.importLibrary(libraryUrl, hash.get("token"));
|
||||
} else {
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
if (!libraryUrlTokens) {
|
||||
initializeScene({ collabAPI }).then((data) => {
|
||||
loadImages(data);
|
||||
if (data.scene) {
|
||||
@ -355,6 +345,8 @@ const ExcalidrawWrapper = () => {
|
||||
setLangCode(langCode);
|
||||
excalidrawAPI.updateScene({
|
||||
...localDataState,
|
||||
});
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: getLibraryItemsFromStorage(),
|
||||
});
|
||||
collabAPI.setUsername(username || "");
|
||||
|
@ -17,6 +17,20 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
#### Features
|
||||
|
||||
- Added [`useHandleLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#useHandleLibrary) hook to automatically handle importing of libraries when `#addLibrary` URL hash key is present, and potentially for initializing library as well [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
|
||||
|
||||
Also added [`parseLibraryTokensFromUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#parseLibraryTokensFromUrl) to help in manually importing library from URL if desired.
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
- Libraries are no longer automatically initialized from URL when `#addLibrary` hash key is present. Host apps now need to handle this themselves with the help of either of the above APIs (`useHandleLibrary` is recommended).
|
||||
|
||||
- Added [`updateLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateLibrary) API to update (replace/merge) the library [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
- `updateScene` API no longer supports passing `libraryItems`. Instead, use the `updateLibrary` API.
|
||||
|
||||
- Add support for integrating custom elements [#5164](https://github.com/excalidraw/excalidraw/pull/5164).
|
||||
|
||||
- Add [`onPointerDown`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) callback which gets triggered on pointer down events.
|
||||
|
@ -480,20 +480,21 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
|
||||
| --- | --- | --- |
|
||||
| ready | `boolean` | This is set to true once Excalidraw is rendered |
|
||||
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
|
||||
| [updateScene](#updateScene) | <pre>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </pre> | updates the scene with the sceneData |
|
||||
| [addFiles](#addFiles) | <pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre> | add files data to the appState |
|
||||
| [updateScene](#updateScene) | <code>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </code> | updates the scene with the sceneData |
|
||||
| [updateLibrary](#updateLibrary) | <code>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/library.ts#L136">opts</a>) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>> </code> | updates the scene with the sceneData |
|
||||
| [addFiles](#addFiles) | <code>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </code> | add files data to the appState |
|
||||
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
||||
| getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene |
|
||||
| getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></pre> | Returns all the elements excluding the deleted in the scene |
|
||||
| getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></pre> | Returns current appState |
|
||||
| getSceneElementsIncludingDeleted | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements including the deleted in the scene |
|
||||
| getSceneElements | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements excluding the deleted in the scene |
|
||||
| getAppState | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></code> | Returns current appState |
|
||||
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
|
||||
| scrollToContent | <pre> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>[]) => void </pre> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
|
||||
| scrollToContent | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>[]) => void </code> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
|
||||
| refresh | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. |
|
||||
| [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
||||
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
|
||||
| [id](#id) | string | Unique ID for the excalidraw component. |
|
||||
| [getFiles](#getFiles) | <pre>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </pre> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
|
||||
| [setActiveTool](#setActiveTool) | <pre>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void</pre> | This API can be used to set the active tool |
|
||||
| [getFiles](#getFiles) | <code>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </code> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
|
||||
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
|
||||
|
||||
#### `readyPromise`
|
||||
|
||||
@ -519,6 +520,28 @@ You can use this function to update the scene with the sceneData. It accepts the
|
||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. |
|
||||
|
||||
### `updateLibrary`
|
||||
|
||||
<pre>
|
||||
(opts: {
|
||||
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>;
|
||||
merge?: boolean;
|
||||
prompt?: boolean;
|
||||
openLibraryMenu?: boolean;
|
||||
defaultStatus?: "unpublished" | "published";
|
||||
}) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>>
|
||||
</pre>
|
||||
|
||||
You can use this function to update the library. It accepts the below attributes.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `libraryItems` | | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224) | The `libraryItems` to be replaced/merged with current library |
|
||||
| `merge` | boolean | `false` | Whether to merge with existing library items. |
|
||||
| `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
|
||||
| `openLibraryMenu` | boolean | `false` | Whether to open the library menu before importing. |
|
||||
| `defaultStatus` | <code>"unpublished" | "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
|
||||
|
||||
### `addFiles`
|
||||
|
||||
<pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre>
|
||||
@ -1067,12 +1090,10 @@ This function loads the scene data from the blob (or file). If you pass `localAp
|
||||
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
|
||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||
// if you need, you can check what data you're dealing with before
|
||||
// passing down
|
||||
if (contents.type === MIME_TYPES.excalidraw) {
|
||||
excalidrawAPI.updateScene(contents.data);
|
||||
} else {
|
||||
excalidrawAPI.updateScene(contents.data);
|
||||
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||
excalidrawAPI.updateLibrary(contents.data);
|
||||
}
|
||||
```
|
||||
|
||||
@ -1151,6 +1172,51 @@ mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/
|
||||
|
||||
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
|
||||
|
||||
#### `parseLibraryTokensFromUrl`
|
||||
|
||||
**How to use**
|
||||
|
||||
```js
|
||||
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw-next";
|
||||
```
|
||||
|
||||
**Signature**
|
||||
|
||||
<pre>
|
||||
parseLibraryTokensFromUrl(): {
|
||||
libraryUrl: string;
|
||||
idToken: string | null;
|
||||
} | null
|
||||
</pre>
|
||||
|
||||
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
|
||||
|
||||
#### `useHandleLibrary`
|
||||
|
||||
**How to use**
|
||||
|
||||
```js
|
||||
import { useHandleLibrary } from "@excalidraw/excalidraw-next";
|
||||
|
||||
export const App = () => {
|
||||
// ...
|
||||
useHandleLibrary({ excalidrawAPI });
|
||||
};
|
||||
```
|
||||
|
||||
**Signature**
|
||||
|
||||
<pre>
|
||||
useHandleLibrary(opts: {
|
||||
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L432">ExcalidrawAPI</a>,
|
||||
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>
|
||||
});
|
||||
</pre>
|
||||
|
||||
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
|
||||
|
||||
In the future, we will be adding support for handling library persistence to browser storage (or elsewhere).
|
||||
|
||||
### Exported constants
|
||||
|
||||
#### `FONT_FAMILY`
|
||||
|
@ -26,6 +26,7 @@ const {
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
Excalidraw,
|
||||
useHandleLibrary,
|
||||
MIME_TYPES,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
@ -63,9 +64,7 @@ const renderTopRightUI = () => {
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const excalidrawRef = useRef(null);
|
||||
const appRef = useRef(null);
|
||||
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
@ -82,7 +81,15 @@ export default function App() {
|
||||
if (!initialStatePromiseRef.current.promise) {
|
||||
initialStatePromiseRef.current.promise = resolvablePromise();
|
||||
}
|
||||
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
|
||||
useHandleLibrary({ excalidrawAPI });
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const fetchData = async () => {
|
||||
const res = await fetch("/rocket.jpeg");
|
||||
const imageData = await res.blob();
|
||||
@ -100,23 +107,12 @@ export default function App() {
|
||||
];
|
||||
|
||||
initialStatePromiseRef.current.promise.resolve(InitialData);
|
||||
excalidrawRef.current.addFiles(imagesArray);
|
||||
excalidrawAPI.addFiles(imagesArray);
|
||||
};
|
||||
};
|
||||
fetchData();
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
const onHashChange = () => {
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
const libraryUrl = hash.get("addLibrary");
|
||||
if (libraryUrl) {
|
||||
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
|
||||
}
|
||||
};
|
||||
window.addEventListener("hashchange", onHashChange, false);
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", onHashChange);
|
||||
};
|
||||
}, []);
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<>
|
||||
@ -124,7 +120,7 @@ export default function App() {
|
||||
<button
|
||||
className="custom-element"
|
||||
onClick={() =>
|
||||
excalidrawRef.current.setActiveTool({
|
||||
excalidrawAPI.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
})
|
||||
@ -143,7 +139,14 @@ export default function App() {
|
||||
const loadSceneOrLibrary = async () => {
|
||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||
excalidrawRef.current.updateScene(contents.data);
|
||||
if (contents.type === MIME_TYPES.excalidraw) {
|
||||
excalidrawAPI.updateScene(contents.data);
|
||||
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: contents.data.libraryItems,
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateScene = () => {
|
||||
@ -175,7 +178,7 @@ export default function App() {
|
||||
viewBackgroundColor: "#edf2ff",
|
||||
},
|
||||
};
|
||||
excalidrawRef.current.updateScene(sceneData);
|
||||
excalidrawAPI.updateScene(sceneData);
|
||||
};
|
||||
|
||||
const onLinkOpen = useCallback((element, event) => {
|
||||
@ -195,14 +198,16 @@ export default function App() {
|
||||
|
||||
const onCopy = async (type) => {
|
||||
await exportToClipboard({
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
appState: excalidrawRef.current.getAppState(),
|
||||
files: excalidrawRef.current.getFiles(),
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: excalidrawAPI.getAppState(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
type,
|
||||
});
|
||||
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
||||
};
|
||||
|
||||
const [pointerData, setPointerData] = useState(null);
|
||||
|
||||
const onPointerDown = (activeTool, pointerDownState) => {
|
||||
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
||||
const { x, y } = pointerDownState.origin;
|
||||
@ -215,7 +220,7 @@ export default function App() {
|
||||
appRef.current.querySelectorAll(".comment-icon");
|
||||
commentIconsElements.forEach((ele) => {
|
||||
const id = ele.id;
|
||||
const appstate = excalidrawRef.current.getAppState();
|
||||
const appstate = excalidrawAPI.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
||||
appstate,
|
||||
@ -233,7 +238,7 @@ export default function App() {
|
||||
return withBatchedUpdatesThrottled((event) => {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX: event.clientX, clientY: event.clientY },
|
||||
excalidrawRef.current.getAppState(),
|
||||
excalidrawAPI.getAppState(),
|
||||
);
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
@ -257,7 +262,7 @@ export default function App() {
|
||||
return withBatchedUpdates((event) => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||
excalidrawRef.current.setActiveTool({ type: "selection" });
|
||||
excalidrawAPI.setActiveTool({ type: "selection" });
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
pointerDownState.y,
|
||||
@ -280,10 +285,10 @@ export default function App() {
|
||||
};
|
||||
const renderCommentIcons = () => {
|
||||
return Object.values(commentIcons).map((commentIcon) => {
|
||||
const appState = excalidrawRef.current.getAppState();
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
||||
excalidrawRef.current.getAppState(),
|
||||
excalidrawAPI.getAppState(),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
@ -319,7 +324,10 @@ export default function App() {
|
||||
pointerDownState.onMove = onPointerMove;
|
||||
pointerDownState.onUp = onPointerUp;
|
||||
|
||||
excalidrawRef.current.setCustomType("comment");
|
||||
excalidrawAPI.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="comment-avatar">
|
||||
@ -349,7 +357,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
const renderComment = () => {
|
||||
const appState = excalidrawRef.current.getAppState();
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: comment.x, sceneY: comment.y },
|
||||
appState,
|
||||
@ -416,14 +424,14 @@ export default function App() {
|
||||
<button
|
||||
className="reset-scene"
|
||||
onClick={() => {
|
||||
excalidrawRef.current.resetScene();
|
||||
excalidrawAPI.resetScene();
|
||||
}}
|
||||
>
|
||||
Reset Scene
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
excalidrawRef.current.updateScene({
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: [
|
||||
{
|
||||
status: "published",
|
||||
@ -501,9 +509,9 @@ export default function App() {
|
||||
username: "fallback",
|
||||
avatarUrl: "https://example.com",
|
||||
});
|
||||
excalidrawRef.current.updateScene({ collaborators });
|
||||
excalidrawAPI.updateScene({ collaborators });
|
||||
} else {
|
||||
excalidrawRef.current.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
collaborators: new Map(),
|
||||
});
|
||||
}
|
||||
@ -523,15 +531,26 @@ export default function App() {
|
||||
Copy to Clipboard as JSON
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1em",
|
||||
justifyContent: "center",
|
||||
marginTop: "1em",
|
||||
}}
|
||||
>
|
||||
<div>x: {pointerData?.pointer.x ?? 0}</div>
|
||||
<div>y: {pointerData?.pointer.y ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
ref={excalidrawRef}
|
||||
ref={(api) => setExcalidrawAPI(api)}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) => {
|
||||
console.info("Elements :", elements, "State : ", state);
|
||||
}}
|
||||
onPointerUpdate={(payload) => console.info(payload)}
|
||||
onPointerUpdate={(payload) => setPointerData(payload)}
|
||||
onCollabButtonClick={() =>
|
||||
window.alert("You clicked on collab button")
|
||||
}
|
||||
@ -571,7 +590,7 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const svg = await exportToSvg({
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
@ -580,7 +599,7 @@ export default function App() {
|
||||
height: 100,
|
||||
},
|
||||
embedScene: true,
|
||||
files: excalidrawRef.current.getFiles(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
appRef.current.querySelector(".export-svg").innerHTML =
|
||||
svg.outerHTML;
|
||||
@ -593,14 +612,14 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const blob = await exportToBlob({
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
mimeType: "image/png",
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportEmbedScene,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawRef.current.getFiles(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
setBlobUrl(window.URL.createObjectURL(blob));
|
||||
}}
|
||||
@ -614,12 +633,12 @@ export default function App() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const canvas = await exportToCanvas({
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawRef.current.getFiles(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = "30px Virgil";
|
||||
|
@ -214,3 +214,8 @@ export {
|
||||
newElementWith,
|
||||
bumpVersion,
|
||||
} from "../../element/mutateElement";
|
||||
|
||||
export {
|
||||
parseLibraryTokensFromUrl,
|
||||
useHandleLibrary,
|
||||
} from "../../data/library";
|
||||
|
17
src/types.ts
17
src/types.ts
@ -206,7 +206,7 @@ export declare class GestureEvent extends UIEvent {
|
||||
/** @deprecated legacy: do not use outside of migration paths */
|
||||
export type LibraryItem_v1 = readonly NonDeleted<ExcalidrawElement>[];
|
||||
/** @deprecated legacy: do not use outside of migration paths */
|
||||
export type LibraryItems_v1 = readonly LibraryItem_v1[];
|
||||
type LibraryItems_v1 = readonly LibraryItem_v1[];
|
||||
|
||||
/** v2 library item */
|
||||
export type LibraryItem = {
|
||||
@ -219,6 +219,18 @@ export type LibraryItem = {
|
||||
error?: string;
|
||||
};
|
||||
export type LibraryItems = readonly LibraryItem[];
|
||||
export type LibraryItems_anyVersion = LibraryItems | LibraryItems_v1;
|
||||
|
||||
export type LibraryItemsSource =
|
||||
| ((
|
||||
currentLibraryItems: LibraryItems,
|
||||
) =>
|
||||
| Blob
|
||||
| LibraryItems_anyVersion
|
||||
| Promise<LibraryItems_anyVersion | Blob>)
|
||||
| Blob
|
||||
| LibraryItems_anyVersion
|
||||
| Promise<LibraryItems_anyVersion | Blob>;
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// NOTE ready/readyPromise props are optional for host apps' sake (our own
|
||||
@ -301,7 +313,6 @@ export type SceneData = {
|
||||
appState?: ImportedDataState["appState"];
|
||||
collaborators?: Map<string, Collaborator>;
|
||||
commitToHistory?: boolean;
|
||||
libraryItems?: LibraryItems | LibraryItems_v1;
|
||||
};
|
||||
|
||||
export enum UserIdleState {
|
||||
@ -436,6 +447,7 @@ export type PointerDownState = Readonly<{
|
||||
|
||||
export type ExcalidrawImperativeAPI = {
|
||||
updateScene: InstanceType<typeof App>["updateScene"];
|
||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||
resetScene: InstanceType<typeof App>["resetScene"];
|
||||
getSceneElementsIncludingDeleted: InstanceType<
|
||||
typeof App
|
||||
@ -448,7 +460,6 @@ export type ExcalidrawImperativeAPI = {
|
||||
getAppState: () => InstanceType<typeof App>["state"];
|
||||
getFiles: () => InstanceType<typeof App>["files"];
|
||||
refresh: InstanceType<typeof App>["refresh"];
|
||||
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
|
||||
setToastMessage: InstanceType<typeof App>["setToastMessage"];
|
||||
addFiles: (data: BinaryFileData[]) => void;
|
||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user