unilab-prints/src/utils/printIframe.js
2025-11-10 18:13:36 +01:00

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 {}
}
}