114 lines
3.8 KiB
JavaScript
114 lines
3.8 KiB
JavaScript
// Print the given element using a hidden iframe, setting a custom default
|
|
// filename by changing ONLY the iframe's document.title (not the host).
|
|
export async function printViaIframe({
|
|
root, // HTMLElement to print (required)
|
|
filename = 'report.pdf', // default download name
|
|
extraCss = '', // optional extra CSS to inject
|
|
} = {}) {
|
|
if (!(root instanceof Element)) {
|
|
throw new Error('printViaIframe: "root" must be an HTMLElement');
|
|
}
|
|
|
|
// Clone the root so we can normalize asset URLs safely
|
|
const clone = root.cloneNode(true);
|
|
|
|
// Normalize <img src> to absolute URLs so they load inside the blob iframe
|
|
clone.querySelectorAll('img').forEach(img => {
|
|
const src = img.getAttribute('src');
|
|
if (!src) return;
|
|
try {
|
|
// Handle "/", "./", "../", and absolute http(s)
|
|
const abs = new URL(src, window.location.origin).href;
|
|
img.setAttribute('src', abs);
|
|
} catch { /* ignore */ }
|
|
});
|
|
|
|
// Gather existing <link rel="stylesheet"> so the iframe uses the same CSS bundle(s)
|
|
const linksHtml = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
.map(l => `<link rel="stylesheet" href="${l.href}">`)
|
|
.join('\n');
|
|
|
|
// Gather inline <style> tags content
|
|
const stylesHtml = Array.from(document.querySelectorAll('style'))
|
|
.map(s => s.textContent)
|
|
.join('\n');
|
|
|
|
// Minimal print CSS + your extras
|
|
const printCss = `
|
|
@page { size: A4; margin: 10mm 10mm 15mm 10mm; }
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
thead { display: table-header-group; }
|
|
tfoot { display: table-footer-group; }
|
|
tr, th, td, img { break-inside: avoid; }
|
|
}
|
|
/* Keep colors/borders as on screen */
|
|
#report, #report * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
/* Optional crisp table defaults */
|
|
#report table { border-collapse: collapse; width: 100%; table-layout: fixed; }
|
|
#report th, #report td { border: 1px solid #000; padding: 1px; box-sizing: border-box; }
|
|
${extraCss || ''}`;
|
|
|
|
// Build a standalone HTML for the iframe
|
|
const safeTitle = String(filename).replace(/\.pdf$/i, '');
|
|
const html = `<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>${safeTitle}</title>
|
|
${linksHtml}
|
|
<style>${stylesHtml}\n${printCss}</style>
|
|
</head>
|
|
<body>
|
|
<!-- Wrap in #report so your existing CSS targets still apply -->
|
|
<div id="report">${clone.outerHTML}</div>
|
|
<script>
|
|
(function () {
|
|
function readyToPrint() {
|
|
var imgs = Array.from(document.images).filter(i => !i.complete);
|
|
if (imgs.length === 0) return Promise.resolve();
|
|
return Promise.all(imgs.map(i => new Promise(r => { i.onload = r; i.onerror = r; })));
|
|
}
|
|
readyToPrint().then(function () {
|
|
setTimeout(function () {
|
|
window.focus();
|
|
window.print();
|
|
// Tell parent to cleanup after print opens
|
|
setTimeout(function () { parent.postMessage({ __closePrintIframe: true }, '*'); }, 100);
|
|
}, 0);
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
|
|
// Create the iframe pointed to a blob URL
|
|
const blob = new Blob([html], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
const iframe = document.createElement('iframe');
|
|
iframe.style.position = 'fixed';
|
|
iframe.style.right = '0';
|
|
iframe.style.bottom = '0';
|
|
iframe.style.width = '0';
|
|
iframe.style.height = '0';
|
|
iframe.style.border = '0';
|
|
iframe.src = url;
|
|
document.body.appendChild(iframe);
|
|
|
|
// Cleanup when the iframe signals it printed (or after a timeout)
|
|
const onMsg = (ev) => {
|
|
if (ev?.data?.__closePrintIframe) finish();
|
|
};
|
|
const timer = setTimeout(finish, 10000);
|
|
window.addEventListener('message', onMsg);
|
|
|
|
function finish() {
|
|
try {
|
|
window.removeEventListener('message', onMsg);
|
|
clearTimeout(timer);
|
|
URL.revokeObjectURL(url);
|
|
iframe.remove();
|
|
} catch {}
|
|
}
|
|
}
|