1066 lines
41 KiB
JavaScript
1066 lines
41 KiB
JavaScript
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
import { t } from "@rustpad/i18n";
|
|
import { toast } from "@rustpad/toast";
|
|
import { loadHighlight, loadMermaid } from "@rustpad/vendor-libs";
|
|
|
|
const STORAGE_PREFIX = "rustpad:pdf-export:";
|
|
|
|
const PAGE_FORMATS = Object.freeze({
|
|
"a4-portrait": { cssSize: "A4 portrait", widthMm: 210, heightMm: 297 },
|
|
"a4-landscape": { cssSize: "A4 landscape", widthMm: 297, heightMm: 210 },
|
|
"a3-portrait": { cssSize: "A3 portrait", widthMm: 297, heightMm: 420 },
|
|
"a3-landscape": { cssSize: "A3 landscape", widthMm: 420, heightMm: 297 },
|
|
"a5-portrait": { cssSize: "A5 portrait", widthMm: 148, heightMm: 210 },
|
|
"letter-portrait": { cssSize: "letter portrait", widthMm: 215.9, heightMm: 279.4 },
|
|
"letter-landscape": { cssSize: "letter landscape", widthMm: 279.4, heightMm: 215.9 },
|
|
"screen-16-9": { cssSize: "338.667mm 190.5mm", widthMm: 338.667, heightMm: 190.5 },
|
|
"screen-4-3": { cssSize: "254mm 190.5mm", widthMm: 254, heightMm: 190.5 },
|
|
});
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function saved(key, fallback) {
|
|
try { return localStorage.getItem(`${STORAGE_PREFIX}${key}`) || fallback; } catch { return fallback; }
|
|
}
|
|
|
|
function save(key, value) {
|
|
try { localStorage.setItem(`${STORAGE_PREFIX}${key}`, String(value)); } catch { /* optional preference */ }
|
|
}
|
|
|
|
function stylesheetLinks() {
|
|
return [...document.querySelectorAll('link[rel="stylesheet"][href]')]
|
|
.map(link => `<link rel="stylesheet" href="${escapeHtml(link.href)}">`)
|
|
.join("");
|
|
}
|
|
|
|
async function ensureSyntaxHighlight(root) {
|
|
const nodes = [...root.querySelectorAll('pre code[class*="language-"]:not(.language-mermaid)')];
|
|
if (!nodes.length) return;
|
|
|
|
let hljs;
|
|
try { hljs = await loadHighlight(); } catch { return; }
|
|
|
|
const highlightText = (text, language) => {
|
|
try {
|
|
return language
|
|
? hljs.highlight(text, { language, ignoreIllegals: true }).value
|
|
: hljs.highlightAuto(text).value;
|
|
} catch {
|
|
return hljs.highlightAuto(text).value;
|
|
}
|
|
};
|
|
|
|
nodes.forEach(node => {
|
|
const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9);
|
|
const lines = [...node.querySelectorAll(".code-line")];
|
|
|
|
if (lines.length) {
|
|
lines.forEach(line => {
|
|
if (line.querySelector('[class*="hljs-"]')) return;
|
|
line.innerHTML = highlightText(line.textContent || "", language);
|
|
});
|
|
node.classList.add("hljs");
|
|
return;
|
|
}
|
|
|
|
if (!node.querySelector('[class*="hljs-"]')) {
|
|
node.innerHTML = highlightText(node.textContent || "", language);
|
|
}
|
|
node.classList.add("hljs");
|
|
});
|
|
}
|
|
|
|
function mmToPx(doc) {
|
|
const probe = doc.createElement("div");
|
|
probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;width:100mm;height:100mm;";
|
|
doc.body.appendChild(probe);
|
|
const pxPerMm = probe.getBoundingClientRect().height / 100;
|
|
probe.remove();
|
|
return pxPerMm;
|
|
}
|
|
|
|
function nextFrame(view) {
|
|
return new Promise(resolve => view.requestAnimationFrame(() => resolve()));
|
|
}
|
|
|
|
async function waitForPrintableLayout(doc) {
|
|
try { await doc.fonts?.ready; } catch { /* print with available fonts */ }
|
|
|
|
const images = [...doc.images];
|
|
if (images.length) {
|
|
const imageReady = Promise.all(images.map(image => {
|
|
if (image.complete) {
|
|
try { return image.decode?.() || Promise.resolve(); } catch { return Promise.resolve(); }
|
|
}
|
|
return new Promise(resolve => {
|
|
image.addEventListener("load", resolve, { once: true });
|
|
image.addEventListener("error", resolve, { once: true });
|
|
});
|
|
}));
|
|
await Promise.race([
|
|
imageReady,
|
|
new Promise(resolve => setTimeout(resolve, 1500)),
|
|
]);
|
|
}
|
|
|
|
await nextFrame(doc.defaultView);
|
|
await nextFrame(doc.defaultView);
|
|
}
|
|
|
|
function isBlankPrintLine(node) {
|
|
if (!(node instanceof Element) || !node.classList.contains("preview-source-line")) return false;
|
|
if (node.textContent.trim()) return false;
|
|
return node.children.length === 1 && node.firstElementChild?.tagName === "BR";
|
|
}
|
|
|
|
function trimTrailingBlankPrintLines(root) {
|
|
let node = root.lastElementChild;
|
|
while (isBlankPrintLine(node)) {
|
|
const remove = node;
|
|
node = node.previousElementSibling;
|
|
remove.remove();
|
|
}
|
|
}
|
|
|
|
function makePdfLayoutMarker(doc, directive) {
|
|
const marker = doc.createElement("div");
|
|
marker.className = "pdf-layout-marker";
|
|
marker.dataset.pdfDirective = directive;
|
|
marker.setAttribute("aria-hidden", "true");
|
|
return marker;
|
|
}
|
|
|
|
function extractPdfLayout(root) {
|
|
const layout = { headerHtml: "", footerHtml: "", verticalAlign: null };
|
|
const headers = [];
|
|
const footers = [];
|
|
|
|
[...root.querySelectorAll(".pdf-layout-marker[data-pdf-directive]")].forEach(marker => {
|
|
const directive = String(marker.dataset.pdfDirective || "").toLowerCase();
|
|
if (directive === "align_top" || directive === "align_center") {
|
|
layout.verticalAlign = directive === "align_center" ? "center" : "top";
|
|
marker.remove();
|
|
return;
|
|
}
|
|
|
|
let block = marker.nextElementSibling;
|
|
while (isBlankPrintLine(block)) {
|
|
const remove = block;
|
|
block = block.nextElementSibling;
|
|
remove.remove();
|
|
}
|
|
marker.remove();
|
|
if (!block || block.classList?.contains("pdf-page-break")) return;
|
|
|
|
block.remove();
|
|
if (directive === "header") headers.push(block.outerHTML);
|
|
if (directive === "footer") footers.push(block.outerHTML);
|
|
});
|
|
|
|
root.querySelectorAll(".pdf-layout-marker").forEach(marker => marker.remove());
|
|
trimTrailingBlankPrintLines(root);
|
|
layout.headerHtml = headers.join("");
|
|
layout.footerHtml = footers.join("");
|
|
return layout;
|
|
}
|
|
|
|
function collapseHeadingGaps(root) {
|
|
root.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(heading => {
|
|
let next = heading.nextElementSibling;
|
|
while (isBlankPrintLine(next)) {
|
|
const remove = next;
|
|
next = next.nextElementSibling;
|
|
remove.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
function directPrintBlock(root, node) {
|
|
let block = node;
|
|
while (block?.parentElement && block.parentElement !== root) block = block.parentElement;
|
|
return block?.parentElement === root ? block : null;
|
|
}
|
|
|
|
function shortLeadBlock(node) {
|
|
if (!node) return false;
|
|
if (node.matches("h1, h2, h3, h4, h5, h6")) return true;
|
|
if (!node.matches("p")) return false;
|
|
const text = node.textContent.trim();
|
|
return text.length > 0 && text.length <= 320;
|
|
}
|
|
|
|
function previousNonBlank(node) {
|
|
let current = node?.previousElementSibling || null;
|
|
while (isBlankPrintLine(current)) current = current.previousElementSibling;
|
|
return current;
|
|
}
|
|
|
|
function groupAtomicVisuals(root) {
|
|
const blocks = new Set();
|
|
root.querySelectorAll(".mermaid, .rustpad-media").forEach(node => {
|
|
const block = directPrintBlock(root, node);
|
|
if (block) blocks.add(block);
|
|
});
|
|
root.querySelectorAll("img").forEach(node => {
|
|
const block = directPrintBlock(root, node);
|
|
if (block && !block.textContent.trim()) blocks.add(block);
|
|
});
|
|
|
|
for (const block of blocks) {
|
|
if (block.parentElement !== root) continue;
|
|
block.classList.add("pdf-atomic-visual");
|
|
|
|
let first = block;
|
|
const lead = previousNonBlank(block);
|
|
if (shortLeadBlock(lead)) {
|
|
first = lead;
|
|
if (lead.matches("p")) {
|
|
const heading = previousNonBlank(lead);
|
|
if (heading?.matches("h1, h2, h3, h4, h5, h6")) first = heading;
|
|
}
|
|
}
|
|
|
|
if (first === block) continue;
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "pdf-keep-unit";
|
|
root.insertBefore(wrapper, first);
|
|
|
|
let current = first;
|
|
while (current) {
|
|
const next = current.nextElementSibling;
|
|
if (isBlankPrintLine(current)) current.remove();
|
|
else wrapper.appendChild(current);
|
|
if (current === block) break;
|
|
current = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isShortList(node) {
|
|
if (!node?.matches?.("ul, ol")) return false;
|
|
const items = [...node.children].filter(child => child.matches?.("li"));
|
|
if (!items.length || items.length > 8) return false;
|
|
if (node.querySelector(":scope > li > ul, :scope > li > ol")) return false;
|
|
return node.textContent.trim().length <= 900;
|
|
}
|
|
|
|
function isShortCodeBlock(node) {
|
|
if (!node?.matches?.("pre")) return false;
|
|
const text = node.textContent || "";
|
|
const lines = text ? text.split(/\r?\n/).length : 0;
|
|
return lines > 0 && lines <= 12 && text.length <= 1400;
|
|
}
|
|
|
|
function markShortKeepBlocks(root) {
|
|
root.querySelectorAll("ul, ol").forEach(list => {
|
|
if (isShortList(list)) list.classList.add("pdf-keep-short");
|
|
});
|
|
root.querySelectorAll("pre").forEach(pre => {
|
|
if (isShortCodeBlock(pre)) pre.classList.add("pdf-keep-short");
|
|
});
|
|
}
|
|
|
|
function isHeadingLead(node) {
|
|
if (!node || node.classList?.contains("pdf-page-break")) return false;
|
|
if (node.matches?.("p, blockquote")) {
|
|
const text = node.textContent.trim();
|
|
return text.length > 0 && text.length <= 520;
|
|
}
|
|
return isShortList(node) || isShortCodeBlock(node);
|
|
}
|
|
|
|
function groupHeadingsWithLead(root) {
|
|
const headings = [...root.querySelectorAll(":scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6")];
|
|
|
|
headings.forEach(heading => {
|
|
if (heading.parentElement !== root) return;
|
|
|
|
let lead = heading.nextElementSibling;
|
|
while (isBlankPrintLine(lead)) {
|
|
const remove = lead;
|
|
lead = lead.nextElementSibling;
|
|
remove.remove();
|
|
}
|
|
if (!isHeadingLead(lead)) return;
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "pdf-keep-unit pdf-heading-lead";
|
|
root.insertBefore(wrapper, heading);
|
|
wrapper.appendChild(heading);
|
|
wrapper.appendChild(lead);
|
|
|
|
if (lead.matches?.("p, blockquote")) {
|
|
let next = wrapper.nextElementSibling;
|
|
while (isBlankPrintLine(next)) {
|
|
const remove = next;
|
|
next = next.nextElementSibling;
|
|
remove.remove();
|
|
}
|
|
if (isShortList(next) || isShortCodeBlock(next)) wrapper.appendChild(next);
|
|
}
|
|
});
|
|
}
|
|
|
|
function markAtomicTextBlocks(root) {
|
|
root.querySelectorAll("h1, h2, h3, h4, h5, h6, p, blockquote, dt, dd").forEach(node => {
|
|
if (!node.closest(".pdf-keep-unit")) node.classList.add("pdf-atomic-text");
|
|
});
|
|
}
|
|
|
|
function preparePrintPagination(root) {
|
|
collapseHeadingGaps(root);
|
|
markShortKeepBlocks(root);
|
|
groupAtomicVisuals(root);
|
|
groupHeadingsWithLead(root);
|
|
markAtomicTextBlocks(root);
|
|
}
|
|
|
|
async function ensurePrintableMermaid(root, darkMode = false) {
|
|
const nodes = [...root.querySelectorAll(".mermaid")];
|
|
if (!nodes.length) return;
|
|
|
|
root.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
|
|
|
const originals = nodes.map(node => ({
|
|
node,
|
|
html: node.innerHTML,
|
|
processed: node.getAttribute("data-processed"),
|
|
source: node.dataset.mermaidSource || node.textContent || "",
|
|
hadSvg: Boolean(node.querySelector("svg")),
|
|
}));
|
|
const renderable = originals.filter(item => item.node.dataset.mermaidSource || !item.hadSvg);
|
|
if (!renderable.length) return;
|
|
|
|
const staging = document.createElement("div");
|
|
staging.setAttribute("aria-hidden", "true");
|
|
staging.style.cssText = "position:fixed;left:-200vw;top:0;width:1200px;visibility:hidden;pointer-events:none;z-index:-1";
|
|
|
|
try {
|
|
const mermaid = await loadMermaid();
|
|
renderable.forEach(({ node, source }) => {
|
|
node.removeAttribute("data-processed");
|
|
node.textContent = source;
|
|
});
|
|
|
|
staging.appendChild(root);
|
|
document.body.appendChild(staging);
|
|
mermaid.initialize({ startOnLoad: false, theme: darkMode ? "dark" : "default", securityLevel: "strict" });
|
|
await mermaid.run({ nodes: renderable.map(item => item.node) });
|
|
root.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
|
} catch {
|
|
originals.forEach(({ node, html, processed, hadSvg }) => {
|
|
node.innerHTML = html;
|
|
if (processed === null) node.removeAttribute("data-processed");
|
|
else node.setAttribute("data-processed", processed);
|
|
if (hadSvg) return;
|
|
const message = document.createElement("p");
|
|
message.className = "error mermaid-error";
|
|
message.textContent = t("public.mermaidFailed", {}, "Failed to load Mermaid.");
|
|
node.parentNode?.insertBefore(message, node);
|
|
});
|
|
} finally {
|
|
if (root.parentElement === staging) staging.removeChild(root);
|
|
staging.remove();
|
|
}
|
|
}
|
|
|
|
function measureLastPageFill(doc, documentRoot, pageContentHeight) {
|
|
const content = documentRoot.querySelector(":scope > .pdf-content");
|
|
if (!content) return null;
|
|
|
|
const width = documentRoot.getBoundingClientRect().width;
|
|
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(pageContentHeight) || pageContentHeight <= 0) return null;
|
|
|
|
const probe = doc.createElement("div");
|
|
probe.setAttribute("aria-hidden", "true");
|
|
probe.style.cssText = [
|
|
"position:absolute",
|
|
"visibility:hidden",
|
|
"pointer-events:none",
|
|
"top:0",
|
|
`left:${-(Math.ceil(width) + 200)}px`,
|
|
`width:${width}px`,
|
|
`height:${pageContentHeight}px`,
|
|
`column-width:${width}px`,
|
|
"column-gap:0",
|
|
"column-fill:auto",
|
|
"overflow:visible",
|
|
"box-sizing:border-box",
|
|
"z-index:-1",
|
|
].join(";");
|
|
|
|
for (const child of documentRoot.children) {
|
|
if (child === content) break;
|
|
if (child.matches?.(".pdf-document-title, .pdf-custom-header")) probe.appendChild(child.cloneNode(true));
|
|
}
|
|
|
|
const flow = content.cloneNode(true);
|
|
flow.querySelectorAll(".pdf-page-break").forEach(pageBreak => {
|
|
pageBreak.style.breakAfter = "column";
|
|
pageBreak.style.pageBreakAfter = "auto";
|
|
});
|
|
probe.appendChild(flow);
|
|
|
|
const sentinel = doc.createElement("span");
|
|
sentinel.style.cssText = "display:inline-block;width:1px;height:1px;margin:0;padding:0;border:0;vertical-align:top;";
|
|
probe.appendChild(sentinel);
|
|
doc.body.appendChild(probe);
|
|
|
|
const probeRect = probe.getBoundingClientRect();
|
|
const sentinelRect = sentinel.getBoundingClientRect();
|
|
const fill = sentinelRect.bottom - probeRect.top;
|
|
probe.remove();
|
|
|
|
if (!Number.isFinite(fill) || fill < 0 || fill > pageContentHeight + 4) return null;
|
|
return Math.max(0, Math.min(pageContentHeight, fill));
|
|
}
|
|
|
|
function fallbackLastPageFill(documentRoot, spacer, pageContentHeight) {
|
|
const rootTop = documentRoot.getBoundingClientRect().top;
|
|
const contentEnd = Math.max(0, spacer.getBoundingClientRect().top - rootTop);
|
|
return ((contentEnd % pageContentHeight) + pageContentHeight) % pageContentHeight;
|
|
}
|
|
|
|
function alignFooterToLastPage(doc, pageHeightMm, marginMm) {
|
|
const documentRoot = doc.querySelector(".pdf-document");
|
|
const spacer = doc.querySelector(".pdf-footer-spacer");
|
|
const footer = doc.querySelector(".pdf-document-footer");
|
|
if (!documentRoot || !spacer || !footer) return;
|
|
|
|
spacer.style.height = "0px";
|
|
footer.style.marginTop = "0px";
|
|
|
|
const pxPerMm = mmToPx(doc);
|
|
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
|
const footerHeight = footer.getBoundingClientRect().height;
|
|
const bottomGuard = Math.max(2, 1.5 * pxPerMm);
|
|
const minGap = 4 * pxPerMm;
|
|
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
|
|
|
|
// The multicolumn pagination probe is only needed when the document contains
|
|
// an explicit PDF page break. For ordinary flowing documents Chromium's
|
|
// column layout can report the sentinel at the end of a full column, which
|
|
// incorrectly makes the footer look as if it no longer fits and creates an
|
|
// otherwise empty extra page containing only the footer.
|
|
const fallbackFill = fallbackLastPageFill(documentRoot, spacer, pageContentHeight);
|
|
const hasExplicitPageBreak = Boolean(doc.querySelector(".pdf-content .pdf-page-break"));
|
|
const measuredFill = hasExplicitPageBreak
|
|
? measureLastPageFill(doc, documentRoot, pageContentHeight)
|
|
: null;
|
|
const positionOnPage = measuredFill ?? fallbackFill;
|
|
const available = Math.max(0, pageContentHeight - positionOnPage);
|
|
|
|
const spacerHeight = available >= footerHeight + minGap + bottomGuard
|
|
? available - footerHeight - bottomGuard
|
|
: available + pageContentHeight - footerHeight - bottomGuard;
|
|
|
|
spacer.style.height = `${Math.max(0, spacerHeight)}px`;
|
|
}
|
|
|
|
function fitOversizedKeepUnits(doc, pageHeightMm, marginMm) {
|
|
const root = doc.querySelector(".pdf-content");
|
|
if (!root) return;
|
|
const pxPerMm = mmToPx(doc);
|
|
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
|
const maxHeight = Math.max(40, pageContentHeight - Math.max(2, pxPerMm));
|
|
|
|
root.querySelectorAll(".pdf-keep-unit, .pdf-keep-short, .pdf-atomic-text").forEach(unit => {
|
|
unit.classList.remove("pdf-keep-unit--oversized");
|
|
let rect = unit.getBoundingClientRect();
|
|
if (rect.height <= maxHeight) return;
|
|
|
|
const scalable = unit.querySelector(".mermaid svg, img, .rustpad-media, video, canvas");
|
|
if (scalable) {
|
|
const scalableRect = scalable.getBoundingClientRect();
|
|
const overhead = Math.max(0, rect.height - scalableRect.height);
|
|
const targetHeight = Math.floor(maxHeight - overhead - 2);
|
|
if (targetHeight >= 48 && targetHeight < scalableRect.height) {
|
|
scalable.style.setProperty("max-height", `${targetHeight}px`, "important");
|
|
scalable.style.setProperty("height", "auto", "important");
|
|
}
|
|
rect = unit.getBoundingClientRect();
|
|
}
|
|
|
|
if (rect.height > maxHeight) unit.classList.add("pdf-keep-unit--oversized");
|
|
});
|
|
}
|
|
|
|
function alignContentVertically(doc, pageHeightMm, marginMm, mode) {
|
|
const documentRoot = doc.querySelector(".pdf-document");
|
|
const content = doc.querySelector(".pdf-content");
|
|
const footer = doc.querySelector(".pdf-document-footer");
|
|
if (!documentRoot || !content || !footer) return;
|
|
|
|
content.style.marginTop = "0px";
|
|
if (mode !== "center" || content.querySelector(".pdf-page-break")) return;
|
|
|
|
const pxPerMm = mmToPx(doc);
|
|
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
|
const rootRect = documentRoot.getBoundingClientRect();
|
|
const contentRect = content.getBoundingClientRect();
|
|
const footerHeight = footer.getBoundingClientRect().height;
|
|
const headerHeight = Math.max(0, contentRect.top - rootRect.top);
|
|
const bottomGuard = Math.max(2, 1.5 * pxPerMm);
|
|
const minGap = 4 * pxPerMm;
|
|
const used = headerHeight + contentRect.height + footerHeight + minGap + bottomGuard;
|
|
if (!Number.isFinite(pageContentHeight) || used >= pageContentHeight) return;
|
|
|
|
const freeSpace = pageContentHeight - headerHeight - footerHeight - bottomGuard - contentRect.height;
|
|
content.style.marginTop = `${Math.max(0, freeSpace / 2)}px`;
|
|
}
|
|
|
|
function preparePreview(preview) {
|
|
const clone = preview.cloneNode(true);
|
|
clone.removeAttribute("id");
|
|
clone.classList.remove("preview");
|
|
clone.classList.add("pdf-content");
|
|
|
|
const computed = getComputedStyle(preview);
|
|
clone.style.fontFamily = computed.fontFamily;
|
|
clone.style.fontSize = computed.fontSize;
|
|
clone.style.lineHeight = computed.lineHeight;
|
|
|
|
clone.querySelectorAll('[contenteditable="true"]').forEach(node => node.removeAttribute("contenteditable"));
|
|
clone.querySelectorAll(".image-alias-tools, .image-alias-resize-handle").forEach(node => node.remove());
|
|
clone.querySelectorAll("details").forEach(node => {
|
|
node.open = true;
|
|
const replacement = document.createElement("section");
|
|
[...node.attributes].forEach(attribute => replacement.setAttribute(attribute.name, attribute.value));
|
|
replacement.setAttribute("open", "");
|
|
while (node.firstChild) replacement.appendChild(node.firstChild);
|
|
node.replaceWith(replacement);
|
|
});
|
|
clone.querySelectorAll("input, button, select, textarea").forEach(node => { node.tabIndex = -1; });
|
|
|
|
if (clone.classList.contains("preview--raw")) {
|
|
clone.querySelectorAll(".preview-source-line").forEach(line => {
|
|
const text = line.textContent.trim();
|
|
if (/^\[(?:PDF_PAGEBREAK|PAGEBREAK)\]$/i.test(text)) {
|
|
const pageBreak = document.createElement("div");
|
|
pageBreak.className = "pdf-page-break";
|
|
pageBreak.setAttribute("aria-hidden", "true");
|
|
line.replaceWith(pageBreak);
|
|
return;
|
|
}
|
|
const directive = text.match(/^\[PDF_(HEADER|FOOTER|ALIGN_TOP|ALIGN_CENTER)\]$/i);
|
|
if (directive) line.replaceWith(makePdfLayoutMarker(document, directive[1].toLowerCase()));
|
|
});
|
|
}
|
|
|
|
clone.querySelectorAll("hr.page-break-marker, hr[data-page-break='true']").forEach(marker => {
|
|
const pageBreak = document.createElement("div");
|
|
pageBreak.className = "pdf-page-break";
|
|
pageBreak.setAttribute("aria-hidden", "true");
|
|
marker.replaceWith(pageBreak);
|
|
});
|
|
|
|
trimTrailingBlankPrintLines(clone);
|
|
return clone;
|
|
}
|
|
|
|
function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
|
const safeMargin = Math.max(0, Number(marginMm) || 0);
|
|
const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * safeMargin));
|
|
const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (2 * safeMargin));
|
|
const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 20);
|
|
const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9);
|
|
const pageBackground = darkMode ? "#15181d" : "#fff";
|
|
const textColor = darkMode ? "#e6e9ed" : "#16181d";
|
|
const headingColor = darkMode ? "#f1f3f5" : "#111318";
|
|
const borderColor = darkMode ? "#3a414b" : "#d7dce2";
|
|
const footerColor = darkMode ? "#aeb5bf" : "#9299a3";
|
|
const colorScheme = darkMode ? "dark" : "light";
|
|
|
|
return `
|
|
@page { size: ${cssSize}; margin: ${safeMargin}mm; background: ${pageBackground}; }
|
|
html {
|
|
color-scheme: ${colorScheme};
|
|
background: ${pageBackground} !important;
|
|
print-color-adjust: exact !important;
|
|
-webkit-print-color-adjust: exact !important;
|
|
}
|
|
body {
|
|
width: ${pageContentWidthMm}mm;
|
|
max-width: ${pageContentWidthMm}mm;
|
|
min-height: 100vh;
|
|
margin: 0 !important;
|
|
background: ${pageBackground} !important;
|
|
color: ${textColor} !important;
|
|
print-color-adjust: exact !important;
|
|
-webkit-print-color-adjust: exact !important;
|
|
}
|
|
.pdf-document {
|
|
width: ${pageContentWidthMm}mm;
|
|
min-height: ${pageContentHeightMm}mm;
|
|
box-sizing: border-box;
|
|
background: ${pageBackground} !important;
|
|
color: ${textColor} !important;
|
|
}
|
|
.pdf-document-title,
|
|
.pdf-custom-header {
|
|
margin: 0 0 1.2em;
|
|
padding: 0 0 .45em;
|
|
border-bottom: 1px solid ${borderColor};
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
.pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: ${headingColor}; }
|
|
.pdf-custom-header > :first-child { margin-top: 0 !important; }
|
|
.pdf-custom-header > :last-child { margin-bottom: 0 !important; }
|
|
.pdf-footer-spacer {
|
|
display: block;
|
|
width: 100%;
|
|
height: 0;
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
}
|
|
.pdf-document-footer {
|
|
box-sizing: border-box;
|
|
margin-top: 0;
|
|
padding-top: 1.5mm;
|
|
border-top: .2mm solid ${borderColor};
|
|
color: ${footerColor};
|
|
font: 500 7.5pt/1.2 Inter, ui-sans-serif, system-ui, sans-serif;
|
|
letter-spacing: .01em;
|
|
text-align: center;
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
.pdf-custom-footer > :first-child { margin-top: 0 !important; }
|
|
.pdf-custom-footer > :last-child { margin-bottom: 0 !important; }
|
|
.pdf-content {
|
|
width: 100%;
|
|
min-height: 0;
|
|
margin: 0;
|
|
padding: 0 !important;
|
|
overflow: visible !important;
|
|
color: ${textColor} !important;
|
|
box-sizing: border-box;
|
|
}
|
|
.pdf-content .preview-source-line::before { display: none !important; content: none !important; }
|
|
.pdf-content .preview-source-line { min-height: 0; position: static !important; }
|
|
.pdf-content .preview-editable:hover,
|
|
.pdf-content .preview-editable:focus { background: transparent !important; box-shadow: none !important; }
|
|
.pdf-content a { color: inherit !important; text-decoration: underline; }
|
|
|
|
/* Book-like pagination: keep small semantic units together, not whole sections. */
|
|
.pdf-content h1,
|
|
.pdf-content h2,
|
|
.pdf-content h3 {
|
|
break-after: auto;
|
|
page-break-after: auto;
|
|
}
|
|
.pdf-content h4,
|
|
.pdf-content h5,
|
|
.pdf-content h6,
|
|
.pdf-content .markdown-details > summary,
|
|
.pdf-content dt {
|
|
break-after: avoid-page;
|
|
page-break-after: avoid;
|
|
}
|
|
.pdf-content .footnotes li {
|
|
break-inside: auto;
|
|
page-break-inside: auto;
|
|
}
|
|
.pdf-content p,
|
|
.pdf-content li,
|
|
.pdf-content dd { orphans: 3; widows: 3; }
|
|
|
|
.pdf-content ul,
|
|
.pdf-content ol,
|
|
.pdf-content dl,
|
|
.pdf-content .table-wrap,
|
|
.pdf-content table,
|
|
.pdf-content .markdown-alert,
|
|
.pdf-content .markdown-details,
|
|
.pdf-content .markdown-details__content,
|
|
.pdf-content .footnotes,
|
|
.pdf-content pre {
|
|
break-inside: auto;
|
|
page-break-inside: auto;
|
|
}
|
|
.pdf-content li {
|
|
break-inside: auto;
|
|
page-break-inside: auto;
|
|
}
|
|
.pdf-content li:has(> ul),
|
|
.pdf-content li:has(> ol) {
|
|
break-inside: auto;
|
|
page-break-inside: auto;
|
|
}
|
|
|
|
.pdf-content .table-wrap { overflow: visible !important; }
|
|
.pdf-content table { width: 100% !important; }
|
|
.pdf-content thead { display: table-header-group; }
|
|
.pdf-content tfoot { display: table-footer-group; }
|
|
.pdf-content tr {
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
.pdf-content pre {
|
|
overflow: visible !important;
|
|
white-space: pre-wrap !important;
|
|
orphans: 3;
|
|
widows: 3;
|
|
box-decoration-break: clone;
|
|
-webkit-box-decoration-break: clone;
|
|
}
|
|
.pdf-content pre code,
|
|
.pdf-content pre.code-with-lines .code-line {
|
|
white-space: pre-wrap !important;
|
|
overflow-wrap: anywhere;
|
|
word-break: break-word;
|
|
}
|
|
.pdf-content pre.code-with-lines .code-line {
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
.pdf-content .markdown-alert,
|
|
.pdf-content .markdown-details {
|
|
box-decoration-break: clone;
|
|
-webkit-box-decoration-break: clone;
|
|
}
|
|
|
|
.pdf-content .pdf-keep-unit,
|
|
.pdf-content .pdf-atomic-text,
|
|
.pdf-content .pdf-keep-short {
|
|
/*
|
|
* Chromium can paint a thin fragment of a block at the bottom of the
|
|
* previous page before moving the actual block to the next page.
|
|
* Atomic inline-blocks prevent that paint fragment instead of relying
|
|
* only on break-inside: avoid, which triggers the Chromium artifact.
|
|
*/
|
|
display: inline-block;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
vertical-align: top;
|
|
box-sizing: border-box;
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
.pdf-content dd.pdf-atomic-text {
|
|
width: calc(100% - 1.5em);
|
|
max-width: calc(100% - 1.5em);
|
|
}
|
|
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
|
.pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; }
|
|
.pdf-content .pdf-keep-unit.pdf-keep-unit--oversized,
|
|
.pdf-content .pdf-atomic-text.pdf-keep-unit--oversized,
|
|
.pdf-content .pdf-keep-short.pdf-keep-unit--oversized {
|
|
display: block;
|
|
width: auto;
|
|
max-width: none;
|
|
break-inside: auto;
|
|
page-break-inside: auto;
|
|
}
|
|
.pdf-content .pdf-heading-lead > h1,
|
|
.pdf-content .pdf-heading-lead > h2,
|
|
.pdf-content .pdf-heading-lead > h3 { margin-bottom: .18em; }
|
|
.pdf-content .pdf-atomic-visual {
|
|
display: inline-block;
|
|
width: 100%;
|
|
vertical-align: top;
|
|
box-sizing: border-box;
|
|
break-before: avoid-page;
|
|
page-break-before: avoid;
|
|
}
|
|
|
|
/* Atomic visual items are scaled to the printable page instead of being fragmented. */
|
|
.pdf-content img {
|
|
max-width: 100% !important;
|
|
max-height: ${atomicMaxHeightMm}mm !important;
|
|
width: auto;
|
|
height: auto !important;
|
|
object-fit: contain;
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
/* Dark PDF changes the document theme only. Never recolor raster images. */
|
|
filter: none !important;
|
|
-webkit-filter: none !important;
|
|
opacity: 1 !important;
|
|
mix-blend-mode: normal !important;
|
|
forced-color-adjust: none !important;
|
|
color-scheme: normal !important;
|
|
}
|
|
.pdf-content picture,
|
|
.pdf-content picture > img,
|
|
.pdf-content video,
|
|
.pdf-content canvas {
|
|
filter: none !important;
|
|
-webkit-filter: none !important;
|
|
opacity: 1 !important;
|
|
mix-blend-mode: normal !important;
|
|
forced-color-adjust: none !important;
|
|
color-scheme: normal !important;
|
|
}
|
|
.pdf-content .markdown-alias-image--sized {
|
|
height: auto !important;
|
|
aspect-ratio: auto !important;
|
|
}
|
|
.pdf-content .markdown-alias-image--sized > img {
|
|
width: auto !important;
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
}
|
|
.pdf-content .mermaid {
|
|
overflow: hidden !important;
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
.pdf-content .mermaid svg {
|
|
display: block;
|
|
width: auto !important;
|
|
max-width: 100% !important;
|
|
max-height: ${atomicMaxHeightMm}mm !important;
|
|
height: auto !important;
|
|
margin-inline: auto;
|
|
}
|
|
.pdf-content .rustpad-media {
|
|
width: min(100%, ${mediaMaxWidthMm}mm) !important;
|
|
max-width: 100% !important;
|
|
margin-inline: auto !important;
|
|
box-shadow: none !important;
|
|
break-inside: avoid-page;
|
|
page-break-inside: avoid;
|
|
}
|
|
.pdf-content .rustpad-media__player,
|
|
.pdf-content .rustpad-media__player iframe,
|
|
.pdf-content .rustpad-media__player video,
|
|
.pdf-content video.rustpad-media__player {
|
|
max-height: ${atomicMaxHeightMm}mm !important;
|
|
}
|
|
|
|
.pdf-document,
|
|
.pdf-document * { print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; }
|
|
|
|
${darkMode ? `
|
|
.pdf-content a { color: #b9b4d8 !important; }
|
|
.pdf-content code:not(pre code) { background: #242931 !important; border-color: #414852 !important; color: #e2e5e9 !important; }
|
|
.pdf-content pre { background: #1b1f25 !important; border-color: #3a414b !important; color: #d2d6dc !important; }
|
|
.pdf-content pre code,
|
|
.pdf-content .hljs { background: transparent !important; color: #d2d6dc !important; }
|
|
.pdf-content .hljs-keyword,
|
|
.pdf-content .hljs-selector-tag,
|
|
.pdf-content .hljs-literal,
|
|
.pdf-content .hljs-section,
|
|
.pdf-content .hljs-link { color: #c2acd0 !important; }
|
|
.pdf-content .hljs-string,
|
|
.pdf-content .hljs-attr,
|
|
.pdf-content .hljs-regexp,
|
|
.pdf-content .hljs-template-tag,
|
|
.pdf-content .hljs-template-variable { color: #b8c9a1 !important; }
|
|
.pdf-content .hljs-number,
|
|
.pdf-content .hljs-symbol,
|
|
.pdf-content .hljs-bullet { color: #d8a08e !important; }
|
|
.pdf-content .hljs-title,
|
|
.pdf-content .hljs-title.class_,
|
|
.pdf-content .hljs-title.function_,
|
|
.pdf-content .hljs-built_in,
|
|
.pdf-content .hljs-type { color: #9fb6d8 !important; }
|
|
.pdf-content .hljs-variable,
|
|
.pdf-content .hljs-selector-id,
|
|
.pdf-content .hljs-selector-class,
|
|
.pdf-content .hljs-property,
|
|
.pdf-content .hljs-attribute { color: #d39ca2 !important; }
|
|
.pdf-content .hljs-comment,
|
|
.pdf-content .hljs-quote { color: #9299a3 !important; font-style: italic; }
|
|
.pdf-content .hljs-meta { color: #cdbb8f !important; }
|
|
.pdf-content blockquote { border-left-color: #85818f !important; color: #cfd3d8 !important; }
|
|
.pdf-content mark { background: #5b5338 !important; color: #eee5b8 !important; }
|
|
.pdf-content th { background: #242930 !important; color: #e1e4e8 !important; }
|
|
.pdf-content .markdown-alert--success { border-color: #63806f !important; background: rgba(99, 128, 111, .12) !important; }
|
|
.pdf-content .markdown-alert--info { border-color: #6e8198 !important; background: rgba(110, 129, 152, .12) !important; }
|
|
.pdf-content .markdown-alert--warning { border-color: #9a8968 !important; background: rgba(154, 137, 104, .12) !important; }
|
|
.pdf-content .markdown-alert--danger { border-color: #956f73 !important; background: rgba(149, 111, 115, .12) !important; }
|
|
.pdf-content .task-checkbox { accent-color: #8c8798 !important; }
|
|
` : `
|
|
.pdf-content a { color: #5d50a5 !important; }
|
|
.pdf-content code:not(pre code) { background: #eeeaf7 !important; border-color: #bcb2a6 !important; color: #403a33 !important; }
|
|
.pdf-content pre { background: #efeae2 !important; border-color: #e2dcd3 !important; color: #413b34 !important; }
|
|
.pdf-content pre code,
|
|
.pdf-content .hljs { background: transparent !important; color: #403a33 !important; }
|
|
.pdf-content .hljs-keyword,
|
|
.pdf-content .hljs-selector-tag,
|
|
.pdf-content .hljs-literal,
|
|
.pdf-content .hljs-section,
|
|
.pdf-content .hljs-link { color: #7046a5 !important; }
|
|
.pdf-content .hljs-string,
|
|
.pdf-content .hljs-attr,
|
|
.pdf-content .hljs-regexp,
|
|
.pdf-content .hljs-template-tag,
|
|
.pdf-content .hljs-template-variable { color: #36704a !important; }
|
|
.pdf-content .hljs-number,
|
|
.pdf-content .hljs-symbol,
|
|
.pdf-content .hljs-bullet { color: #a54c25 !important; }
|
|
.pdf-content .hljs-title,
|
|
.pdf-content .hljs-title.class_,
|
|
.pdf-content .hljs-title.function_,
|
|
.pdf-content .hljs-built_in,
|
|
.pdf-content .hljs-type { color: #3568b8 !important; }
|
|
.pdf-content .hljs-variable,
|
|
.pdf-content .hljs-selector-id,
|
|
.pdf-content .hljs-selector-class,
|
|
.pdf-content .hljs-property,
|
|
.pdf-content .hljs-attribute { color: #9b3443 !important; }
|
|
.pdf-content .hljs-comment,
|
|
.pdf-content .hljs-quote { color: #756c61 !important; font-style: italic; }
|
|
.pdf-content .hljs-meta { color: #7e5d1d !important; }
|
|
.pdf-content blockquote { border-left-color: #6859b5 !important; color: #625a50 !important; }
|
|
.pdf-content mark { background: #f4e7be !important; color: #624700 !important; }
|
|
.pdf-content th { background: #f5f1ea !important; color: #29251f !important; }
|
|
.pdf-content .markdown-alert--success { border-color: #4c8064 !important; background: rgba(61, 121, 88, .10) !important; }
|
|
.pdf-content .markdown-alert--info { border-color: #56759c !important; background: rgba(68, 103, 148, .09) !important; }
|
|
.pdf-content .markdown-alert--warning { border-color: #a27831 !important; background: rgba(155, 112, 35, .11) !important; }
|
|
.pdf-content .markdown-alert--danger { border-color: #a54b55 !important; background: rgba(155, 52, 67, .09) !important; }
|
|
.pdf-content .task-checkbox { accent-color: #6859b5 !important; }
|
|
`}
|
|
|
|
.pdf-page-break {
|
|
display: block;
|
|
width: 100%;
|
|
height: 0;
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
break-after: page;
|
|
page-break-after: always;
|
|
}
|
|
`;
|
|
}
|
|
|
|
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, darkMode, verticalAlign }) {
|
|
const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"];
|
|
const safeMargin = [0, 8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12;
|
|
const printWindow = window.open("", "_blank");
|
|
if (!printWindow) throw new Error(t("editor.pdfPopupBlocked", {}, "The PDF window was blocked by the browser."));
|
|
try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ }
|
|
|
|
const content = preparePreview(preview);
|
|
const pdfLayout = extractPdfLayout(content);
|
|
const resolvedVerticalAlign = pdfLayout.verticalAlign || (verticalAlign === "center" ? "center" : "top");
|
|
const titleBlock = includeTitle && title
|
|
? `<header class="pdf-document-title"><h1>${escapeHtml(title)}</h1></header>`
|
|
: "";
|
|
const customHeader = pdfLayout.headerHtml
|
|
? `<header class="pdf-custom-header">${pdfLayout.headerHtml}</header>`
|
|
: "";
|
|
const lang = escapeHtml(document.documentElement.lang || "en");
|
|
const base = escapeHtml(`${location.origin}/`);
|
|
const css = printStyles(format.cssSize, format.widthMm, format.heightMm, safeMargin, darkMode);
|
|
const footerText = escapeHtml(t("editor.pdfGeneratedFooter", {}, "Generated by RustPad"));
|
|
const footerBlock = pdfLayout.footerHtml
|
|
? `<footer class="pdf-document-footer pdf-custom-footer">${pdfLayout.footerHtml}</footer>`
|
|
: `<footer class="pdf-document-footer pdf-generated-footer" aria-hidden="true">${footerText}</footer>`;
|
|
|
|
return (async () => {
|
|
await ensurePrintableMermaid(content, darkMode);
|
|
await ensureSyntaxHighlight(content);
|
|
preparePrintPagination(content);
|
|
|
|
printWindow.document.open();
|
|
printWindow.document.write(`<!doctype html><html lang="${lang}" data-theme="${darkMode ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="${base}"><title>${escapeHtml(title || "RustPad")}</title>${stylesheetLinks()}<style>${css}</style></head><body><main class="pdf-document">${customHeader}${titleBlock}${content.outerHTML}<div class="pdf-footer-spacer" aria-hidden="true"></div>${footerBlock}</main></body></html>`);
|
|
printWindow.document.close();
|
|
|
|
const runPrint = async () => {
|
|
await waitForPrintableLayout(printWindow.document);
|
|
fitOversizedKeepUnits(printWindow.document, format.heightMm, safeMargin);
|
|
alignContentVertically(printWindow.document, format.heightMm, safeMargin, resolvedVerticalAlign);
|
|
alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin);
|
|
await nextFrame(printWindow);
|
|
fitOversizedKeepUnits(printWindow.document, format.heightMm, safeMargin);
|
|
alignContentVertically(printWindow.document, format.heightMm, safeMargin, resolvedVerticalAlign);
|
|
alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin);
|
|
await nextFrame(printWindow);
|
|
printWindow.focus();
|
|
printWindow.print();
|
|
};
|
|
|
|
printWindow.addEventListener("afterprint", () => printWindow.close(), { once: true });
|
|
if (printWindow.document.readyState === "complete") await runPrint();
|
|
else await new Promise(resolve => {
|
|
printWindow.addEventListener("load", async () => { await runPrint(); resolve(); }, { once: true });
|
|
});
|
|
})().catch(error => {
|
|
try { printWindow.close(); } catch { /* best effort */ }
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
export function bindPdfExport({ button, dialog, form, preview, title }) {
|
|
if (!button || !dialog || !form || !preview) return;
|
|
const formatSelect = form.querySelector("#pdf-page-format");
|
|
const marginSelect = form.querySelector("#pdf-page-margin");
|
|
const verticalAlignSelect = form.querySelector("#pdf-vertical-align");
|
|
const darkMode = form.querySelector("#pdf-dark-mode");
|
|
const includeTitle = form.querySelector("#pdf-include-title");
|
|
const cancelButtons = form.querySelectorAll("[data-pdf-cancel]");
|
|
|
|
const savedFormat = saved("format", "a4-portrait");
|
|
formatSelect.value = Object.hasOwn(PAGE_FORMATS, savedFormat) ? savedFormat : "a4-portrait";
|
|
if (marginSelect) {
|
|
const savedMargin = saved("margin", "12");
|
|
marginSelect.value = ["0", "8", "12", "20"].includes(savedMargin) ? savedMargin : "12";
|
|
}
|
|
if (verticalAlignSelect) {
|
|
const savedVerticalAlign = saved("vertical-align", "top");
|
|
verticalAlignSelect.value = ["top", "center"].includes(savedVerticalAlign) ? savedVerticalAlign : "top";
|
|
}
|
|
if (darkMode) darkMode.checked = saved("dark-mode", "on") !== "off";
|
|
includeTitle.checked = saved("include-title", "off") === "on";
|
|
|
|
button.addEventListener("click", () => dialog.showModal());
|
|
cancelButtons.forEach(cancel => cancel.addEventListener("click", () => dialog.close("cancel")));
|
|
dialog.addEventListener("click", event => { if (event.target === dialog) dialog.close("cancel"); });
|
|
|
|
form.addEventListener("submit", event => {
|
|
event.preventDefault();
|
|
const formatKey = formatSelect.value;
|
|
const marginMm = Number(marginSelect?.value ?? 12);
|
|
save("format", formatKey);
|
|
save("margin", marginMm);
|
|
save("vertical-align", verticalAlignSelect?.value === "center" ? "center" : "top");
|
|
save("dark-mode", darkMode?.checked === false ? "off" : "on");
|
|
save("include-title", includeTitle.checked ? "on" : "off");
|
|
try {
|
|
const printJob = openPrintWindow({
|
|
preview,
|
|
title: typeof title === "function" ? title() : String(title || ""),
|
|
formatKey,
|
|
marginMm,
|
|
includeTitle: includeTitle.checked,
|
|
darkMode: darkMode?.checked !== false,
|
|
verticalAlign: verticalAlignSelect?.value === "center" ? "center" : "top",
|
|
});
|
|
dialog.close("confirm");
|
|
void printJob.catch(error => {
|
|
toast.danger(error?.message || t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), {
|
|
title: t("editor.exportPdf", {}, "Export PDF"),
|
|
});
|
|
});
|
|
} catch (error) {
|
|
toast.danger(error?.message || t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), {
|
|
title: t("editor.exportPdf", {}, "Export PDF"),
|
|
});
|
|
}
|
|
});
|
|
}
|