|
@@ -1,4 +1,10 @@
|
|
|
-import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
|
+import React, {
|
|
|
+ useCallback,
|
|
|
+ useEffect,
|
|
|
+ useMemo,
|
|
|
+ useRef,
|
|
|
+ useState,
|
|
|
+} from "react";
|
|
|
import { serializeLibraryAsJSON } from "../data/json";
|
|
|
import { t } from "../i18n";
|
|
|
import {
|
|
@@ -14,12 +20,22 @@ import Spinner from "./Spinner";
|
|
|
import { duplicateElements } from "../element/newElement";
|
|
|
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
|
|
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
|
|
|
-import LibraryMenuSection from "./LibraryMenuSection";
|
|
|
+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,
|
|
@@ -29,6 +45,8 @@ export default function LibraryMenuItems({
|
|
|
theme,
|
|
|
id,
|
|
|
libraryReturnUrl,
|
|
|
+ onSelectItems,
|
|
|
+ selectedItems,
|
|
|
}: {
|
|
|
isLoading: boolean;
|
|
|
libraryItems: LibraryItems;
|
|
@@ -38,8 +56,9 @@ export default function LibraryMenuItems({
|
|
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
|
|
theme: UIAppState["theme"];
|
|
|
id: string;
|
|
|
+ selectedItems: LibraryItem["id"][];
|
|
|
+ onSelectItems: (id: LibraryItem["id"][]) => void;
|
|
|
}) {
|
|
|
- const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
|
|
const libraryContainerRef = useRef<HTMLDivElement>(null);
|
|
|
const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
|
|
|
|
|
@@ -49,13 +68,16 @@ export default function LibraryMenuItems({
|
|
|
libraryContainerRef.current?.scrollTo(0, scrollPosition);
|
|
|
}
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
- const { svgCache } = useLibraryCache();
|
|
|
|
|
|
- const unpublishedItems = libraryItems.filter(
|
|
|
- (item) => item.status !== "published",
|
|
|
+ const { svgCache } = useLibraryCache();
|
|
|
+ const unpublishedItems = useMemo(
|
|
|
+ () => libraryItems.filter((item) => item.status !== "published"),
|
|
|
+ [libraryItems],
|
|
|
);
|
|
|
- const publishedItems = libraryItems.filter(
|
|
|
- (item) => item.status === "published",
|
|
|
+
|
|
|
+ const publishedItems = useMemo(
|
|
|
+ () => libraryItems.filter((item) => item.status === "published"),
|
|
|
+ [libraryItems],
|
|
|
);
|
|
|
|
|
|
const showBtn = !libraryItems.length && !pendingElements.length;
|
|
@@ -69,50 +91,56 @@ export default function LibraryMenuItems({
|
|
|
LibraryItem["id"] | null
|
|
|
>(null);
|
|
|
|
|
|
- const onItemSelectToggle = (
|
|
|
- id: LibraryItem["id"],
|
|
|
- event: React.MouseEvent,
|
|
|
- ) => {
|
|
|
- const shouldSelect = !selectedItems.includes(id);
|
|
|
+ const onItemSelectToggle = useCallback(
|
|
|
+ (id: LibraryItem["id"], event: React.MouseEvent) => {
|
|
|
+ const shouldSelect = !selectedItems.includes(id);
|
|
|
|
|
|
- const orderedItems = [...unpublishedItems, ...publishedItems];
|
|
|
+ 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 (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) {
|
|
|
- setSelectedItems([...selectedItems, id]);
|
|
|
- return;
|
|
|
- }
|
|
|
+ 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;
|
|
|
- },
|
|
|
- [],
|
|
|
- );
|
|
|
+ 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;
|
|
|
+ },
|
|
|
+ [],
|
|
|
+ );
|
|
|
|
|
|
- setSelectedItems(nextSelectedIds);
|
|
|
+ onSelectItems(nextSelectedIds);
|
|
|
+ } else {
|
|
|
+ onSelectItems([...selectedItems, id]);
|
|
|
+ }
|
|
|
+ setLastSelectedItem(id);
|
|
|
} else {
|
|
|
- setSelectedItems([...selectedItems, id]);
|
|
|
+ setLastSelectedItem(null);
|
|
|
+ onSelectItems(selectedItems.filter((_id) => _id !== id));
|
|
|
}
|
|
|
- setLastSelectedItem(id);
|
|
|
- } else {
|
|
|
- setLastSelectedItem(null);
|
|
|
- setSelectedItems(selectedItems.filter((_id) => _id !== id));
|
|
|
- }
|
|
|
- };
|
|
|
+ },
|
|
|
+ [
|
|
|
+ lastSelectedItem,
|
|
|
+ onSelectItems,
|
|
|
+ publishedItems,
|
|
|
+ selectedItems,
|
|
|
+ unpublishedItems,
|
|
|
+ ],
|
|
|
+ );
|
|
|
|
|
|
const getInsertedElements = useCallback(
|
|
|
(id: string) => {
|
|
@@ -136,37 +164,45 @@ export default function LibraryMenuItems({
|
|
|
[libraryItems, selectedItems],
|
|
|
);
|
|
|
|
|
|
- const onItemDrag = (id: LibraryItem["id"], event: React.DragEvent) => {
|
|
|
- event.dataTransfer.setData(
|
|
|
- MIME_TYPES.excalidrawlib,
|
|
|
- serializeLibraryAsJSON(getInsertedElements(id)),
|
|
|
- );
|
|
|
- };
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- const isItemSelected = (id: LibraryItem["id"] | null) => {
|
|
|
- if (!id) {
|
|
|
- return false;
|
|
|
- }
|
|
|
+ return selectedItems.includes(id);
|
|
|
+ },
|
|
|
+ [selectedItems],
|
|
|
+ );
|
|
|
|
|
|
- return selectedItems.includes(id);
|
|
|
- };
|
|
|
+ const onAddToLibraryClick = useCallback(() => {
|
|
|
+ onAddToLibrary(pendingElements);
|
|
|
+ }, [pendingElements, onAddToLibrary]);
|
|
|
|
|
|
const onItemClick = useCallback(
|
|
|
(id: LibraryItem["id"] | null) => {
|
|
|
- if (!id) {
|
|
|
- onAddToLibrary(pendingElements);
|
|
|
- } else {
|
|
|
+ if (id) {
|
|
|
onInsertLibraryItems(getInsertedElements(id));
|
|
|
}
|
|
|
},
|
|
|
- [
|
|
|
- getInsertedElements,
|
|
|
- onAddToLibrary,
|
|
|
- onInsertLibraryItems,
|
|
|
- pendingElements,
|
|
|
- ],
|
|
|
+ [getInsertedElements, onInsertLibraryItems],
|
|
|
);
|
|
|
|
|
|
+ const itemsRenderedPerBatch =
|
|
|
+ svgCache.size >= libraryItems.length
|
|
|
+ ? CACHED_ITEMS_RENDERED_PER_BATCH
|
|
|
+ : ITEMS_RENDERED_PER_BATCH;
|
|
|
+
|
|
|
return (
|
|
|
<div
|
|
|
className="library-menu-items-container"
|
|
@@ -181,7 +217,7 @@ export default function LibraryMenuItems({
|
|
|
{!isLibraryEmpty && (
|
|
|
<LibraryDropdownMenu
|
|
|
selectedItems={selectedItems}
|
|
|
- onSelectItems={setSelectedItems}
|
|
|
+ onSelectItems={onSelectItems}
|
|
|
className="library-menu-dropdown-container--in-heading"
|
|
|
/>
|
|
|
)}
|
|
@@ -225,20 +261,28 @@ export default function LibraryMenuItems({
|
|
|
</div>
|
|
|
</div>
|
|
|
) : (
|
|
|
- <LibraryMenuSection
|
|
|
- items={[
|
|
|
- // append pending library item
|
|
|
- ...(pendingElements.length
|
|
|
- ? [{ id: null, elements: pendingElements }]
|
|
|
- : []),
|
|
|
- ...unpublishedItems,
|
|
|
- ]}
|
|
|
- onItemSelectToggle={onItemSelectToggle}
|
|
|
- onItemDrag={onItemDrag}
|
|
|
- onClick={onItemClick}
|
|
|
- isItemSelected={isItemSelected}
|
|
|
- svgCache={svgCache}
|
|
|
- />
|
|
|
+ <LibraryMenuSectionGrid>
|
|
|
+ {pendingElements.length > 0 && (
|
|
|
+ <LibraryMenuSection
|
|
|
+ itemsRenderedPerBatch={itemsRenderedPerBatch}
|
|
|
+ items={[{ id: null, elements: pendingElements }]}
|
|
|
+ onItemSelectToggle={onItemSelectToggle}
|
|
|
+ onItemDrag={onItemDrag}
|
|
|
+ onClick={onAddToLibraryClick}
|
|
|
+ isItemSelected={isItemSelected}
|
|
|
+ svgCache={svgCache}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <LibraryMenuSection
|
|
|
+ itemsRenderedPerBatch={itemsRenderedPerBatch}
|
|
|
+ items={unpublishedItems}
|
|
|
+ onItemSelectToggle={onItemSelectToggle}
|
|
|
+ onItemDrag={onItemDrag}
|
|
|
+ onClick={onItemClick}
|
|
|
+ isItemSelected={isItemSelected}
|
|
|
+ svgCache={svgCache}
|
|
|
+ />
|
|
|
+ </LibraryMenuSectionGrid>
|
|
|
)}
|
|
|
</>
|
|
|
|
|
@@ -251,14 +295,17 @@ export default function LibraryMenuItems({
|
|
|
</div>
|
|
|
)}
|
|
|
{publishedItems.length > 0 ? (
|
|
|
- <LibraryMenuSection
|
|
|
- items={publishedItems}
|
|
|
- onItemSelectToggle={onItemSelectToggle}
|
|
|
- onItemDrag={onItemDrag}
|
|
|
- onClick={onItemClick}
|
|
|
- isItemSelected={isItemSelected}
|
|
|
- svgCache={svgCache}
|
|
|
- />
|
|
|
+ <LibraryMenuSectionGrid>
|
|
|
+ <LibraryMenuSection
|
|
|
+ itemsRenderedPerBatch={itemsRenderedPerBatch}
|
|
|
+ items={publishedItems}
|
|
|
+ onItemSelectToggle={onItemSelectToggle}
|
|
|
+ onItemDrag={onItemDrag}
|
|
|
+ onClick={onItemClick}
|
|
|
+ isItemSelected={isItemSelected}
|
|
|
+ svgCache={svgCache}
|
|
|
+ />
|
|
|
+ </LibraryMenuSectionGrid>
|
|
|
) : unpublishedItems.length > 0 ? (
|
|
|
<div
|
|
|
style={{
|
|
@@ -285,7 +332,7 @@ export default function LibraryMenuItems({
|
|
|
>
|
|
|
<LibraryDropdownMenu
|
|
|
selectedItems={selectedItems}
|
|
|
- onSelectItems={setSelectedItems}
|
|
|
+ onSelectItems={onSelectItems}
|
|
|
/>
|
|
|
</LibraryMenuControlButtons>
|
|
|
)}
|