From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- .../excalidraw/components/LibraryMenuItems.tsx | 342 +++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 packages/excalidraw/components/LibraryMenuItems.tsx (limited to 'packages/excalidraw/components/LibraryMenuItems.tsx') diff --git a/packages/excalidraw/components/LibraryMenuItems.tsx b/packages/excalidraw/components/LibraryMenuItems.tsx new file mode 100644 index 0000000..aa2c3e6 --- /dev/null +++ b/packages/excalidraw/components/LibraryMenuItems.tsx @@ -0,0 +1,342 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { serializeLibraryAsJSON } from "../data/json"; +import { t } from "../i18n"; +import type { + ExcalidrawProps, + LibraryItem, + LibraryItems, + UIAppState, +} from "../types"; +import { arrayToMap } from "../utils"; +import Stack from "./Stack"; +import { MIME_TYPES } from "../constants"; +import Spinner from "./Spinner"; +import { duplicateElements } from "../element/newElement"; +import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; +import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent"; +import { + LibraryMenuSection, + LibraryMenuSectionGrid, +} from "./LibraryMenuSection"; +import { useScrollPosition } from "../hooks/useScrollPosition"; +import { useLibraryCache } from "../hooks/useLibraryItemSvg"; + +import "./LibraryMenuItems.scss"; + +// using an odd number of items per batch so the rendering creates an irregular +// pattern which looks more organic +const ITEMS_RENDERED_PER_BATCH = 17; +// when render outputs cached we can render many more items per batch to +// speed it up +const CACHED_ITEMS_RENDERED_PER_BATCH = 64; + +export default function LibraryMenuItems({ + isLoading, + libraryItems, + onAddToLibrary, + onInsertLibraryItems, + pendingElements, + theme, + id, + libraryReturnUrl, + onSelectItems, + selectedItems, +}: { + isLoading: boolean; + libraryItems: LibraryItems; + pendingElements: LibraryItem["elements"]; + onInsertLibraryItems: (libraryItems: LibraryItems) => void; + onAddToLibrary: (elements: LibraryItem["elements"]) => void; + libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; + theme: UIAppState["theme"]; + id: string; + selectedItems: LibraryItem["id"][]; + onSelectItems: (id: LibraryItem["id"][]) => void; +}) { + const libraryContainerRef = useRef(null); + const scrollPosition = useScrollPosition(libraryContainerRef); + + // This effect has to be called only on first render, therefore `scrollPosition` isn't in the dependency array + useEffect(() => { + if (scrollPosition > 0) { + libraryContainerRef.current?.scrollTo(0, scrollPosition); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const { svgCache } = useLibraryCache(); + const unpublishedItems = useMemo( + () => libraryItems.filter((item) => item.status !== "published"), + [libraryItems], + ); + + const publishedItems = useMemo( + () => libraryItems.filter((item) => item.status === "published"), + [libraryItems], + ); + + const showBtn = !libraryItems.length && !pendingElements.length; + + const isLibraryEmpty = + !pendingElements.length && + !unpublishedItems.length && + !publishedItems.length; + + const [lastSelectedItem, setLastSelectedItem] = useState< + LibraryItem["id"] | null + >(null); + + const onItemSelectToggle = useCallback( + (id: LibraryItem["id"], event: React.MouseEvent) => { + const shouldSelect = !selectedItems.includes(id); + + const orderedItems = [...unpublishedItems, ...publishedItems]; + + if (shouldSelect) { + if (event.shiftKey && lastSelectedItem) { + const rangeStart = orderedItems.findIndex( + (item) => item.id === lastSelectedItem, + ); + const rangeEnd = orderedItems.findIndex((item) => item.id === id); + + if (rangeStart === -1 || rangeEnd === -1) { + onSelectItems([...selectedItems, id]); + return; + } + + const selectedItemsMap = arrayToMap(selectedItems); + const nextSelectedIds = orderedItems.reduce( + (acc: LibraryItem["id"][], item, idx) => { + if ( + (idx >= rangeStart && idx <= rangeEnd) || + selectedItemsMap.has(item.id) + ) { + acc.push(item.id); + } + return acc; + }, + [], + ); + + onSelectItems(nextSelectedIds); + } else { + onSelectItems([...selectedItems, id]); + } + setLastSelectedItem(id); + } else { + setLastSelectedItem(null); + onSelectItems(selectedItems.filter((_id) => _id !== id)); + } + }, + [ + lastSelectedItem, + onSelectItems, + publishedItems, + selectedItems, + unpublishedItems, + ], + ); + + const getInsertedElements = useCallback( + (id: string) => { + let targetElements; + if (selectedItems.includes(id)) { + targetElements = libraryItems.filter((item) => + selectedItems.includes(item.id), + ); + } else { + targetElements = libraryItems.filter((item) => item.id === id); + } + return targetElements.map((item) => { + return { + ...item, + // duplicate each library item before inserting on canvas to confine + // ids and bindings to each library item. See #6465 + elements: duplicateElements(item.elements, { randomizeSeed: true }), + }; + }); + }, + [libraryItems, selectedItems], + ); + + const onItemDrag = useCallback( + (id: LibraryItem["id"], event: React.DragEvent) => { + event.dataTransfer.setData( + MIME_TYPES.excalidrawlib, + serializeLibraryAsJSON(getInsertedElements(id)), + ); + }, + [getInsertedElements], + ); + + const isItemSelected = useCallback( + (id: LibraryItem["id"] | null) => { + if (!id) { + return false; + } + + return selectedItems.includes(id); + }, + [selectedItems], + ); + + const onAddToLibraryClick = useCallback(() => { + onAddToLibrary(pendingElements); + }, [pendingElements, onAddToLibrary]); + + const onItemClick = useCallback( + (id: LibraryItem["id"] | null) => { + if (id) { + onInsertLibraryItems(getInsertedElements(id)); + } + }, + [getInsertedElements, onInsertLibraryItems], + ); + + const itemsRenderedPerBatch = + svgCache.size >= libraryItems.length + ? CACHED_ITEMS_RENDERED_PER_BATCH + : ITEMS_RENDERED_PER_BATCH; + + return ( +
+ {!isLibraryEmpty && ( + + )} + 0 ? 1 : "0 1 auto", + marginBottom: 0, + }} + ref={libraryContainerRef} + > + <> + {!isLibraryEmpty && ( +
+ {t("labels.personalLib")} +
+ )} + {isLoading && ( +
+ +
+ )} + {!pendingElements.length && !unpublishedItems.length ? ( +
+
+ {t("library.noItems")} +
+
+ {publishedItems.length > 0 + ? t("library.hint_emptyPrivateLibrary") + : t("library.hint_emptyLibrary")} +
+
+ ) : ( + + {pendingElements.length > 0 && ( + + )} + + + )} + + + <> + {(publishedItems.length > 0 || + pendingElements.length > 0 || + unpublishedItems.length > 0) && ( +
+ {t("labels.excalidrawLib")} +
+ )} + {publishedItems.length > 0 ? ( + + + + ) : unpublishedItems.length > 0 ? ( +
+ {t("library.noItems")} +
+ ) : null} + + + {showBtn && ( + + + + )} +
+
+ ); +} -- cgit v1.2.3