import { useRef, useState, useEffect, useCallback, RefObject, forwardRef, } from "react"; import Library, { distributeLibraryItemsOnSquareGrid, libraryItemsAtom, } from "../data/library"; import { t } from "../i18n"; import { randomId } from "../random"; import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types"; import "./LibraryMenu.scss"; import LibraryMenuItems from "./LibraryMenuItems"; import { EVENT, VERSIONS } from "../constants"; import { KEYS } from "../keys"; import { trackEvent } from "../analytics"; import { useAtom } from "jotai"; import { jotaiScope } from "../jotai"; import Spinner from "./Spinner"; import { useDevice, useExcalidrawElements, useExcalidrawSetAppState, } from "./App"; import { Sidebar } from "./Sidebar/Sidebar"; import { getSelectedElements } from "../scene"; import { NonDeletedExcalidrawElement } from "../element/types"; import { LibraryMenuHeader } from "./LibraryMenuHeaderContent"; const 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 LibraryMenuWrapper = forwardRef< HTMLDivElement, { children: React.ReactNode } >(({ children }, ref) => { return (
{children}
); }); export const LibraryMenuContent = ({ onInsertLibraryItems, pendingElements, onAddToLibrary, setAppState, libraryReturnUrl, library, id, appState, selectedItems, onSelectItems, }: { pendingElements: LibraryItem["elements"]; onInsertLibraryItems: (libraryItems: LibraryItems) => void; onAddToLibrary: () => void; setAppState: React.Component["setState"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; library: Library; id: string; appState: AppState; selectedItems: LibraryItem["id"][]; onSelectItems: (id: LibraryItem["id"][]) => void; }) => { const referrer = libraryReturnUrl || window.location.origin + window.location.pathname; const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); const addToLibrary = useCallback( async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => { trackEvent("element", "addToLibrary", "ui"); if (elements.some((element) => element.type === "image")) { return setAppState({ errorMessage: "Support for adding images to the library coming soon!", }); } const nextItems: LibraryItems = [ { status: "unpublished", elements, id: randomId(), created: Date.now(), }, ...libraryItems, ]; onAddToLibrary(); library.setLibrary(nextItems).catch(() => { setAppState({ errorMessage: t("alerts.errorAddingToLibrary") }); }); }, [onAddToLibrary, library, setAppState], ); if ( libraryItemsData.status === "loading" && !libraryItemsData.isInitialized ) { return (
{t("labels.libraryLoadingMessage")}
); } return ( addToLibrary(elements, libraryItemsData.libraryItems) } onInsertLibraryItems={onInsertLibraryItems} pendingElements={pendingElements} selectedItems={selectedItems} onSelectItems={onSelectItems} /> {t("labels.libraries")} ); }; export const LibraryMenu: React.FC<{ appState: AppState; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; focusContainer: () => void; library: Library; id: string; }> = ({ appState, onInsertElements, libraryReturnUrl, focusContainer, library, id, }) => { const setAppState = useExcalidrawSetAppState(); const elements = useExcalidrawElements(); const device = useDevice(); const [selectedItems, setSelectedItems] = useState([]); const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); const ref = useRef(null); const closeLibrary = useCallback(() => { const isDialogOpen = !!document.querySelector(".Dialog"); // Prevent closing if any dialog is open if (isDialogOpen) { return; } setAppState({ openSidebar: null }); }, [setAppState]); useOnClickOutside( ref, useCallback( (event) => { // If click on the library icon, do nothing so that LibraryButton // can toggle library menu if ((event.target as Element).closest(".ToolIcon__library")) { return; } if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) { closeLibrary(); } }, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar], ), ); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( event.key === KEYS.ESCAPE && (!appState.isSidebarDocked || !device.canDeviceFitSidebar) ) { closeLibrary(); } }; document.addEventListener(EVENT.KEYDOWN, handleKeyDown); return () => { document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); }; }, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]); const deselectItems = useCallback(() => { setAppState({ selectedElementIds: {}, selectedGroupIds: {}, }); }, [setAppState]); const removeFromLibrary = useCallback( async (libraryItems: LibraryItems) => { const nextItems = libraryItems.filter( (item) => !selectedItems.includes(item.id), ); library.setLibrary(nextItems).catch(() => { setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); }); setSelectedItems([]); }, [library, setAppState, selectedItems, setSelectedItems], ); const resetLibrary = useCallback(() => { library.resetLibrary(); focusContainer(); }, [library, focusContainer]); return ( { trackEvent( "library", `toggleLibraryDock (${docked ? "dock" : "undock"})`, `sidebar (${device.isMobile ? "mobile" : "desktop"})`, ); }} ref={ref} > removeFromLibrary(libraryItemsData.libraryItems) } resetLibrary={resetLibrary} /> { onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems)); }} onAddToLibrary={deselectItems} setAppState={setAppState} libraryReturnUrl={libraryReturnUrl} library={library} id={id} appState={appState} selectedItems={selectedItems} onSelectItems={setSelectedItems} /> ); };