|
@@ -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,34 +71,135 @@ 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 referrer =
|
|
|
+ libraryReturnUrl || window.location.origin + window.location.pathname;
|
|
|
+
|
|
|
+ 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 nextItems: LibraryItems = [
|
|
|
+ {
|
|
|
+ status: "unpublished",
|
|
|
+ elements,
|
|
|
+ id: randomId(),
|
|
|
+ created: Date.now(),
|
|
|
+ },
|
|
|
+ ...libraryItems,
|
|
|
+ ];
|
|
|
+ onAddToLibrary();
|
|
|
+ library.setLibrary(nextItems).catch(() => {
|
|
|
+ setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ [onAddToLibrary, library, setAppState],
|
|
|
+ );
|
|
|
+
|
|
|
+ if (
|
|
|
+ libraryItemsData.status === "loading" &&
|
|
|
+ !libraryItemsData.isInitialized
|
|
|
+ ) {
|
|
|
+ return (
|
|
|
+ <LibraryMenuWrapper>
|
|
|
+ <div className="layer-ui__library-message">
|
|
|
+ <Spinner size="2em" />
|
|
|
+ <span>{t("labels.libraryLoadingMessage")}</span>
|
|
|
+ </div>
|
|
|
+ </LibraryMenuWrapper>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <LibraryMenuWrapper>
|
|
|
+ <LibraryMenuItems
|
|
|
+ isLoading={libraryItemsData.status === "loading"}
|
|
|
+ libraryItems={libraryItemsData.libraryItems}
|
|
|
+ onAddToLibrary={(elements) =>
|
|
|
+ addToLibrary(elements, libraryItemsData.libraryItems)
|
|
|
+ }
|
|
|
+ onInsertLibraryItems={onInsertLibraryItems}
|
|
|
+ pendingElements={pendingElements}
|
|
|
+ selectedItems={selectedItems}
|
|
|
+ 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(
|
|
@@ -112,10 +210,10 @@ export const LibraryMenu = ({
|
|
|
return;
|
|
|
}
|
|
|
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
|
|
|
- onClose();
|
|
|
+ closeLibrary();
|
|
|
}
|
|
|
},
|
|
|
- [onClose, appState.isSidebarDocked, device.canDeviceFitSidebar],
|
|
|
+ [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
|
|
|
),
|
|
|
);
|
|
|
|
|
@@ -125,24 +223,21 @@ export const LibraryMenu = ({
|
|
|
event.key === KEYS.ESCAPE &&
|
|
|
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
|
|
|
) {
|
|
|
- onClose();
|
|
|
+ closeLibrary();
|
|
|
}
|
|
|
};
|
|
|
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
|
return () => {
|
|
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
|
};
|
|
|
- }, [onClose, appState.isSidebarDocked, device.canDeviceFitSidebar]);
|
|
|
+ }, [closeLibrary, 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 [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
|
|
+ const deselectItems = useCallback(() => {
|
|
|
+ setAppState({
|
|
|
+ selectedElementIds: {},
|
|
|
+ selectedGroupIds: {},
|
|
|
+ });
|
|
|
+ }, [setAppState]);
|
|
|
|
|
|
const removeFromLibrary = useCallback(
|
|
|
async (libraryItems: LibraryItems) => {
|
|
@@ -162,139 +257,50 @@ export const LibraryMenu = ({
|
|
|
focusContainer();
|
|
|
}, [library, focusContainer]);
|
|
|
|
|
|
- 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 nextItems: LibraryItems = [
|
|
|
- {
|
|
|
- status: "unpublished",
|
|
|
- elements,
|
|
|
- id: randomId(),
|
|
|
- created: Date.now(),
|
|
|
- },
|
|
|
- ...libraryItems,
|
|
|
- ];
|
|
|
- onAddToLibrary();
|
|
|
- library.setLibrary(nextItems).catch(() => {
|
|
|
- setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
|
|
- });
|
|
|
- },
|
|
|
- [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}>
|
|
|
- <div className="layer-ui__library-message">
|
|
|
- <Spinner size="2em" />
|
|
|
- <span>{t("labels.libraryLoadingMessage")}</span>
|
|
|
- </div>
|
|
|
- </LibraryMenuWrapper>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
- <LibraryMenuWrapper ref={ref}>
|
|
|
- {showPublishLibraryDialog && (
|
|
|
- <PublishLibrary
|
|
|
- onClose={() => setShowPublishLibraryDialog(false)}
|
|
|
- libraryItems={getSelectedItems(
|
|
|
- libraryItemsData.libraryItems,
|
|
|
- selectedItems,
|
|
|
- )}
|
|
|
+ <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}
|
|
|
- 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))
|
|
|
+ setAppState={setAppState}
|
|
|
+ selectedItems={selectedItems}
|
|
|
+ onSelectItems={setSelectedItems}
|
|
|
+ library={library}
|
|
|
+ onRemoveFromLibrary={() =>
|
|
|
+ removeFromLibrary(libraryItemsData.libraryItems)
|
|
|
}
|
|
|
+ resetLibrary={resetLibrary}
|
|
|
/>
|
|
|
- )}
|
|
|
- {publishLibSuccess && renderPublishSuccess()}
|
|
|
- <LibraryMenuItems
|
|
|
- isLoading={libraryItemsData.status === "loading"}
|
|
|
- libraryItems={libraryItemsData.libraryItems}
|
|
|
- onRemoveFromLibrary={() =>
|
|
|
- removeFromLibrary(libraryItemsData.libraryItems)
|
|
|
- }
|
|
|
- onAddToLibrary={(elements) =>
|
|
|
- addToLibrary(elements, libraryItemsData.libraryItems)
|
|
|
- }
|
|
|
- onInsertLibraryItems={onInsertLibraryItems}
|
|
|
- pendingElements={pendingElements}
|
|
|
+ </Sidebar.Header>
|
|
|
+ <LibraryMenuContent
|
|
|
+ pendingElements={getSelectedElements(elements, appState, true)}
|
|
|
+ onInsertLibraryItems={(libraryItems) => {
|
|
|
+ onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
|
|
+ }}
|
|
|
+ onAddToLibrary={deselectItems}
|
|
|
setAppState={setAppState}
|
|
|
- appState={appState}
|
|
|
libraryReturnUrl={libraryReturnUrl}
|
|
|
library={library}
|
|
|
- theme={appState.theme}
|
|
|
- files={files}
|
|
|
id={id}
|
|
|
+ appState={appState}
|
|
|
selectedItems={selectedItems}
|
|
|
- onSelectItems={(ids) => setSelectedItems(ids)}
|
|
|
- onPublish={() => setShowPublishLibraryDialog(true)}
|
|
|
- resetLibrary={resetLibrary}
|
|
|
+ onSelectItems={setSelectedItems}
|
|
|
/>
|
|
|
- </LibraryMenuWrapper>
|
|
|
+ </Sidebar>
|
|
|
);
|
|
|
};
|