ultisuite-client/components/drive/richtext/docs-excalidraw-editor.tsx
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

160 lines
5.0 KiB
TypeScript

"use client"
import dynamic from "next/dynamic"
import { memo, useCallback, useState } from "react"
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"
import type { DocsGraphicDrawSavePayload } from "@/lib/drive/docs-graphic-draw-bridge"
export function docsExcalidrawEditorKey(drawScene: string | null): string {
if (!drawScene) return "new-draw"
return `draw-${drawScene.length}-${drawScene.slice(0, 48)}`
}
const ExcalidrawEditor = dynamic(
async () => {
await import("@excalidraw/excalidraw/index.css")
const { Excalidraw, restoreElements, restoreAppState } = await import(
"@excalidraw/excalidraw"
)
type ExcalidrawInitialDataState = import("@excalidraw/excalidraw/types").ExcalidrawInitialDataState
type BinaryFiles = import("@excalidraw/excalidraw/types").BinaryFiles
const { memo, useEffect, useMemo, useRef } = await import("react")
function parseInitialData(drawScene: string | null): ExcalidrawInitialDataState | undefined {
if (!drawScene) return undefined
try {
const data = JSON.parse(drawScene) as {
elements?: Parameters<typeof restoreElements>[0]
appState?: Parameters<typeof restoreAppState>[0]
files?: BinaryFiles
}
const initialData: ExcalidrawInitialDataState = {
elements: restoreElements(data.elements ?? [], null),
appState: restoreAppState(data.appState ?? {}, null),
files: data.files,
}
return initialData
} catch {
return undefined
}
}
const Inner = memo(function Inner({
drawScene,
onReady,
}: {
drawScene: string | null
onReady: (api: ExcalidrawImperativeAPI) => void
}) {
const apiRef = useRef<ExcalidrawImperativeAPI | null>(null)
const initialData = useMemo(() => parseInitialData(drawScene), [drawScene])
useEffect(() => {
const api = apiRef.current
if (!api || !drawScene) return
const data = parseInitialData(drawScene)
if (!data) return
api.updateScene({
elements: data.elements ?? [],
appState: data.appState,
files: data.files,
})
}, [drawScene])
return (
<div className="h-full min-h-0 w-full">
<Excalidraw
excalidrawAPI={(api) => {
apiRef.current = api
onReady(api)
}}
initialData={initialData}
langCode="fr-FR"
UIOptions={{
canvasActions: {
changeViewBackgroundColor: true,
clearCanvas: true,
export: false,
loadScene: false,
saveToActiveFile: false,
toggleTheme: false,
},
}}
/>
</div>
)
})
return Inner
},
{
ssr: false,
loading: () => (
<div className="flex h-full min-h-[360px] items-center justify-center text-sm text-muted-foreground">
Chargement de l&apos;éditeur
</div>
),
}
)
function readSvgDimensions(svg: SVGSVGElement): { width: number; height: number } {
const viewBox = svg.getAttribute("viewBox")
if (viewBox) {
const parts = viewBox.split(/\s+/).map(Number)
if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
return {
width: Math.max(24, Math.round(parts[2])),
height: Math.max(24, Math.round(parts[3])),
}
}
}
const width = Number(svg.getAttribute("width"))
const height = Number(svg.getAttribute("height"))
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
return { width: Math.round(width), height: Math.round(height) }
}
return { width: 320, height: 240 }
}
function DocsExcalidrawEditorInner({
drawScene,
onReady,
}: {
drawScene: string | null
onReady: (api: ExcalidrawImperativeAPI) => void
}) {
return <ExcalidrawEditor drawScene={drawScene} onReady={onReady} />
}
export const DocsExcalidrawEditor = memo(DocsExcalidrawEditorInner)
export function useDocsExcalidrawSave(api: ExcalidrawImperativeAPI | null) {
const [saving, setSaving] = useState(false)
const exportDrawing = useCallback(async (): Promise<DocsGraphicDrawSavePayload | null> => {
if (!api) return null
setSaving(true)
try {
const elements = api.getSceneElements()
const appState = api.getAppState()
const files = api.getFiles()
const { exportToSvg, serializeAsJSON } = await import("@excalidraw/excalidraw")
const drawScene = serializeAsJSON(elements, appState, files, "local")
const svg = await exportToSvg({
elements,
appState,
files,
skipInliningFonts: true,
})
const svgString = new XMLSerializer().serializeToString(svg)
const src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`
const { width, height } = readSvgDimensions(svg)
return { drawScene, src, width, height }
} finally {
setSaving(false)
}
}, [api])
return { exportDrawing, saving }
}