feat: factor out url library init & switch to updateLibrary API (#5115)

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
David Luzar 2022-05-11 15:08:54 +02:00 committed by GitHub
parent 2537b225ac
commit cad6097d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 235 deletions

View File

@ -70,14 +70,12 @@ import {
TEXT_TO_CENTER_SNAP_THRESHOLD, TEXT_TO_CENTER_SNAP_THRESHOLD,
THEME, THEME,
TOUCH_CTX_MENU_TIMEOUT, TOUCH_CTX_MENU_TIMEOUT,
URL_HASH_KEYS,
URL_QUERY_KEYS,
VERTICAL_ALIGN, VERTICAL_ALIGN,
ZOOM_STEP, ZOOM_STEP,
} from "../constants"; } from "../constants";
import { loadFromBlob } from "../data"; import { loadFromBlob } from "../data";
import Library from "../data/library"; import Library from "../data/library";
import { restore, restoreElements, restoreLibraryItems } from "../data/restore"; import { restore, restoreElements } from "../data/restore";
import { import {
dragNewElement, dragNewElement,
dragSelectedElements, dragSelectedElements,
@ -234,7 +232,6 @@ import {
isSupportedImageFile, isSupportedImageFile,
loadSceneOrLibraryFromBlob, loadSceneOrLibraryFromBlob,
normalizeFile, normalizeFile,
loadLibraryFromBlob,
resizeImageFile, resizeImageFile,
SVGStringToFile, SVGStringToFile,
} from "../data/blob"; } from "../data/blob";
@ -261,7 +258,6 @@ import {
isPointHittingLinkIcon, isPointHittingLinkIcon,
isLocalLink, isLocalLink,
} from "../element/Hyperlink"; } from "../element/Hyperlink";
import { AbortError } from "../errors";
const defaultDeviceTypeContext: DeviceType = { const defaultDeviceTypeContext: DeviceType = {
isMobile: false, isMobile: false,
@ -361,6 +357,8 @@ class App extends React.Component<AppProps, AppState> {
this.id = nanoid(); this.id = nanoid();
this.library = new Library(this);
if (excalidrawRef) { if (excalidrawRef) {
const readyPromise = const readyPromise =
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) || ("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
@ -370,6 +368,7 @@ class App extends React.Component<AppProps, AppState> {
ready: true, ready: true,
readyPromise, readyPromise,
updateScene: this.updateScene, updateScene: this.updateScene,
updateLibrary: this.library.updateLibrary,
addFiles: this.addFiles, addFiles: this.addFiles,
resetScene: this.resetScene, resetScene: this.resetScene,
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted, getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
@ -381,7 +380,6 @@ class App extends React.Component<AppProps, AppState> {
getAppState: () => this.state, getAppState: () => this.state,
getFiles: () => this.files, getFiles: () => this.files,
refresh: this.refresh, refresh: this.refresh,
importLibrary: this.importLibraryFromUrl,
setToastMessage: this.setToastMessage, setToastMessage: this.setToastMessage,
id: this.id, id: this.id,
setActiveTool: this.setActiveTool, setActiveTool: this.setActiveTool,
@ -400,7 +398,6 @@ class App extends React.Component<AppProps, AppState> {
}; };
this.scene = new Scene(); this.scene = new Scene();
this.library = new Library(this);
this.history = new History(); this.history = new History();
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
this.syncActionResult, this.syncActionResult,
@ -698,54 +695,6 @@ class App extends React.Component<AppProps, AppState> {
this.onSceneUpdated(); 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 = () => { private resetHistory = () => {
this.history.clear(); this.history.clear();
}; };
@ -790,7 +739,14 @@ class App extends React.Component<AppProps, AppState> {
try { try {
initialData = (await this.props.initialData) || null; initialData = (await this.props.initialData) || null;
if (initialData?.libraryItems) { 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) { } catch (error: any) {
console.error(error); console.error(error);
@ -802,10 +758,10 @@ class App extends React.Component<AppProps, AppState> {
}, },
}; };
} }
const scene = restore(initialData, null, null); const scene = restore(initialData, null, null);
scene.appState = { scene.appState = {
...scene.appState, ...scene.appState,
isLibraryOpen: this.state.isLibraryOpen,
activeTool: activeTool:
scene.appState.activeTool.type === "image" scene.appState.activeTool.type === "image"
? { ...scene.appState.activeTool, type: "selection" } ? { ...scene.appState.activeTool, type: "selection" }
@ -834,20 +790,6 @@ class App extends React.Component<AppProps, AppState> {
...scene, ...scene,
commitToHistory: true, 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() { public async componentDidMount() {
@ -1691,14 +1633,6 @@ class App extends React.Component<AppProps, AppState> {
appState?: Pick<AppState, K> | null; appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"]; collaborators?: SceneData["collaborators"];
commitToHistory?: SceneData["commitToHistory"]; commitToHistory?: SceneData["commitToHistory"];
libraryItems?:
| ((
currentLibraryItems: LibraryItems,
) =>
| Required<SceneData>["libraryItems"]
| Promise<Required<SceneData>["libraryItems"]>)
| Required<SceneData>["libraryItems"]
| Promise<Required<SceneData>["libraryItems"]>;
}) => { }) => {
if (sceneData.commitToHistory) { if (sceneData.commitToHistory) {
this.history.resumeRecording(); this.history.resumeRecording();
@ -1715,23 +1649,6 @@ class App extends React.Component<AppProps, AppState> {
if (sceneData.collaborators) { if (sceneData.collaborators) {
this.setState({ collaborators: 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, commitToHistory: true,
}); });
} else if (ret.type === MIME_TYPES.excalidrawlib) { } else if (ret.type === MIME_TYPES.excalidrawlib) {
this.library await this.library
.importLibrary(file) .updateLibrary({
.then(() => { libraryItems: file,
this.setState({ merge: true,
isLoading: false, openLibraryMenu: true,
});
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.setState({ this.setState({ errorMessage: t("errors.importLibraryError") });
isLoading: false,
errorMessage: t("errors.importLibraryError"),
});
}); });
} }
} catch (error: any) { } catch (error: any) {

View File

@ -1,6 +1,6 @@
import { chunk } from "lodash"; import { chunk } from "lodash";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json"; import { saveLibraryAsJSON } from "../data/json";
import Library from "../data/library"; import Library from "../data/library";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
@ -23,6 +23,7 @@ import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { fileOpen } from "../data/filesystem";
const LibraryMenuItems = ({ const LibraryMenuItems = ({
isLoading, isLoading,
@ -107,13 +108,23 @@ const LibraryMenuItems = ({
title={t("buttons.load")} title={t("buttons.load")}
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
icon={load} icon={load}
onClick={() => { onClick={async () => {
importLibraryFromJSON(library) try {
.catch(muteFSAbortError) await fileOpen({
.catch((error) => { description: "Excalidraw library files",
console.error(error); // ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
setAppState({ errorMessage: t("errors.importLibraryError") }); // 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" className="library-actions--load"
/> />

View File

@ -17,7 +17,6 @@ import {
ExportedLibraryData, ExportedLibraryData,
ImportedLibraryData, ImportedLibraryData,
} from "./types"; } from "./types";
import Library from "./library";
/** /**
* Strips out files which are only referenced by deleted elements * 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);
};

View File

@ -1,10 +1,18 @@
import { loadLibraryFromBlob } from "./blob"; import { loadLibraryFromBlob } from "./blob";
import { LibraryItems, LibraryItem } from "../types"; import {
LibraryItems,
LibraryItem,
ExcalidrawImperativeAPI,
LibraryItemsSource,
} from "../types";
import { restoreLibraryItems } from "./restore"; import { restoreLibraryItems } from "./restore";
import type App from "../components/App"; import type App from "../components/App";
import { ImportedDataState } from "./types";
import { atom } from "jotai"; import { atom } from "jotai";
import { jotaiStore } 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<{ export const libraryItemsAtom = atom<{
status: "loading" | "loaded"; status: "loading" | "loaded";
@ -102,36 +110,6 @@ class Library {
return this.setLibrary([]); 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. * @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 = ( setLibrary = (
/** /**
* LibraryItems that will replace current items. Can be a function which * LibraryItems that will replace current items. Can be a function which
@ -204,3 +241,102 @@ class Library {
} }
export default 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]);
};

View File

@ -1,5 +1,10 @@
import { ExcalidrawElement } from "../element/types"; 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 type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
@ -19,7 +24,7 @@ export interface ImportedDataState {
elements?: readonly ExcalidrawElement[] | null; elements?: readonly ExcalidrawElement[] | null;
appState?: Readonly<Partial<AppState>> | null; appState?: Readonly<Partial<AppState>> | null;
scrollToContent?: boolean; scrollToContent?: boolean;
libraryItems?: LibraryItems | LibraryItems_v1; libraryItems?: LibraryItems_anyVersion;
files?: BinaryFiles; files?: BinaryFiles;
} }

View File

@ -4,13 +4,7 @@ import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { ErrorDialog } from "../components/ErrorDialog"; import { ErrorDialog } from "../components/ErrorDialog";
import { TopErrorBoundary } from "../components/TopErrorBoundary"; import { TopErrorBoundary } from "../components/TopErrorBoundary";
import { import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
APP_NAME,
EVENT,
TITLE_TIMEOUT,
URL_HASH_KEYS,
VERSION_TIMEOUT,
} from "../constants";
import { loadFromBlob } from "../data/blob"; import { loadFromBlob } from "../data/blob";
import { import {
ExcalidrawElement, ExcalidrawElement,
@ -72,6 +66,7 @@ import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData"; import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync"; import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx"; import clsx from "clsx";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
const languageDetector = new LanguageDetector(); const languageDetector = new LanguageDetector();
languageDetector.init({ languageDetector.init({
@ -232,6 +227,11 @@ const ExcalidrawWrapper = () => {
const collabAPI = useContext(CollabContext)?.api; const collabAPI = useContext(CollabContext)?.api;
useHandleLibrary({
excalidrawAPI,
getInitialLibraryItems: getLibraryItemsFromStorage,
});
useEffect(() => { useEffect(() => {
if (!collabAPI || !excalidrawAPI) { if (!collabAPI || !excalidrawAPI) {
return; return;
@ -301,8 +301,6 @@ const ExcalidrawWrapper = () => {
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds }); LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
} }
} }
data.scene.libraryItems = getLibraryItemsFromStorage();
}; };
initializeScene({ collabAPI }).then((data) => { initializeScene({ collabAPI }).then((data) => {
@ -310,18 +308,10 @@ const ExcalidrawWrapper = () => {
initialStatePromiseRef.current.promise.resolve(data.scene); initialStatePromiseRef.current.promise.resolve(data.scene);
}); });
const onHashChange = (event: HashChangeEvent) => { const onHashChange = async (event: HashChangeEvent) => {
event.preventDefault(); event.preventDefault();
const hash = new URLSearchParams(window.location.hash.slice(1)); const libraryUrlTokens = parseLibraryTokensFromUrl();
const libraryUrl = hash.get(URL_HASH_KEYS.addLibrary); if (!libraryUrlTokens) {
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 {
initializeScene({ collabAPI }).then((data) => { initializeScene({ collabAPI }).then((data) => {
loadImages(data); loadImages(data);
if (data.scene) { if (data.scene) {
@ -355,6 +345,8 @@ const ExcalidrawWrapper = () => {
setLangCode(langCode); setLangCode(langCode);
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...localDataState, ...localDataState,
});
excalidrawAPI.updateLibrary({
libraryItems: getLibraryItemsFromStorage(), libraryItems: getLibraryItemsFromStorage(),
}); });
collabAPI.setUsername(username || ""); collabAPI.setUsername(username || "");

View File

@ -17,6 +17,20 @@ Please add the latest change on the top under the correct section.
#### Features #### 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 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. - Add [`onPointerDown`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) callback which gets triggered on pointer down events.

View File

@ -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 | | 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) | | 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 | | [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 |
| [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 | | [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. | | 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 | | 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 | <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 | | 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 | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></pre> | Returns current appState | | 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 | | 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> &#124; <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> &#124; <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. | | 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 | | [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. | | 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. | | [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. | | [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) | <pre>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</pre> | This API can be used to set the active tool | | [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
#### `readyPromise` #### `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`. | | `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) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> &#124; ((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) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. | | `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> &#124; ((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) &#124; 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" &#124; "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
### `addFiles` ### `addFiles`
<pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre> <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"; import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
const contents = await loadSceneOrLibraryFromBlob(file, null, null); 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) { if (contents.type === MIME_TYPES.excalidraw) {
excalidrawAPI.updateScene(contents.data); excalidrawAPI.updateScene(contents.data);
} else { } else if (contents.type === MIME_TYPES.excalidrawlib) {
excalidrawAPI.updateScene(contents.data); 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. 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 ### Exported constants
#### `FONT_FAMILY` #### `FONT_FAMILY`

View File

@ -26,6 +26,7 @@ const {
exportToBlob, exportToBlob,
exportToClipboard, exportToClipboard,
Excalidraw, Excalidraw,
useHandleLibrary,
MIME_TYPES, MIME_TYPES,
} = window.ExcalidrawLib; } = window.ExcalidrawLib;
@ -63,9 +64,7 @@ const renderTopRightUI = () => {
}; };
export default function App() { export default function App() {
const excalidrawRef = useRef(null);
const appRef = useRef(null); const appRef = useRef(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false); const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false); const [zenModeEnabled, setZenModeEnabled] = useState(false);
const [gridModeEnabled, setGridModeEnabled] = useState(false); const [gridModeEnabled, setGridModeEnabled] = useState(false);
@ -82,7 +81,15 @@ export default function App() {
if (!initialStatePromiseRef.current.promise) { if (!initialStatePromiseRef.current.promise) {
initialStatePromiseRef.current.promise = resolvablePromise(); initialStatePromiseRef.current.promise = resolvablePromise();
} }
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
useHandleLibrary({ excalidrawAPI });
useEffect(() => { useEffect(() => {
if (!excalidrawAPI) {
return;
}
const fetchData = async () => { const fetchData = async () => {
const res = await fetch("/rocket.jpeg"); const res = await fetch("/rocket.jpeg");
const imageData = await res.blob(); const imageData = await res.blob();
@ -100,23 +107,12 @@ export default function App() {
]; ];
initialStatePromiseRef.current.promise.resolve(InitialData); initialStatePromiseRef.current.promise.resolve(InitialData);
excalidrawRef.current.addFiles(imagesArray); excalidrawAPI.addFiles(imagesArray);
}; };
}; };
fetchData(); 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 = () => { const renderFooter = () => {
return ( return (
<> <>
@ -124,7 +120,7 @@ export default function App() {
<button <button
className="custom-element" className="custom-element"
onClick={() => onClick={() =>
excalidrawRef.current.setActiveTool({ excalidrawAPI.setActiveTool({
type: "custom", type: "custom",
customType: "comment", customType: "comment",
}) })
@ -143,7 +139,14 @@ export default function App() {
const loadSceneOrLibrary = async () => { const loadSceneOrLibrary = async () => {
const file = await fileOpen({ description: "Excalidraw or library file" }); const file = await fileOpen({ description: "Excalidraw or library file" });
const contents = await loadSceneOrLibraryFromBlob(file, null, null); 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 = () => { const updateScene = () => {
@ -175,7 +178,7 @@ export default function App() {
viewBackgroundColor: "#edf2ff", viewBackgroundColor: "#edf2ff",
}, },
}; };
excalidrawRef.current.updateScene(sceneData); excalidrawAPI.updateScene(sceneData);
}; };
const onLinkOpen = useCallback((element, event) => { const onLinkOpen = useCallback((element, event) => {
@ -195,14 +198,16 @@ export default function App() {
const onCopy = async (type) => { const onCopy = async (type) => {
await exportToClipboard({ await exportToClipboard({
elements: excalidrawRef.current.getSceneElements(), elements: excalidrawAPI.getSceneElements(),
appState: excalidrawRef.current.getAppState(), appState: excalidrawAPI.getAppState(),
files: excalidrawRef.current.getFiles(), files: excalidrawAPI.getFiles(),
type, type,
}); });
window.alert(`Copied to clipboard as ${type} sucessfully`); window.alert(`Copied to clipboard as ${type} sucessfully`);
}; };
const [pointerData, setPointerData] = useState(null);
const onPointerDown = (activeTool, pointerDownState) => { const onPointerDown = (activeTool, pointerDownState) => {
if (activeTool.type === "custom" && activeTool.customType === "comment") { if (activeTool.type === "custom" && activeTool.customType === "comment") {
const { x, y } = pointerDownState.origin; const { x, y } = pointerDownState.origin;
@ -215,7 +220,7 @@ export default function App() {
appRef.current.querySelectorAll(".comment-icon"); appRef.current.querySelectorAll(".comment-icon");
commentIconsElements.forEach((ele) => { commentIconsElements.forEach((ele) => {
const id = ele.id; const id = ele.id;
const appstate = excalidrawRef.current.getAppState(); const appstate = excalidrawAPI.getAppState();
const { x, y } = sceneCoordsToViewportCoords( const { x, y } = sceneCoordsToViewportCoords(
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y }, { sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
appstate, appstate,
@ -233,7 +238,7 @@ export default function App() {
return withBatchedUpdatesThrottled((event) => { return withBatchedUpdatesThrottled((event) => {
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY }, { clientX: event.clientX, clientY: event.clientY },
excalidrawRef.current.getAppState(), excalidrawAPI.getAppState(),
); );
const distance = distance2d( const distance = distance2d(
pointerDownState.x, pointerDownState.x,
@ -257,7 +262,7 @@ export default function App() {
return withBatchedUpdates((event) => { return withBatchedUpdates((event) => {
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove); window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp); window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
excalidrawRef.current.setActiveTool({ type: "selection" }); excalidrawAPI.setActiveTool({ type: "selection" });
const distance = distance2d( const distance = distance2d(
pointerDownState.x, pointerDownState.x,
pointerDownState.y, pointerDownState.y,
@ -280,10 +285,10 @@ export default function App() {
}; };
const renderCommentIcons = () => { const renderCommentIcons = () => {
return Object.values(commentIcons).map((commentIcon) => { return Object.values(commentIcons).map((commentIcon) => {
const appState = excalidrawRef.current.getAppState(); const appState = excalidrawAPI.getAppState();
const { x, y } = sceneCoordsToViewportCoords( const { x, y } = sceneCoordsToViewportCoords(
{ sceneX: commentIcon.x, sceneY: commentIcon.y }, { sceneX: commentIcon.x, sceneY: commentIcon.y },
excalidrawRef.current.getAppState(), excalidrawAPI.getAppState(),
); );
return ( return (
<div <div
@ -319,7 +324,10 @@ export default function App() {
pointerDownState.onMove = onPointerMove; pointerDownState.onMove = onPointerMove;
pointerDownState.onUp = onPointerUp; pointerDownState.onUp = onPointerUp;
excalidrawRef.current.setCustomType("comment"); excalidrawAPI.setActiveTool({
type: "custom",
customType: "comment",
});
}} }}
> >
<div className="comment-avatar"> <div className="comment-avatar">
@ -349,7 +357,7 @@ export default function App() {
}; };
const renderComment = () => { const renderComment = () => {
const appState = excalidrawRef.current.getAppState(); const appState = excalidrawAPI.getAppState();
const { x, y } = sceneCoordsToViewportCoords( const { x, y } = sceneCoordsToViewportCoords(
{ sceneX: comment.x, sceneY: comment.y }, { sceneX: comment.x, sceneY: comment.y },
appState, appState,
@ -416,14 +424,14 @@ export default function App() {
<button <button
className="reset-scene" className="reset-scene"
onClick={() => { onClick={() => {
excalidrawRef.current.resetScene(); excalidrawAPI.resetScene();
}} }}
> >
Reset Scene Reset Scene
</button> </button>
<button <button
onClick={() => { onClick={() => {
excalidrawRef.current.updateScene({ excalidrawAPI.updateLibrary({
libraryItems: [ libraryItems: [
{ {
status: "published", status: "published",
@ -501,9 +509,9 @@ export default function App() {
username: "fallback", username: "fallback",
avatarUrl: "https://example.com", avatarUrl: "https://example.com",
}); });
excalidrawRef.current.updateScene({ collaborators }); excalidrawAPI.updateScene({ collaborators });
} else { } else {
excalidrawRef.current.updateScene({ excalidrawAPI.updateScene({
collaborators: new Map(), collaborators: new Map(),
}); });
} }
@ -523,15 +531,26 @@ export default function App() {
Copy to Clipboard as JSON Copy to Clipboard as JSON
</button> </button>
</div> </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>
<div className="excalidraw-wrapper"> <div className="excalidraw-wrapper">
<Excalidraw <Excalidraw
ref={excalidrawRef} ref={(api) => setExcalidrawAPI(api)}
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}
onChange={(elements, state) => { onChange={(elements, state) => {
console.info("Elements :", elements, "State : ", state); console.info("Elements :", elements, "State : ", state);
}} }}
onPointerUpdate={(payload) => console.info(payload)} onPointerUpdate={(payload) => setPointerData(payload)}
onCollabButtonClick={() => onCollabButtonClick={() =>
window.alert("You clicked on collab button") window.alert("You clicked on collab button")
} }
@ -571,7 +590,7 @@ export default function App() {
<button <button
onClick={async () => { onClick={async () => {
const svg = await exportToSvg({ const svg = await exportToSvg({
elements: excalidrawRef.current.getSceneElements(), elements: excalidrawAPI.getSceneElements(),
appState: { appState: {
...initialData.appState, ...initialData.appState,
exportWithDarkMode, exportWithDarkMode,
@ -580,7 +599,7 @@ export default function App() {
height: 100, height: 100,
}, },
embedScene: true, embedScene: true,
files: excalidrawRef.current.getFiles(), files: excalidrawAPI.getFiles(),
}); });
appRef.current.querySelector(".export-svg").innerHTML = appRef.current.querySelector(".export-svg").innerHTML =
svg.outerHTML; svg.outerHTML;
@ -593,14 +612,14 @@ export default function App() {
<button <button
onClick={async () => { onClick={async () => {
const blob = await exportToBlob({ const blob = await exportToBlob({
elements: excalidrawRef.current.getSceneElements(), elements: excalidrawAPI.getSceneElements(),
mimeType: "image/png", mimeType: "image/png",
appState: { appState: {
...initialData.appState, ...initialData.appState,
exportEmbedScene, exportEmbedScene,
exportWithDarkMode, exportWithDarkMode,
}, },
files: excalidrawRef.current.getFiles(), files: excalidrawAPI.getFiles(),
}); });
setBlobUrl(window.URL.createObjectURL(blob)); setBlobUrl(window.URL.createObjectURL(blob));
}} }}
@ -614,12 +633,12 @@ export default function App() {
<button <button
onClick={async () => { onClick={async () => {
const canvas = await exportToCanvas({ const canvas = await exportToCanvas({
elements: excalidrawRef.current.getSceneElements(), elements: excalidrawAPI.getSceneElements(),
appState: { appState: {
...initialData.appState, ...initialData.appState,
exportWithDarkMode, exportWithDarkMode,
}, },
files: excalidrawRef.current.getFiles(), files: excalidrawAPI.getFiles(),
}); });
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil"; ctx.font = "30px Virgil";

View File

@ -214,3 +214,8 @@ export {
newElementWith, newElementWith,
bumpVersion, bumpVersion,
} from "../../element/mutateElement"; } from "../../element/mutateElement";
export {
parseLibraryTokensFromUrl,
useHandleLibrary,
} from "../../data/library";

View File

@ -206,7 +206,7 @@ export declare class GestureEvent extends UIEvent {
/** @deprecated legacy: do not use outside of migration paths */ /** @deprecated legacy: do not use outside of migration paths */
export type LibraryItem_v1 = readonly NonDeleted<ExcalidrawElement>[]; export type LibraryItem_v1 = readonly NonDeleted<ExcalidrawElement>[];
/** @deprecated legacy: do not use outside of migration paths */ /** @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 */ /** v2 library item */
export type LibraryItem = { export type LibraryItem = {
@ -219,6 +219,18 @@ export type LibraryItem = {
error?: string; error?: string;
}; };
export type LibraryItems = readonly LibraryItem[]; 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 // NOTE ready/readyPromise props are optional for host apps' sake (our own
@ -301,7 +313,6 @@ export type SceneData = {
appState?: ImportedDataState["appState"]; appState?: ImportedDataState["appState"];
collaborators?: Map<string, Collaborator>; collaborators?: Map<string, Collaborator>;
commitToHistory?: boolean; commitToHistory?: boolean;
libraryItems?: LibraryItems | LibraryItems_v1;
}; };
export enum UserIdleState { export enum UserIdleState {
@ -436,6 +447,7 @@ export type PointerDownState = Readonly<{
export type ExcalidrawImperativeAPI = { export type ExcalidrawImperativeAPI = {
updateScene: InstanceType<typeof App>["updateScene"]; updateScene: InstanceType<typeof App>["updateScene"];
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
resetScene: InstanceType<typeof App>["resetScene"]; resetScene: InstanceType<typeof App>["resetScene"];
getSceneElementsIncludingDeleted: InstanceType< getSceneElementsIncludingDeleted: InstanceType<
typeof App typeof App
@ -448,7 +460,6 @@ export type ExcalidrawImperativeAPI = {
getAppState: () => InstanceType<typeof App>["state"]; getAppState: () => InstanceType<typeof App>["state"];
getFiles: () => InstanceType<typeof App>["files"]; getFiles: () => InstanceType<typeof App>["files"];
refresh: InstanceType<typeof App>["refresh"]; refresh: InstanceType<typeof App>["refresh"];
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
setToastMessage: InstanceType<typeof App>["setToastMessage"]; setToastMessage: InstanceType<typeof App>["setToastMessage"];
addFiles: (data: BinaryFileData[]) => void; addFiles: (data: BinaryFileData[]) => void;
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>; readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;