feat: render library into Sidebar
on mobile (#5774)
This commit is contained in:
parent
e9067de173
commit
941b2d7042
@ -1,12 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
||||
import { exportCanvas } from "../data";
|
||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
@ -26,7 +26,7 @@ import { Section } from "./Section";
|
||||
import { HelpDialog } from "./HelpDialog";
|
||||
import Stack from "./Stack";
|
||||
import { UserList } from "./UserList";
|
||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||
import Library from "../data/library";
|
||||
import { JSONExportDialog } from "./JSONExportDialog";
|
||||
import { LibraryButton } from "./LibraryButton";
|
||||
import { isImageFileHandle } from "../data/blob";
|
||||
@ -40,7 +40,7 @@ import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./Footer";
|
||||
import { hostSidebarCountersAtom, Sidebar } from "./Sidebar/Sidebar";
|
||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
@ -247,42 +247,6 @@ const LayerUI = ({
|
||||
</Section>
|
||||
);
|
||||
|
||||
const closeLibrary = useCallback(() => {
|
||||
const isDialogOpen = !!document.querySelector(".Dialog");
|
||||
|
||||
// Prevent closing if any dialog is open
|
||||
if (isDialogOpen) {
|
||||
return;
|
||||
}
|
||||
setAppState({ openSidebar: null });
|
||||
}, [setAppState]);
|
||||
|
||||
const deselectItems = useCallback(() => {
|
||||
setAppState({
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
});
|
||||
}, [setAppState]);
|
||||
|
||||
const libraryMenu =
|
||||
appState.openSidebar === "library" ? (
|
||||
<LibraryMenu
|
||||
pendingElements={getSelectedElements(elements, appState, true)}
|
||||
onClose={closeLibrary}
|
||||
onInsertLibraryItems={(libraryItems) => {
|
||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||
}}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
focusContainer={focusContainer}
|
||||
library={library}
|
||||
files={files}
|
||||
id={id}
|
||||
appState={appState}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const renderFixedSideContainer = () => {
|
||||
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
||||
appState,
|
||||
@ -381,6 +345,21 @@ const LayerUI = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderSidebars = () => {
|
||||
return appState.openSidebar === "customSidebar" ? (
|
||||
renderCustomSidebar?.() || null
|
||||
) : appState.openSidebar === "library" ? (
|
||||
<LibraryMenu
|
||||
appState={appState}
|
||||
onInsertElements={onInsertElements}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
focusContainer={focusContainer}
|
||||
library={library}
|
||||
id={id}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
||||
|
||||
return (
|
||||
@ -416,7 +395,6 @@ const LayerUI = ({
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
@ -429,7 +407,7 @@ const LayerUI = ({
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderCustomSidebar={renderCustomSidebar}
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
/>
|
||||
)}
|
||||
@ -484,26 +462,7 @@ const LayerUI = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{appState.openSidebar === "customSidebar" ? (
|
||||
renderCustomSidebar?.()
|
||||
) : appState.openSidebar === "library" ? (
|
||||
<Sidebar
|
||||
__isInternal
|
||||
// necessary to remount when switching between internal
|
||||
// and custom (host app) sidebar, so that the `props.onClose`
|
||||
// is colled correctly
|
||||
key="library"
|
||||
onDock={(docked) => {
|
||||
trackEvent(
|
||||
"library",
|
||||
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
|
||||
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{libraryMenu}
|
||||
</Sidebar>
|
||||
) : null}
|
||||
{renderSidebars()}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,10 +1,16 @@
|
||||
@import "open-color/open-color";
|
||||
|
||||
.excalidraw {
|
||||
.layer-ui__library-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layer-ui__library {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
flex: 1 1 auto;
|
||||
|
||||
.layer-ui__library-header {
|
||||
display: flex;
|
||||
@ -23,16 +29,100 @@
|
||||
}
|
||||
|
||||
.layer-ui__sidebar {
|
||||
.layer-ui__library {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.library-menu-items-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.library-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
align-items: center;
|
||||
|
||||
button .library-actions-counter {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: 50%;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 1px;
|
||||
font-size: 0.7rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--remove {
|
||||
background-color: $oc-red-7;
|
||||
&:hover {
|
||||
background-color: $oc-red-8;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-red-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-red-7;
|
||||
}
|
||||
}
|
||||
|
||||
&--export {
|
||||
background-color: $oc-lime-5;
|
||||
|
||||
&:hover {
|
||||
background-color: $oc-lime-7;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $oc-lime-8;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-lime-5;
|
||||
}
|
||||
}
|
||||
|
||||
&--publish {
|
||||
background-color: $oc-cyan-6;
|
||||
&:hover {
|
||||
background-color: $oc-cyan-7;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-cyan-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
label {
|
||||
margin-left: -0.2em;
|
||||
margin-right: 1.1em;
|
||||
color: $oc-white;
|
||||
font-size: 0.86em;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-cyan-6;
|
||||
}
|
||||
}
|
||||
|
||||
&--load {
|
||||
background-color: $oc-blue-6;
|
||||
&:hover {
|
||||
background-color: $oc-blue-7;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-blue-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layer-ui__library-message {
|
||||
padding: 2em 4em;
|
||||
min-width: 200px;
|
||||
|
@ -6,29 +6,31 @@ import {
|
||||
RefObject,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import Library, { libraryItemsAtom } from "../data/library";
|
||||
import Library, {
|
||||
distributeLibraryItemsOnSquareGrid,
|
||||
libraryItemsAtom,
|
||||
} from "../data/library";
|
||||
import { t } from "../i18n";
|
||||
import { randomId } from "../random";
|
||||
import {
|
||||
LibraryItems,
|
||||
LibraryItem,
|
||||
AppState,
|
||||
BinaryFiles,
|
||||
ExcalidrawProps,
|
||||
} from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import PublishLibrary from "./PublishLibrary";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
|
||||
|
||||
import "./LibraryMenu.scss";
|
||||
import LibraryMenuItems from "./LibraryMenuItems";
|
||||
import { EVENT } from "../constants";
|
||||
import { EVENT, VERSIONS } from "../constants";
|
||||
import { KEYS } from "../keys";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useAtom } from "jotai";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import Spinner from "./Spinner";
|
||||
import { useDevice } from "./App";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawElements,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import { Sidebar } from "./Sidebar/Sidebar";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
|
||||
|
||||
const useOnClickOutside = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
@ -58,11 +60,6 @@ const useOnClickOutside = (
|
||||
}, [ref, cb]);
|
||||
};
|
||||
|
||||
const getSelectedItems = (
|
||||
libraryItems: LibraryItems,
|
||||
selectedItems: LibraryItem["id"][],
|
||||
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
||||
|
||||
const LibraryMenuWrapper = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children: React.ReactNode }
|
||||
@ -74,94 +71,34 @@ const LibraryMenuWrapper = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export const LibraryMenu = ({
|
||||
onClose,
|
||||
export const LibraryMenuContent = ({
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
onAddToLibrary,
|
||||
setAppState,
|
||||
files,
|
||||
libraryReturnUrl,
|
||||
focusContainer,
|
||||
library,
|
||||
id,
|
||||
appState,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
}: {
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onClose: () => void;
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: () => void;
|
||||
files: BinaryFiles;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
focusContainer: () => void;
|
||||
library: Library;
|
||||
id: string;
|
||||
appState: AppState;
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const device = useDevice();
|
||||
useOnClickOutside(
|
||||
ref,
|
||||
useCallback(
|
||||
(event) => {
|
||||
// If click on the library icon, do nothing so that LibraryButton
|
||||
// can toggle library menu
|
||||
if ((event.target as Element).closest(".ToolIcon__library")) {
|
||||
return;
|
||||
}
|
||||
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose, appState.isSidebarDocked, device.canDeviceFitSidebar],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
};
|
||||
}, [onClose, appState.isSidebarDocked, device.canDeviceFitSidebar]);
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
||||
useState(false);
|
||||
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
|
||||
url: string;
|
||||
authorName: string;
|
||||
}>(null);
|
||||
const referrer =
|
||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const removeFromLibrary = useCallback(
|
||||
async (libraryItems: LibraryItems) => {
|
||||
const nextItems = libraryItems.filter(
|
||||
(item) => !selectedItems.includes(item.id),
|
||||
);
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||
});
|
||||
setSelectedItems([]);
|
||||
},
|
||||
[library, setAppState, selectedItems, setSelectedItems],
|
||||
);
|
||||
|
||||
const resetLibrary = useCallback(() => {
|
||||
library.resetLibrary();
|
||||
focusContainer();
|
||||
}, [library, focusContainer]);
|
||||
|
||||
const addToLibrary = useCallback(
|
||||
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
@ -187,60 +124,12 @@ export const LibraryMenu = ({
|
||||
[onAddToLibrary, library, setAppState],
|
||||
);
|
||||
|
||||
const renderPublishSuccess = useCallback(() => {
|
||||
return (
|
||||
<Dialog
|
||||
onCloseRequest={() => setPublishLibSuccess(null)}
|
||||
title={t("publishSuccessDialog.title")}
|
||||
className="publish-library-success"
|
||||
small={true}
|
||||
>
|
||||
<p>
|
||||
{t("publishSuccessDialog.content", {
|
||||
authorName: publishLibSuccess!.authorName,
|
||||
})}{" "}
|
||||
<a
|
||||
href={publishLibSuccess?.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("publishSuccessDialog.link")}
|
||||
</a>
|
||||
</p>
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
label={t("buttons.close")}
|
||||
onClick={() => setPublishLibSuccess(null)}
|
||||
data-testid="publish-library-success-close"
|
||||
className="publish-library-success-close"
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||
|
||||
const onPublishLibSuccess = useCallback(
|
||||
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
nextLibItems.forEach((libItem) => {
|
||||
if (selectedItems.includes(libItem.id)) {
|
||||
libItem.status = "published";
|
||||
}
|
||||
});
|
||||
library.setLibrary(nextLibItems);
|
||||
},
|
||||
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
||||
);
|
||||
|
||||
if (
|
||||
libraryItemsData.status === "loading" &&
|
||||
!libraryItemsData.isInitialized
|
||||
) {
|
||||
return (
|
||||
<LibraryMenuWrapper ref={ref}>
|
||||
<LibraryMenuWrapper>
|
||||
<div className="layer-ui__library-message">
|
||||
<Spinner size="2em" />
|
||||
<span>{t("labels.libraryLoadingMessage")}</span>
|
||||
@ -250,51 +139,168 @@ export const LibraryMenu = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<LibraryMenuWrapper ref={ref}>
|
||||
{showPublishLibraryDialog && (
|
||||
<PublishLibrary
|
||||
onClose={() => setShowPublishLibraryDialog(false)}
|
||||
libraryItems={getSelectedItems(
|
||||
libraryItemsData.libraryItems,
|
||||
selectedItems,
|
||||
)}
|
||||
appState={appState}
|
||||
onSuccess={(data) =>
|
||||
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
||||
}
|
||||
onError={(error) => window.alert(error)}
|
||||
updateItemsInStorage={() =>
|
||||
library.setLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onRemove={(id: string) =>
|
||||
setSelectedItems(selectedItems.filter((_id) => _id !== id))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{publishLibSuccess && renderPublishSuccess()}
|
||||
<LibraryMenuWrapper>
|
||||
<LibraryMenuItems
|
||||
isLoading={libraryItemsData.status === "loading"}
|
||||
libraryItems={libraryItemsData.libraryItems}
|
||||
onRemoveFromLibrary={() =>
|
||||
removeFromLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onAddToLibrary={(elements) =>
|
||||
addToLibrary(elements, libraryItemsData.libraryItems)
|
||||
}
|
||||
onInsertLibraryItems={onInsertLibraryItems}
|
||||
pendingElements={pendingElements}
|
||||
setAppState={setAppState}
|
||||
appState={appState}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
library={library}
|
||||
theme={appState.theme}
|
||||
files={files}
|
||||
id={id}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={(ids) => setSelectedItems(ids)}
|
||||
onPublish={() => setShowPublishLibraryDialog(true)}
|
||||
resetLibrary={resetLibrary}
|
||||
onSelectItems={onSelectItems}
|
||||
/>
|
||||
<a
|
||||
className="library-menu-browse-button"
|
||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${
|
||||
appState.theme
|
||||
}&version=${VERSIONS.excalidrawLibrary}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const LibraryMenu: React.FC<{
|
||||
appState: AppState;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
focusContainer: () => void;
|
||||
library: Library;
|
||||
id: string;
|
||||
}> = ({
|
||||
appState,
|
||||
onInsertElements,
|
||||
libraryReturnUrl,
|
||||
focusContainer,
|
||||
library,
|
||||
id,
|
||||
}) => {
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
const device = useDevice();
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const closeLibrary = useCallback(() => {
|
||||
const isDialogOpen = !!document.querySelector(".Dialog");
|
||||
|
||||
// Prevent closing if any dialog is open
|
||||
if (isDialogOpen) {
|
||||
return;
|
||||
}
|
||||
setAppState({ openSidebar: null });
|
||||
}, [setAppState]);
|
||||
|
||||
useOnClickOutside(
|
||||
ref,
|
||||
useCallback(
|
||||
(event) => {
|
||||
// If click on the library icon, do nothing so that LibraryButton
|
||||
// can toggle library menu
|
||||
if ((event.target as Element).closest(".ToolIcon__library")) {
|
||||
return;
|
||||
}
|
||||
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
|
||||
closeLibrary();
|
||||
}
|
||||
},
|
||||
[closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
|
||||
) {
|
||||
closeLibrary();
|
||||
}
|
||||
};
|
||||
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
};
|
||||
}, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]);
|
||||
|
||||
const deselectItems = useCallback(() => {
|
||||
setAppState({
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
});
|
||||
}, [setAppState]);
|
||||
|
||||
const removeFromLibrary = useCallback(
|
||||
async (libraryItems: LibraryItems) => {
|
||||
const nextItems = libraryItems.filter(
|
||||
(item) => !selectedItems.includes(item.id),
|
||||
);
|
||||
library.setLibrary(nextItems).catch(() => {
|
||||
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||
});
|
||||
setSelectedItems([]);
|
||||
},
|
||||
[library, setAppState, selectedItems, setSelectedItems],
|
||||
);
|
||||
|
||||
const resetLibrary = useCallback(() => {
|
||||
library.resetLibrary();
|
||||
focusContainer();
|
||||
}, [library, focusContainer]);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
__isInternal
|
||||
// necessary to remount when switching between internal
|
||||
// and custom (host app) sidebar, so that the `props.onClose`
|
||||
// is colled correctly
|
||||
key="library"
|
||||
className="layer-ui__library-sidebar"
|
||||
onDock={(docked) => {
|
||||
trackEvent(
|
||||
"library",
|
||||
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
|
||||
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Sidebar.Header className="layer-ui__library-header">
|
||||
<LibraryMenuHeader
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={setSelectedItems}
|
||||
library={library}
|
||||
onRemoveFromLibrary={() =>
|
||||
removeFromLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
resetLibrary={resetLibrary}
|
||||
/>
|
||||
</Sidebar.Header>
|
||||
<LibraryMenuContent
|
||||
pendingElements={getSelectedElements(elements, appState, true)}
|
||||
onInsertLibraryItems={(libraryItems) => {
|
||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||
}}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
library={library}
|
||||
id={id}
|
||||
appState={appState}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={setSelectedItems}
|
||||
/>
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
258
src/components/LibraryMenuHeaderContent.tsx
Normal file
258
src/components/LibraryMenuHeaderContent.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { saveLibraryAsJSON } from "../data/json";
|
||||
import Library, { libraryItemsAtom } from "../data/library";
|
||||
import { t } from "../i18n";
|
||||
import { AppState, LibraryItem, LibraryItems } from "../types";
|
||||
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import PublishLibrary from "./PublishLibrary";
|
||||
import { Dialog } from "./Dialog";
|
||||
|
||||
const getSelectedItems = (
|
||||
libraryItems: LibraryItems,
|
||||
selectedItems: LibraryItem["id"][],
|
||||
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
||||
|
||||
export const LibraryMenuHeader: React.FC<{
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
selectedItems: LibraryItem["id"][];
|
||||
library: Library;
|
||||
onRemoveFromLibrary: () => void;
|
||||
resetLibrary: () => void;
|
||||
onSelectItems: (items: LibraryItem["id"][]) => void;
|
||||
appState: AppState;
|
||||
}> = ({
|
||||
setAppState,
|
||||
selectedItems,
|
||||
library,
|
||||
onRemoveFromLibrary,
|
||||
resetLibrary,
|
||||
onSelectItems,
|
||||
appState,
|
||||
}) => {
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const content = selectedItems.length
|
||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||
: t("alerts.resetLibrary");
|
||||
const title = selectedItems.length
|
||||
? t("confirmDialog.removeItemsFromLib")
|
||||
: t("confirmDialog.resetLibrary");
|
||||
return (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
if (selectedItems.length) {
|
||||
onRemoveFromLibrary();
|
||||
} else {
|
||||
resetLibrary();
|
||||
}
|
||||
setShowRemoveLibAlert(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowRemoveLibAlert(false);
|
||||
}}
|
||||
title={title}
|
||||
>
|
||||
<p>{content}</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
||||
|
||||
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
||||
|
||||
const itemsSelected = !!selectedItems.length;
|
||||
const items = itemsSelected
|
||||
? libraryItemsData.libraryItems.filter((item) =>
|
||||
selectedItems.includes(item.id),
|
||||
)
|
||||
: libraryItemsData.libraryItems;
|
||||
const resetLabel = itemsSelected
|
||||
? t("buttons.remove")
|
||||
: t("buttons.resetLibrary");
|
||||
|
||||
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
||||
useState(false);
|
||||
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
|
||||
url: string;
|
||||
authorName: string;
|
||||
}>(null);
|
||||
const renderPublishSuccess = useCallback(() => {
|
||||
return (
|
||||
<Dialog
|
||||
onCloseRequest={() => setPublishLibSuccess(null)}
|
||||
title={t("publishSuccessDialog.title")}
|
||||
className="publish-library-success"
|
||||
small={true}
|
||||
>
|
||||
<p>
|
||||
{t("publishSuccessDialog.content", {
|
||||
authorName: publishLibSuccess!.authorName,
|
||||
})}{" "}
|
||||
<a
|
||||
href={publishLibSuccess?.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("publishSuccessDialog.link")}
|
||||
</a>
|
||||
</p>
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
label={t("buttons.close")}
|
||||
onClick={() => setPublishLibSuccess(null)}
|
||||
data-testid="publish-library-success-close"
|
||||
className="publish-library-success-close"
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}, [setPublishLibSuccess, publishLibSuccess]);
|
||||
|
||||
const onPublishLibSuccess = useCallback(
|
||||
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
|
||||
setShowPublishLibraryDialog(false);
|
||||
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
|
||||
const nextLibItems = libraryItems.slice();
|
||||
nextLibItems.forEach((libItem) => {
|
||||
if (selectedItems.includes(libItem.id)) {
|
||||
libItem.status = "published";
|
||||
}
|
||||
});
|
||||
library.setLibrary(nextLibItems);
|
||||
},
|
||||
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
||||
);
|
||||
|
||||
const onLibraryImport = async () => {
|
||||
try {
|
||||
await library.updateLibrary({
|
||||
libraryItems: fileOpen({
|
||||
description: "Excalidraw library files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
*/
|
||||
}),
|
||||
merge: true,
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.name === "AbortError") {
|
||||
console.warn(error);
|
||||
return;
|
||||
}
|
||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||
}
|
||||
};
|
||||
|
||||
const onLibraryExport = async () => {
|
||||
const libraryItems = itemsSelected
|
||||
? items
|
||||
: await library.getLatestLibrary();
|
||||
saveLibraryAsJSON(libraryItems)
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="library-actions">
|
||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||
{showPublishLibraryDialog && (
|
||||
<PublishLibrary
|
||||
onClose={() => setShowPublishLibraryDialog(false)}
|
||||
libraryItems={getSelectedItems(
|
||||
libraryItemsData.libraryItems,
|
||||
selectedItems,
|
||||
)}
|
||||
appState={appState}
|
||||
onSuccess={(data) =>
|
||||
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
||||
}
|
||||
onError={(error) => window.alert(error)}
|
||||
updateItemsInStorage={() =>
|
||||
library.setLibrary(libraryItemsData.libraryItems)
|
||||
}
|
||||
onRemove={(id: string) =>
|
||||
onSelectItems(selectedItems.filter((_id) => _id !== id))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{publishLibSuccess && renderPublishSuccess()}
|
||||
{!itemsSelected && (
|
||||
<ToolButton
|
||||
key="import"
|
||||
type="button"
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
icon={load}
|
||||
onClick={onLibraryImport}
|
||||
className="library-actions--load"
|
||||
/>
|
||||
)}
|
||||
{!!items.length && (
|
||||
<>
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportToFileIcon}
|
||||
onClick={onLibraryExport}
|
||||
className="library-actions--export"
|
||||
>
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
<ToolButton
|
||||
key="reset"
|
||||
type="button"
|
||||
title={resetLabel}
|
||||
aria-label={resetLabel}
|
||||
icon={trash}
|
||||
onClick={() => setShowRemoveLibAlert(true)}
|
||||
className="library-actions--remove"
|
||||
>
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
</>
|
||||
)}
|
||||
{itemsSelected && (
|
||||
<Tooltip label={t("hints.publishLibrary")}>
|
||||
<ToolButton
|
||||
type="button"
|
||||
aria-label={t("buttons.publishLibrary")}
|
||||
label={t("buttons.publishLibrary")}
|
||||
icon={publishIcon}
|
||||
className="library-actions--publish"
|
||||
onClick={() => setShowPublishLibraryDialog(true)}
|
||||
>
|
||||
<label>{t("buttons.publishLibrary")}</label>
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -6,93 +6,6 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.library-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
align-items: center;
|
||||
|
||||
button .library-actions-counter {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: 50%;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 1px;
|
||||
font-size: 0.7rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--remove {
|
||||
background-color: $oc-red-7;
|
||||
&:hover {
|
||||
background-color: $oc-red-8;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-red-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-red-7;
|
||||
}
|
||||
}
|
||||
|
||||
&--export {
|
||||
background-color: $oc-lime-5;
|
||||
|
||||
&:hover {
|
||||
background-color: $oc-lime-7;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $oc-lime-8;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-lime-5;
|
||||
}
|
||||
}
|
||||
|
||||
&--publish {
|
||||
background-color: $oc-cyan-6;
|
||||
&:hover {
|
||||
background-color: $oc-cyan-7;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-cyan-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
label {
|
||||
margin-left: -0.2em;
|
||||
margin-right: 1.1em;
|
||||
color: $oc-white;
|
||||
font-size: 0.86em;
|
||||
}
|
||||
.library-actions-counter {
|
||||
color: $oc-cyan-6;
|
||||
}
|
||||
}
|
||||
|
||||
&--load {
|
||||
background-color: $oc-blue-6;
|
||||
&:hover {
|
||||
background-color: $oc-blue-7;
|
||||
}
|
||||
&:active {
|
||||
background-color: $oc-blue-9;
|
||||
}
|
||||
svg {
|
||||
color: $oc-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
@ -1,223 +1,35 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||
import Library from "../data/library";
|
||||
import React, { useState } from "react";
|
||||
import { serializeLibraryAsJSON } from "../data/json";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFiles,
|
||||
ExcalidrawProps,
|
||||
LibraryItem,
|
||||
LibraryItems,
|
||||
} from "../types";
|
||||
import { arrayToMap, chunk, muteFSAbortError } from "../utils";
|
||||
import { useDevice } from "./App";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
|
||||
import { LibraryItem, LibraryItems } from "../types";
|
||||
import { arrayToMap, chunk } from "../utils";
|
||||
import { LibraryUnit } from "./LibraryUnit";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
import "./LibraryMenuItems.scss";
|
||||
import { MIME_TYPES, VERSIONS } from "../constants";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import Spinner from "./Spinner";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { Sidebar } from "./Sidebar/Sidebar";
|
||||
|
||||
const CELLS_PER_ROW = 4;
|
||||
|
||||
const LibraryMenuItems = ({
|
||||
isLoading,
|
||||
libraryItems,
|
||||
onRemoveFromLibrary,
|
||||
onAddToLibrary,
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
theme,
|
||||
setAppState,
|
||||
appState,
|
||||
libraryReturnUrl,
|
||||
library,
|
||||
files,
|
||||
id,
|
||||
selectedItems,
|
||||
onSelectItems,
|
||||
onPublish,
|
||||
resetLibrary,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
libraryItems: LibraryItems;
|
||||
pendingElements: LibraryItem["elements"];
|
||||
onRemoveFromLibrary: () => void;
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
||||
theme: AppState["theme"];
|
||||
files: BinaryFiles;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
appState: AppState;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
library: Library;
|
||||
id: string;
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
onPublish: () => void;
|
||||
resetLibrary: () => void;
|
||||
}) => {
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const content = selectedItems.length
|
||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||
: t("alerts.resetLibrary");
|
||||
const title = selectedItems.length
|
||||
? t("confirmDialog.removeItemsFromLib")
|
||||
: t("confirmDialog.resetLibrary");
|
||||
return (
|
||||
<ConfirmDialog
|
||||
onConfirm={() => {
|
||||
if (selectedItems.length) {
|
||||
onRemoveFromLibrary();
|
||||
} else {
|
||||
resetLibrary();
|
||||
}
|
||||
setShowRemoveLibAlert(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowRemoveLibAlert(false);
|
||||
}}
|
||||
title={title}
|
||||
>
|
||||
<p>{content}</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
||||
|
||||
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
||||
const device = useDevice();
|
||||
const renderLibraryActions = () => {
|
||||
const itemsSelected = !!selectedItems.length;
|
||||
const items = itemsSelected
|
||||
? libraryItems.filter((item) => selectedItems.includes(item.id))
|
||||
: libraryItems;
|
||||
const resetLabel = itemsSelected
|
||||
? t("buttons.remove")
|
||||
: t("buttons.resetLibrary");
|
||||
return (
|
||||
<div className="library-actions">
|
||||
{!itemsSelected && (
|
||||
<ToolButton
|
||||
key="import"
|
||||
type="button"
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
icon={load}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await library.updateLibrary({
|
||||
libraryItems: fileOpen({
|
||||
description: "Excalidraw library files",
|
||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||
/*
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
*/
|
||||
}),
|
||||
merge: true,
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.name === "AbortError") {
|
||||
console.warn(error);
|
||||
return;
|
||||
}
|
||||
setAppState({ errorMessage: t("errors.importLibraryError") });
|
||||
}
|
||||
}}
|
||||
className="library-actions--load"
|
||||
/>
|
||||
)}
|
||||
{!!items.length && (
|
||||
<>
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportToFileIcon}
|
||||
onClick={async () => {
|
||||
const libraryItems = itemsSelected
|
||||
? items
|
||||
: await library.getLatestLibrary();
|
||||
saveLibraryAsJSON(libraryItems)
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
className="library-actions--export"
|
||||
>
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
<ToolButton
|
||||
key="reset"
|
||||
type="button"
|
||||
title={resetLabel}
|
||||
aria-label={resetLabel}
|
||||
icon={trash}
|
||||
onClick={() => setShowRemoveLibAlert(true)}
|
||||
className="library-actions--remove"
|
||||
>
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
</>
|
||||
)}
|
||||
{itemsSelected && (
|
||||
<Tooltip label={t("hints.publishLibrary")}>
|
||||
<ToolButton
|
||||
type="button"
|
||||
aria-label={t("buttons.publishLibrary")}
|
||||
label={t("buttons.publishLibrary")}
|
||||
icon={publishIcon}
|
||||
className="library-actions--publish"
|
||||
onClick={onPublish}
|
||||
>
|
||||
{!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
|
||||
{selectedItems.length > 0 && (
|
||||
<span className="library-actions-counter">
|
||||
{selectedItems.length}
|
||||
</span>
|
||||
)}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{device.isMobile && (
|
||||
<div className="library-menu-browse-button--mobile">
|
||||
<a
|
||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||
VERSIONS.excalidrawLibrary
|
||||
}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
|
||||
|
||||
const referrer =
|
||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||
|
||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||
LibraryItem["id"] | null
|
||||
>(null);
|
||||
@ -294,7 +106,6 @@ const LibraryMenuItems = ({
|
||||
<Stack.Col key={params.key}>
|
||||
<LibraryUnit
|
||||
elements={params.item?.elements}
|
||||
files={files}
|
||||
isPending={!params.item?.id && !!params.item?.elements}
|
||||
onClick={params.onClick || (() => {})}
|
||||
id={params.item?.id || null}
|
||||
@ -370,8 +181,21 @@ const LibraryMenuItems = ({
|
||||
(item) => item.status === "published",
|
||||
);
|
||||
|
||||
const renderLibraryMenuItems = () => {
|
||||
return (
|
||||
<div
|
||||
className="library-menu-items-container"
|
||||
style={
|
||||
publishedItems.length || unpublishedItems.length
|
||||
? {
|
||||
flex: "1 1 0",
|
||||
overflowY: "auto",
|
||||
}
|
||||
: {
|
||||
marginBottom: "2rem",
|
||||
flex: 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack.Col
|
||||
className="library-menu-items-container__items"
|
||||
align="start"
|
||||
@ -443,8 +267,8 @@ const LibraryMenuItems = ({
|
||||
|
||||
<>
|
||||
{(publishedItems.length > 0 ||
|
||||
(!device.isMobile &&
|
||||
(pendingElements.length > 0 || unpublishedItems.length > 0))) && (
|
||||
pendingElements.length > 0 ||
|
||||
unpublishedItems.length > 0) && (
|
||||
<div className="separator">{t("labels.excalidrawLib")}</div>
|
||||
)}
|
||||
{publishedItems.length > 0 ? (
|
||||
@ -466,45 +290,6 @@ const LibraryMenuItems = ({
|
||||
) : null}
|
||||
</>
|
||||
</Stack.Col>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLibraryFooter = () => {
|
||||
return (
|
||||
<a
|
||||
className="library-menu-browse-button"
|
||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||
VERSIONS.excalidrawLibrary
|
||||
}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="library-menu-items-container"
|
||||
style={
|
||||
device.isMobile
|
||||
? {
|
||||
minHeight: "200px",
|
||||
maxHeight: "70vh",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||
{/* NOTE using SidebarHeader here isn't semantic since this may render
|
||||
outside of a sidebar, but for now it doesn't matter */}
|
||||
<Sidebar.Header className="layer-ui__library-header">
|
||||
{renderLibraryActions()}
|
||||
</Sidebar.Header>
|
||||
{renderLibraryMenuItems()}
|
||||
{!device.isMobile && renderLibraryFooter()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import oc from "open-color";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDevice } from "../components/App";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { BinaryFiles, LibraryItem } from "../types";
|
||||
import { LibraryItem } from "../types";
|
||||
import "./LibraryUnit.scss";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
|
||||
@ -23,7 +23,6 @@ const PLUS_ICON = (
|
||||
export const LibraryUnit = ({
|
||||
id,
|
||||
elements,
|
||||
files,
|
||||
isPending,
|
||||
onClick,
|
||||
selected,
|
||||
@ -32,7 +31,6 @@ export const LibraryUnit = ({
|
||||
}: {
|
||||
id: LibraryItem["id"] | /** for pending item */ null;
|
||||
elements?: LibraryItem["elements"];
|
||||
files: BinaryFiles;
|
||||
isPending?: boolean;
|
||||
onClick: () => void;
|
||||
selected: boolean;
|
||||
@ -56,7 +54,7 @@ export const LibraryUnit = ({
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
},
|
||||
files,
|
||||
null,
|
||||
);
|
||||
node.innerHTML = svg.outerHTML;
|
||||
})();
|
||||
@ -64,7 +62,7 @@ export const LibraryUnit = ({
|
||||
return () => {
|
||||
node.innerHTML = "";
|
||||
};
|
||||
}, [elements, files]);
|
||||
}, [elements]);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useDevice().isMobile;
|
||||
|
@ -28,7 +28,6 @@ type MobileMenuProps = {
|
||||
renderImageExportDialog: () => React.ReactNode;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
libraryMenu: JSX.Element | null;
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
@ -44,14 +43,13 @@ type MobileMenuProps = {
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
appState,
|
||||
elements,
|
||||
libraryMenu,
|
||||
actionManager,
|
||||
renderJSONExportDialog,
|
||||
renderImageExportDialog,
|
||||
@ -65,7 +63,7 @@ export const MobileMenu = ({
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
renderCustomSidebar,
|
||||
renderSidebars,
|
||||
device,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
@ -111,7 +109,6 @@ export const MobileMenu = ({
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
</Stack.Row>
|
||||
{libraryMenu && <Island padding={2}>{libraryMenu}</Island>}
|
||||
</Stack.Col>
|
||||
)}
|
||||
</Section>
|
||||
@ -184,7 +181,7 @@ export const MobileMenu = ({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{appState.openSidebar === "customSidebar" && renderCustomSidebar?.()}
|
||||
{renderSidebars()}
|
||||
{!appState.viewModeEnabled && renderToolbar()}
|
||||
{!appState.openMenu && appState.showStats && (
|
||||
<Stats
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import { Island } from ".././Island";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiScope } from "../../jotai";
|
||||
@ -19,7 +25,10 @@ import { updateObject } from "../../utils";
|
||||
* the host app may render (mount/unmount) multiple different sidebar */
|
||||
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 });
|
||||
|
||||
export const Sidebar = ({
|
||||
export const Sidebar = Object.assign(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
onClose,
|
||||
onDock,
|
||||
@ -33,7 +42,9 @@ export const Sidebar = ({
|
||||
// sidebars, if both are open.
|
||||
/** @private internal */
|
||||
__isInternal?: boolean;
|
||||
}>) => {
|
||||
}>,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const [hostSidebarCounters, setHostSidebarCounters] = useAtom(
|
||||
hostSidebarCountersAtom,
|
||||
jotaiScope,
|
||||
@ -45,7 +56,7 @@ export const Sidebar = ({
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (docked === undefined) {
|
||||
// ugly hack to get initial state out of AppState without susbcribing
|
||||
// ugly hack to get initial state out of AppState without subscribing
|
||||
// to it as a whole (once we have granular subscriptions, we'll move
|
||||
// to that)
|
||||
//
|
||||
@ -107,7 +118,11 @@ export const Sidebar = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Island padding={2} className={clsx("layer-ui__sidebar", className)}>
|
||||
<Island
|
||||
padding={2}
|
||||
className={clsx("layer-ui__sidebar", className)}
|
||||
ref={ref}
|
||||
>
|
||||
<SidebarPropsContext.Provider value={headerPropsRef.current}>
|
||||
<SidebarHeaderComponents.Context>
|
||||
<SidebarHeaderComponents.Component __isFallback />
|
||||
@ -116,6 +131,9 @@ export const Sidebar = ({
|
||||
</SidebarPropsContext.Provider>
|
||||
</Island>
|
||||
);
|
||||
};
|
||||
|
||||
Sidebar.Header = SidebarHeaderComponents.Component;
|
||||
},
|
||||
),
|
||||
{
|
||||
Header: SidebarHeaderComponents.Component,
|
||||
},
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user