import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { ActionIcon, Button, Card, Group, Image, Text, useComputedColorScheme, } from "@mantine/core"; import { useState } from "react"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { svgStringToFile } from "@/lib"; import { useDisclosure } from "@mantine/hooks"; import { getFileUrl } from "@/lib/config.ts"; import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types"; import { IAttachment } from "@/lib/types"; import ReactClearModal from "react-clear-modal"; import clsx from "clsx"; import { IconEdit } from "@tabler/icons-react"; import { lazy } from "react"; import { Suspense } from "react"; import { useTranslation } from "react-i18next"; const Excalidraw = lazy(() => import("@excalidraw/excalidraw").then((module) => ({ default: module.Excalidraw, })), ); export default function ExcalidrawView(props: NodeViewProps) { const { t } = useTranslation(); const { node, updateAttributes, editor, selected } = props; const { src, title, width, attachmentId } = node.attrs; const [excalidrawAPI, setExcalidrawAPI] = useState(null); const [excalidrawData, setExcalidrawData] = useState(null); const [opened, { open, close }] = useDisclosure(false); const computedColorScheme = useComputedColorScheme(); const handleOpen = async () => { if (!editor.isEditable) { return; } try { if (src) { const url = getFileUrl(src); const request = await fetch(url, { credentials: "include", cache: "no-store", }); const { loadFromBlob } = await import("@excalidraw/excalidraw"); const data = await loadFromBlob(await request.blob(), null, null); setExcalidrawData(data); } } catch (err) { console.error(err); } finally { open(); } }; const handleSave = async () => { if (!excalidrawAPI) { return; } const { exportToSvg } = await import("@excalidraw/excalidraw"); const svg = await exportToSvg({ elements: excalidrawAPI?.getSceneElements(), appState: { exportEmbedScene: true, exportWithDarkMode: false, }, files: excalidrawAPI?.getFiles(), }); const serializer = new XMLSerializer(); let svgString = serializer.serializeToString(svg); svgString = svgString.replace( /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, "https://unpkg.com/@excalidraw/excalidraw@latest", ); const fileName = "diagram.excalidraw.svg"; const excalidrawSvgFile = await svgStringToFile(svgString, fileName); const pageId = editor.storage?.pageId; let attachment: IAttachment = null; if (attachmentId) { attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); } else { attachment = await uploadFile(excalidrawSvgFile, pageId); } updateAttributes({ src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, title: attachment.fileName, size: attachment.fileSize, attachmentId: attachment.id, }); close(); }; return (
setExcalidrawAPI(api)} initialData={{ ...excalidrawData, scrollToContent: true, }} theme={computedColorScheme} />
{src ? (
e.detail === 2 && handleOpen()} radius="md" fit="contain" w={width} src={getFileUrl(src)} alt={title} className={clsx( selected ? "ProseMirror-selectednode" : "", "alignCenter", )} /> {selected && editor.isEditable && ( )}
) : ( e.detail === 2 && handleOpen()} p="xs" style={{ display: "flex", justifyContent: "center", alignItems: "center", }} withBorder className={clsx(selected ? "ProseMirror-selectednode" : "")} >
{t("Double-click to edit Excalidraw diagram")}
)}
); }