"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import type { ReactNode } from "react" import { useEditor, EditorContent } from "@tiptap/react" import { HocuspocusProvider } from "@hocuspocus/provider" import * as Y from "yjs" import { toast } from "sonner" import { DocsChrome } from "@/components/drive/richtext/docs-chrome" import { DocsEditorWorkspace } from "@/components/drive/richtext/docs-editor-workspace" import { DocsLoadingSplash } from "@/components/drive/richtext/docs-loading-splash" import { DocsStatusBar } from "@/components/drive/richtext/docs-page-view" import { DocsToolbar } from "@/components/drive/richtext/docs-toolbar" import { buildRichTextExtensions, RICHTEXT_EDITOR_CLASS } from "@/lib/drive/richtext-extensions" import type { RichTextSaveStatus, RichTextSessionResponse } from "@/lib/drive/richtext-types" import type { DriveShare, DriveFileInfo } from "@/lib/api/types" import { useDocsEditMenu } from "@/lib/drive/use-docs-edit-menu" import { useDocsFileMenu } from "@/lib/drive/use-docs-file-menu" import { useDocsFormatMenu } from "@/lib/drive/use-docs-format-menu" import { useDocsInsertMenu } from "@/lib/drive/use-docs-insert-menu" import type { DocsViewMenuActions, DocsViewMenuState } from "@/components/drive/richtext/docs-view-menu" import { useDocsViewSettings } from "@/lib/drive/docs-view-settings" import { useDocsKeyboardShortcutsStore } from "@/lib/stores/docs-keyboard-shortcuts-store" import { useCollabPresence } from "@/lib/drive/use-collab-presence" import { apiClient } from "@/lib/api/client" import { driveDownloadApiPath } from "@/lib/api/drive-download" import { buildRegionPatch } from "@/lib/drive/docs-header-footer-layout" import { buildPageSetupForFormat, buildPageSetupFromDraft, resolveDocumentPageLayout, type DocPageSetup, } from "@/lib/drive/doc-page-setup" import { readUserPageSetupDefaults } from "@/lib/drive/docs-page-defaults" import { ensureMinimalTipTapDoc, isEmptyTipTapDoc } from "@/lib/drive/richtext-content" import { importFileToTipTap } from "@/lib/drive/richtext-import" import { migrateBase64ImagesInContent } from "@/lib/drive/docs-graphic-assets" import { isUltidocPath } from "@/lib/drive/richtext-formats" import { defaultDocumentParagraphStyles, type DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles" import { DocsParagraphStylesProvider } from "@/lib/drive/docs-paragraph-styles-context" import { useDocsParagraphStyles } from "@/lib/drive/use-docs-paragraph-styles" import { resolveRichTextDocumentLoadingPhase, type DocsLoadingPhase, } from "@/lib/drive/docs-loading-phase" import { buildDocsExportSnapshot } from "@/lib/drive/docs-export-snapshot" import { printDocsDocument } from "@/lib/drive/docs-print" import { DocsAiPanel, DocsAiPanelToggle } from "@/components/ai/docs-ai-panel" import { useDocsAiPanelStore } from "@/lib/ai/use-docs-ai-panel" import { cn } from "@/lib/utils" const SAVE_DEBOUNCE_MS = 2000 /** Align with Hocuspocus store debounce + buffer */ const COLLAB_SAVE_IDLE_MS = 2000 const PAGE_SETUP_DEBOUNCE_MS = 1500 export type RichTextDocsChromeProps = { title: string onRename?: (next: string) => Promise renameDisabled?: boolean backHref?: string backLabel?: string showBack?: boolean shares?: DriveShare[] onShareClick?: () => void showShare?: boolean showAccount?: boolean trailing?: ReactNode moveFile?: DriveFileInfo onFileMoved?: (newPath: string) => void file?: DriveFileInfo onRenameRequest?: () => void renameSignal?: number } export function RichTextDocumentEditor({ session, mode, userName, userColor, onSaveStatus, fetchSourceBytes, importApi, chrome, deferSplash = false, onLoadingChange, }: { session: RichTextSessionResponse mode: "edit" | "view" userName: string userColor: string onSaveStatus?: (status: RichTextSaveStatus) => void fetchSourceBytes?: (path: string) => Promise importApi?: (body: { source_path: string content: Record pageSetup?: DocPageSetup | null }) => Promise chrome?: RichTextDocsChromeProps deferSplash?: boolean onLoadingChange?: (loading: boolean, phase: DocsLoadingPhase) => void }) { const editable = mode === "edit" const collaboration = session.collaboration && Boolean(session.wsUrl && session.token) const ydocRef = useRef(null) if (collaboration && !ydocRef.current) { ydocRef.current = new Y.Doc() } const ydoc = collaboration ? ydocRef.current : null const [provider, setProvider] = useState(null) const [collabSynced, setCollabSynced] = useState(false) const [collabError, setCollabError] = useState(null) const [importDone, setImportDone] = useState(!session.importRequired) const [contentImportPending, setContentImportPending] = useState(session.importRequired) const [importedContent, setImportedContent] = useState | null>(null) const [documentParagraphStyles, setDocumentParagraphStyles] = useState( () => session.paragraphStyles ?? defaultDocumentParagraphStyles() ) const [documentPageSetup, setDocumentPageSetup] = useState( session.pageSetup ?? null ) const [saveStatus, setSaveStatus] = useState("idle") const [pageCount, setPageCount] = useState(1) const [currentPage, setCurrentPage] = useState(1) const [regionEditor, setRegionEditor] = useState(null) const saveTimer = useRef | null>(null) const saveIdleTimer = useRef | null>(null) const pageSetupTimer = useRef | null>(null) const documentPageSetupRef = useRef(session.pageSetup ?? null) const saveStatusRef = useRef("idle") const reloadAfterReimportRef = useRef(false) const purgeReimportingRef = useRef(false) const providerRef = useRef(null) const { settings, setPageFormatId, setZoom, toggleSpellcheck, toggleChromeCollapsed, setEditorMode, setCommentsDisplay, toggleOutlineSidebarExpanded, toggleShowLayout, toggleShowRuler, toggleShowEquationToolbar, toggleShowNonPrintableChars, } = useDocsViewSettings() const shellRef = useRef(null) const getPageStackElementRef = useRef<() => HTMLElement | null>(() => null) const presenceUsers = useCollabPresence(provider, { name: userName, color: userColor }) const pageLayout = useMemo( () => resolveDocumentPageLayout(documentPageSetup, settings.pageFormatId), [documentPageSetup, settings.pageFormatId] ) const activePageFormatId = pageLayout.format.id const reportSaveStatus = useCallback( (status: RichTextSaveStatus) => { saveStatusRef.current = status setSaveStatus(status) onSaveStatus?.(status) }, [onSaveStatus] ) const persistPageSetup = useCallback( async (setup: DocPageSetup) => { if (!editable) return try { const body = JSON.stringify({ pageSetup: setup }) if (session.saveUrl) { const res = await fetch(session.saveUrl, { method: "PUT", headers: { "Content-Type": "application/json" }, body, }) if (!res.ok) throw new Error("save failed") } else { await apiClient.put("/richtext/save", { path: session.canonicalPath, pageSetup: setup, }) } reportSaveStatus("saved") } catch { reportSaveStatus("error") } }, [editable, reportSaveStatus, session.canonicalPath, session.saveUrl] ) const schedulePageSetupPatch = useCallback( (patch: Partial, options?: { immediate?: boolean }) => { const base = documentPageSetupRef.current ?? buildPageSetupForFormat(settings.pageFormatId, null) const next = { ...base, ...patch } documentPageSetupRef.current = next setDocumentPageSetup(next) if (next.formatId) setPageFormatId(next.formatId) const flush = () => { const setup = documentPageSetupRef.current if (!setup) return void persistPageSetup(setup) } if (options?.immediate) { if (pageSetupTimer.current) clearTimeout(pageSetupTimer.current) if (saveStatusRef.current === "idle") { reportSaveStatus("saving") } flush() return } if (saveStatusRef.current === "idle") { reportSaveStatus("saving") } if (pageSetupTimer.current) clearTimeout(pageSetupTimer.current) pageSetupTimer.current = setTimeout(flush, PAGE_SETUP_DEBOUNCE_MS) }, [persistPageSetup, reportSaveStatus, settings.pageFormatId, setPageFormatId] ) const handlePageFormatChange = useCallback( (formatId: typeof settings.pageFormatId) => { schedulePageSetupPatch(buildPageSetupForFormat(formatId, documentPageSetupRef.current), { immediate: true, }) }, [schedulePageSetupPatch, settings.pageFormatId] ) const handleRegionContentChange = useCallback( ( region: "header" | "footer", content: Record, meta: { pageIndex: number; contentHeightPx: number }, options?: { immediate?: boolean } ) => { const base = documentPageSetupRef.current ?? buildPageSetupForFormat(settings.pageFormatId, null) schedulePageSetupPatch( buildRegionPatch(base, region, meta.pageIndex, content, meta.contentHeightPx), options ) }, [schedulePageSetupPatch, settings.pageFormatId] ) const handlePageSetupPatch = useCallback( (patch: Partial, options?: { immediate?: boolean }) => { schedulePageSetupPatch(patch, options) }, [schedulePageSetupPatch] ) useEffect(() => { documentPageSetupRef.current = documentPageSetup }, [documentPageSetup]) useEffect(() => { if (session.pageSetup) setDocumentPageSetup(session.pageSetup) }, [session.pageSetup]) useEffect(() => { if (session.pageSetup) return setDocumentPageSetup((current) => { if (current) return current const setup = buildPageSetupFromDraft( readUserPageSetupDefaults(settings.pageFormatId), null ) if (setup.formatId) setPageFormatId(setup.formatId) return setup }) }, [session.pageSetup, settings.pageFormatId, setPageFormatId]) const handlePageCountChange = useCallback((count: number) => { setPageCount(count) setCurrentPage((page) => Math.min(page, count)) }, []) const handleCurrentPageChange = useCallback((page: number) => { setCurrentPage(page) }, []) const handlePurgeSidecarAndReimport = useCallback(async () => { if (!editable) return if (!session.sourcePath) { toast.error("Chemin source introuvable — ouvrez le document depuis le fichier DOCX") return } if (isUltidocPath(session.sourcePath)) { toast.error("Aucun fichier source à réimporter") return } if ( !window.confirm( "Supprimer le sidecar (.ultidoc.json) et réimporter le document source ? Cette action est temporaire (dev)." ) ) { return } reportSaveStatus("saving") purgeReimportingRef.current = true reloadAfterReimportRef.current = true try { providerRef.current?.destroy() providerRef.current = null setProvider(null) setCollabSynced(false) if (ydocRef.current) { ydocRef.current.destroy() ydocRef.current = collaboration ? new Y.Doc() : null } await apiClient.delete(`/drive/files${session.canonicalPath}`) setDocumentPageSetup(null) setImportedContent(null) setImportDone(false) setContentImportPending(true) toast.success("Sidecar purgé — réimport en cours…") } catch { purgeReimportingRef.current = false reloadAfterReimportRef.current = false reportSaveStatus("error") toast.error("Impossible de purger le sidecar") } }, [collaboration, editable, reportSaveStatus, session.canonicalPath, session.sourcePath]) const markCollabDirty = useCallback(() => { if (saveStatusRef.current !== "saving") { reportSaveStatus("saving") } if (saveIdleTimer.current) clearTimeout(saveIdleTimer.current) saveIdleTimer.current = setTimeout(() => { reportSaveStatus("saved") }, COLLAB_SAVE_IDLE_MS) }, [reportSaveStatus]) useEffect(() => { return () => { if (saveTimer.current) clearTimeout(saveTimer.current) if (saveIdleTimer.current) clearTimeout(saveIdleTimer.current) if (pageSetupTimer.current) clearTimeout(pageSetupTimer.current) } }, []) useEffect(() => { setContentImportPending(session.importRequired) setImportDone(!session.importRequired) }, [session.importRequired, session.canonicalPath]) useEffect(() => { if (session.importRequired || !session.sourcePath) return let cancelled = false void (async () => { try { let parsed: { content?: Record } if (session.documentUrl) { const res = await fetch(session.documentUrl) if (!res.ok) return parsed = JSON.parse(await res.text()) as { content?: Record } } else { const blob = await apiClient.getBlob(driveDownloadApiPath(session.canonicalPath)) parsed = JSON.parse(await blob.text()) as { content?: Record } } if (cancelled || !isEmptyTipTapDoc(parsed.content)) return setContentImportPending(true) setImportDone(false) } catch { /* keep current import flags */ } })() return () => { cancelled = true } }, [session.canonicalPath, session.documentUrl, session.importRequired, session.sourcePath]) useEffect(() => { if (!contentImportPending || importDone) return let cancelled = false void (async () => { reportSaveStatus("saving") try { const source = session.sourcePath if (!source) { throw new Error("Chemin source manquant pour le réimport") } const buf = fetchSourceBytes ? await fetchSourceBytes(source) : await (await apiClient.getBlob(driveDownloadApiPath(source))).arrayBuffer() const imported = await importFileToTipTap(source.split("/").pop() ?? "file.docx", buf) if (cancelled) return const content = ensureMinimalTipTapDoc(imported.content as Record) const payload = { source_path: source, content, pageSetup: imported.pageSetup ?? undefined, } if (importApi) { await importApi(payload) } else { await apiClient.post("/richtext/import", payload) } if (!cancelled) { if (imported.pageSetup) setDocumentPageSetup(imported.pageSetup) if (reloadAfterReimportRef.current) { reloadAfterReimportRef.current = false window.location.reload() return } setImportedContent(content) setContentImportPending(false) setImportDone(true) purgeReimportingRef.current = false reportSaveStatus("saved") } } catch (error) { if (!cancelled) { purgeReimportingRef.current = false reloadAfterReimportRef.current = false reportSaveStatus("error") toast.error(error instanceof Error ? error.message : "Réimport échoué") } } })() return () => { cancelled = true } }, [ contentImportPending, importDone, session.sourcePath, fetchSourceBytes, importApi, reportSaveStatus, ]) useEffect(() => { if (purgeReimportingRef.current) return if (!collaboration || !ydoc || !importDone) return setCollabSynced(false) setCollabError(null) const p = new HocuspocusProvider({ url: session.wsUrl, name: session.roomId, token: session.token, document: ydoc, sessionAwareness: false, onSynced: () => setCollabSynced(true), onAuthenticationFailed: ({ reason }) => { setCollabError(reason ?? "Authentification collaboration refusée") setCollabSynced(false) }, }) providerRef.current = p setProvider(p) return () => { p.destroy() providerRef.current = null setProvider(null) setCollabSynced(false) } }, [collaboration, importDone, session.roomId, session.token, session.wsUrl, ydoc]) const persistDocument = useCallback( async (json: Record) => { let content = json try { content = await migrateBase64ImagesInContent(json, async ({ dataUrl }) => { const res = await apiClient.post<{ assetId: string; url: string }>( "/richtext/assets", { path: session.canonicalPath, dataUrl } ) return { assetId: res.assetId, url: res.url } }) } catch { /* keep base64 fallback */ } const doc = { schemaVersion: 1, editor: "tiptap", content } const body = JSON.stringify(doc) const savePromise = session.saveUrl ? fetch(session.saveUrl, { method: "PUT", headers: { "Content-Type": "application/json" }, body, }).then((res) => { if (!res.ok) throw new Error("save failed") }) : apiClient.put("/richtext/save", { path: session.canonicalPath, document: content }) await savePromise }, [session.canonicalPath, session.saveUrl] ) const scheduleSave = useCallback( (json: Record, options?: { immediate?: boolean }) => { if (!editable || collaboration) return if (saveTimer.current) clearTimeout(saveTimer.current) if (saveStatusRef.current !== "saving") { reportSaveStatus("saving") } const runSave = () => { void persistDocument(json) .then(() => reportSaveStatus("saved")) .catch(() => reportSaveStatus("error")) } if (options?.immediate) { runSave() return } saveTimer.current = setTimeout(runSave, SAVE_DEBOUNCE_MS) }, [collaboration, editable, persistDocument, reportSaveStatus] ) const collabReady = !collaboration || (Boolean(provider) && collabSynced) const editorEnabled = importDone && collabReady const extensions = useMemo( () => buildRichTextExtensions({ collaboration: collaboration && ydoc ? { document: ydoc } : undefined, collaborationCaret: collaboration && provider ? { provider, user: { name: userName, color: userColor } } : undefined, editable, }), [collaboration, ydoc, provider, userName, userColor, editable] ) const editor = useEditor( { immediatelyRender: false, editable, extensions, editorProps: { attributes: { class: RICHTEXT_EDITOR_CLASS, }, }, onUpdate: ({ editor: ed }) => { if (collaboration) { markCollabDirty() return } scheduleSave(ed.getJSON() as Record) }, }, [editorEnabled, extensions, collaboration, markCollabDirty, scheduleSave] ) useEffect(() => { if (!editor || editor.isDestroyed) return const flushAfterDrawSave = () => { if (!editable) return const json = editor.getJSON() as Record if (collaboration) { if (saveStatusRef.current !== "saving") { reportSaveStatus("saving") } void persistDocument(json) .then(() => reportSaveStatus("saved")) .catch(() => reportSaveStatus("error")) return } scheduleSave(json, { immediate: true }) } window.addEventListener("ultidocs:graphic-draw-saved", flushAfterDrawSave) return () => window.removeEventListener("ultidocs:graphic-draw-saved", flushAfterDrawSave) }, [collaboration, editable, editor, persistDocument, reportSaveStatus, scheduleSave]) useEffect(() => { if (!editor || editor.isDestroyed) return const syncSpellcheck = () => { if (editor.isDestroyed || !editor.isInitialized) return const dom = editor.view.dom dom.spellcheck = settings.spellcheck if (settings.spellcheck) { dom.setAttribute("spellcheck", "true") dom.removeAttribute("autocorrect") dom.removeAttribute("autocapitalize") } else { dom.setAttribute("spellcheck", "false") dom.setAttribute("autocorrect", "off") dom.setAttribute("autocapitalize", "off") } } syncSpellcheck() editor.on("create", syncSpellcheck) return () => { editor.off("create", syncSpellcheck) } }, [editor, settings.spellcheck]) const editMenu = useDocsEditMenu({ editor, disabled: !editable, }) const insertMenu = useDocsInsertMenu({ editor, disabled: !editable, pageSetup: documentPageSetup, onPageSetupPatch: handlePageSetupPatch, }) const paragraphStyles = useDocsParagraphStyles({ editor, initialDocumentStyles: documentParagraphStyles, editable, canonicalPath: session.canonicalPath, saveUrl: session.saveUrl, }) useEffect(() => { setDocumentParagraphStyles(paragraphStyles.state.documentStyles) }, [paragraphStyles.state.documentStyles]) useEffect(() => { if (session.paragraphStyles) setDocumentParagraphStyles(session.paragraphStyles) }, [session.paragraphStyles]) const paragraphStylesContextValue = useMemo( () => ({ state: paragraphStyles.state, applyStyle: paragraphStyles.applyStyle, updateStyleFromSelection: paragraphStyles.updateStyleFromSelection, createUserStyle: paragraphStyles.createUserStyle, updateDocumentStyle: paragraphStyles.updateDocumentStyle, }), [paragraphStyles] ) const getExportSnapshot = useCallback(() => { if (!editor || !chrome?.file) return null return buildDocsExportSnapshot({ editor, sourceName: chrome.file.name, title: chrome.title, pageSetup: documentPageSetup, fallbackFormatId: settings.pageFormatId, paragraphStyles: paragraphStyles.state.mergedCatalog, pageCount, getPageStackElement: () => getPageStackElementRef.current(), }) }, [ chrome?.file, chrome?.title, documentPageSetup, editor, pageCount, paragraphStyles.state.mergedCatalog, settings.pageFormatId, ]) const handlePrintDocument = useCallback(async () => { const snapshot = getExportSnapshot() if (!snapshot) { toast.error("Impossible d'imprimer le document") return } try { await printDocsDocument(snapshot) } catch (error) { console.error("[docs] print failed", error) toast.error("Impossible d'imprimer le document") } }, [getExportSnapshot]) const fileMenu = useDocsFileMenu({ file: chrome?.file, editor, pageSetup: documentPageSetup, fallbackFormatId: settings.pageFormatId, getExportSnapshot, onPageSetupApply: (setup) => { documentPageSetupRef.current = setup schedulePageSetupPatch(setup, { immediate: true }) }, onPurgeSidecarAndReimport: () => void handlePurgeSidecarAndReimport(), onShareClick: chrome?.onShareClick, onRenameRequest: chrome?.onRenameRequest, onFileMoved: chrome?.onFileMoved, disabled: !editable, }) const formatMenu = useDocsFormatMenu({ editor, disabled: !editable, onPageSetup: fileMenu.actions.onPageSetup, }) const handleFullscreen = useCallback(() => { const el = shellRef.current if (!el) return if (document.fullscreenElement) { void document.exitFullscreen() return } void el.requestFullscreen?.() }, []) const viewMenuState = useMemo( () => ({ editorMode: settings.editorMode, commentsDisplay: settings.commentsDisplay, showLayout: settings.showLayout, showRuler: settings.showRuler, showEquationToolbar: settings.showEquationToolbar, showNonPrintableChars: settings.showNonPrintableChars, }), [settings] ) const viewMenuActions = useMemo( () => ({ onEditorModeChange: setEditorMode, onCommentsDisplayChange: setCommentsDisplay, onToggleOutlineSidebar: toggleOutlineSidebarExpanded, onToggleShowLayout: toggleShowLayout, onToggleShowRuler: toggleShowRuler, onToggleShowEquationToolbar: toggleShowEquationToolbar, onToggleShowNonPrintableChars: toggleShowNonPrintableChars, onFullscreen: handleFullscreen, }), [ handleFullscreen, setCommentsDisplay, setEditorMode, toggleOutlineSidebarExpanded, toggleShowEquationToolbar, toggleShowLayout, toggleShowNonPrintableChars, toggleShowRuler, ] ) const closeDocsAiPanel = useDocsAiPanelStore((s) => s.closePanel) useEffect(() => { closeDocsAiPanel() }, [session.canonicalPath, closeDocsAiPanel]) const chromeProps = chrome ? { ...chrome, trailing: ( <> {chrome.trailing} ), fileMenuActions: fileMenu.actions, fileMenuDialogs: fileMenu.dialogs, fileMenuDisabled: fileMenu.disabled, editMenuActions: editMenu.actions, editMenuState: editMenu.state, editMenuDisabled: editMenu.disabled, insertMenuActions: insertMenu.actions, insertMenuDialogs: insertMenu.dialogs, insertMenuDisabled: insertMenu.disabled, insertMenuPageElementsEnabled: insertMenu.pageElementsEnabled, formatMenuActions: formatMenu.actions, formatMenuState: formatMenu.state, formatMenuDisabled: formatMenu.disabled, viewMenuActions, viewMenuState, viewMenuDisabled: false, } : undefined useEffect(() => { if (!editor || editor.isDestroyed) return const canEdit = editable && settings.editorMode !== "view" editor.setEditable(canEdit) }, [editor, editable, settings.editorMode]) useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { const id = useDocsKeyboardShortcutsStore.getState().matchEvent( event, (definition) => definition.scope === "document" && definition.handler === "custom" ) if (id === "view.showNonPrintable") { event.preventDefault() toggleShowNonPrintableChars() } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, [toggleShowNonPrintableChars]) useEffect(() => { if (!editor || collaboration || !importDone) return if (importedContent) { editor.commands.setContent(importedContent) setImportedContent(null) return } if (contentImportPending) return let cancelled = false void (async () => { try { let parsed: { content?: Record; pageSetup?: DocPageSetup } if (session.documentUrl) { const res = await fetch(session.documentUrl) if (!res.ok) throw new Error("load failed") parsed = JSON.parse(await res.text()) as { content?: Record; pageSetup?: DocPageSetup } } else { const blob = await apiClient.getBlob(driveDownloadApiPath(session.canonicalPath)) parsed = JSON.parse(await blob.text()) as { content?: Record; pageSetup?: DocPageSetup } } if (!cancelled && parsed.pageSetup) { setDocumentPageSetup(parsed.pageSetup) } else if (!cancelled) { setDocumentPageSetup( (current) => current ?? buildPageSetupFromDraft(readUserPageSetupDefaults(settings.pageFormatId), null) ) } if (!cancelled && parsed.content && !isEmptyTipTapDoc(parsed.content)) { editor.commands.setContent(parsed.content) } else if (!cancelled && session.sourcePath) { setContentImportPending(true) setImportDone(false) } } catch { /* blank */ } })() return () => { cancelled = true } }, [ editor, collaboration, importDone, importedContent, contentImportPending, session.canonicalPath, session.documentUrl, session.sourcePath, settings.pageFormatId, ]) const documentLoading = !editorEnabled || !editor const loadingPhase = resolveRichTextDocumentLoadingPhase({ contentImportPending, importDone, importRequired: session.importRequired, collaboration: Boolean(collaboration), collabSynced, }) useEffect(() => { if (!deferSplash) return onLoadingChange?.(documentLoading, loadingPhase) }, [deferSplash, documentLoading, loadingPhase, onLoadingChange]) if (collabError) { return (
Collaboration indisponible : {collabError}
) } if (!deferSplash && documentLoading) { return ( ) } if (deferSplash && documentLoading) { return null } if (!editor) { return null } return (
{chromeProps && !settings.chromeCollapsed ? ( ) : null} {chrome ? (
{ getPageStackElementRef.current = getPageStack }} toolbarShellClassName={ settings.chromeCollapsed ? "docs-toolbar-shell--collapsed" : undefined } toolbar={ editable ? ( void handlePrintDocument()} embedded /> ) : null } />
) : (
)} {chrome ? ( ) : null}
) }