2022-04-20 14:40:03 +02:00
|
|
|
import {
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
useEffect,
|
|
|
|
useCallback,
|
|
|
|
RefObject,
|
|
|
|
forwardRef,
|
|
|
|
} from "react";
|
|
|
|
import Library, { libraryItemsAtom } from "../data/library";
|
2021-11-17 23:53:43 +05:30
|
|
|
import { t } from "../i18n";
|
|
|
|
import { randomId } from "../random";
|
|
|
|
import {
|
|
|
|
LibraryItems,
|
|
|
|
LibraryItem,
|
|
|
|
AppState,
|
|
|
|
BinaryFiles,
|
|
|
|
ExcalidrawProps,
|
|
|
|
} from "../types";
|
|
|
|
import { Dialog } from "./Dialog";
|
|
|
|
import { Island } from "./Island";
|
|
|
|
import PublishLibrary from "./PublishLibrary";
|
|
|
|
import { ToolButton } from "./ToolButton";
|
|
|
|
|
|
|
|
import "./LibraryMenu.scss";
|
|
|
|
import LibraryMenuItems from "./LibraryMenuItems";
|
|
|
|
import { EVENT } from "../constants";
|
|
|
|
import { KEYS } from "../keys";
|
2021-11-26 12:46:23 +01:00
|
|
|
import { arrayToMap } from "../utils";
|
2022-03-28 14:46:40 +02:00
|
|
|
import { trackEvent } from "../analytics";
|
2022-04-20 14:40:03 +02:00
|
|
|
import { useAtom } from "jotai";
|
|
|
|
import { jotaiScope } from "../jotai";
|
|
|
|
import Spinner from "./Spinner";
|
2021-11-17 23:53:43 +05:30
|
|
|
|
|
|
|
const useOnClickOutside = (
|
|
|
|
ref: RefObject<HTMLElement>,
|
|
|
|
cb: (event: MouseEvent) => void,
|
|
|
|
) => {
|
|
|
|
useEffect(() => {
|
|
|
|
const listener = (event: MouseEvent) => {
|
|
|
|
if (!ref.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
event.target instanceof Element &&
|
|
|
|
(ref.current.contains(event.target) ||
|
|
|
|
!document.body.contains(event.target))
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
cb(event);
|
|
|
|
};
|
|
|
|
document.addEventListener("pointerdown", listener, false);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener("pointerdown", listener);
|
|
|
|
};
|
|
|
|
}, [ref, cb]);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getSelectedItems = (
|
|
|
|
libraryItems: LibraryItems,
|
|
|
|
selectedItems: LibraryItem["id"][],
|
|
|
|
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
const LibraryMenuWrapper = forwardRef<
|
|
|
|
HTMLDivElement,
|
|
|
|
{ children: React.ReactNode }
|
|
|
|
>(({ children }, ref) => {
|
|
|
|
return (
|
|
|
|
<Island padding={1} ref={ref} className="layer-ui__library">
|
|
|
|
{children}
|
|
|
|
</Island>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2021-11-17 23:53:43 +05:30
|
|
|
export const LibraryMenu = ({
|
|
|
|
onClose,
|
|
|
|
onInsertShape,
|
|
|
|
pendingElements,
|
|
|
|
onAddToLibrary,
|
|
|
|
theme,
|
|
|
|
setAppState,
|
|
|
|
files,
|
|
|
|
libraryReturnUrl,
|
|
|
|
focusContainer,
|
|
|
|
library,
|
|
|
|
id,
|
|
|
|
appState,
|
|
|
|
}: {
|
|
|
|
pendingElements: LibraryItem["elements"];
|
|
|
|
onClose: () => void;
|
|
|
|
onInsertShape: (elements: LibraryItem["elements"]) => void;
|
|
|
|
onAddToLibrary: () => void;
|
|
|
|
theme: AppState["theme"];
|
|
|
|
files: BinaryFiles;
|
|
|
|
setAppState: React.Component<any, AppState>["setState"];
|
|
|
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
|
|
|
focusContainer: () => void;
|
|
|
|
library: Library;
|
|
|
|
id: string;
|
|
|
|
appState: AppState;
|
|
|
|
}) => {
|
|
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
useOnClickOutside(ref, (event) => {
|
|
|
|
// If click on the library icon, do nothing.
|
|
|
|
if ((event.target as Element).closest(".ToolIcon__library")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
onClose();
|
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
|
if (event.key === KEYS.ESCAPE) {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
|
|
|
};
|
|
|
|
}, [onClose]);
|
|
|
|
|
|
|
|
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
|
|
|
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
|
|
|
useState(false);
|
|
|
|
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
|
|
|
|
url: string;
|
|
|
|
authorName: string;
|
|
|
|
}>(null);
|
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
2021-11-17 23:53:43 +05:30
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
const removeFromLibrary = useCallback(
|
|
|
|
async (libraryItems: LibraryItems) => {
|
|
|
|
const nextItems = libraryItems.filter(
|
|
|
|
(item) => !selectedItems.includes(item.id),
|
|
|
|
);
|
|
|
|
library.saveLibrary(nextItems).catch(() => {
|
|
|
|
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
|
|
|
});
|
|
|
|
setSelectedItems([]);
|
|
|
|
},
|
|
|
|
[library, setAppState, selectedItems, setSelectedItems],
|
|
|
|
);
|
2021-11-17 23:53:43 +05:30
|
|
|
|
|
|
|
const resetLibrary = useCallback(() => {
|
|
|
|
library.resetLibrary();
|
|
|
|
focusContainer();
|
|
|
|
}, [library, focusContainer]);
|
|
|
|
|
|
|
|
const addToLibrary = useCallback(
|
2022-04-20 14:40:03 +02:00
|
|
|
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
|
2022-03-28 14:46:40 +02:00
|
|
|
trackEvent("element", "addToLibrary", "ui");
|
2021-11-17 23:53:43 +05:30
|
|
|
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(),
|
|
|
|
},
|
2022-04-20 14:40:03 +02:00
|
|
|
...libraryItems,
|
2021-11-17 23:53:43 +05:30
|
|
|
];
|
|
|
|
onAddToLibrary();
|
2022-04-20 14:40:03 +02:00
|
|
|
library.saveLibrary(nextItems).catch(() => {
|
2021-11-17 23:53:43 +05:30
|
|
|
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(
|
2022-04-20 14:40:03 +02:00
|
|
|
(data, libraryItems: LibraryItems) => {
|
2021-11-17 23:53:43 +05:30
|
|
|
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.saveLibrary(nextLibItems);
|
|
|
|
},
|
2022-04-20 14:40:03 +02:00
|
|
|
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
|
2021-11-17 23:53:43 +05:30
|
|
|
);
|
|
|
|
|
2021-11-26 12:46:23 +01:00
|
|
|
const [lastSelectedItem, setLastSelectedItem] = useState<
|
|
|
|
LibraryItem["id"] | null
|
|
|
|
>(null);
|
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
if (libraryItemsData.status === "loading") {
|
|
|
|
return (
|
|
|
|
<LibraryMenuWrapper ref={ref}>
|
|
|
|
<div className="layer-ui__library-message">
|
|
|
|
<Spinner size="2em" />
|
|
|
|
<span>{t("labels.libraryLoadingMessage")}</span>
|
|
|
|
</div>
|
|
|
|
</LibraryMenuWrapper>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<LibraryMenuWrapper ref={ref}>
|
2021-11-17 23:53:43 +05:30
|
|
|
{showPublishLibraryDialog && (
|
|
|
|
<PublishLibrary
|
|
|
|
onClose={() => setShowPublishLibraryDialog(false)}
|
2022-04-20 14:40:03 +02:00
|
|
|
libraryItems={getSelectedItems(
|
|
|
|
libraryItemsData.libraryItems,
|
|
|
|
selectedItems,
|
|
|
|
)}
|
2021-11-17 23:53:43 +05:30
|
|
|
appState={appState}
|
2022-04-20 14:40:03 +02:00
|
|
|
onSuccess={(data) =>
|
|
|
|
onPublishLibSuccess(data, libraryItemsData.libraryItems)
|
|
|
|
}
|
2021-11-17 23:53:43 +05:30
|
|
|
onError={(error) => window.alert(error)}
|
2022-04-20 14:40:03 +02:00
|
|
|
updateItemsInStorage={() =>
|
|
|
|
library.saveLibrary(libraryItemsData.libraryItems)
|
|
|
|
}
|
2021-11-17 23:53:43 +05:30
|
|
|
onRemove={(id: string) =>
|
|
|
|
setSelectedItems(selectedItems.filter((_id) => _id !== id))
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{publishLibSuccess && renderPublishSuccess()}
|
2022-04-20 14:40:03 +02:00
|
|
|
<LibraryMenuItems
|
|
|
|
libraryItems={libraryItemsData.libraryItems}
|
|
|
|
onRemoveFromLibrary={() =>
|
|
|
|
removeFromLibrary(libraryItemsData.libraryItems)
|
|
|
|
}
|
|
|
|
onAddToLibrary={(elements) =>
|
|
|
|
addToLibrary(elements, libraryItemsData.libraryItems)
|
|
|
|
}
|
|
|
|
onInsertShape={onInsertShape}
|
|
|
|
pendingElements={pendingElements}
|
|
|
|
setAppState={setAppState}
|
|
|
|
libraryReturnUrl={libraryReturnUrl}
|
|
|
|
library={library}
|
|
|
|
theme={theme}
|
|
|
|
files={files}
|
|
|
|
id={id}
|
|
|
|
selectedItems={selectedItems}
|
|
|
|
onToggle={(id, event) => {
|
|
|
|
const shouldSelect = !selectedItems.includes(id);
|
2021-11-17 23:53:43 +05:30
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
if (shouldSelect) {
|
|
|
|
if (event.shiftKey && lastSelectedItem) {
|
|
|
|
const rangeStart = libraryItemsData.libraryItems.findIndex(
|
|
|
|
(item) => item.id === lastSelectedItem,
|
|
|
|
);
|
|
|
|
const rangeEnd = libraryItemsData.libraryItems.findIndex(
|
|
|
|
(item) => item.id === id,
|
|
|
|
);
|
2021-11-26 12:46:23 +01:00
|
|
|
|
2022-04-20 14:40:03 +02:00
|
|
|
if (rangeStart === -1 || rangeEnd === -1) {
|
2021-11-26 12:46:23 +01:00
|
|
|
setSelectedItems([...selectedItems, id]);
|
2022-04-20 14:40:03 +02:00
|
|
|
return;
|
2021-11-26 12:46:23 +01:00
|
|
|
}
|
2022-04-20 14:40:03 +02:00
|
|
|
|
|
|
|
const selectedItemsMap = arrayToMap(selectedItems);
|
|
|
|
const nextSelectedIds = libraryItemsData.libraryItems.reduce(
|
|
|
|
(acc: LibraryItem["id"][], item, idx) => {
|
|
|
|
if (
|
|
|
|
(idx >= rangeStart && idx <= rangeEnd) ||
|
|
|
|
selectedItemsMap.has(item.id)
|
|
|
|
) {
|
|
|
|
acc.push(item.id);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
|
|
|
setSelectedItems(nextSelectedIds);
|
2021-11-17 23:53:43 +05:30
|
|
|
} else {
|
2022-04-20 14:40:03 +02:00
|
|
|
setSelectedItems([...selectedItems, id]);
|
2021-11-17 23:53:43 +05:30
|
|
|
}
|
2022-04-20 14:40:03 +02:00
|
|
|
setLastSelectedItem(id);
|
|
|
|
} else {
|
|
|
|
setLastSelectedItem(null);
|
|
|
|
setSelectedItems(selectedItems.filter((_id) => _id !== id));
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
onPublish={() => setShowPublishLibraryDialog(true)}
|
|
|
|
resetLibrary={resetLibrary}
|
|
|
|
/>
|
|
|
|
</LibraryMenuWrapper>
|
2021-11-17 23:53:43 +05:30
|
|
|
);
|
|
|
|
};
|