diff --git a/src/actions/actionAddToLibrary.ts b/src/actions/actionAddToLibrary.ts new file mode 100644 index 00000000..87780fcb --- /dev/null +++ b/src/actions/actionAddToLibrary.ts @@ -0,0 +1,23 @@ +import { register } from "./register"; +import { getSelectedElements } from "../scene"; +import { getNonDeletedElements } from "../element"; +import { deepCopyElement } from "../element/newElement"; +import { loadLibrary, saveLibrary } from "../data/localStorage"; + +export const actionAddToLibrary = register({ + name: "addToLibrary", + perform: (elements, appState) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + ); + + loadLibrary().then((items) => { + saveLibrary([...items, selectedElements.map(deepCopyElement)]); + }); + + return false; + }, + contextMenuOrder: 6, + contextItemLabel: "labels.addToLibrary", +}); diff --git a/src/actions/index.ts b/src/actions/index.ts index 959f0b5d..162c4905 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -49,3 +49,5 @@ export { export { actionGroup, actionUngroup } from "./actionGroup"; export { actionGoToCollaborator } from "./actionNavigate"; + +export { actionAddToLibrary } from "./actionAddToLibrary"; diff --git a/src/actions/types.ts b/src/actions/types.ts index f8e295bb..f2642192 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -62,7 +62,8 @@ export type ActionName = | "toggleShortcuts" | "group" | "ungroup" - | "goToCollaborator"; + | "goToCollaborator" + | "addToLibrary"; export interface Action { name: ActionName; diff --git a/src/appState.ts b/src/appState.ts index ad44c226..04c1eaf9 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -58,6 +58,7 @@ export const getDefaultAppState = (): AppState => { selectedGroupIds: {}, width: window.innerWidth, height: window.innerHeight, + isLibraryOpen: false, }; }; @@ -76,6 +77,7 @@ export const clearAppStateForLocalStorage = (appState: AppState) => { errorMessage, showShortcutsDialog, editingLinearElement, + isLibraryOpen, ...exportedState } = appState; return exportedState; diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index c0fbf3b1..15e1c740 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -85,12 +85,21 @@ export const SelectedShapeActions = ({ ); }; +const LIBRARY_ICON = ( + // fa-th-large + + + +); + export const ShapesSwitcher = ({ elementType, setAppState, + isLibraryOpen, }: { elementType: ExcalidrawElement["type"]; - setAppState: any; + setAppState: (appState: Partial) => void; + isLibraryOpen: boolean; }) => ( <> {SHAPES.map(({ value, icon, key }, index) => { @@ -119,9 +128,21 @@ export const ShapesSwitcher = ({ setCursorForShape(value); setAppState({}); }} - > + /> ); })} + { + setAppState({ isLibraryOpen: !isLibraryOpen }); + }} + /> ); diff --git a/src/components/App.tsx b/src/components/App.tsx index b1edb6b9..cce721cb 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -299,6 +299,9 @@ class App extends React.Component { }); }} onLockToggle={this.toggleLock} + onInsertShape={(elements) => + this.addElementsFromPasteOrLibrary(elements) + } zenModeEnabled={zenModeEnabled} toggleZenMode={this.toggleZenMode} lng={getLanguage().lng} @@ -870,7 +873,7 @@ class App extends React.Component { if (data.error) { alert(data.error); } else if (data.elements) { - this.addElementsFromPaste(data.elements); + this.addElementsFromPasteOrLibrary(data.elements); } else if (data.text) { this.addTextFromPaste(data.text); } @@ -879,8 +882,10 @@ class App extends React.Component { }, ); - private addElementsFromPaste = ( + private addElementsFromPasteOrLibrary = ( clipboardElements: readonly ExcalidrawElement[], + clientX = cursorX, + clientY = cursorY, ) => { const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements); @@ -888,7 +893,7 @@ class App extends React.Component { const elementsCenterY = distance(minY, maxY) / 2; const { x, y } = viewportCoordsToSceneCoords( - { clientX: cursorX, clientY: cursorY }, + { clientX, clientY }, this.state, this.canvas, window.devicePixelRatio, @@ -911,6 +916,7 @@ class App extends React.Component { ]); history.resumeRecording(); this.setState({ + isLibraryOpen: false, selectedElementIds: newElements.reduce((map, element) => { map[element.id] = true; return map; @@ -1355,6 +1361,10 @@ class App extends React.Component { return; } + if (event.code === "Digit9") { + this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); + } + const shape = findShapeByKey(event.key); if (isArrowKey(event.key)) { @@ -3135,6 +3145,18 @@ class App extends React.Component { }; private handleCanvasOnDrop = (event: React.DragEvent) => { + const libraryShapes = event.dataTransfer.getData( + "application/vnd.excalidraw.json", + ); + if (libraryShapes !== "") { + this.addElementsFromPasteOrLibrary( + JSON.parse(libraryShapes), + event.clientX, + event.clientY, + ); + return; + } + const file = event.dataTransfer?.files[0]; if ( file?.type === "application/json" || diff --git a/src/components/LayerUI.scss b/src/components/LayerUI.scss index 3fb1b923..c8c0e7ce 100644 --- a/src/components/LayerUI.scss +++ b/src/components/LayerUI.scss @@ -1,5 +1,22 @@ @import "open-color/open-color"; +.layer-ui__library { + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.layer-ui__library-message { + padding: 10px 20px; + max-width: 200px; +} + +.layer-ui__library-items { + max-height: 50vh; + overflow: auto; +} + .layer-ui__wrapper { .encrypted-icon { position: relative; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 3f173451..26ff35af 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -1,10 +1,20 @@ -import React from "react"; +import React, { + useRef, + useState, + RefObject, + useEffect, + useCallback, +} from "react"; import { showSelectedShapeActions } from "../element"; -import { calculateScrollCenter } from "../scene"; +import { calculateScrollCenter, getSelectedElements } from "../scene"; import { exportCanvas } from "../data"; -import { AppState } from "../types"; -import { NonDeletedExcalidrawElement } from "../element/types"; +import { AppState, LibraryItems } from "../types"; +import { + NonDeletedExcalidrawElement, + ExcalidrawElement, + NonDeleted, +} from "../element/types"; import { ActionManager } from "../actions/manager"; import { Island } from "./Island"; @@ -32,6 +42,8 @@ import { GitHubCorner } from "./GitHubCorner"; import { Tooltip } from "./Tooltip"; import "./LayerUI.scss"; +import { LibraryUnit } from "./LibraryUnit"; +import { loadLibrary, saveLibrary } from "../data/localStorage"; interface LayerUIProps { actionManager: ActionManager; @@ -43,11 +55,182 @@ interface LayerUIProps { onUsernameChange: (username: string) => void; onRoomDestroy: () => void; onLockToggle: () => void; + onInsertShape: (elements: readonly NonDeleted[]) => void; zenModeEnabled: boolean; toggleZenMode: () => void; lng: string; } +function useOnClickOutside( + ref: RefObject, + cb: (event: MouseEvent) => void, +) { + useEffect(() => { + const listener = (event: MouseEvent) => { + if (!ref.current) { + return; + } + + if ( + event.target instanceof Element && + (ref.current.contains(event.target) || + !document.body.contains(event.target)) + ) { + return; + } + + cb(event); + }; + document.addEventListener("pointerdown", listener, false); + + return () => { + document.removeEventListener("pointerdown", listener); + }; + }, [ref, cb]); +} + +const LibraryMenuItems = ({ + library, + onRemoveFromLibrary, + onAddToLibrary, + onInsertShape, + pendingElements, +}: { + library: LibraryItems; + pendingElements: NonDeleted[]; + onClickOutside: (event: MouseEvent) => void; + onRemoveFromLibrary: (index: number) => void; + onInsertShape: (elements: readonly NonDeleted[]) => void; + onAddToLibrary: (elements: NonDeleted[]) => void; +}) => { + const numCells = library.length + (pendingElements.length > 0 ? 1 : 0); + const CELLS_PER_ROW = 3; + const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW)); + const rows = []; + let addedPendingElements = false; + + for (let row = 0; row < numRows; row++) { + const i = CELLS_PER_ROW * row; + const children = []; + for (let j = 0; j < 3; j++) { + const shouldAddPendingElements: boolean = + pendingElements.length > 0 && + !addedPendingElements && + i + j >= library.length; + addedPendingElements = addedPendingElements || shouldAddPendingElements; + + children.push( + + + , + ); + } + rows.push( + + {children} + , + ); + } + + return ( + + {rows} + + ); +}; + +const LibraryMenu = ({ + onClickOutside, + onInsertShape, + pendingElements, + onAddToLibrary, +}: { + pendingElements: NonDeleted[]; + onClickOutside: (event: MouseEvent) => void; + onInsertShape: (elements: readonly NonDeleted[]) => void; + onAddToLibrary: () => void; +}) => { + const ref = useRef(null); + useOnClickOutside(ref, onClickOutside); + + const [libraryItems, setLibraryItems] = useState([]); + + const [loadingState, setIsLoading] = useState< + "preloading" | "loading" | "ready" + >("preloading"); + + const loadingTimerRef = useRef(null); + + useEffect(() => { + Promise.race([ + new Promise((resolve) => { + loadingTimerRef.current = setTimeout(() => { + resolve("loading"); + }, 100); + }), + loadLibrary().then((items) => { + setLibraryItems(items); + setIsLoading("ready"); + }), + ]).then((data) => { + if (data === "loading") { + setIsLoading("loading"); + } + }); + return () => { + clearTimeout(loadingTimerRef.current!); + }; + }, []); + + const removeFromLibrary = useCallback(async (indexToRemove) => { + const items = await loadLibrary(); + const nextItems = items.filter((_, index) => index !== indexToRemove); + saveLibrary(nextItems); + setLibraryItems(nextItems); + }, []); + + const addToLibrary = useCallback( + async (elements: NonDeleted[]) => { + const items = await loadLibrary(); + const nextItems = [...items, elements]; + onAddToLibrary(); + saveLibrary(nextItems); + setLibraryItems(nextItems); + }, + [onAddToLibrary], + ); + + return loadingState === "preloading" ? null : ( + + {loadingState === "loading" ? ( +
+ {t("labels.libraryLoadingMessage")} +
+ ) : ( + + )} +
+ ); +}; + const LayerUI = ({ actionManager, appState, @@ -58,6 +241,7 @@ const LayerUI = ({ onUsernameChange, onRoomDestroy, onLockToggle, + onInsertShape, zenModeEnabled, toggleZenMode, }: LayerUIProps) => { @@ -167,11 +351,33 @@ const LayerUI = ({ ); + const closeLibrary = useCallback( + (event) => { + setAppState({ isLibraryOpen: false }); + }, + [setAppState], + ); + + const deselectItems = useCallback(() => { + setAppState({ + selectedElementIds: {}, + selectedGroupIds: {}, + }); + }, [setAppState]); + const renderFixedSideContainer = () => { const shouldRenderSelectedShapeActions = showSelectedShapeActions( appState, elements, ); + const libraryMenu = appState.isLibraryOpen ? ( + + ) : null; return ( @@ -193,6 +399,7 @@ const LayerUI = ({ @@ -203,6 +410,7 @@ const LayerUI = ({ title={t("toolBar.lock")} /> + {libraryMenu} )} diff --git a/src/components/LibraryUnit.scss b/src/components/LibraryUnit.scss new file mode 100644 index 00000000..cf6aba0e --- /dev/null +++ b/src/components/LibraryUnit.scss @@ -0,0 +1,75 @@ +.library-unit { + align-items: center; + border: 1px solid #ccc; + display: flex; + height: 126px; // match width + justify-content: center; + position: relative; + width: 126px; // exactly match the toolbar width when 3 are lined up + padding +} + +.library-unit__dragger { + display: flex; + height: 100%; + width: 100%; +} + +.library-unit__dragger > svg { + flex-grow: 1; + max-height: 100%; + max-width: 100%; +} + +.library-unit__removeFromLibrary, +.library-unit__removeFromLibrary:hover, +.library-unit__removeFromLibrary:active { + align-items: center; + background: none; + border: none; + display: flex; + justify-content: center; + margin: 0; + padding: 0; + position: absolute; + right: 5px; + top: 5px; +} + +.library-unit__removeFromLibrary > svg { + height: 16px; + width: 16px; +} + +.library-unit__pulse { + transform: scale(1); + animation: library-unit__pulse-animation 1s ease-in infinite; +} + +.library-unit__adder { + position: absolute; + left: 50%; + top: 50%; + width: 20px; + height: 20px; + margin-left: -10px; + margin-top: -10px; + pointer-events: none; +} + +.library-unit__active { + cursor: pointer; +} + +@keyframes library-unit__pulse-animation { + 0% { + transform: scale(0.95); + } + + 50% { + transform: scale(1); + } + + 100% { + transform: scale(0.95); + } +} diff --git a/src/components/LibraryUnit.tsx b/src/components/LibraryUnit.tsx new file mode 100644 index 00000000..6724d9a9 --- /dev/null +++ b/src/components/LibraryUnit.tsx @@ -0,0 +1,93 @@ +import React, { useRef, useEffect, useState } from "react"; +import { exportToSvg } from "../scene/export"; +import { ExcalidrawElement, NonDeleted } from "../element/types"; +import { close } from "../components/icons"; + +import "./LibraryUnit.scss"; +import { t } from "../i18n"; + +// fa-plus +const PLUS_ICON = ( + + + +); + +export const LibraryUnit = ({ + elements, + pendingElements, + onRemoveFromLibrary, + onClick, +}: { + elements?: NonDeleted[]; + pendingElements?: NonDeleted[]; + onRemoveFromLibrary: () => void; + onClick: () => void; +}) => { + const ref = useRef(null); + useEffect(() => { + const elementsToRender = elements || pendingElements; + if (!elementsToRender) { + return; + } + const svg = exportToSvg(elementsToRender, { + exportBackground: false, + viewBackgroundColor: "#fff", + shouldAddWatermark: false, + }); + for (const child of ref.current!.children) { + if (child.tagName !== "svg") { + continue; + } + ref.current!.removeChild(child); + } + ref.current!.appendChild(svg); + + const current = ref.current!; + return () => { + current.removeChild(svg); + }; + }, [elements, pendingElements]); + + const [isHovered, setIsHovered] = useState(false); + + const adder = isHovered && pendingElements && ( +
{PLUS_ICON}
+ ); + + return ( +
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.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[][];