|
|
|
@ -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>
|
|
|
|
|
)}
|
|
|
|
|