mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
perf: memoize rendering of library (#6622)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
82d8d02697
commit
253c5c7866
7 changed files with 377 additions and 306 deletions
|
@ -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;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
onSelectItems(nextSelectedIds);
|
||||
} else {
|
||||
onSelectItems([...selectedItems, id]);
|
||||
}
|
||||
|
||||
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);
|
||||
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 = (id: LibraryItem["id"] | null) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
const isItemSelected = useCallback(
|
||||
(id: LibraryItem["id"] | null) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selectedItems.includes(id);
|
||||
};
|
||||
return selectedItems.includes(id);
|
||||
},
|
||||
[selectedItems],
|
||||
);
|
||||
|
||||
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>
|
||||
)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue