import React, { useEffect, useMemo, useRef } from 'react'; import { Excalidraw, exportToBlob, exportToSvg } from '@excalidraw/excalidraw'; import type { AppState, ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'; import type { ExcalidrawWrapperProps, Scene, SceneChangeDetail, ReadyDetail } from './types'; /** * React wrapper mounted inside the custom element. * It relies on r2wc to pass the host element via a non-attribute property `__host`. */ export default function ExcalidrawWrapper(props: ExcalidrawWrapperProps & { __host?: HTMLElement; hostRef?: HTMLElement }) { const apiRef = useRef(null); const hostRef = useRef(null); const pendingReadyRef = useRef(null); const pendingSceneEventsRef = useRef([]); const lastDetailRef = useRef(null); const resolveHost = (candidate?: HTMLElement | null) => { if (candidate && hostRef.current !== candidate) { hostRef.current = candidate; console.log('[excalidraw-editor] host resolved', { hasHost: true }); } }; resolveHost((props as any).hostRef as HTMLElement | undefined); resolveHost((props as any).__host as HTMLElement | undefined); // Sync dark/light mode with export utils defaults const theme = props.theme === 'dark' ? 'dark' : 'light'; const lang = props.lang || 'fr'; const onChange = (elements: any[], appState: Partial, files: any) => { // CRITICAL DEBUG: Log raw parameters console.log('[excalidraw-editor] 🔍 onChange called', { elementsLength: elements?.length, elementsIsArray: Array.isArray(elements), elementsRaw: elements, appStateKeys: appState ? Object.keys(appState) : [], filesKeys: files ? Object.keys(files) : [] }); const detail: SceneChangeDetail = { elements, appState, files, source: 'user' }; lastDetailRef.current = detail; console.log('[excalidraw-editor] 📝 SCENE-CHANGE will dispatch', { elCount: Array.isArray(elements) ? elements.length : 'n/a', elementsType: typeof elements, isArray: Array.isArray(elements), viewMode: appState?.viewModeEnabled, propsReadOnly: props.readOnly, firstElement: elements?.[0] ? { id: elements[0].id, type: elements[0].type } : null }); const host = hostRef.current; if (!host) { console.warn('[excalidraw-editor] host unavailable during scene-change, queueing event'); pendingSceneEventsRef.current.push(detail); return; } // Always dispatch - Angular will handle filtering via hash comparison host.dispatchEvent(new CustomEvent('scene-change', { detail, bubbles: true, composed: true })); console.log('[excalidraw-editor] ✅ SCENE-CHANGE dispatched to host'); }; // When API becomes available const onApiReady = (api: ExcalidrawImperativeAPI) => { console.log('[excalidraw-editor] onApiReady', !!api); apiRef.current = api; const host = hostRef.current; if (host) { (host as any).__excalidrawAPI = api; host.dispatchEvent(new CustomEvent('ready', { detail: { apiAvailable: true }, bubbles: true, composed: true })); // Force a couple of refresh cycles to layout the canvas once API is ready try { api.refresh?.(); } catch {} queueMicrotask(() => { try { api.refresh?.(); } catch {} }); setTimeout(() => { try { api.refresh?.(); } catch {} }, 0); } else { pendingReadyRef.current = api; } }; // Expose imperative export helpers via the host methods (define.ts wires prototypes) useEffect(() => { resolveHost((props as any).hostRef as HTMLElement | undefined); resolveHost((props as any).__host as HTMLElement | undefined); const host = hostRef.current; if (!host) return; if (pendingReadyRef.current) { (host as any).__excalidrawAPI = pendingReadyRef.current; host.dispatchEvent(new CustomEvent('ready', { detail: { apiAvailable: true }, bubbles: true, composed: true })); console.log('[excalidraw-editor] flushed pending ready event'); pendingReadyRef.current = null; } if (pendingSceneEventsRef.current.length > 0) { const queued = pendingSceneEventsRef.current.splice(0); console.log('[excalidraw-editor] flushing queued scene events', { count: queued.length }); queued.forEach((detail) => { host.dispatchEvent(new CustomEvent('scene-change', { detail, bubbles: true, composed: true })); }); } (host as any).__getScene = () => { const api = apiRef.current; if (!api) return { elements: [], appState: {}, files: {} } as Scene; return { elements: api.getSceneElements?.() ?? [], appState: api.getAppState?.() ?? {}, files: api.getFiles?.() ?? {}, } as Scene; }; (host as any).__getLastEventScene = () => { const ld = lastDetailRef.current; if (!ld) return undefined; return { elements: ld.elements ?? [], appState: ld.appState ?? {}, files: ld.files ?? {} } as Scene; }; (host as any).__emitSceneChange = () => { const api = apiRef.current; if (!api) return; const elements = api.getSceneElements?.() ?? []; const appState = api.getAppState?.() ?? {} as Partial; const files = api.getFiles?.() ?? {}; const detail = { elements, appState, files, source: 'flush' } as any; try { host.dispatchEvent(new CustomEvent('scene-change', { detail, bubbles: true, composed: true })); } catch {} }; (host as any).__exportPNG = async (opts: { withBackground?: boolean } = {}) => { const api = apiRef.current; if (!api) return undefined; const elements = api.getSceneElements?.() ?? []; const appState = { ...(api.getAppState?.() ?? {}), exportBackground: !!opts.withBackground } as any; const files = api.getFiles?.() ?? {}; return await exportToBlob({ elements, appState, files, mimeType: 'image/png', quality: 1 }); }; (host as any).__exportSVG = async (opts: { withBackground?: boolean } = {}) => { const api = apiRef.current; if (!api) return undefined; const elements = api.getSceneElements?.() ?? []; const appState = { ...(api.getAppState?.() ?? {}), exportBackground: !!opts.withBackground } as any; const files = api.getFiles?.() ?? {}; const svgEl = await exportToSvg({ elements, appState, files }); const svgText = new XMLSerializer().serializeToString(svgEl); return new Blob([svgText], { type: 'image/svg+xml' }); }; }); // Ensure canvas refreshes when the host size becomes available or changes useEffect(() => { const host = hostRef.current; if (!host) return; // Force an initial refresh shortly after mount to layout the canvas const t = setTimeout(() => { try { apiRef.current?.refresh?.(); } catch {} }, 0); let ro: ResizeObserver | null = null; try { ro = new ResizeObserver(() => { try { apiRef.current?.refresh?.(); } catch {} }); ro.observe(host); } catch {} return () => { clearTimeout(t); try { ro?.disconnect(); } catch {} }; }); // Map React props to Excalidraw props // IMPORTANT: Freeze initialData on first mount to avoid resetting the scene const initialDataRef = useRef(props.initialData as any); return (
); }