perf: memoize rendering of library (#6622)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Arnost Pleskot 2023-05-31 15:37:13 +02:00 committed by GitHub
parent 82d8d02697
commit 253c5c7866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 377 additions and 306 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useState, useCallback, useMemo, useRef } from "react";
import Library, {
distributeLibraryItemsOnSquareGrid,
libraryItemsAtom,
@ -27,6 +27,8 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import { NonDeletedExcalidrawElement } from "../element/types";
export const isLibraryMenuOpenAtom = atom(false);
@ -42,7 +44,9 @@ export const LibraryMenuContent = ({
libraryReturnUrl,
library,
id,
appState,
theme,
selectedItems,
onSelectItems,
}: {
pendingElements: LibraryItem["elements"];
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
@ -51,33 +55,47 @@ export const LibraryMenuContent = ({
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library;
id: string;
appState: UIAppState;
theme: UIAppState["theme"];
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => {
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const addToLibrary = useCallback(
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
trackEvent("element", "addToLibrary", "ui");
if (elements.some((element) => element.type === "image")) {
return setAppState({
errorMessage: "Support for adding images to the library coming soon!",
const _onAddToLibrary = useCallback(
(elements: LibraryItem["elements"]) => {
const addToLibrary = async (
processedElements: LibraryItem["elements"],
libraryItems: LibraryItems,
) => {
trackEvent("element", "addToLibrary", "ui");
if (processedElements.some((element) => element.type === "image")) {
return setAppState({
errorMessage:
"Support for adding images to the library coming soon!",
});
}
const nextItems: LibraryItems = [
{
status: "unpublished",
elements: processedElements,
id: randomId(),
created: Date.now(),
},
...libraryItems,
];
onAddToLibrary();
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
}
const nextItems: LibraryItems = [
{
status: "unpublished",
elements,
id: randomId(),
created: Date.now(),
},
...libraryItems,
];
onAddToLibrary();
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
};
addToLibrary(elements, libraryItemsData.libraryItems);
},
[onAddToLibrary, library, setAppState],
[onAddToLibrary, library, setAppState, libraryItemsData.libraryItems],
);
const libraryItems = useMemo(
() => libraryItemsData.libraryItems,
[libraryItemsData],
);
if (
@ -103,15 +121,15 @@ export const LibraryMenuContent = ({
<LibraryMenuWrapper>
<LibraryMenuItems
isLoading={libraryItemsData.status === "loading"}
libraryItems={libraryItemsData.libraryItems}
onAddToLibrary={(elements) =>
addToLibrary(elements, libraryItemsData.libraryItems)
}
libraryItems={libraryItems}
onAddToLibrary={_onAddToLibrary}
onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements}
id={id}
libraryReturnUrl={libraryReturnUrl}
theme={appState.theme}
theme={theme}
onSelectItems={onSelectItems}
selectedItems={selectedItems}
/>
{showBtn && (
<LibraryMenuControlButtons
@ -119,13 +137,36 @@ export const LibraryMenuContent = ({
style={{ padding: "16px 12px 0 12px" }}
id={id}
libraryReturnUrl={libraryReturnUrl}
theme={appState.theme}
theme={theme}
/>
)}
</LibraryMenuWrapper>
);
};
const usePendingElementsMemo = (
appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[],
) => {
const create = () => getSelectedElements(elements, appState, true);
const val = useRef(create());
const prevAppState = useRef<UIAppState>(appState);
const prevElements = useRef(elements);
if (
!isShallowEqual(
appState.selectedElementIds,
prevAppState.current.selectedElementIds,
) ||
!isShallowEqual(elements, prevElements.current)
) {
val.current = create();
prevAppState.current = appState;
prevElements.current = elements;
}
return val.current;
};
/**
* This component is meant to be rendered inside <Sidebar.Tab/> inside our
* <DefaultSidebar/> or host apps Sidebar components.
@ -136,9 +177,19 @@ export const LibraryMenu = () => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const memoizedLibrary = useMemo(() => library, [library]);
// BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
const pendingElements = usePendingElementsMemo(appState, elements);
const onAddToLibrary = useCallback(() => {
// deselect canvas elements
const onInsertLibraryItems = useCallback(
(libraryItems: LibraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
},
[onInsertElements],
);
const deselectItems = useCallback(() => {
setAppState({
selectedElementIds: {},
selectedGroupIds: {},
@ -147,16 +198,16 @@ export const LibraryMenu = () => {
return (
<LibraryMenuContent
pendingElements={getSelectedElements(elements, appState, true)}
onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={onAddToLibrary}
pendingElements={pendingElements}
onInsertLibraryItems={onInsertLibraryItems}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={appProps.libraryReturnUrl}
library={library}
library={memoizedLibrary}
id={id}
appState={appState}
theme={appState.theme}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/>
);
};

View File

@ -73,6 +73,12 @@
}
}
&__grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 1rem;
}
.separator {
width: 100%;
display: flex;

View File

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

View File

@ -1,16 +1,10 @@
import React, { useEffect, useMemo, useState } from "react";
import { LibraryUnit } from "./LibraryUnit";
import React, { memo, ReactNode, useEffect, useState } from "react";
import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
import { LibraryItem } from "../types";
import Stack from "./Stack";
import clsx from "clsx";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { SvgCache } from "../hooks/useLibraryItemSvg";
import { useTransition } from "../hooks/useTransition";
const ITEMS_PER_ROW = 4;
const ROWS_RENDERED_PER_BATCH = 6;
const CACHED_ROWS_RENDERED_PER_BATCH = 16;
type LibraryOrPendingItem = (
| LibraryItem
| /* pending library item */ {
@ -26,91 +20,58 @@ interface Props {
onItemDrag: (id: LibraryItem["id"], event: React.DragEvent) => void;
isItemSelected: (id: LibraryItem["id"] | null) => boolean;
svgCache: SvgCache;
itemsRenderedPerBatch: number;
}
function LibraryRow({
items,
onItemSelectToggle,
onItemDrag,
isItemSelected,
onClick,
svgCache,
}: Props) {
return (
<Stack.Row className="library-menu-items-container__row">
{items.map((item) => (
<Stack.Col key={item.id}>
<LibraryUnit
elements={item?.elements}
isPending={!item?.id && !!item?.elements}
onClick={onClick}
id={item?.id || null}
selected={isItemSelected(item.id)}
onToggle={onItemSelectToggle}
onDrag={onItemDrag}
svgCache={svgCache}
/>
</Stack.Col>
))}
</Stack.Row>
);
}
export const LibraryMenuSectionGrid = ({
children,
}: {
children: ReactNode;
}) => {
return <div className="library-menu-items-container__grid">{children}</div>;
};
const EmptyLibraryRow = () => (
<Stack.Row className="library-menu-items-container__row" gap={1}>
{Array.from({ length: ITEMS_PER_ROW }).map((_, index) => (
<Stack.Col key={index}>
<div className={clsx("library-unit", "library-unit--skeleton")} />
</Stack.Col>
))}
</Stack.Row>
export const LibraryMenuSection = memo(
({
items,
onItemSelectToggle,
onItemDrag,
isItemSelected,
onClick,
svgCache,
itemsRenderedPerBatch,
}: Props) => {
const [, startTransition] = useTransition();
const [index, setIndex] = useState(0);
useEffect(() => {
if (index < items.length) {
startTransition(() => {
setIndex(index + itemsRenderedPerBatch);
});
}
}, [index, items.length, startTransition, itemsRenderedPerBatch]);
return (
<>
{items.map((item, i) => {
return i < index ? (
<LibraryUnit
elements={item?.elements}
isPending={!item?.id && !!item?.elements}
onClick={onClick}
svgCache={svgCache}
id={item?.id}
selected={isItemSelected(item.id)}
onToggle={onItemSelectToggle}
onDrag={onItemDrag}
key={item?.id ?? i}
/>
) : (
<EmptyLibraryUnit key={i} />
);
})}
</>
);
},
);
function LibraryMenuSection({
items,
onItemSelectToggle,
onItemDrag,
isItemSelected,
onClick,
svgCache,
}: Props) {
const rows = Math.ceil(items.length / ITEMS_PER_ROW);
const [, startTransition] = useTransition();
const [index, setIndex] = useState(0);
const rowsRenderedPerBatch = useMemo(() => {
return svgCache.size === 0
? ROWS_RENDERED_PER_BATCH
: CACHED_ROWS_RENDERED_PER_BATCH;
}, [svgCache]);
useEffect(() => {
if (index < rows) {
startTransition(() => {
setIndex(index + rowsRenderedPerBatch);
});
}
}, [index, rows, startTransition, rowsRenderedPerBatch]);
return (
<>
{Array.from({ length: rows }).map((_, i) =>
i < index ? (
<LibraryRow
key={i}
items={items.slice(i * ITEMS_PER_ROW, (i + 1) * ITEMS_PER_ROW)}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/>
) : (
<EmptyLibraryRow key={i} />
),
)}
</>
);
}
export default LibraryMenuSection;

View File

@ -30,7 +30,7 @@
var(--color-gray-10)
);
background-size: 200% 200%;
animation: library-unit__skeleton-opacity-animation 0.3s linear;
animation: library-unit__skeleton-opacity-animation 0.2s linear;
}
}

View File

@ -1,5 +1,5 @@
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import { memo, useEffect, useRef, useState } from "react";
import { useDevice } from "../components/App";
import { LibraryItem } from "../types";
import "./LibraryUnit.scss";
@ -7,96 +7,101 @@ import { CheckboxItem } from "./CheckboxItem";
import { PlusIcon } from "./icons";
import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
export const LibraryUnit = ({
id,
elements,
isPending,
onClick,
selected,
onToggle,
onDrag,
svgCache,
}: {
id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"];
isPending?: boolean;
onClick: (id: LibraryItem["id"] | null) => void;
selected: boolean;
onToggle: (id: string, event: React.MouseEvent) => void;
onDrag: (id: string, event: React.DragEvent) => void;
svgCache: SvgCache;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
const svg = useLibraryItemSvg(id, elements, svgCache);
export const LibraryUnit = memo(
({
id,
elements,
isPending,
onClick,
selected,
onToggle,
onDrag,
svgCache,
}: {
id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"];
isPending?: boolean;
onClick: (id: LibraryItem["id"] | null) => void;
selected: boolean;
onToggle: (id: string, event: React.MouseEvent) => void;
onDrag: (id: string, event: React.DragEvent) => void;
svgCache: SvgCache;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
const svg = useLibraryItemSvg(id, elements, svgCache);
useEffect(() => {
const node = ref.current;
useEffect(() => {
const node = ref.current;
if (!node) {
return;
}
if (!node) {
return;
}
if (svg) {
svg.querySelector(".style-fonts")?.remove();
node.innerHTML = svg.outerHTML;
}
if (svg) {
node.innerHTML = svg.outerHTML;
}
return () => {
node.innerHTML = "";
};
}, [elements, svg]);
return () => {
node.innerHTML = "";
};
}, [svg]);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useDevice().isMobile;
const adder = isPending && (
<div className="library-unit__adder">{PlusIcon}</div>
);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useDevice().isMobile;
const adder = isPending && (
<div className="library-unit__adder">{PlusIcon}</div>
);
return (
<div
className={clsx("library-unit", {
"library-unit__active": elements,
"library-unit--hover": elements && isHovered,
"library-unit--selected": selected,
"library-unit--skeleton": !svg,
})}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
return (
<div
className={clsx("library-unit__dragger", {
"library-unit__pulse": !!isPending,
className={clsx("library-unit", {
"library-unit__active": elements,
"library-unit--hover": elements && isHovered,
"library-unit--selected": selected,
"library-unit--skeleton": !svg,
})}
ref={ref}
draggable={!!elements}
onClick={
!!elements || !!isPending
? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick(id);
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={clsx("library-unit__dragger", {
"library-unit__pulse": !!isPending,
})}
ref={ref}
draggable={!!elements}
onClick={
!!elements || !!isPending
? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick(id);
}
}
}
: undefined
}
onDragStart={(event) => {
if (!id) {
event.preventDefault();
return;
: undefined
}
setIsHovered(false);
onDrag(id, event);
}}
/>
{adder}
{id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem
checked={selected}
onChange={(checked, event) => onToggle(id, event)}
className="library-unit__checkbox"
onDragStart={(event) => {
if (!id) {
event.preventDefault();
return;
}
setIsHovered(false);
onDrag(id, event);
}}
/>
)}
</div>
);
};
{adder}
{id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem
checked={selected}
onChange={(checked, event) => onToggle(id, event)}
className="library-unit__checkbox"
/>
)}
</div>
);
},
);
export const EmptyLibraryUnit = () => (
<div className="library-unit library-unit--skeleton" />
);

View File

@ -39,6 +39,7 @@ export const useLibraryItemSvg = (
// When there is no svg in cache export it and save to cache
(async () => {
const exportedSvg = await exportLibraryItemToSvg(elements);
exportedSvg.querySelector(".style-fonts")?.remove();
if (exportedSvg) {
svgCache.set(id, exportedSvg);