feat: make file handling more robust (#5057)

This commit is contained in:
David Luzar 2022-05-09 15:53:04 +02:00 committed by GitHub
parent 0d70690ec8
commit d2e687ed0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 328 additions and 152 deletions

View File

@ -230,7 +230,10 @@ import {
dataURLToFile, dataURLToFile,
generateIdFromFile, generateIdFromFile,
getDataURL, getDataURL,
getFileFromEvent,
isSupportedImageFile, isSupportedImageFile,
loadSceneOrLibraryFromBlob,
normalizeFile,
loadLibraryFromBlob, loadLibraryFromBlob,
resizeImageFile, resizeImageFile,
SVGStringToFile, SVGStringToFile,
@ -242,7 +245,7 @@ import {
updateImageCache as _updateImageCache, updateImageCache as _updateImageCache,
} from "../element/image"; } from "../element/image";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { fileOpen, nativeFileSystemSupported } from "../data/filesystem"; import { fileOpen, FileSystemHandle } from "../data/filesystem";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getApproxMinLineHeight, getApproxMinLineHeight,
@ -771,25 +774,10 @@ class App extends React.Component<AppProps, AppState> {
} }
const fileHandle = launchParams.files[0]; const fileHandle = launchParams.files[0];
const blob: Blob = await fileHandle.getFile(); const blob: Blob = await fileHandle.getFile();
blob.handle = fileHandle; this.loadFileToCanvas(
loadFromBlob( new File([blob], blob.name || "", { type: blob.type }),
blob, fileHandle,
this.state, );
this.scene.getElementsIncludingDeleted(),
)
.then((scene) => {
this.syncActionResult({
...scene,
appState: {
...(scene.appState || this.state),
isLoading: false,
},
commitToHistory: true,
});
})
.catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
}, },
); );
} }
@ -1651,10 +1639,11 @@ class App extends React.Component<AppProps, AppState> {
try { try {
const webShareTargetCache = await caches.open("web-share-target"); const webShareTargetCache = await caches.open("web-share-target");
const file = await webShareTargetCache.match("shared-file"); const response = await webShareTargetCache.match("shared-file");
if (file) { if (response) {
const blob = await file.blob(); const blob = await response.blob();
this.loadFileToCanvas(blob); const file = new File([blob], blob.name || "", { type: blob.type });
this.loadFileToCanvas(file, null);
await webShareTargetCache.delete("shared-file"); await webShareTargetCache.delete("shared-file");
window.history.replaceState(null, APP_NAME, window.location.pathname); window.history.replaceState(null, APP_NAME, window.location.pathname);
} }
@ -5240,32 +5229,21 @@ class App extends React.Component<AppProps, AppState> {
}; };
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => { private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
try { // must be retrieved first, in the same frame
const file = event.dataTransfer.files.item(0); const { file, fileHandle } = await getFileFromEvent(event);
try {
if (isSupportedImageFile(file)) { if (isSupportedImageFile(file)) {
// first attempt to decode scene from the image if it's embedded // first attempt to decode scene from the image if it's embedded
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
if (file?.type === MIME_TYPES.png || file?.type === MIME_TYPES.svg) { if (file?.type === MIME_TYPES.png || file?.type === MIME_TYPES.svg) {
try { try {
if (nativeFileSystemSupported) {
try {
// This will only work as of Chrome 86,
// but can be safely ignored on older releases.
const item = event.dataTransfer.items[0];
(file as any).handle = await (
item as any
).getAsFileSystemHandle();
} catch (error: any) {
console.warn(error.name, error.message);
}
}
const scene = await loadFromBlob( const scene = await loadFromBlob(
file, file,
this.state, this.state,
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
fileHandle,
); );
this.syncActionResult({ this.syncActionResult({
...scene, ...scene,
@ -5317,52 +5295,54 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const file = event.dataTransfer?.files.item(0); if (file) {
if ( // atetmpt to parse an excalidraw/excalidrawlib file
file?.type === MIME_TYPES.excalidrawlib || await this.loadFileToCanvas(file, fileHandle);
file?.name?.endsWith(".excalidrawlib")
) {
this.setState({ isLibraryOpen: true });
this.library.importLibrary(file).catch((error) => {
console.error(error);
this.setState({
isLoading: false,
errorMessage: t("errors.importLibraryError"),
});
});
// default: assume an Excalidraw file regardless of extension/MimeType
} else if (file) {
this.setState({ isLoading: true });
if (nativeFileSystemSupported) {
try {
// This will only work as of Chrome 86,
// but can be safely ignored on older releases.
const item = event.dataTransfer.items[0];
(file as any).handle = await (item as any).getAsFileSystemHandle();
} catch (error: any) {
console.warn(error.name, error.message);
}
}
await this.loadFileToCanvas(file);
} }
}; };
loadFileToCanvas = (file: Blob) => { loadFileToCanvas = async (
loadFromBlob(file, this.state, this.scene.getElementsIncludingDeleted()) file: File,
.then((scene) => { fileHandle: FileSystemHandle | null,
) => {
file = await normalizeFile(file);
try {
const ret = await loadSceneOrLibraryFromBlob(
file,
this.state,
this.scene.getElementsIncludingDeleted(),
fileHandle,
);
if (ret.type === MIME_TYPES.excalidraw) {
this.setState({ isLoading: true });
this.syncActionResult({ this.syncActionResult({
...scene, ...ret.data,
appState: { appState: {
...(scene.appState || this.state), ...(ret.data.appState || this.state),
isLoading: false, isLoading: false,
}, },
replaceFiles: true, replaceFiles: true,
commitToHistory: true, commitToHistory: true,
}); });
}) } else if (ret.type === MIME_TYPES.excalidrawlib) {
.catch((error) => { this.library
this.setState({ isLoading: false, errorMessage: error.message }); .importLibrary(file)
}); .then(() => {
this.setState({
isLoading: false,
});
})
.catch((error) => {
console.error(error);
this.setState({
isLoading: false,
errorMessage: t("errors.importLibraryError"),
});
});
}
} catch (error: any) {
this.setState({ isLoading: false, errorMessage: error.message });
}
}; };
private handleCanvasContextMenu = ( private handleCanvasContextMenu = (

View File

@ -8,7 +8,7 @@ import { t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types"; import { AppState, DataURL, LibraryItem } from "../types";
import { bytesToHexString } from "../utils"; import { bytesToHexString } from "../utils";
import { FileSystemHandle } from "./filesystem"; import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json"; import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore"; import { restore, restoreLibraryItems } from "./restore";
import { ImportedLibraryData } from "./types"; import { ImportedLibraryData } from "./types";
@ -123,40 +123,72 @@ export const isSupportedImageFile = (
); );
}; };
export const loadSceneOrLibraryFromBlob = async (
blob: Blob | File,
/** @see restore.localAppState */
localAppState: AppState | null,
localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null,
) => {
const contents = await parseFileContents(blob);
try {
const data = JSON.parse(contents);
if (isValidExcalidrawData(data)) {
return {
type: MIME_TYPES.excalidraw,
data: restore(
{
elements: clearElementsForExport(data.elements || []),
appState: {
theme: localAppState?.theme,
fileHandle: fileHandle || blob.handle || null,
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(
data.elements || [],
localAppState,
null,
)
: {}),
},
files: data.files,
},
localAppState,
localElements,
),
};
} else if (isValidLibrary(data)) {
return {
type: MIME_TYPES.excalidrawlib,
data,
};
}
throw new Error(t("alerts.couldNotLoadInvalidFile"));
} catch (error: any) {
console.error(error.message);
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
};
export const loadFromBlob = async ( export const loadFromBlob = async (
blob: Blob, blob: Blob,
/** @see restore.localAppState */ /** @see restore.localAppState */
localAppState: AppState | null, localAppState: AppState | null,
localElements: readonly ExcalidrawElement[] | null, localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null,
) => { ) => {
const contents = await parseFileContents(blob); const ret = await loadSceneOrLibraryFromBlob(
try { blob,
const data = JSON.parse(contents); localAppState,
if (!isValidExcalidrawData(data)) { localElements,
throw new Error(t("alerts.couldNotLoadInvalidFile")); fileHandle,
} );
const result = restore( if (ret.type !== MIME_TYPES.excalidraw) {
{
elements: clearElementsForExport(data.elements || []),
appState: {
theme: localAppState?.theme,
fileHandle: blob.handle || null,
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null)
: {}),
},
files: data.files,
},
localAppState,
localElements,
);
return result;
} catch (error: any) {
console.error(error.message);
throw new Error(t("alerts.couldNotLoadInvalidFile")); throw new Error(t("alerts.couldNotLoadInvalidFile"));
} }
return ret.data;
}; };
export const loadLibraryFromBlob = async ( export const loadLibraryFromBlob = async (
@ -200,7 +232,7 @@ export const generateIdFromFile = async (file: File): Promise<FileId> => {
try { try {
const hashBuffer = await window.crypto.subtle.digest( const hashBuffer = await window.crypto.subtle.digest(
"SHA-1", "SHA-1",
await file.arrayBuffer(), await blobToArrayBuffer(file),
); );
return bytesToHexString(new Uint8Array(hashBuffer)) as FileId; return bytesToHexString(new Uint8Array(hashBuffer)) as FileId;
} catch (error: any) { } catch (error: any) {
@ -289,3 +321,125 @@ export const SVGStringToFile = (SVGString: string, filename: string = "") => {
type: MIME_TYPES.svg, type: MIME_TYPES.svg,
}) as File & { type: typeof MIME_TYPES.svg }; }) as File & { type: typeof MIME_TYPES.svg };
}; };
export const getFileFromEvent = async (
event: React.DragEvent<HTMLDivElement>,
) => {
const file = event.dataTransfer.files.item(0);
const fileHandle = await getFileHandle(event);
return { file: file ? await normalizeFile(file) : null, fileHandle };
};
export const getFileHandle = async (
event: React.DragEvent<HTMLDivElement>,
): Promise<FileSystemHandle | null> => {
if (nativeFileSystemSupported) {
try {
const item = event.dataTransfer.items[0];
const handle: FileSystemHandle | null =
(await (item as any).getAsFileSystemHandle()) || null;
return handle;
} catch (error: any) {
console.warn(error.name, error.message);
return null;
}
}
return null;
};
/**
* attemps to detect if a buffer is a valid image by checking its leading bytes
*/
const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
let mimeType: ValueOf<Pick<typeof MIME_TYPES, "png" | "jpg" | "gif">> | null =
null;
const first8Bytes = `${[...new Uint8Array(buffer).slice(0, 8)].join(" ")} `;
// uint8 leading bytes
const headerBytes = {
// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
png: "137 80 78 71 13 10 26 10 ",
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
// jpg is a bit wonky. Checking the first three bytes should be enough,
// but may yield false positives. (https://stackoverflow.com/a/23360709/927631)
jpg: "255 216 255 ",
// https://en.wikipedia.org/wiki/GIF#Example_GIF_file
gif: "71 73 70 56 57 97 ",
};
if (first8Bytes === headerBytes.png) {
mimeType = MIME_TYPES.png;
} else if (first8Bytes.startsWith(headerBytes.jpg)) {
mimeType = MIME_TYPES.jpg;
} else if (first8Bytes.startsWith(headerBytes.gif)) {
mimeType = MIME_TYPES.gif;
}
return mimeType;
};
export const createFile = (
blob: File | Blob | ArrayBuffer,
mimeType: ValueOf<typeof MIME_TYPES>,
name: string | undefined,
) => {
return new File([blob], name || "", {
type: mimeType,
});
};
/** attemps to detect correct mimeType if none is set, or if an image
* has an incorrect extension.
* Note: doesn't handle missing .excalidraw/.excalidrawlib extension */
export const normalizeFile = async (file: File) => {
if (!file.type) {
if (file?.name?.endsWith(".excalidrawlib")) {
file = createFile(
await blobToArrayBuffer(file),
MIME_TYPES.excalidrawlib,
file.name,
);
} else if (file?.name?.endsWith(".excalidraw")) {
file = createFile(
await blobToArrayBuffer(file),
MIME_TYPES.excalidraw,
file.name,
);
} else {
const buffer = await blobToArrayBuffer(file);
const mimeType = getActualMimeTypeFromImage(buffer);
if (mimeType) {
file = createFile(buffer, mimeType, file.name);
}
}
// when the file is an image, make sure the extension corresponds to the
// actual mimeType (this is an edge case, but happens sometime)
} else if (isSupportedImageFile(file)) {
const buffer = await blobToArrayBuffer(file);
const mimeType = getActualMimeTypeFromImage(buffer);
if (mimeType && mimeType !== file.type) {
file = createFile(buffer, mimeType, file.name);
}
}
return file;
};
export const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
if ("arrayBuffer" in blob) {
return blob.arrayBuffer();
}
// Safari
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
if (!event.target?.result) {
return reject(new Error("Couldn't convert blob to ArrayBuffer"));
}
resolve(event.target.result as ArrayBuffer);
};
reader.readAsArrayBuffer(blob);
});
};

View File

@ -1,4 +1,5 @@
import { ENCRYPTION_KEY_BITS } from "../constants"; import { ENCRYPTION_KEY_BITS } from "../constants";
import { blobToArrayBuffer } from "./blob";
export const IV_LENGTH_BYTES = 12; export const IV_LENGTH_BYTES = 12;
@ -58,7 +59,7 @@ export const encryptData = async (
: data instanceof Uint8Array : data instanceof Uint8Array
? data ? data
: data instanceof Blob : data instanceof Blob
? await data.arrayBuffer() ? await blobToArrayBuffer(data)
: data; : data;
// We use symmetric encryption. AES-GCM is the recommended algorithm and // We use symmetric encryption. AES-GCM is the recommended algorithm and

View File

@ -3,28 +3,12 @@ import tEXt from "png-chunk-text";
import encodePng from "png-chunks-encode"; import encodePng from "png-chunks-encode";
import { stringToBase64, encode, decode, base64ToString } from "./encode"; import { stringToBase64, encode, decode, base64ToString } from "./encode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { blobToArrayBuffer } from "./blob";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PNG // PNG
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
if ("arrayBuffer" in blob) {
return blob.arrayBuffer();
}
// Safari
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
if (!event.target?.result) {
return reject(new Error("couldn't convert blob to ArrayBuffer"));
}
resolve(event.target.result as ArrayBuffer);
};
reader.readAsArrayBuffer(blob);
});
};
export const getTEXtChunk = async ( export const getTEXtChunk = async (
blob: Blob, blob: Blob,
): Promise<{ keyword: string; text: string } | null> => { ): Promise<{ keyword: string; text: string } | null> => {

View File

@ -9,7 +9,7 @@ import {
import { clearElementsForDatabase, clearElementsForExport } from "../element"; import { clearElementsForDatabase, clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems } from "../types"; import { AppState, BinaryFiles, LibraryItems } from "../types";
import { isImageFileHandle, loadFromBlob } from "./blob"; import { isImageFileHandle, loadFromBlob, normalizeFile } from "./blob";
import { import {
ExportedDataState, ExportedDataState,
@ -93,13 +93,13 @@ export const loadFromJSON = async (
localAppState: AppState, localAppState: AppState,
localElements: readonly ExcalidrawElement[] | null, localElements: readonly ExcalidrawElement[] | null,
) => { ) => {
const blob = await fileOpen({ const file = await fileOpen({
description: "Excalidraw files", description: "Excalidraw files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442 // ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files. // gets resolved. Else, iOS users cannot open `.excalidraw` files.
// extensions: ["json", "excalidraw", "png", "svg"], // extensions: ["json", "excalidraw", "png", "svg"],
}); });
return loadFromBlob(blob, localAppState, localElements); return loadFromBlob(await normalizeFile(file), localAppState, localElements);
}; };
export const isValidExcalidrawData = (data?: { export const isValidExcalidrawData = (data?: {

2
src/global.d.ts vendored
View File

@ -35,6 +35,8 @@ type Mutable<T> = {
-readonly [P in keyof T]: T[P]; -readonly [P in keyof T]: T[P];
}; };
type ValueOf<T> = T[keyof T];
type Merge<M, N> = Omit<M, keyof N> & N; type Merge<M, N> = Omit<M, keyof N> & N;
/** utility type to assert that the second type is a subtype of the first type. /** utility type to assert that the second type is a subtype of the first type.

View File

@ -17,6 +17,7 @@ Please add the latest change on the top under the correct section.
#### Features #### Features
- Exported [`loadSceneOrLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadSceneOrLibraryFromBlob) function [#5057](https://github.com/excalidraw/excalidraw/pull/5057).
- Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135). - Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135).
- Support [`src`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `src` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114). - Support [`src`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `src` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114).
- Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101). - Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101).

View File

@ -383,7 +383,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. | | [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
| [`initialData`](#initialData) | <pre>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState<a> } </pre> | null | The initial data with which app loads. | | [`initialData`](#initialData) | <pre>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState<a> } </pre> | null | The initial data with which app loads. |
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) &#124; [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) &#124; [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) &#124; <pre>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</pre> | | Ref to be passed to Excalidraw | | [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) &#124; [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) &#124; [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) &#124; <pre>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</pre> | | Ref to be passed to Excalidraw |
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked | | [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode | | [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
@ -405,7 +405,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
| [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void &#124; Promise&lt;any&gt; </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. | | [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void &#124; Promise&lt;any&gt; </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load | | [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas | | [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas |
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked | | [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked |
### Dimensions of Excalidraw ### Dimensions of Excalidraw
@ -419,9 +419,9 @@ Every time component updates, this callback if passed will get triggered and has
(excalidrawElements, appState, files) => void; (excalidrawElements, appState, files) => void;
``` ```
1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) in the scene. 1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) in the scene.
2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene. 2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66) of the scene.
3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene. 3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
@ -433,8 +433,8 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. | | `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | The elements with which Excalidraw should be mounted. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | The App state with which Excalidraw should be mounted. | | `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66) | 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 | | `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#L200) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. | | `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)> | This library items with which Excalidraw should be mounted. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. | | `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. |
@ -481,11 +481,11 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
| [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) | <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 | | [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 |
| 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#L78">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene | | 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#L78">ExcalidrawElement[]</a></pre> | Returns all the elements excluding 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#L42">AppState</a></pre> | Returns current appState | | getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></pre> | 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#L78">ExcalidrawElement</a> &#124; <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>[]) => void </pre> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. | | 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. |
| 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. |
@ -549,7 +549,7 @@ This callback is triggered when mouse pointer is updated.
``` ```
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported. 1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene. 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66) of the scene.
3. `canvas`: The `HTMLCanvasElement` of the scene. 3. `canvas`: The `HTMLCanvasElement` of the scene.
#### `langCode` #### `langCode`
@ -568,7 +568,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw-next";
#### `renderTopRightUI` #### `renderTopRightUI`
<pre> <pre>
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX (isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>) => JSX
</pre> </pre>
A function returning JSX to render custom UI in the top right corner of the app. A function returning JSX to render custom UI in the top right corner of the app.
@ -576,7 +576,7 @@ A function returning JSX to render custom UI in the top right corner of the app.
#### `renderFooter` #### `renderFooter`
<pre> <pre>
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX (isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>) => JSX
</pre> </pre>
A function returning JSX to render custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker). A function returning JSX to render custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
@ -753,7 +753,7 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
**_Signature_** **_Signature_**
<pre> <pre>
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17">ImportedDataState["appState"]</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a> restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17">ImportedDataState["appState"]</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>
</pre> </pre>
**_How to use_** **_How to use_**
@ -762,7 +762,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
import { restoreAppState } from "@excalidraw/excalidraw-next"; import { restoreAppState } from "@excalidraw/excalidraw-next";
``` ```
This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) and if any key is missing, it will be set to default value. This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66) and if any key is missing, it will be set to default value.
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable. When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable.
@ -771,7 +771,7 @@ When `localAppState` is supplied, it's used in place of values that are missing
**_Signature_** **_Signature_**
<pre> <pre>
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a> restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>
</pre> </pre>
**_How to use_** **_How to use_**
@ -789,7 +789,7 @@ When `localElements` are supplied, they are used to ensure that existing restore
**_Signature_** **_Signature_**
<pre> <pre>
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a> restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
</pre> </pre>
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`. See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
@ -883,8 +883,8 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
<pre> <pre>
exportToSvg({ exportToSvg({
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>, elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>,
exportPadding?: number, exportPadding?: number,
metadata?: string, metadata?: string,
files?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">BinaryFiles</a> files?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">BinaryFiles</a>
@ -893,8 +893,8 @@ exportToSvg({
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | The elements to exported as svg | | elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | | The elements to exported as svg |
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene | | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
| exportPadding | number | 10 | The padding to be added on canvas | | exportPadding | number | 10 | The padding to be added on canvas |
| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. | | files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. |
@ -945,8 +945,8 @@ Copies the scene data in the specified format (determined by `type`) to clipboar
<pre> <pre>
serializeAsJSON({ serializeAsJSON({
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>, elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a>,
}): string }): string
</pre> </pre>
@ -973,7 +973,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo
<pre> <pre>
import { getSceneVersion } from "@excalidraw/excalidraw-next"; import { getSceneVersion } from "@excalidraw/excalidraw-next";
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>) getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>)
</pre> </pre>
This function returns the current scene version. This function returns the current scene version.
@ -983,7 +983,7 @@ This function returns the current scene version.
**_Signature_** **_Signature_**
<pre> <pre>
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>): boolean isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>): boolean
</pre> </pre>
**How to use** **How to use**
@ -1014,15 +1014,53 @@ This function loads the library from the blob.
```js ```js
import { loadFromBlob } from "@excalidraw/excalidraw-next"; import { loadFromBlob } from "@excalidraw/excalidraw-next";
const scene = await loadFromBlob(file, null, null);
excalidrawAPI.updateScene(scene);
``` ```
**Signature** **Signature**
<pre> <pre>
loadFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a> | null) loadFromBlob(
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a> | null,
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
fileHandle?: FileSystemHandle | null
) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a>>
</pre> </pre>
This function loads the scene data from the blob. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob` This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data.
#### `loadSceneOrLibraryFromBlob`
**How to use**
```js
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);
}
```
**Signature**
<pre>
loadSceneOrLibraryFromBlob(
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a> | null,
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
fileHandle?: FileSystemHandle | null
) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
</pre>
This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain neither valid scene data or library data.
#### `getFreeDrawSvgPath` #### `getFreeDrawSvgPath`

View File

@ -5,6 +5,8 @@ import Sidebar from "./sidebar/Sidebar";
import "./App.scss"; import "./App.scss";
import initialData from "./initialData"; import initialData from "./initialData";
import { fileOpen } from "../../../data/filesystem";
import { loadSceneOrLibraryFromBlob } from "../../utils";
// This is so that we use the bundled excalidraw.development.js file instead // This is so that we use the bundled excalidraw.development.js file instead
// of the actual source code // of the actual source code
@ -104,6 +106,12 @@ 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);
};
const updateScene = () => { const updateScene = () => {
const sceneData = { const sceneData = {
elements: [ elements: [
@ -165,6 +173,7 @@ export default function App() {
<h1> Excalidraw Example</h1> <h1> Excalidraw Example</h1>
<Sidebar> <Sidebar>
<div className="button-wrapper"> <div className="button-wrapper">
<button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
<button className="update-scene" onClick={updateScene}> <button className="update-scene" onClick={updateScene}>
Update Scene Update Scene
</button> </button>

View File

@ -196,6 +196,7 @@ export {
serializeLibraryAsJSON, serializeLibraryAsJSON,
loadLibraryFromBlob, loadLibraryFromBlob,
loadFromBlob, loadFromBlob,
loadSceneOrLibraryFromBlob,
getFreeDrawSvgPath, getFreeDrawSvgPath,
exportToClipboard, exportToClipboard,
mergeLibraryItems, mergeLibraryItems,

View File

@ -16,6 +16,8 @@ import {
copyToClipboard, copyToClipboard,
} from "../clipboard"; } from "../clipboard";
export { MIME_TYPES };
type ExportOpts = { type ExportOpts = {
elements: readonly NonDeleted<ExcalidrawElement>[]; elements: readonly NonDeleted<ExcalidrawElement>[];
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>; appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
@ -192,6 +194,10 @@ export const exportToClipboard = async (
}; };
export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json"; export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
export { loadFromBlob, loadLibraryFromBlob } from "../data/blob"; export {
loadFromBlob,
loadSceneOrLibraryFromBlob,
loadLibraryFromBlob,
} from "../data/blob";
export { getFreeDrawSvgPath } from "../renderer/renderElement"; export { getFreeDrawSvgPath } from "../renderer/renderElement";
export { mergeLibraryItems } from "../data/library"; export { mergeLibraryItems } from "../data/library";