import { useRef, useState, useEffect, useCallback, RefObject, forwardRef, } from "react"; import Library, { libraryItemsAtom } from "../data/library"; import { t } from "../i18n"; import { randomId } from "../random"; import { LibraryItems, LibraryItem, AppState, BinaryFiles, ExcalidrawProps, } from "../types"; import { Dialog } from "./Dialog"; import { Island } from "./Island"; import PublishLibrary from "./PublishLibrary"; import { ToolButton } from "./ToolButton"; import "./LibraryMenu.scss"; import LibraryMenuItems from "./LibraryMenuItems"; import { EVENT } from "../constants"; import { KEYS } from "../keys"; import { trackEvent } from "../analytics"; import { useAtom } from "jotai"; import { jotaiScope } from "../jotai"; import Spinner from "./Spinner"; import { useDevice } from "./App"; 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 getSelectedItems = ( libraryItems: LibraryItems, selectedItems: LibraryItem["id"][], ) => libraryItems.filter((item) => selectedItems.includes(item.id)); const LibraryMenuWrapper = forwardRef< HTMLDivElement, { children: React.ReactNode } >(({ children }, ref) => { return ( {children} ); }); export const LibraryMenu = ({ onClose, onInsertLibraryItems, pendingElements, onAddToLibrary, theme, setAppState, files, libraryReturnUrl, focusContainer, library, id, appState, }: { pendingElements: LibraryItem["elements"]; onClose: () => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void; onAddToLibrary: () => void; theme: AppState["theme"]; files: BinaryFiles; setAppState: React.Component["setState"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; focusContainer: () => void; library: Library; id: string; appState: AppState; }) => { const ref = useRef(null); const device = useDevice(); useOnClickOutside( ref, useCallback( (event) => { // If click on the library icon, do nothing. if ((event.target as Element).closest(".ToolIcon__library")) { return; } if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) { onClose(); } }, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar], ), ); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( event.key === KEYS.ESCAPE && (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) ) { onClose(); } }; document.addEventListener(EVENT.KEYDOWN, handleKeyDown); return () => { document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); }; }, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]); const [selectedItems, setSelectedItems] = useState([]); const [showPublishLibraryDialog, setShowPublishLibraryDialog] = useState(false); const [publishLibSuccess, setPublishLibSuccess] = useState(null); const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); 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]); 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], ); const renderPublishSuccess = useCallback(() => { return ( setPublishLibSuccess(null)} title={t("publishSuccessDialog.title")} className="publish-library-success" small={true} >

{t("publishSuccessDialog.content", { authorName: publishLibSuccess!.authorName, })}{" "} {t("publishSuccessDialog.link")}

setPublishLibSuccess(null)} data-testid="publish-library-success-close" className="publish-library-success-close" />
); }, [setPublishLibSuccess, publishLibSuccess]); const onPublishLibSuccess = useCallback( (data: { url: string; authorName: string }, libraryItems: LibraryItems) => { setShowPublishLibraryDialog(false); setPublishLibSuccess({ url: data.url, authorName: data.authorName }); const nextLibItems = libraryItems.slice(); nextLibItems.forEach((libItem) => { if (selectedItems.includes(libItem.id)) { libItem.status = "published"; } }); library.setLibrary(nextLibItems); }, [setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library], ); if ( libraryItemsData.status === "loading" && !libraryItemsData.isInitialized ) { return (
{t("labels.libraryLoadingMessage")}
); } return ( {showPublishLibraryDialog && ( setShowPublishLibraryDialog(false)} libraryItems={getSelectedItems( libraryItemsData.libraryItems, selectedItems, )} appState={appState} onSuccess={(data) => onPublishLibSuccess(data, libraryItemsData.libraryItems) } onError={(error) => window.alert(error)} updateItemsInStorage={() => library.setLibrary(libraryItemsData.libraryItems) } onRemove={(id: string) => setSelectedItems(selectedItems.filter((_id) => _id !== id)) } /> )} {publishLibSuccess && renderPublishSuccess()} removeFromLibrary(libraryItemsData.libraryItems) } onAddToLibrary={(elements) => addToLibrary(elements, libraryItemsData.libraryItems) } onInsertLibraryItems={onInsertLibraryItems} pendingElements={pendingElements} setAppState={setAppState} appState={appState} libraryReturnUrl={libraryReturnUrl} library={library} theme={theme} files={files} id={id} selectedItems={selectedItems} onSelectItems={(ids) => setSelectedItems(ids)} onPublish={() => setShowPublishLibraryDialog(true)} resetLibrary={resetLibrary} /> ); };