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

@ -17,6 +17,20 @@ Please add the latest change on the top under the correct section.
#### Features
- Added [`useHandleLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#useHandleLibrary) hook to automatically handle importing of libraries when `#addLibrary` URL hash key is present, and potentially for initializing library as well [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
Also added [`parseLibraryTokensFromUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#parseLibraryTokensFromUrl) to help in manually importing library from URL if desired.
##### BREAKING CHANGE
- Libraries are no longer automatically initialized from URL when `#addLibrary` hash key is present. Host apps now need to handle this themselves with the help of either of the above APIs (`useHandleLibrary` is recommended).
- Added [`updateLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateLibrary) API to update (replace/merge) the library [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
##### BREAKING CHANGE
- `updateScene` API no longer supports passing `libraryItems`. Instead, use the `updateLibrary` API.
- Add support for integrating custom elements [#5164](https://github.com/excalidraw/excalidraw/pull/5164).
- Add [`onPointerDown`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) callback which gets triggered on pointer down events.

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 |
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
| [updateScene](#updateScene) | <pre>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </pre> | updates the scene with the sceneData |
| [addFiles](#addFiles) | <pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre> | add files data to the appState |
| [updateScene](#updateScene) | <code>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </code> | updates the scene with the sceneData |
| [updateLibrary](#updateLibrary) | <code>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/library.ts#L136">opts</a>) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>> </code> | updates the scene with the sceneData |
| [addFiles](#addFiles) | <code>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </code> | add files data to the appState |
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene |
| getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></pre> | Returns all the elements excluding the deleted in the scene |
| getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></pre> | Returns current appState |
| getSceneElementsIncludingDeleted | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements including the deleted in the scene |
| getSceneElements | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements excluding the deleted in the scene |
| getAppState | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></code> | Returns current appState |
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
| scrollToContent | <pre> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> &#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. |
| [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
| [id](#id) | string | Unique ID for the excalidraw component. |
| [getFiles](#getFiles) | <pre>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </pre> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
| [setActiveTool](#setActiveTool) | <pre>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</pre> | This API can be used to set the active tool |
| [getFiles](#getFiles) | <code>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </code> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
#### `readyPromise`
@ -519,6 +520,28 @@ You can use this function to update the scene with the sceneData. It accepts the
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) &#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`
<pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre>
@ -1067,12 +1090,10 @@ This function loads the scene data from the blob (or file). If you pass `localAp
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
// if you need, you can check what data you're dealing with before
// passing down
if (contents.type === MIME_TYPES.excalidraw) {
excalidrawAPI.updateScene(contents.data);
} else {
excalidrawAPI.updateScene(contents.data);
} else if (contents.type === MIME_TYPES.excalidrawlib) {
excalidrawAPI.updateLibrary(contents.data);
}
```
@ -1151,6 +1172,51 @@ mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
#### `parseLibraryTokensFromUrl`
**How to use**
```js
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw-next";
```
**Signature**
<pre>
parseLibraryTokensFromUrl(): {
libraryUrl: string;
idToken: string | null;
} | null
</pre>
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
#### `useHandleLibrary`
**How to use**
```js
import { useHandleLibrary } from "@excalidraw/excalidraw-next";
export const App = () => {
// ...
useHandleLibrary({ excalidrawAPI });
};
```
**Signature**
<pre>
useHandleLibrary(opts: {
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L432">ExcalidrawAPI</a>,
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>
});
</pre>
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
In the future, we will be adding support for handling library persistence to browser storage (or elsewhere).
### Exported constants
#### `FONT_FAMILY`

View file

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

View file

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