import { useRef, useState, useEffect, useCallback, RefObject } from "react"; import Library 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 { arrayToMap } from "../utils"; 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)); export const LibraryMenu = ({ onClose, onInsertShape, pendingElements, onAddToLibrary, theme, setAppState, files, libraryReturnUrl, focusContainer, library, id, appState, }: { pendingElements: LibraryItem["elements"]; onClose: () => void; onInsertShape: (elements: LibraryItem["elements"]) => 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); useOnClickOutside(ref, (event) => { // If click on the library icon, do nothing. if ((event.target as Element).closest(".ToolIcon__library")) { return; } onClose(); }); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === KEYS.ESCAPE) { onClose(); } }; document.addEventListener(EVENT.KEYDOWN, handleKeyDown); return () => { document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); }; }, [onClose]); const [libraryItems, setLibraryItems] = useState([]); const [loadingState, setIsLoading] = useState< "preloading" | "loading" | "ready" >("preloading"); const [selectedItems, setSelectedItems] = useState([]); const [showPublishLibraryDialog, setShowPublishLibraryDialog] = useState(false); const [publishLibSuccess, setPublishLibSuccess] = useState(null); const loadingTimerRef = useRef(null); useEffect(() => { Promise.race([ new Promise((resolve) => { loadingTimerRef.current = window.setTimeout(() => { resolve("loading"); }, 100); }), library.loadLibrary().then((items) => { setLibraryItems(items); setIsLoading("ready"); }), ]).then((data) => { if (data === "loading") { setIsLoading("loading"); } }); return () => { clearTimeout(loadingTimerRef.current!); }; }, [library]); const removeFromLibrary = useCallback(async () => { const items = await library.loadLibrary(); const nextItems = items.filter((item) => !selectedItems.includes(item.id)); library.saveLibrary(nextItems).catch((error) => { setLibraryItems(items); setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") }); }); setSelectedItems([]); setLibraryItems(nextItems); }, [library, setAppState, selectedItems, setSelectedItems]); const resetLibrary = useCallback(() => { library.resetLibrary(); setLibraryItems([]); focusContainer(); }, [library, focusContainer]); const addToLibrary = useCallback( async (elements: LibraryItem["elements"]) => { if (elements.some((element) => element.type === "image")) { return setAppState({ errorMessage: "Support for adding images to the library coming soon!", }); } const items = await library.loadLibrary(); const nextItems: LibraryItems = [ { status: "unpublished", elements, id: randomId(), created: Date.now(), }, ...items, ]; onAddToLibrary(); library.saveLibrary(nextItems).catch((error) => { setLibraryItems(items); setAppState({ errorMessage: t("alerts.errorAddingToLibrary") }); }); setLibraryItems(nextItems); }, [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) => { 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.saveLibrary(nextLibItems); setLibraryItems(nextLibItems); }, [ setShowPublishLibraryDialog, setPublishLibSuccess, libraryItems, selectedItems, library, ], ); const [lastSelectedItem, setLastSelectedItem] = useState< LibraryItem["id"] | null >(null); return loadingState === "preloading" ? null : ( {showPublishLibraryDialog && ( setShowPublishLibraryDialog(false)} libraryItems={getSelectedItems(libraryItems, selectedItems)} appState={appState} onSuccess={onPublishLibSuccess} onError={(error) => window.alert(error)} updateItemsInStorage={() => library.saveLibrary(libraryItems)} onRemove={(id: string) => setSelectedItems(selectedItems.filter((_id) => _id !== id)) } /> )} {publishLibSuccess && renderPublishSuccess()} {loadingState === "loading" ? (
{t("labels.libraryLoadingMessage")}
) : ( { const shouldSelect = !selectedItems.includes(id); if (shouldSelect) { if (event.shiftKey && lastSelectedItem) { const rangeStart = libraryItems.findIndex( (item) => item.id === lastSelectedItem, ); const rangeEnd = libraryItems.findIndex( (item) => item.id === id, ); if (rangeStart === -1 || rangeEnd === -1) { setSelectedItems([...selectedItems, id]); return; } const selectedItemsMap = arrayToMap(selectedItems); const nextSelectedIds = libraryItems.reduce( (acc: LibraryItem["id"][], item, idx) => { if ( (idx >= rangeStart && idx <= rangeEnd) || selectedItemsMap.has(item.id) ) { acc.push(item.id); } return acc; }, [], ); setSelectedItems(nextSelectedIds); } else { setSelectedItems([...selectedItems, id]); } setLastSelectedItem(id); } else { setLastSelectedItem(null); setSelectedItems(selectedItems.filter((_id) => _id !== id)); } }} onPublish={() => setShowPublishLibraryDialog(true)} resetLibrary={resetLibrary} /> )}
); };