import type { DocPageLayout } from "@/lib/drive/doc-page-setup" import { buildParagraphStylesCss } from "@/lib/drive/docs-paragraph-styles-css" import type { DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles" import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot" import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants" const PRINT_STYLE_ID = "docs-print-dynamic-styles" const PRINT_IFRAME_ID = "docs-print-iframe" function buildPageRule(pageLayout: DocPageLayout): string { const wMm = pageLayout.format.widthMm const hMm = pageLayout.format.heightMm const landscape = pageLayout.format.widthMm > pageLayout.format.heightMm const size = landscape ? `${hMm}mm ${wMm}mm` : `${wMm}mm ${hMm}mm` return `@page { size: ${size}; margin: 0; }` } function buildPrintLayoutRules(pageLayout: DocPageLayout): string { const pageWidth = pageLayout.widthPx const pageHeight = pageLayout.heightPx return ` html, body { margin: 0 !important; padding: 0 !important; background: white !important; width: ${pageWidth}px !important; min-height: 0 !important; overflow: visible !important; } .docs-print-root { margin: 0 !important; padding: 0 !important; width: ${pageWidth}px !important; background: white !important; } [data-docs-page-stack] { position: relative !important; left: 0 !important; top: 0 !important; transform: none !important; transform-origin: top left !important; width: ${pageWidth}px !important; margin: 0 !important; } .ultidrive-docs-page { width: ${pageWidth}px !important; height: ${pageHeight}px !important; box-shadow: none !important; border: none !important; break-after: page; page-break-after: always; print-color-adjust: exact; -webkit-print-color-adjust: exact; } .ultidrive-docs-page:last-child { break-after: auto; page-break-after: auto; } .docs-body-margin-mask { border-color: transparent !important; } .docs-hf-chrome, .docs-graphic-handle, .docs-graphic-rotate-handle, .docs-graphic-outline, .docs-graphic-snap-guides { display: none !important; } .docs-hf-band .docs-region-editor-root { border: none !important; background: transparent !important; } ` } type StyleRestore = () => void function patchInlineStyle(el: HTMLElement, property: string, value: string): StyleRestore { const previous = el.style.getPropertyValue(property) const priority = el.style.getPropertyPriority(property) el.style.setProperty(property, value) return () => { if (previous) { el.style.setProperty(property, previous, priority) } else { el.style.removeProperty(property) } } } /** Collapse on-screen page gaps and zoom offsets so print matches page size. */ function adjustPageStackForPrint( stack: HTMLElement, pageLayout: DocPageLayout, pageCount: number ): StyleRestore { const restores: StyleRestore[] = [] const pageWidth = pageLayout.widthPx const pageHeight = pageLayout.heightPx const pageStep = pageHeight + DOCS_PAGE_GAP_PX const gapTotal = Math.max(0, pageCount - 1) * DOCS_PAGE_GAP_PX restores.push(patchInlineStyle(stack, "transform", "none")) restores.push(patchInlineStyle(stack, "left", "0")) restores.push(patchInlineStyle(stack, "width", `${pageWidth}px`)) let wrapper = stack.parentElement while (wrapper && !wrapper.classList.contains("ultidrive-docs-canvas")) { restores.push(patchInlineStyle(wrapper, "width", `${pageWidth}px`)) restores.push(patchInlineStyle(wrapper, "height", "auto")) restores.push(patchInlineStyle(wrapper, "min-height", "0")) restores.push(patchInlineStyle(wrapper, "padding-top", "0")) restores.push(patchInlineStyle(wrapper, "padding-bottom", "0")) restores.push(patchInlineStyle(wrapper, "margin-left", "0")) restores.push(patchInlineStyle(wrapper, "margin-right", "0")) wrapper = wrapper.parentElement } const stackHeight = parseFloat(stack.style.height) if (Number.isFinite(stackHeight) && gapTotal > 0) { restores.push(patchInlineStyle(stack, "height", `${stackHeight - gapTotal}px`)) } stack.querySelectorAll("[style]").forEach((el) => { const top = parseFloat(el.style.top) if (!Number.isFinite(top) || top <= 0) return const pageIndex = Math.floor(top / pageStep) const nextTop = top - pageIndex * DOCS_PAGE_GAP_PX if (nextTop !== top) { restores.push(patchInlineStyle(el, "top", `${nextTop}px`)) } }) stack.querySelectorAll(".docs-page-flow-spacer").forEach((spacer) => { const height = parseFloat(spacer.style.height) if (Number.isFinite(height) && height >= DOCS_PAGE_GAP_PX) { restores.push( patchInlineStyle(spacer, "height", `${height - DOCS_PAGE_GAP_PX}px`) ) } }) const surface = stack.querySelector(".ultidrive-docs-editor-surface") if (surface) { const surfaceHeight = parseFloat(surface.style.height) if (Number.isFinite(surfaceHeight) && gapTotal > 0) { restores.push( patchInlineStyle(surface, "height", `${surfaceHeight - gapTotal}px`) ) } } return () => { for (let i = restores.length - 1; i >= 0; i -= 1) { restores[i]() } } } function buildDynamicPrintStyles( pageLayout: DocPageLayout, paragraphStyles: DocParagraphStylesCatalog ): string { return [ buildPageRule(pageLayout), buildPrintLayoutRules(pageLayout), buildParagraphStylesCss(paragraphStyles, ".ultidrive-richtext-editor, .ultidrive-richtext-region-editor"), ].join("\n") } function injectPrintStyles( pageLayout: DocPageLayout, paragraphStyles: DocParagraphStylesCatalog ): () => void { const existing = document.getElementById(PRINT_STYLE_ID) existing?.remove() const style = document.createElement("style") style.id = PRINT_STYLE_ID style.textContent = buildDynamicPrintStyles(pageLayout, paragraphStyles) document.head.appendChild(style) return () => { style.remove() } } function copyDocumentStyles(targetDoc: Document): void { document.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => { targetDoc.head.appendChild(node.cloneNode(true)) }) } async function waitForDocumentReady(doc: Document): Promise { const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')) await Promise.all( links.map( (link) => new Promise((resolve) => { const sheet = link as HTMLLinkElement if (sheet.sheet) { resolve() return } link.addEventListener("load", () => resolve(), { once: true }) link.addEventListener("error", () => resolve(), { once: true }) }) ) ) if (doc.fonts?.ready) { await doc.fonts.ready } await new Promise((resolve) => requestAnimationFrame(() => resolve())) await new Promise((resolve) => requestAnimationFrame(() => resolve())) } function removePrintIframe(): void { document.getElementById(PRINT_IFRAME_ID)?.remove() } async function printStackInIframe( stack: HTMLElement, snapshot: DocsExportSnapshot ): Promise { removePrintIframe() const iframe = document.createElement("iframe") iframe.id = PRINT_IFRAME_ID iframe.setAttribute("aria-hidden", "true") iframe.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0;pointer-events:none" document.body.appendChild(iframe) const doc = iframe.contentDocument const win = iframe.contentWindow if (!doc || !win) { removePrintIframe() throw new Error("Impossible de prĂ©parer la fenĂȘtre d'impression") } copyDocumentStyles(doc) const printStyle = doc.createElement("style") printStyle.textContent = buildDynamicPrintStyles( snapshot.pageLayout, snapshot.paragraphStyles ) doc.head.appendChild(printStyle) doc.documentElement.classList.add("docs-printing") const root = doc.createElement("div") root.className = "docs-print-root" root.appendChild(stack.cloneNode(true)) doc.body.replaceChildren(root) await waitForDocumentReady(doc) await new Promise((resolve, reject) => { const cleanup = () => { win.removeEventListener("afterprint", onAfterPrint) removePrintIframe() } const onAfterPrint = () => { cleanup() resolve() } win.addEventListener("afterprint", onAfterPrint) try { win.focus() win.print() } catch (error) { cleanup() reject(error) return } window.setTimeout(() => { if (document.getElementById(PRINT_IFRAME_ID)) { cleanup() resolve() } }, 2000) }) } async function waitForPrintLayout(): Promise { if (typeof document !== "undefined" && document.fonts?.ready) { await document.fonts.ready } await new Promise((resolve) => requestAnimationFrame(() => resolve())) await new Promise((resolve) => requestAnimationFrame(() => resolve())) } export type DocsPrintMode = "print" | "pdf-capture" export async function prepareDocsPrintEnvironment( snapshot: DocsExportSnapshot, mode: DocsPrintMode = "print" ): Promise<() => void> { const root = document.documentElement root.classList.add("docs-printing") if (mode === "pdf-capture") { root.classList.add("docs-pdf-capture") } const removeStyles = injectPrintStyles(snapshot.pageLayout, snapshot.paragraphStyles) const stack = snapshot.getPageStackElement() const restoreStackLayout = stack != null ? adjustPageStackForPrint(stack, snapshot.pageLayout, snapshot.pageCount) : () => {} await waitForPrintLayout() return () => { restoreStackLayout() root.classList.remove("docs-printing", "docs-pdf-capture") removeStyles() } } export async function printDocsDocument(snapshot: DocsExportSnapshot): Promise { const cleanup = await prepareDocsPrintEnvironment(snapshot, "print") try { const stack = snapshot.getPageStackElement() if (!stack) { window.print() return } await printStackInIframe(stack, snapshot) } finally { cleanup() } }