From 069472a7feaa8abc54a28bb7ae8c585e0fbdc06c Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Apr 2026 16:25:12 -0600 Subject: [PATCH 1/2] feat: support documentTitle for PDF printing via document.title guard For PDF iframes, Chrome uses document.title (not the iframe's ) for "Save as PDF" filenames. Since print() on a PDF iframe is non-blocking, Chrome reads document.title asynchronously after return. This adds a MutationObserver-based guard that temporarily holds document.title to the desired value, preventing framework re-renders (React <Head>, Vue router, etc.) from resetting it during that window. Adds documentTitleHoldMs param (default 3000ms) to let users tune the hold duration. Closes #487 Closes #66 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/index.d.ts | 1 + src/js/init.js | 1 + src/js/print.js | 45 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 5c5d325..c6b87e0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -9,6 +9,7 @@ declare namespace printJS { fallbackPrintable?: string; type?: PrintTypes; documentTitle?: string; + documentTitleHoldMs?: number; header?: any; headerStyle?: string; footer?: any; diff --git a/src/js/init.js b/src/js/init.js index 8d2515e..b0b7978 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -35,6 +35,7 @@ export default { frameRemoveDelay: null, printableElement: null, documentTitle: 'Document', + documentTitleHoldMs: 3000, targetStyle: ['clear', 'display', 'width', 'min-width', 'height', 'min-height', 'max-height'], targetStyles: ['border', 'box', 'break', 'text-decoration'], ignoreElements: [], diff --git a/src/js/print.js b/src/js/print.js index 40f9629..cdf6572 100644 --- a/src/js/print.js +++ b/src/js/print.js @@ -1,6 +1,29 @@ import Browser from './browser' import { cleanUp } from './functions' +function guardDocumentTitle (title, holdMs, onDone) { + if (!title) { onDone(); return } + + var originalTitle = document.title + document.title = title + + var titleEl = document.querySelector('title') + var observer = null + + if (titleEl && typeof MutationObserver !== 'undefined') { + observer = new MutationObserver(function () { + if (document.title !== title) document.title = title + }) + observer.observe(titleEl, { characterData: true, childList: true, subtree: true }) + } + + setTimeout(function () { + if (observer) observer.disconnect() + document.title = originalTitle + onDone() + }, holdMs) +} + const Print = { send: (params, printFrame) => { // Append iframe element to document body @@ -51,6 +74,16 @@ const Print = { } function performPrint (iframeElement, params) { + var isPdfWithTitle = params.type === 'pdf' && + params.documentTitle && + params.documentTitle !== 'Document' + + if (isPdfWithTitle) { + guardDocumentTitle(params.documentTitle, params.documentTitleHoldMs || 3000, function () { + cleanUp(params) + }) + } + try { iframeElement.focus() @@ -59,15 +92,15 @@ function performPrint (iframeElement, params) { try { iframeElement.contentWindow.document.execCommand('print', false, null) } catch (e) { - setTimeout(function(){ + setTimeout(function () { iframeElement.contentWindow.print() - },1000) + }, 1000) } } else { // Other browsers - setTimeout(function(){ + setTimeout(function () { iframeElement.contentWindow.print() - },1000) + }, 1000) } } catch (error) { params.onError(error) @@ -78,7 +111,9 @@ function performPrint (iframeElement, params) { iframeElement.style.left = '-1px' } - cleanUp(params) + if (!isPdfWithTitle) { + cleanUp(params) + } } } From 397cb64c9669acb12d8809f07b2daca7e5ae57ab Mon Sep 17 00:00:00 2001 From: Daniel <dan@gobloom.io> Date: Thu, 23 Apr 2026 16:35:16 -0600 Subject: [PATCH 2/2] test: add manual test cases for PDF documentTitle guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two buttons added: - "Print PDF with documentTitle" — basic test, verify Chrome's print dialog shows "My Custom Report" as the filename - "Print PDF with documentTitle (simulated framework reset)" — uses a setInterval to aggressively reset document.title, proving the MutationObserver guard holds the title stable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- test/manual/index.html | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/manual/index.html b/test/manual/index.html index 56f154e..5efb031 100644 --- a/test/manual/index.html +++ b/test/manual/index.html @@ -14,6 +14,30 @@ }) } + function printPdfWithDocumentTitle() { + printJS({ + printable: '/test/manual/test.pdf', + type: 'pdf', + documentTitle: 'My Custom Report' + }) + } + + function printPdfWithDocumentTitleAndSimulatedReset() { + // Simulate a framework (React, Vue, etc.) aggressively resetting document.title + var interval = setInterval(function () { + document.title = 'RESET BY FRAMEWORK' + }, 50) + + // Stop the simulation after 5s so it doesn't run forever + setTimeout(function () { clearInterval(interval) }, 5000) + + printJS({ + printable: '/test/manual/test.pdf', + type: 'pdf', + documentTitle: 'Guarded Title Test' + }) + } + function printPdfWithModal() { printJS({ printable: '/test/manual/test.pdf', @@ -288,7 +312,13 @@ <h1>Print.js Test Page</h1> <button onClick='printPdfKioskPrint();'> Print PDF In --kiosk-printing Chrome frameRemoveDelay </button> - + <button onClick='printPdfWithDocumentTitle();'> + Print PDF with documentTitle + </button> + <button onClick='printPdfWithDocumentTitleAndSimulatedReset();'> + Print PDF with documentTitle (simulated framework reset) + </button> + </p> <div> <h2>Form Elements</h2>