aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/ElementLinkDialog.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/ElementLinkDialog.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/ElementLinkDialog.tsx')
-rw-r--r--packages/excalidraw/components/ElementLinkDialog.tsx174
1 files changed, 174 insertions, 0 deletions
diff --git a/packages/excalidraw/components/ElementLinkDialog.tsx b/packages/excalidraw/components/ElementLinkDialog.tsx
new file mode 100644
index 0000000..2ec3eaa
--- /dev/null
+++ b/packages/excalidraw/components/ElementLinkDialog.tsx
@@ -0,0 +1,174 @@
+import { TextField } from "./TextField";
+import type { AppProps, AppState, UIAppState } from "../types";
+import DialogActionButton from "./DialogActionButton";
+import { getSelectedElements } from "../scene";
+import {
+ defaultGetElementLinkFromSelection,
+ getLinkIdAndTypeFromSelection,
+} from "../element/elementLink";
+import { mutateElement } from "../element/mutateElement";
+import { useCallback, useEffect, useState } from "react";
+import { t } from "../i18n";
+import type { ElementsMap, ExcalidrawElement } from "../element/types";
+import { ToolButton } from "./ToolButton";
+import { TrashIcon } from "./icons";
+import { KEYS } from "../keys";
+
+import "./ElementLinkDialog.scss";
+import { normalizeLink } from "../data/url";
+
+const ElementLinkDialog = ({
+ sourceElementId,
+ onClose,
+ elementsMap,
+ appState,
+ generateLinkForSelection = defaultGetElementLinkFromSelection,
+}: {
+ sourceElementId: ExcalidrawElement["id"];
+ elementsMap: ElementsMap;
+ appState: UIAppState;
+ onClose?: () => void;
+ generateLinkForSelection: AppProps["generateLinkForSelection"];
+}) => {
+ const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
+
+ const [nextLink, setNextLink] = useState<string | null>(originalLink);
+ const [linkEdited, setLinkEdited] = useState(false);
+
+ useEffect(() => {
+ const selectedElements = getSelectedElements(elementsMap, appState);
+ let nextLink = originalLink;
+
+ if (selectedElements.length > 0 && generateLinkForSelection) {
+ const idAndType = getLinkIdAndTypeFromSelection(
+ selectedElements,
+ appState as AppState,
+ );
+
+ if (idAndType) {
+ nextLink = normalizeLink(
+ generateLinkForSelection(idAndType.id, idAndType.type),
+ );
+ }
+ }
+
+ setNextLink(nextLink);
+ }, [
+ elementsMap,
+ appState,
+ appState.selectedElementIds,
+ originalLink,
+ generateLinkForSelection,
+ ]);
+
+ const handleConfirm = useCallback(() => {
+ if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
+ const elementToLink = elementsMap.get(sourceElementId);
+ elementToLink &&
+ mutateElement(elementToLink, {
+ link: nextLink,
+ });
+ }
+
+ if (!nextLink && linkEdited && sourceElementId) {
+ const elementToLink = elementsMap.get(sourceElementId);
+ elementToLink &&
+ mutateElement(elementToLink, {
+ link: null,
+ });
+ }
+
+ onClose?.();
+ }, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ appState.openDialog?.name === "elementLinkSelector" &&
+ event.key === KEYS.ENTER
+ ) {
+ handleConfirm();
+ }
+
+ if (
+ appState.openDialog?.name === "elementLinkSelector" &&
+ event.key === KEYS.ESCAPE
+ ) {
+ onClose?.();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [appState, onClose, handleConfirm]);
+
+ return (
+ <div className="ElementLinkDialog">
+ <div className="ElementLinkDialog__header">
+ <h2>{t("elementLink.title")}</h2>
+ <p>{t("elementLink.desc")}</p>
+ </div>
+
+ <div className="ElementLinkDialog__input">
+ <TextField
+ value={nextLink ?? ""}
+ onChange={(value) => {
+ if (!linkEdited) {
+ setLinkEdited(true);
+ }
+ setNextLink(value);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === KEYS.ENTER) {
+ handleConfirm();
+ }
+ }}
+ className="ElementLinkDialog__input-field"
+ selectOnRender
+ />
+
+ {originalLink && nextLink && (
+ <ToolButton
+ type="button"
+ title={t("buttons.remove")}
+ aria-label={t("buttons.remove")}
+ label={t("buttons.remove")}
+ onClick={() => {
+ // removes the link from the input
+ // but doesn't update the element
+
+ // when confirmed, will remove the link from the element
+ setNextLink(null);
+ setLinkEdited(true);
+ }}
+ className="ElementLinkDialog__remove"
+ icon={TrashIcon}
+ />
+ )}
+ </div>
+
+ <div className="ElementLinkDialog__actions">
+ <DialogActionButton
+ label={t("buttons.cancel")}
+ onClick={() => {
+ onClose?.();
+ }}
+ style={{
+ marginRight: 10,
+ }}
+ />
+
+ <DialogActionButton
+ label={t("buttons.confirm")}
+ onClick={handleConfirm}
+ actionType="primary"
+ />
+ </div>
+ </div>
+ );
+};
+
+export default ElementLinkDialog;