Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
349 lines
10 KiB
TypeScript
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()
|
|
}
|
|
}
|