Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
"use client"
|
|
|
|
import "@/styles/excalidraw.css"
|
|
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 () => {
|
|
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'é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 }
|
|
}
|