feat(drive): update document printing and PDF export functionality
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Replaced `html2canvas` with `html2canvas-pro` for improved rendering in document capture. - Enhanced error handling in print functions to provide user feedback on print failures. - Introduced new `docs-page-capture` module to streamline page capture logic for PDF exports. - Refactored PDF export process to utilize captured canvases, improving performance and reliability. - Updated print styles for better document layout during printing and PDF generation.
This commit is contained in:
parent
76eff3c351
commit
bd9605c853
@ -671,12 +671,13 @@ export function RichTextDocumentEditor({
|
|||||||
const handlePrintDocument = useCallback(async () => {
|
const handlePrintDocument = useCallback(async () => {
|
||||||
const snapshot = getExportSnapshot()
|
const snapshot = getExportSnapshot()
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
window.print()
|
toast.error("Impossible d'imprimer le document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await printDocsDocument(snapshot)
|
await printDocsDocument(snapshot)
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error("[docs] print failed", error)
|
||||||
toast.error("Impossible d'imprimer le document")
|
toast.error("Impossible d'imprimer le document")
|
||||||
}
|
}
|
||||||
}, [getExportSnapshot])
|
}, [getExportSnapshot])
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -193,7 +194,13 @@ function DocsToolbarInner({
|
|||||||
sepAfter: false,
|
sepAfter: false,
|
||||||
node: (
|
node: (
|
||||||
<>
|
<>
|
||||||
<ToolbarIconBtn label="Imprimer" onClick={() => onPrint?.() ?? window.print()}>
|
<ToolbarIconBtn
|
||||||
|
label="Imprimer"
|
||||||
|
onClick={() => {
|
||||||
|
if (onPrint) onPrint()
|
||||||
|
else toast.error("Impossible d'imprimer le document")
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Printer className="size-4" />
|
<Printer className="size-4" />
|
||||||
</ToolbarIconBtn>
|
</ToolbarIconBtn>
|
||||||
<ToolbarIconBtn
|
<ToolbarIconBtn
|
||||||
|
|||||||
255
lib/drive/docs-page-capture.ts
Normal file
255
lib/drive/docs-page-capture.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import type { DocPageLayout } from "@/lib/drive/doc-page-setup"
|
||||||
|
import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants"
|
||||||
|
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
||||||
|
|
||||||
|
export type DocsPageCaptureTarget = {
|
||||||
|
stack: HTMLElement
|
||||||
|
pageWidth: number
|
||||||
|
pageHeight: number
|
||||||
|
pageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDocsPageCaptureTarget(
|
||||||
|
snapshot: DocsExportSnapshot
|
||||||
|
): DocsPageCaptureTarget | null {
|
||||||
|
const stack = snapshot.getPageStackElement()
|
||||||
|
if (!stack) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
stack,
|
||||||
|
pageWidth: Number(stack.dataset.docsPageWidth ?? snapshot.pageLayout.widthPx),
|
||||||
|
pageHeight: Number(stack.dataset.docsPageHeight ?? snapshot.pageLayout.heightPx),
|
||||||
|
pageCount: snapshot.pageCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStackCloneForCapture(
|
||||||
|
stack: HTMLElement,
|
||||||
|
pageLayout: DocPageLayout,
|
||||||
|
pageCount: number
|
||||||
|
): void {
|
||||||
|
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
|
||||||
|
|
||||||
|
stack.style.transform = "none"
|
||||||
|
stack.style.transformOrigin = "top left"
|
||||||
|
stack.style.left = "0"
|
||||||
|
stack.style.marginLeft = "0"
|
||||||
|
stack.style.position = "relative"
|
||||||
|
stack.style.width = `${pageWidth}px`
|
||||||
|
stack.classList.remove("left-1/2", "-translate-x-1/2", "top-0")
|
||||||
|
|
||||||
|
stack.querySelectorAll<HTMLElement>(".docs-page-gap-band").forEach((el) => {
|
||||||
|
el.style.display = "none"
|
||||||
|
})
|
||||||
|
stack.querySelectorAll<HTMLElement>(".docs-page-inter-margin-gutter").forEach((el) => {
|
||||||
|
el.style.display = "none"
|
||||||
|
})
|
||||||
|
|
||||||
|
const stackHeight = parseFloat(stack.style.height)
|
||||||
|
if (Number.isFinite(stackHeight) && gapTotal > 0) {
|
||||||
|
stack.style.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)
|
||||||
|
el.style.top = `${top - pageIndex * DOCS_PAGE_GAP_PX}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) {
|
||||||
|
spacer.style.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) {
|
||||||
|
surface.style.height = `${surfaceHeight - gapTotal}px`
|
||||||
|
}
|
||||||
|
surface.removeAttribute("contenteditable")
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.querySelectorAll<HTMLElement>(".ProseMirror").forEach((el) => {
|
||||||
|
el.removeAttribute("contenteditable")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneStylesIntoDocument(targetDoc: Document): Promise<void> {
|
||||||
|
const head = targetDoc.head
|
||||||
|
const loads: Promise<void>[] = []
|
||||||
|
|
||||||
|
const styleNodes = document.querySelectorAll('link[rel="stylesheet"], style')
|
||||||
|
for (const node of styleNodes) {
|
||||||
|
if (node instanceof HTMLLinkElement) {
|
||||||
|
const href = node.href
|
||||||
|
if (!href) continue
|
||||||
|
const link = targetDoc.createElement("link")
|
||||||
|
link.rel = "stylesheet"
|
||||||
|
link.href = href
|
||||||
|
if (node.media) link.media = node.media
|
||||||
|
loads.push(
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
link.onload = () => resolve()
|
||||||
|
link.onerror = () => resolve()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
head.appendChild(link)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
head.appendChild(node.cloneNode(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDoc.documentElement.lang = document.documentElement.lang
|
||||||
|
targetDoc.documentElement.className = document.documentElement.className
|
||||||
|
targetDoc.documentElement.classList.remove("dark")
|
||||||
|
targetDoc.documentElement.style.background = "#ffffff"
|
||||||
|
|
||||||
|
await Promise.all(loads)
|
||||||
|
if (targetDoc.fonts?.ready) {
|
||||||
|
await targetDoc.fonts.ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCaptureFrame(
|
||||||
|
stack: HTMLElement,
|
||||||
|
pageLayout: DocPageLayout,
|
||||||
|
pageCount: number
|
||||||
|
): Promise<{ root: HTMLElement; dispose: () => void }> {
|
||||||
|
const pageWidth = pageLayout.widthPx
|
||||||
|
const pageHeight = pageLayout.heightPx
|
||||||
|
const stageHeight = pageCount * pageHeight
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe")
|
||||||
|
iframe.setAttribute("aria-hidden", "true")
|
||||||
|
iframe.style.cssText =
|
||||||
|
"position:fixed;top:0;left:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none;overflow:hidden"
|
||||||
|
|
||||||
|
const docReady = new Promise<Document>((resolve, reject) => {
|
||||||
|
iframe.onload = () => {
|
||||||
|
const doc = iframe.contentDocument
|
||||||
|
if (doc) resolve(doc)
|
||||||
|
else reject(new Error("Impossible de préparer la capture"))
|
||||||
|
}
|
||||||
|
iframe.onerror = () => reject(new Error("Impossible de préparer la capture"))
|
||||||
|
})
|
||||||
|
|
||||||
|
iframe.src = "about:blank"
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
|
||||||
|
const doc = await docReady
|
||||||
|
await cloneStylesIntoDocument(doc)
|
||||||
|
|
||||||
|
const clone = stack.cloneNode(true) as HTMLElement
|
||||||
|
normalizeStackCloneForCapture(clone, pageLayout, pageCount)
|
||||||
|
|
||||||
|
doc.body.replaceChildren()
|
||||||
|
doc.body.style.cssText = `margin:0;padding:0;background:#ffffff;width:${pageWidth}px;height:${stageHeight}px;overflow:hidden`
|
||||||
|
|
||||||
|
const mount = doc.createElement("div")
|
||||||
|
mount.style.cssText = `position:relative;width:${pageWidth}px;height:${stageHeight}px;overflow:hidden`
|
||||||
|
mount.appendChild(clone)
|
||||||
|
doc.body.appendChild(mount)
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: clone,
|
||||||
|
dispose: () => {
|
||||||
|
iframe.remove()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rasterizePageSlice(
|
||||||
|
element: HTMLElement,
|
||||||
|
pageWidth: number,
|
||||||
|
pageHeight: number,
|
||||||
|
yOffset: number,
|
||||||
|
scale: number
|
||||||
|
): Promise<HTMLCanvasElement> {
|
||||||
|
const { default: html2canvas } = await import("html2canvas-pro")
|
||||||
|
const canvas = await html2canvas(element, {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
scale,
|
||||||
|
useCORS: true,
|
||||||
|
logging: false,
|
||||||
|
width: pageWidth,
|
||||||
|
height: pageHeight,
|
||||||
|
x: 0,
|
||||||
|
y: yOffset,
|
||||||
|
scrollX: 0,
|
||||||
|
scrollY: 0,
|
||||||
|
windowWidth: pageWidth,
|
||||||
|
windowHeight: element.scrollHeight,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (canvas.width === 0 || canvas.height === 0) {
|
||||||
|
throw new Error("Capture page vide")
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withFrozenViewport<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
const scrollX = window.scrollX
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
const html = document.documentElement
|
||||||
|
const body = document.body
|
||||||
|
const prevHtmlOverflow = html.style.overflow
|
||||||
|
const prevBodyOverflow = body.style.overflow
|
||||||
|
|
||||||
|
html.style.overflow = "hidden"
|
||||||
|
body.style.overflow = "hidden"
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
|
html.style.overflow = prevHtmlOverflow
|
||||||
|
body.style.overflow = prevBodyOverflow
|
||||||
|
window.scrollTo(scrollX, scrollY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rasterize each page from an isolated iframe clone — no live UI mutation. */
|
||||||
|
export async function captureDocsPagesFromCanvas(
|
||||||
|
snapshot: DocsExportSnapshot,
|
||||||
|
options?: { scale?: number }
|
||||||
|
): Promise<HTMLCanvasElement[]> {
|
||||||
|
const target = resolveDocsPageCaptureTarget(snapshot)
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("Page stack introuvable")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stack, pageWidth, pageHeight, pageCount } = target
|
||||||
|
const preferredScale = options?.scale ?? 2
|
||||||
|
|
||||||
|
return withFrozenViewport(async () => {
|
||||||
|
const { root, dispose } = await createCaptureFrame(stack, snapshot.pageLayout, pageCount)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvases: HTMLCanvasElement[] = []
|
||||||
|
for (let pageIndex = 0; pageIndex < pageCount; pageIndex += 1) {
|
||||||
|
const yOffset = pageIndex * pageHeight
|
||||||
|
let canvas: HTMLCanvasElement
|
||||||
|
try {
|
||||||
|
canvas = await rasterizePageSlice(root, pageWidth, pageHeight, yOffset, preferredScale)
|
||||||
|
} catch (firstError) {
|
||||||
|
if (preferredScale === 1) throw firstError
|
||||||
|
canvas = await rasterizePageSlice(root, pageWidth, pageHeight, yOffset, 1)
|
||||||
|
}
|
||||||
|
canvases.push(canvas)
|
||||||
|
}
|
||||||
|
return canvases
|
||||||
|
} finally {
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants"
|
|
||||||
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
||||||
import { prepareDocsPrintEnvironment } from "@/lib/drive/docs-print"
|
import { captureDocsPagesFromCanvas } from "@/lib/drive/docs-page-capture"
|
||||||
import { exportFileName } from "@/lib/drive/docs-export-download"
|
import { exportFileName } from "@/lib/drive/docs-export-download"
|
||||||
|
|
||||||
function downloadBlob(blob: Blob, fileName: string) {
|
function downloadBlob(blob: Blob, fileName: string) {
|
||||||
@ -13,23 +12,16 @@ function downloadBlob(blob: Blob, fileName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function exportDocsToPdf(snapshot: DocsExportSnapshot): Promise<void> {
|
export async function exportDocsToPdf(snapshot: DocsExportSnapshot): Promise<void> {
|
||||||
const cleanup = await prepareDocsPrintEnvironment(snapshot, "pdf-capture")
|
const canvases = await captureDocsPagesFromCanvas(snapshot, { scale: 2 })
|
||||||
|
|
||||||
try {
|
const [{ jsPDF }] = await Promise.all([import("jspdf")])
|
||||||
const stack = snapshot.getPageStackElement()
|
|
||||||
if (!stack) throw new Error("Page stack introuvable")
|
|
||||||
|
|
||||||
const canvasHost = stack.closest(".ultidrive-docs-canvas") as HTMLElement | null
|
const pageWidth = Number(
|
||||||
if (!canvasHost) throw new Error("Canvas introuvable")
|
snapshot.getPageStackElement()?.dataset.docsPageWidth ?? snapshot.pageLayout.widthPx
|
||||||
|
)
|
||||||
const pageHeight = Number(stack.dataset.docsPageHeight ?? snapshot.pageLayout.heightPx)
|
const pageHeight = Number(
|
||||||
const pageWidth = Number(stack.dataset.docsPageWidth ?? snapshot.pageLayout.widthPx)
|
snapshot.getPageStackElement()?.dataset.docsPageHeight ?? snapshot.pageLayout.heightPx
|
||||||
const pageCount = snapshot.pageCount
|
)
|
||||||
|
|
||||||
const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
|
|
||||||
import("html2canvas"),
|
|
||||||
import("jspdf"),
|
|
||||||
])
|
|
||||||
|
|
||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
orientation: pageWidth > pageHeight ? "landscape" : "portrait",
|
orientation: pageWidth > pageHeight ? "landscape" : "portrait",
|
||||||
@ -40,25 +32,8 @@ export async function exportDocsToPdf(snapshot: DocsExportSnapshot): Promise<voi
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const hostRect = canvasHost.getBoundingClientRect()
|
for (let pageIndex = 0; pageIndex < canvases.length; pageIndex += 1) {
|
||||||
const stackRect = stack.getBoundingClientRect()
|
const imgData = canvases[pageIndex].toDataURL("image/jpeg", 0.92)
|
||||||
|
|
||||||
for (let pageIndex = 0; pageIndex < pageCount; pageIndex += 1) {
|
|
||||||
const yOffset = pageIndex * (pageHeight + DOCS_PAGE_GAP_PX)
|
|
||||||
const canvas = await html2canvas(canvasHost, {
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
scale: 2,
|
|
||||||
useCORS: true,
|
|
||||||
logging: false,
|
|
||||||
width: pageWidth,
|
|
||||||
height: pageHeight,
|
|
||||||
x: stackRect.left - hostRect.left,
|
|
||||||
y: stackRect.top - hostRect.top + yOffset,
|
|
||||||
windowWidth: canvasHost.scrollWidth,
|
|
||||||
windowHeight: canvasHost.scrollHeight,
|
|
||||||
})
|
|
||||||
|
|
||||||
const imgData = canvas.toDataURL("image/jpeg", 0.92)
|
|
||||||
if (pageIndex > 0) pdf.addPage()
|
if (pageIndex > 0) pdf.addPage()
|
||||||
pdf.addImage(
|
pdf.addImage(
|
||||||
imgData,
|
imgData,
|
||||||
@ -72,7 +47,4 @@ export async function exportDocsToPdf(snapshot: DocsExportSnapshot): Promise<voi
|
|||||||
|
|
||||||
const blob = pdf.output("blob")
|
const blob = pdf.output("blob")
|
||||||
downloadBlob(blob, exportFileName(snapshot.sourceName, "pdf"))
|
downloadBlob(blob, exportFileName(snapshot.sourceName, "pdf"))
|
||||||
} finally {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import type { DocPageLayout } from "@/lib/drive/doc-page-setup"
|
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 type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
|
||||||
import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants"
|
import { captureDocsPagesFromCanvas } from "@/lib/drive/docs-page-capture"
|
||||||
|
|
||||||
const PRINT_STYLE_ID = "docs-print-dynamic-styles"
|
|
||||||
const PRINT_IFRAME_ID = "docs-print-iframe"
|
const PRINT_IFRAME_ID = "docs-print-iframe"
|
||||||
|
|
||||||
function buildPageRule(pageLayout: DocPageLayout): string {
|
function buildPageRule(pageLayout: DocPageLayout): string {
|
||||||
@ -15,257 +12,147 @@ function buildPageRule(pageLayout: DocPageLayout): string {
|
|||||||
return `@page { size: ${size}; margin: 0; }`
|
return `@page { size: ${size}; margin: 0; }`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPrintLayoutRules(pageLayout: DocPageLayout): string {
|
function buildPrintStylesheet(pageLayout: DocPageLayout): string {
|
||||||
const pageWidth = pageLayout.widthPx
|
const pageWidth = pageLayout.widthPx
|
||||||
const pageHeight = pageLayout.heightPx
|
const pageHeight = pageLayout.heightPx
|
||||||
return `
|
return `
|
||||||
|
${buildPageRule(pageLayout)}
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0 !important;
|
margin: 0;
|
||||||
padding: 0 !important;
|
padding: 0;
|
||||||
background: white !important;
|
background: white;
|
||||||
width: ${pageWidth}px !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
}
|
}
|
||||||
|
.docs-print-page {
|
||||||
.docs-print-root {
|
width: ${pageWidth}px;
|
||||||
margin: 0 !important;
|
height: ${pageHeight}px;
|
||||||
padding: 0 !important;
|
overflow: hidden;
|
||||||
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;
|
break-after: page;
|
||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
print-color-adjust: exact;
|
print-color-adjust: exact;
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
|
.docs-print-page:last-child {
|
||||||
.ultidrive-docs-page:last-child {
|
|
||||||
break-after: auto;
|
break-after: auto;
|
||||||
page-break-after: auto;
|
page-break-after: auto;
|
||||||
}
|
}
|
||||||
|
.docs-print-page img {
|
||||||
.docs-body-margin-mask {
|
display: block;
|
||||||
border-color: transparent !important;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
.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 canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
function patchInlineStyle(el: HTMLElement, property: string, value: string): StyleRestore {
|
canvas.toBlob(
|
||||||
const previous = el.style.getPropertyValue(property)
|
(blob) => {
|
||||||
const priority = el.style.getPropertyPriority(property)
|
if (blob) resolve(blob)
|
||||||
el.style.setProperty(property, value)
|
else reject(new Error("Impossible de convertir la page en image"))
|
||||||
return () => {
|
},
|
||||||
if (previous) {
|
"image/jpeg",
|
||||||
el.style.setProperty(property, previous, priority)
|
0.92
|
||||||
} 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 {
|
function removePrintIframe(): void {
|
||||||
document.getElementById(PRINT_IFRAME_ID)?.remove()
|
document.getElementById(PRINT_IFRAME_ID)?.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printStackInIframe(
|
async function mountPrintDocument(
|
||||||
stack: HTMLElement,
|
doc: Document,
|
||||||
snapshot: DocsExportSnapshot
|
canvases: HTMLCanvasElement[],
|
||||||
|
pageLayout: DocPageLayout
|
||||||
|
): Promise<() => void> {
|
||||||
|
const pageWidth = pageLayout.widthPx
|
||||||
|
const pageHeight = pageLayout.heightPx
|
||||||
|
const objectUrls: string[] = []
|
||||||
|
|
||||||
|
doc.documentElement.lang = "fr"
|
||||||
|
doc.head.replaceChildren()
|
||||||
|
doc.body.replaceChildren()
|
||||||
|
|
||||||
|
const style = doc.createElement("style")
|
||||||
|
style.textContent = buildPrintStylesheet(pageLayout)
|
||||||
|
doc.head.appendChild(style)
|
||||||
|
|
||||||
|
for (const canvas of canvases) {
|
||||||
|
const blob = await canvasToBlob(canvas)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
objectUrls.push(url)
|
||||||
|
|
||||||
|
const page = doc.createElement("div")
|
||||||
|
page.className = "docs-print-page"
|
||||||
|
page.style.width = `${pageWidth}px`
|
||||||
|
page.style.height = `${pageHeight}px`
|
||||||
|
|
||||||
|
const img = doc.createElement("img")
|
||||||
|
img.alt = ""
|
||||||
|
img.width = pageWidth
|
||||||
|
img.height = pageHeight
|
||||||
|
img.src = url
|
||||||
|
|
||||||
|
page.appendChild(img)
|
||||||
|
doc.body.appendChild(page)
|
||||||
|
await img.decode().catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const url of objectUrls) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printPageCanvasesInIframe(
|
||||||
|
canvases: HTMLCanvasElement[],
|
||||||
|
pageLayout: DocPageLayout
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (canvases.length === 0) {
|
||||||
|
throw new Error("Aucune page à imprimer")
|
||||||
|
}
|
||||||
|
|
||||||
removePrintIframe()
|
removePrintIframe()
|
||||||
|
|
||||||
const iframe = document.createElement("iframe")
|
const iframe = document.createElement("iframe")
|
||||||
iframe.id = PRINT_IFRAME_ID
|
iframe.id = PRINT_IFRAME_ID
|
||||||
iframe.setAttribute("aria-hidden", "true")
|
iframe.setAttribute("aria-hidden", "true")
|
||||||
iframe.style.cssText =
|
iframe.style.cssText =
|
||||||
"position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0;pointer-events:none"
|
"position:fixed;left:-100000px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none"
|
||||||
|
|
||||||
|
const docReady = new Promise<Document>((resolve, reject) => {
|
||||||
|
iframe.onload = () => {
|
||||||
|
const doc = iframe.contentDocument
|
||||||
|
if (doc) resolve(doc)
|
||||||
|
else reject(new Error("Impossible de préparer la fenêtre d'impression"))
|
||||||
|
}
|
||||||
|
iframe.onerror = () => reject(new Error("Impossible de préparer la fenêtre d'impression"))
|
||||||
|
})
|
||||||
|
|
||||||
|
iframe.src = "about:blank"
|
||||||
document.body.appendChild(iframe)
|
document.body.appendChild(iframe)
|
||||||
|
|
||||||
const doc = iframe.contentDocument
|
const doc = await docReady
|
||||||
const win = iframe.contentWindow
|
const win = iframe.contentWindow
|
||||||
if (!doc || !win) {
|
if (!win) {
|
||||||
removePrintIframe()
|
removePrintIframe()
|
||||||
throw new Error("Impossible de préparer la fenêtre d'impression")
|
throw new Error("Impossible de préparer la fenêtre d'impression")
|
||||||
}
|
}
|
||||||
|
|
||||||
copyDocumentStyles(doc)
|
let revokeObjectUrls = () => {}
|
||||||
|
try {
|
||||||
|
revokeObjectUrls = await mountPrintDocument(doc, canvases, pageLayout)
|
||||||
|
|
||||||
const printStyle = doc.createElement("style")
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
printStyle.textContent = buildDynamicPrintStyles(
|
if (doc.fonts?.ready) {
|
||||||
snapshot.pageLayout,
|
await doc.fonts.ready
|
||||||
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) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
win.removeEventListener("afterprint", onAfterPrint)
|
win.removeEventListener("afterprint", onAfterPrint)
|
||||||
|
revokeObjectUrls()
|
||||||
removePrintIframe()
|
removePrintIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,57 +179,24 @@ async function printStackInIframe(
|
|||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
revokeObjectUrls()
|
||||||
|
removePrintIframe()
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
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 type DocsPrintMode = "print" | "pdf-capture"
|
||||||
|
|
||||||
|
/** @deprecated Live DOM is no longer mutated; kept for PDF export compatibility. */
|
||||||
export async function prepareDocsPrintEnvironment(
|
export async function prepareDocsPrintEnvironment(
|
||||||
snapshot: DocsExportSnapshot,
|
_snapshot: DocsExportSnapshot,
|
||||||
mode: DocsPrintMode = "print"
|
_mode: DocsPrintMode = "print"
|
||||||
): Promise<() => void> {
|
): Promise<() => void> {
|
||||||
const root = document.documentElement
|
return () => {}
|
||||||
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> {
|
export async function printDocsDocument(snapshot: DocsExportSnapshot): Promise<void> {
|
||||||
const cleanup = await prepareDocsPrintEnvironment(snapshot, "print")
|
const canvases = await captureDocsPagesFromCanvas(snapshot, { scale: 2 })
|
||||||
|
await printPageCanvasesInIframe(canvases, snapshot.pageLayout)
|
||||||
try {
|
|
||||||
const stack = snapshot.getPageStackElement()
|
|
||||||
if (!stack) {
|
|
||||||
window.print()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await printStackInIframe(stack, snapshot)
|
|
||||||
} finally {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,12 +176,13 @@ export function useDocsFileMenu({
|
|||||||
const handlePrint = useCallback(async () => {
|
const handlePrint = useCallback(async () => {
|
||||||
const snapshot = getExportSnapshot()
|
const snapshot = getExportSnapshot()
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
window.print()
|
toast.error("Impossible d'imprimer le document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await printDocsDocument(snapshot)
|
await printDocsDocument(snapshot)
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error("[docs] print failed", error)
|
||||||
toast.error("Impossible d'imprimer le document")
|
toast.error("Impossible d'imprimer le document")
|
||||||
}
|
}
|
||||||
}, [getExportSnapshot])
|
}, [getExportSnapshot])
|
||||||
|
|||||||
@ -99,7 +99,7 @@
|
|||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
"fractional-indexing": "^3.2.0",
|
"fractional-indexing": "^3.2.0",
|
||||||
"fuse.js": "^7.3.0",
|
"fuse.js": "^7.3.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas-pro": "^2.0.4",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
|
|||||||
@ -245,9 +245,9 @@ importers:
|
|||||||
fuse.js:
|
fuse.js:
|
||||||
specifier: ^7.3.0
|
specifier: ^7.3.0
|
||||||
version: 7.3.0
|
version: 7.3.0
|
||||||
html2canvas:
|
html2canvas-pro:
|
||||||
specifier: ^1.4.1
|
specifier: ^2.0.4
|
||||||
version: 1.4.1
|
version: 2.0.4
|
||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.3
|
specifier: ^8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
@ -2720,6 +2720,10 @@ packages:
|
|||||||
hash.js@1.1.7:
|
hash.js@1.1.7:
|
||||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||||
|
|
||||||
|
html2canvas-pro@2.0.4:
|
||||||
|
resolution: {integrity: sha512-tfL8XNvuITvYQJKgAx4bvANauuLKc88C+ZSZt7HZJveqQBWjBDtkqs/It06UzlqbM+sSq7Cv45rFbuUxOFgmow==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
html2canvas@1.4.1:
|
html2canvas@1.4.1:
|
||||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@ -5981,10 +5985,16 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
minimalistic-assert: 1.0.1
|
minimalistic-assert: 1.0.1
|
||||||
|
|
||||||
|
html2canvas-pro@2.0.4:
|
||||||
|
dependencies:
|
||||||
|
css-line-break: 2.1.0
|
||||||
|
text-segmentation: 1.0.3
|
||||||
|
|
||||||
html2canvas@1.4.1:
|
html2canvas@1.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
css-line-break: 2.1.0
|
css-line-break: 2.1.0
|
||||||
text-segmentation: 1.0.3
|
text-segmentation: 1.0.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@ -51,7 +51,6 @@
|
|||||||
|
|
||||||
.docs-printing.docs-pdf-capture .ultidrive-docs-page {
|
.docs-printing.docs-pdf-capture .ultidrive-docs-page {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-printing.docs-pdf-capture .docs-body-margin-mask {
|
.docs-printing.docs-pdf-capture .docs-body-margin-mask {
|
||||||
@ -59,8 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.docs-printing.docs-pdf-capture .docs-page-gap-band,
|
.docs-printing.docs-pdf-capture .docs-page-gap-band,
|
||||||
.docs-printing.docs-pdf-capture .docs-page-inter-margin-gutter,
|
.docs-printing.docs-pdf-capture .docs-page-inter-margin-gutter {
|
||||||
.docs-printing.docs-pdf-capture .docs-page-rim {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user