ultisuite-client/lib/drive/docs-print.ts
R3D347HR4Y 82ca9a27db
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(drive): refactor document and drawing editors with new metadata handling
- Replaced suite page metadata with drive-specific metadata for document and drawing editors.
- Introduced new `driveEditorPageMetadata` function to manage titles and favicons based on editor type.
- Updated layout components for document and drawing editors to utilize the new metadata structure.
- Enhanced document title handling in various editor components to reflect the current editing context.
- Added new SVG icons for UltiDocs, UltiSheets, UltiSlides, and UltiDraw to improve visual consistency across editors.
- Improved print styles and layout handling for better document rendering in print and PDF formats.
2026-06-15 15:51:09 +02:00

349 lines
10 KiB
TypeScript

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<HTMLElement>("[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<HTMLElement>(".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<HTMLElement>(".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<void> {
const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]'))
await Promise.all(
links.map(
(link) =>
new Promise<void>((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<void>((resolve) => requestAnimationFrame(() => resolve()))
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
}
function removePrintIframe(): void {
document.getElementById(PRINT_IFRAME_ID)?.remove()
}
async function printStackInIframe(
stack: HTMLElement,
snapshot: DocsExportSnapshot
): Promise<void> {
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<void>((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<void> {
if (typeof document !== "undefined" && document.fonts?.ready) {
await document.fonts.ready
}
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
await new Promise<void>((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<void> {
const cleanup = await prepareDocsPrintEnvironment(snapshot, "print")
try {
const stack = snapshot.getPageStackElement()
if (!stack) {
window.print()
return
}
await printStackInIframe(stack, snapshot)
} finally {
cleanup()
}
}