diff --git a/src/actions/actionAddToLibrary.ts b/src/actions/actionAddToLibrary.ts index 8fb7eac9..c952783c 100644 --- a/src/actions/actionAddToLibrary.ts +++ b/src/actions/actionAddToLibrary.ts @@ -2,18 +2,20 @@ import { register } from "./register"; import { getSelectedElements } from "../scene"; import { getNonDeletedElements } from "../element"; import { deepCopyElement } from "../element/newElement"; -import { Library } from "../data/library"; export const actionAddToLibrary = register({ name: "addToLibrary", - perform: (elements, appState) => { + perform: (elements, appState, _, app) => { const selectedElements = getSelectedElements( getNonDeletedElements(elements), appState, ); - Library.loadLibrary().then((items) => { - Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]); + app.library.loadLibrary().then((items) => { + app.library.saveLibrary([ + ...items, + selectedElements.map(deepCopyElement), + ]); }); return false; }, diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index a0339ffa..9e2ce75b 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -9,6 +9,7 @@ import { import { ExcalidrawElement } from "../element/types"; import { AppProps, AppState } from "../types"; import { MODES } from "../constants"; +import Library from "../data/library"; // This is the component, but for now we don't care about anything but its // `canvas` state. @@ -16,6 +17,7 @@ type App = { canvas: HTMLCanvasElement | null; focusContainer: () => void; props: AppProps; + library: Library; }; export class ActionManager implements ActionsManagerInterface { diff --git a/src/actions/types.ts b/src/actions/types.ts index 5af6faa3..a0ddf399 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -1,6 +1,7 @@ import React from "react"; import { ExcalidrawElement } from "../element/types"; import { AppState, ExcalidrawProps } from "../types"; +import Library from "../data/library"; /** if false, the action should be prevented */ export type ActionResult = @@ -15,7 +16,11 @@ export type ActionResult = } | false; -type AppAPI = { canvas: HTMLCanvasElement | null; focusContainer(): void }; +type AppAPI = { + canvas: HTMLCanvasElement | null; + focusContainer(): void; + library: Library; +}; type ActionFn = ( elements: readonly ExcalidrawElement[], diff --git a/src/components/App.tsx b/src/components/App.tsx index 4d3fc13e..4deb01dc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4,6 +4,7 @@ import { RoughCanvas } from "roughjs/bin/canvas"; import rough from "roughjs/bin/rough"; import clsx from "clsx"; import { supported } from "browser-fs-access"; +import { nanoid } from "nanoid"; import { actionAddToLibrary, @@ -68,7 +69,7 @@ import { } from "../constants"; import { loadFromBlob } from "../data"; import { isValidLibrary } from "../data/json"; -import { Library } from "../data/library"; +import Library from "../data/library"; import { restore } from "../data/restore"; import { dragNewElement, @@ -163,7 +164,14 @@ import Scene from "../scene/Scene"; import { SceneState, ScrollBars } from "../scene/types"; import { getNewZoom } from "../scene/zoom"; import { findShapeByKey } from "../shapes"; -import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types"; +import { + AppProps, + AppState, + Gesture, + GestureEvent, + LibraryItems, + SceneData, +} from "../types"; import { debounce, distance, @@ -289,6 +297,7 @@ export type ExcalidrawImperativeAPI = { setToastMessage: InstanceType["setToastMessage"]; readyPromise: ResolvablePromise; ready: true; + id: string; }; class App extends React.Component { @@ -309,6 +318,9 @@ class App extends React.Component { private scene: Scene; private resizeObserver: ResizeObserver | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined; + public library: Library; + public libraryItemsFromStorage: LibraryItems | undefined; + private id: string; constructor(props: AppProps) { super(props); @@ -334,6 +346,8 @@ class App extends React.Component { height: window.innerHeight, }; + this.id = nanoid(); + if (excalidrawRef) { const readyPromise = ("current" in excalidrawRef && excalidrawRef.current?.readyPromise) || @@ -354,6 +368,7 @@ class App extends React.Component { refresh: this.refresh, importLibrary: this.importLibraryFromUrl, setToastMessage: this.setToastMessage, + id: this.id, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); @@ -363,6 +378,7 @@ class App extends React.Component { readyPromise.resolve(api); } this.scene = new Scene(); + this.library = new Library(this); this.actionManager = new ActionManager( this.syncActionResult, @@ -490,6 +506,8 @@ class App extends React.Component { libraryReturnUrl={this.props.libraryReturnUrl} UIOptions={this.props.UIOptions} focusContainer={this.focusContainer} + library={this.library} + id={this.id} />
@@ -650,12 +668,12 @@ class App extends React.Component { throw new Error(); } if ( - token === Library.csrfToken || + token === this.id || window.confirm( t("alerts.confirmAddLibrary", { numShapes: json.library.length }), ) ) { - await Library.importLibrary(blob); + await this.library.importLibrary(blob); // hack to rerender the library items after import if (this.state.isLibraryOpen) { this.setState({ isLibraryOpen: false }); @@ -724,6 +742,9 @@ class App extends React.Component { let initialData = null; try { initialData = (await this.props.initialData) || null; + if (initialData?.libraryItems) { + this.libraryItemsFromStorage = initialData.libraryItems; + } } catch (error) { console.error(error); initialData = { @@ -3713,7 +3734,8 @@ class App extends React.Component { file?.type === MIME_TYPES.excalidrawlib || file?.name?.endsWith(".excalidrawlib") ) { - Library.importLibrary(file) + this.library + .importLibrary(file) .then(() => { // Close and then open to get the libraries updated this.setState({ isLibraryOpen: false }); @@ -4248,7 +4270,6 @@ declare global { setState: React.Component["setState"]; history: SceneHistory; app: InstanceType; - library: typeof Library; }; } } @@ -4273,10 +4294,6 @@ if ( configurable: true, get: () => history, }, - library: { - configurable: true, - value: Library, - }, }); } export default App; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index df521361..ebd6a6f7 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -10,7 +10,6 @@ import { ActionManager } from "../actions/manager"; import { CLASSES } from "../constants"; import { exportCanvas } from "../data"; import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json"; -import { Library } from "../data/library"; import { isTextElement, showSelectedShapeActions } from "../element"; import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; @@ -47,6 +46,7 @@ import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import { Tooltip } from "./Tooltip"; import { UserList } from "./UserList"; +import Library from "../data/library"; interface LayerUIProps { actionManager: ActionManager; @@ -73,6 +73,8 @@ interface LayerUIProps { libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; UIOptions: AppProps["UIOptions"]; focusContainer: () => void; + library: Library; + id: string; } const useOnClickOutside = ( @@ -104,7 +106,7 @@ const useOnClickOutside = ( }; const LibraryMenuItems = ({ - library, + libraryItems, onRemoveFromLibrary, onAddToLibrary, onInsertShape, @@ -113,8 +115,10 @@ const LibraryMenuItems = ({ setLibraryItems, libraryReturnUrl, focusContainer, + library, + id, }: { - library: LibraryItems; + libraryItems: LibraryItems; pendingElements: LibraryItem; onRemoveFromLibrary: (index: number) => void; onInsertShape: (elements: LibraryItem) => void; @@ -123,9 +127,11 @@ const LibraryMenuItems = ({ setLibraryItems: (library: LibraryItems) => void; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; focusContainer: () => void; + library: Library; + id: string; }) => { const isMobile = useIsMobile(); - const numCells = library.length + (pendingElements.length > 0 ? 1 : 0); + const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0); const CELLS_PER_ROW = isMobile ? 4 : 6; const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW)); const rows = []; @@ -143,7 +149,7 @@ const LibraryMenuItems = ({ aria-label={t("buttons.load")} icon={load} onClick={() => { - importLibraryFromJSON() + importLibraryFromJSON(library) .then(() => { // Close and then open to get the libraries updated setAppState({ isLibraryOpen: false }); @@ -155,7 +161,7 @@ const LibraryMenuItems = ({ }); }} /> - {!!library.length && ( + {!!libraryItems.length && ( <> { - saveLibraryAsJSON() + saveLibraryAsJSON(library) .catch(muteFSAbortError) .catch((error) => { setAppState({ errorMessage: error.message }); @@ -179,7 +185,7 @@ const LibraryMenuItems = ({ icon={trash} onClick={() => { if (window.confirm(t("alerts.resetLibrary"))) { - Library.resetLibrary(); + library.resetLibrary(); setLibraryItems([]); focusContainer(); } @@ -190,7 +196,7 @@ const LibraryMenuItems = ({ {t("labels.libraries")} @@ -205,13 +211,13 @@ const LibraryMenuItems = ({ const shouldAddPendingElements: boolean = pendingElements.length > 0 && !addedPendingElements && - y + x >= library.length; + y + x >= libraryItems.length; addedPendingElements = addedPendingElements || shouldAddPendingElements; children.push( , @@ -247,6 +253,8 @@ const LibraryMenu = ({ setAppState, libraryReturnUrl, focusContainer, + library, + id, }: { pendingElements: LibraryItem; onClickOutside: (event: MouseEvent) => void; @@ -255,6 +263,8 @@ const LibraryMenu = ({ setAppState: React.Component["setState"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; focusContainer: () => void; + library: Library; + id: string; }) => { const ref = useRef(null); useOnClickOutside(ref, (event) => { @@ -280,7 +290,7 @@ const LibraryMenu = ({ resolve("loading"); }, 100); }), - Library.loadLibrary().then((items) => { + library.loadLibrary().then((items) => { setLibraryItems(items); setIsLoading("ready"); }), @@ -292,24 +302,33 @@ const LibraryMenu = ({ return () => { clearTimeout(loadingTimerRef.current!); }; - }, []); + }, [library]); - const removeFromLibrary = useCallback(async (indexToRemove) => { - const items = await Library.loadLibrary(); - const nextItems = items.filter((_, index) => index !== indexToRemove); - Library.saveLibrary(nextItems); - setLibraryItems(nextItems); - }, []); + const removeFromLibrary = useCallback( + async (indexToRemove) => { + const items = await library.loadLibrary(); + const nextItems = items.filter((_, index) => index !== indexToRemove); + library.saveLibrary(nextItems).catch((error) => { + setLibraryItems(items); + setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); + }); + setLibraryItems(nextItems); + }, + [library, setAppState], + ); const addToLibrary = useCallback( async (elements: LibraryItem) => { - const items = await Library.loadLibrary(); + const items = await library.loadLibrary(); const nextItems = [...items, elements]; onAddToLibrary(); - Library.saveLibrary(nextItems); + library.saveLibrary(nextItems).catch((error) => { + setLibraryItems(items); + setAppState({ errorMessage: t("alerts.errorAddingToLibrary") }); + }); setLibraryItems(nextItems); }, - [onAddToLibrary], + [onAddToLibrary, library, setAppState], ); return loadingState === "preloading" ? null : ( @@ -320,7 +339,7 @@ const LibraryMenu = ({
) : ( )} @@ -355,6 +376,8 @@ const LayerUI = ({ libraryReturnUrl, UIOptions, focusContainer, + library, + id, }: LayerUIProps) => { const isMobile = useIsMobile(); @@ -526,6 +549,8 @@ const LayerUI = ({ setAppState={setAppState} libraryReturnUrl={libraryReturnUrl} focusContainer={focusContainer} + library={library} + id={id} /> ) : null; diff --git a/src/data/json.ts b/src/data/json.ts index fd9f1a99..6ca7c75b 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -5,12 +5,13 @@ import { clearElementsForExport } from "../element"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; import { loadFromBlob } from "./blob"; -import { Library } from "./library"; + import { ExportedDataState, ImportedDataState, ExportedLibraryData, } from "./types"; +import Library from "./library"; export const serializeAsJSON = ( elements: readonly ExcalidrawElement[], @@ -88,13 +89,13 @@ export const isValidLibrary = (json: any) => { ); }; -export const saveLibraryAsJSON = async () => { - const library = await Library.loadLibrary(); +export const saveLibraryAsJSON = async (library: Library) => { + const libraryItems = await library.loadLibrary(); const data: ExportedLibraryData = { type: EXPORT_DATA_TYPES.excalidrawLibrary, version: 1, source: EXPORT_SOURCE, - library, + library: libraryItems, }; const serialized = JSON.stringify(data, null, 2); const fileName = "library.excalidrawlib"; @@ -108,7 +109,7 @@ export const saveLibraryAsJSON = async () => { }); }; -export const importLibraryFromJSON = async () => { +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 @@ -117,5 +118,5 @@ export const importLibraryFromJSON = async () => { extensions: [".json", ".excalidrawlib"], */ }); - await Library.importLibrary(blob); + await library.importLibrary(blob); }; diff --git a/src/data/library.ts b/src/data/library.ts index 665881c3..337a4d3a 100644 --- a/src/data/library.ts +++ b/src/data/library.ts @@ -1,22 +1,25 @@ import { loadLibraryFromBlob } from "./blob"; import { LibraryItems, LibraryItem } from "../types"; import { restoreElements } from "./restore"; -import { STORAGE_KEYS } from "../constants"; import { getNonDeletedElements } from "../element"; import { NonDeleted, ExcalidrawElement } from "../element/types"; -import { nanoid } from "nanoid"; +import App from "../components/App"; -export class Library { - private static libraryCache: LibraryItems | null = null; - public static csrfToken = nanoid(); +class Library { + private libraryCache: LibraryItems | null = null; + private app: App; - static resetLibrary = () => { - Library.libraryCache = null; - localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); + constructor(app: App) { + this.app = app; + } + + resetLibrary = async () => { + await this.app.props.onLibraryChange?.([]); + this.libraryCache = []; }; /** imports library (currently merges, removing duplicates) */ - static async importLibrary(blob: Blob) { + async importLibrary(blob: Blob) { const libraryFile = await loadLibraryFromBlob(blob); if (!libraryFile || !libraryFile.library) { return; @@ -46,7 +49,7 @@ export class Library { }); }; - const existingLibraryItems = await Library.loadLibrary(); + const existingLibraryItems = await this.loadLibrary(); const filtered = libraryFile.library!.reduce((acc, libraryItem) => { const restored = getNonDeletedElements(restoreElements(libraryItem)); @@ -56,27 +59,27 @@ export class Library { return acc; }, [] as (readonly NonDeleted[])[]); - Library.saveLibrary([...existingLibraryItems, ...filtered]); + await this.saveLibrary([...existingLibraryItems, ...filtered]); } - static loadLibrary = (): Promise => { + loadLibrary = (): Promise => { return new Promise(async (resolve) => { - if (Library.libraryCache) { - return resolve(JSON.parse(JSON.stringify(Library.libraryCache))); + if (this.libraryCache) { + return resolve(JSON.parse(JSON.stringify(this.libraryCache))); } try { - const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); - if (!data) { + const libraryItems = this.app.libraryItemsFromStorage; + if (!libraryItems) { return resolve([]); } - const items = (JSON.parse(data) as LibraryItems).map((elements) => - restoreElements(elements), - ) as Mutable; + const items = libraryItems.map( + (elements) => restoreElements(elements) as LibraryItem, + ); // clone to ensure we don't mutate the cached library elements in the app - Library.libraryCache = JSON.parse(JSON.stringify(items)); + this.libraryCache = JSON.parse(JSON.stringify(items)); resolve(items); } catch (error) { @@ -86,17 +89,19 @@ export class Library { }); }; - static saveLibrary = (items: LibraryItems) => { - const prevLibraryItems = Library.libraryCache; + saveLibrary = async (items: LibraryItems) => { + const prevLibraryItems = this.libraryCache; try { const serializedItems = JSON.stringify(items); - // cache optimistically so that consumers have access to the latest + // cache optimistically so that the app has access to the latest // immediately - Library.libraryCache = JSON.parse(serializedItems); - localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); + this.libraryCache = JSON.parse(serializedItems); + await this.app.props.onLibraryChange?.(items); } catch (error) { - Library.libraryCache = prevLibraryItems; - console.error(error); + this.libraryCache = prevLibraryItems; + throw error; } }; } + +export default Library; diff --git a/src/data/types.ts b/src/data/types.ts index 6e5a17b2..e11045c3 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -17,6 +17,7 @@ export interface ImportedDataState { elements?: readonly ExcalidrawElement[] | null; appState?: Readonly> | null; scrollToContent?: boolean; + libraryItems?: LibraryItems; } export interface ExportedLibraryData { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 36915d6c..8887a1f8 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -14,6 +14,7 @@ import { TopErrorBoundary } from "../components/TopErrorBoundary"; import { APP_NAME, EVENT, + STORAGE_KEYS, TITLE_TIMEOUT, URL_HASH_KEYS, VERSION_TIMEOUT, @@ -30,7 +31,7 @@ import Excalidraw, { defaultLang, languages, } from "../packages/excalidraw/index"; -import { AppState } from "../types"; +import { AppState, LibraryItems } from "../types"; import { debounce, getVersion, @@ -195,6 +196,18 @@ const ExcalidrawWrapper = () => { } initializeScene({ collabAPI }).then((scene) => { + if (scene) { + try { + scene.libraryItems = + JSON.parse( + localStorage.getItem( + STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, + ) as string, + ) || []; + } catch (e) { + console.error(e); + } + } initialStatePromiseRef.current.promise.resolve(scene); }); @@ -310,6 +323,14 @@ const ExcalidrawWrapper = () => { ); }; + const onLibraryChange = async (items: LibraryItems) => { + if (!items.length) { + localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); + return; + } + const serializedItems = JSON.stringify(items); + localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); + }; return ( <> { renderCustomStats={renderCustomStats} detectScroll={false} handleKeyboardGlobally={true} + onLibraryChange={onLibraryChange} /> {excalidrawAPI && } {errorMessage && ( diff --git a/src/locales/en.json b/src/locales/en.json index f126bdde..72581b4a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -142,6 +142,8 @@ "loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?", "collabStopOverridePrompt": "Stopping the session will overwrite your previous, locally stored drawing. Are you sure?\n\n(If you want to keep your local drawing, simply close the browser tab instead.)", "errorLoadingLibrary": "There was an error loading the third party library.", + "errorAddingToLibrary": "Couldn't add item to the library", + "errorRemovingFromLibrary": "Couldn't remove item from the library", "confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?", "imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?", "cannotRestoreFromImage": "Scene couldn't be restored from this image file", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 5fbc2003..c82aa4a2 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -17,9 +17,19 @@ Please add the latest change on the top under the correct section. ### Features +- Make library local to given excalidraw instance (previously, all instances on the same page shared one global library) [#3451](https://github.com/excalidraw/excalidraw/pull/3451). + + - Added prop [onLibraryChange](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onLibraryChange) which if supplied will be called when library is updated. + - Added attribute `libraryItems` to prop [initialData](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#initialdata) which can be used to load excalidraw with existing library items. + - Assign a [unique id](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Id) to the excalidraw component. The id can be accessed via [`ref`](https://github.com/excalidraw/excalidraw/blob/master/src/components/App.tsx#L265). + + #### BREAKING CHANGE + + - From now on the host application is responsible for [persisting the library]((https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onLibraryChange) to LocalStorage (or elsewhere), and [importing it on mount]((https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#initialdata). + - Bind the keyboard events to component and added a prop [`handleKeyboardGlobally`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#handleKeyboardGlobally) which if set to true will bind the keyboard events to document [#3430](https://github.com/excalidraw/excalidraw/pull/3430). - #### BREAKING CHNAGE + #### BREAKING CHANGE - Earlier keyboard events were bind to document but now its bind to Excalidraw component by default. So you will need to set [`handleKeyboardGlobally`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#handleKeyboardGlobally) to true if you want the previous behaviour (bind the keyboard events to document). diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 7b100afe..c1d69e3e 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -367,6 +367,7 @@ To view the full example visit :point_down: | [`onPaste`](#onPaste) |
(data: ClipboardData, event: ClipboardEvent | null) => boolean
| | Callback to be triggered if passed when the something is pasted in to the scene | | [`detectScroll`](#detectScroll) | boolean | true | Indicates whether to update the offsets when nearest ancestor is scrolled. | | [`handleKeyboardGlobally`](#handleKeyboardGlobally) | boolean | false | Indicates whether to bind the keyboard events to document. | +| [`onLibraryChange`](#onLibraryChange) |
(items: LibraryItems) => void | Promise<any> 
| | The callback if supplied is triggered when the library is updated and receives the library items. | ### Dimensions of Excalidraw @@ -395,6 +396,7 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. | | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) | The App state with which Excalidraw should be mounted. | | `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained | +| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L151) | This library items with which Excalidraw should be mounted. | ```json { @@ -445,6 +447,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the | 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. | #### `readyPromise` @@ -599,6 +602,20 @@ Indicates whether to bind keyboard events to `document`. Disabled by default, me Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar). +### onLibraryChange + +Ths callback if supplied will get triggered when the library is updated and has the below signature. + +
+(items: LibraryItems) => void | Promise
+
+ +It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage. + +### id + +The unique id of the excalidraw component. This can be used to identify the excalidraw component, for example importing the library items to the excalidraw component from where it was initiated when you have multiple excalidraw components rendered on the same page as shown in [multiple excalidraw demo](https://codesandbox.io/s/multiple-excalidraw-k1xx5). + ### Extra API's #### `getSceneVersion` diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index ffe10635..3a385cdd 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -32,6 +32,7 @@ const Excalidraw = (props: ExcalidrawProps) => { onPaste, detectScroll = true, handleKeyboardGlobally = false, + onLibraryChange, } = props; const canvasActions = props.UIOptions?.canvasActions; @@ -84,6 +85,7 @@ const Excalidraw = (props: ExcalidrawProps) => { onPaste={onPaste} detectScroll={detectScroll} handleKeyboardGlobally={handleKeyboardGlobally} + onLibraryChange={onLibraryChange} /> ); diff --git a/src/tests/library.test.tsx b/src/tests/library.test.tsx index 9a795245..2a1c25d9 100644 --- a/src/tests/library.test.tsx +++ b/src/tests/library.test.tsx @@ -9,17 +9,17 @@ const { h } = window; describe("library", () => { beforeEach(async () => { - h.library.resetLibrary(); await render(); + h.app.library.resetLibrary(); }); it("import library via drag&drop", async () => { - expect(await h.library.loadLibrary()).toEqual([]); + expect(await h.app.library.loadLibrary()).toEqual([]); await API.drop( await API.loadFile("./fixtures/fixture_library.excalidrawlib"), ); await waitFor(async () => { - expect(await h.library.loadLibrary()).toEqual([ + expect(await h.app.library.loadLibrary()).toEqual([ [expect.objectContaining({ id: "A" })], ]); }); diff --git a/src/types.ts b/src/types.ts index 7b95f383..9b3db79c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -197,6 +197,7 @@ export interface ExcalidrawProps { UIOptions?: UIOptions; detectScroll?: boolean; handleKeyboardGlobally?: boolean; + onLibraryChange?: (libraryItems: LibraryItems) => void | Promise; } export type SceneData = {