setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
{
+ setIsHovered(false);
+ event.dataTransfer.setData(
+ "application/vnd.excalidraw.json",
+ JSON.stringify(elements),
+ );
+ }}
+ />
+ {adder}
+ {elements && isHovered && (
+
+ )}
+
+ );
+};
diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx
index d2f5347a..44366fce 100644
--- a/src/components/MobileMenu.tsx
+++ b/src/components/MobileMenu.tsx
@@ -56,6 +56,7 @@ export const MobileMenu = ({
diff --git a/src/components/ToolButton.tsx b/src/components/ToolButton.tsx
index 75742cf0..e0781bbd 100644
--- a/src/components/ToolButton.tsx
+++ b/src/components/ToolButton.tsx
@@ -63,6 +63,11 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
>
{props.icon || props.label}
+ {props.keyBindingLabel && (
+
+ {props.keyBindingLabel}
+
+ )}
{props.showAriaLabel && (
{props["aria-label"]}
diff --git a/src/data/index.ts b/src/data/index.ts
index 946822dd..74a01785 100644
--- a/src/data/index.ts
+++ b/src/data/index.ts
@@ -348,11 +348,12 @@ export const exportCanvas = async (
window.alert(t("alerts.couldNotCopyToClipboard"));
}
} else if (type === "backend") {
- const appState = getDefaultAppState();
- if (exportBackground) {
- appState.viewBackgroundColor = viewBackgroundColor;
- }
- exportToBackend(elements, appState);
+ exportToBackend(elements, {
+ ...appState,
+ viewBackgroundColor: exportBackground
+ ? appState.viewBackgroundColor
+ : getDefaultAppState().viewBackgroundColor,
+ });
}
// clean up the DOM
diff --git a/src/data/localStorage.ts b/src/data/localStorage.ts
index 58a85d6f..d395a0e1 100644
--- a/src/data/localStorage.ts
+++ b/src/data/localStorage.ts
@@ -1,11 +1,54 @@
import { ExcalidrawElement } from "../element/types";
-import { AppState } from "../types";
+import { AppState, LibraryItems } from "../types";
import { clearAppStateForLocalStorage } from "../appState";
import { restore } from "./restore";
const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
const LOCAL_STORAGE_KEY_COLLAB = "excalidraw-collab";
+const LOCAL_STORAGE_KEY_LIBRARY = "excalidraw-library";
+
+let _LATEST_LIBRARY_ITEMS: LibraryItems | null = null;
+export const loadLibrary = (): Promise
=> {
+ return new Promise(async (resolve) => {
+ if (_LATEST_LIBRARY_ITEMS) {
+ return resolve(JSON.parse(JSON.stringify(_LATEST_LIBRARY_ITEMS)));
+ }
+
+ try {
+ const data = localStorage.getItem(LOCAL_STORAGE_KEY_LIBRARY);
+ if (!data) {
+ return resolve([]);
+ }
+
+ const items = (JSON.parse(data) as ExcalidrawElement[][]).map(
+ (elements) => restore(elements, null).elements,
+ ) as Mutable;
+
+ // clone to ensure we don't mutate the cached library elements in the app
+ _LATEST_LIBRARY_ITEMS = JSON.parse(JSON.stringify(items));
+
+ resolve(items);
+ } catch (e) {
+ console.error(e);
+ resolve([]);
+ }
+ });
+};
+
+export const saveLibrary = (items: LibraryItems) => {
+ const prevLibraryItems = _LATEST_LIBRARY_ITEMS;
+ try {
+ const serializedItems = JSON.stringify(items);
+ // cache optimistically so that consumers have access to the latest
+ // immediately
+ _LATEST_LIBRARY_ITEMS = JSON.parse(serializedItems);
+ localStorage.setItem(LOCAL_STORAGE_KEY_LIBRARY, serializedItems);
+ } catch (e) {
+ _LATEST_LIBRARY_ITEMS = prevLibraryItems;
+ console.error(e);
+ }
+};
export const saveUsernameToLocalStorage = (username: string) => {
try {
diff --git a/src/locales/en.json b/src/locales/en.json
index 0b639f10..7c0b4764 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -65,7 +65,10 @@
"group": "Group selection",
"ungroup": "Ungroup selection",
"collaborators": "Collaborators",
- "toggleGridMode": "Toggle grid mode"
+ "toggleGridMode": "Toggle grid mode",
+ "addToLibrary": "Add to library",
+ "removeFromLibrary": "Remove from library",
+ "libraryLoadingMessage": "Loading library..."
},
"buttons": {
"clearReset": "Reset the canvas",
@@ -115,6 +118,7 @@
"arrow": "Arrow",
"line": "Line",
"text": "Text",
+ "library": "Library",
"lock": "Keep selected tool active after drawing"
},
"headings": {
diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap
index f13cf401..c618479a 100644
--- a/src/tests/__snapshots__/regressionTests.test.tsx.snap
+++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap
@@ -27,6 +27,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -427,6 +428,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -636,6 +638,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -762,6 +765,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -1024,6 +1028,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -1188,6 +1193,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -1390,6 +1396,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -1598,6 +1605,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -1907,6 +1915,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -2302,6 +2311,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4090,6 +4100,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4216,6 +4227,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4342,6 +4354,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4468,6 +4481,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4616,6 +4630,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4764,6 +4779,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -4912,6 +4928,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5060,6 +5077,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5186,6 +5204,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5312,6 +5331,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5460,6 +5480,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5586,6 +5607,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -5734,6 +5756,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -6374,6 +6397,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -6583,6 +6607,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -6650,6 +6675,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -6715,6 +6741,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -7537,6 +7564,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -7936,6 +7964,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -8252,6 +8281,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -8489,6 +8519,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -8651,6 +8682,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -9422,6 +9454,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -10094,6 +10127,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -10671,6 +10705,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -11157,6 +11192,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -11599,6 +11635,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -11956,6 +11993,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -12232,6 +12270,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -12431,6 +12470,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -13253,6 +13293,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -13974,6 +14015,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -14598,6 +14640,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -15129,6 +15172,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -15402,6 +15446,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -15613,6 +15658,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -15892,6 +15938,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -15957,6 +16004,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -16083,6 +16131,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -16148,6 +16197,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -16802,6 +16852,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -16869,6 +16920,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -17297,6 +17349,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
@@ -17373,6 +17426,7 @@ Object {
"gridSize": null,
"height": 768,
"isCollaborating": false,
+ "isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
"isRotating": false,
diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx
index fed5c440..de2d03fe 100644
--- a/src/tests/regressionTests.test.tsx
+++ b/src/tests/regressionTests.test.tsx
@@ -884,6 +884,7 @@ describe("regression tests", () => {
"Copy styles",
"Paste styles",
"Delete",
+ "Add to library",
"Send backward",
"Bring forward",
"Send to back",
@@ -892,7 +893,7 @@ describe("regression tests", () => {
];
expect(contextMenu).not.toBeNull();
- expect(contextMenu?.children.length).toBe(8);
+ expect(contextMenu?.children.length).toBe(9);
options?.forEach((opt, i) => {
expect(opt.textContent).toBe(expectedOptions[i]);
});
@@ -926,6 +927,7 @@ describe("regression tests", () => {
"Paste styles",
"Delete",
"Group selection",
+ "Add to library",
"Send backward",
"Bring forward",
"Send to back",
@@ -934,7 +936,7 @@ describe("regression tests", () => {
];
expect(contextMenu).not.toBeNull();
- expect(contextMenu?.children.length).toBe(9);
+ expect(contextMenu?.children.length).toBe(10);
options?.forEach((opt, i) => {
expect(opt.textContent).toBe(expectedOptions[i]);
});
@@ -973,6 +975,7 @@ describe("regression tests", () => {
"Delete",
"Group selection",
"Ungroup selection",
+ "Add to library",
"Send backward",
"Bring forward",
"Send to back",
@@ -981,7 +984,7 @@ describe("regression tests", () => {
];
expect(contextMenu).not.toBeNull();
- expect(contextMenu?.children.length).toBe(10);
+ expect(contextMenu?.children.length).toBe(11);
options?.forEach((opt, i) => {
expect(opt.textContent).toBe(expectedOptions[i]);
});
diff --git a/src/types.ts b/src/types.ts
index 910ee5b4..f6c16a36 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -81,6 +81,8 @@ export type AppState = {
editingGroupId: GroupId | null;
width: number;
height: number;
+
+ isLibraryOpen: boolean;
};
export type PointerCoords = Readonly<{
@@ -103,3 +105,5 @@ export declare class GestureEvent extends UIEvent {
export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSource] & {
_brand: "socketUpdateData";
};
+
+export type LibraryItems = readonly NonDeleted[][];