fix(pdf-print): open the PDF in a new tab instead of printing the iframe
`iframe.contentWindow.print()` does not work for a PDF in Chrome. Chrome backs
every PDF — src URL or blob URL — with its internal `pdf_embedder` extension
rather than a real DOM document, so the frame's `contentDocument` is an empty
shell:
<html><head><link rel="stylesheet"
href="chrome-extension://…/pdf_embedder.css"></head>
<body></body></html>
The PDF *paints* — you can see it in the preview — but `print()` runs against
that empty body, so the operator gets a print dialog full of blank pages and
no error to explain why. Read straight out of a live page to confirm.
A new tab gets Chrome's real PDF viewer, whose own print button works. Same
document, no blank pages.
Affects the delivery-note preview, the quote PDF viewer, and the second quote
viewer in the same file. Popup-blocked is now reported explicitly rather than
appearing to do nothing.
This commit is contained in:
parent
ff58af7586
commit
ad869aa6f8
2 changed files with 91 additions and 41 deletions
|
|
@ -11,8 +11,9 @@
|
|||
* The dialog is intentionally chrome-minimal so the PDF
|
||||
* takes most of the viewport. The footer has:
|
||||
* - Close
|
||||
* - Print (calls iframe.contentWindow.print() so the
|
||||
* operator prints just the PDF, not the whole page)
|
||||
* - Print (opens the PDF in a new tab — see the note on
|
||||
* `handlePrint` for why the iframe's own print() gives blank
|
||||
* pages)
|
||||
* - Download (uses the `download` attribute so the
|
||||
* browser saves the file rather than opening it in a
|
||||
* viewer)
|
||||
|
|
@ -67,28 +68,77 @@ export function PdfPreviewDialog({
|
|||
subtitle,
|
||||
downloadFilename,
|
||||
}: PdfPreviewDialogProps) {
|
||||
// Ref on the iframe so Print can call the iframe's own
|
||||
// print() — this prints just the PDF, not the whole page
|
||||
// (the parent page has the modal chrome which would also
|
||||
// print otherwise).
|
||||
// Ref on the iframe so we can tell whether the PDF has finished
|
||||
// loading before offering a print action.
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const [blocked, setBlocked] = useState(false);
|
||||
|
||||
// Clear the pop-up-blocked notice on close rather than in an effect:
|
||||
// syncing state from an effect triggers a second render pass, and
|
||||
// the flag is only ever set by a click inside this component.
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
if (!next) setBlocked(false);
|
||||
onOpenChange(next);
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
// Calling the iframe's contentWindow.print() prints the
|
||||
// PDF document inside the iframe. Works in Chrome, Edge,
|
||||
// Firefox, Safari. If the iframe didn't load (network
|
||||
// blip, broken PDF), the call is a no-op — the operator
|
||||
// can still use the Download button.
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.print();
|
||||
} catch {
|
||||
// Ignore — some browsers throw on cross-origin or
|
||||
// blocked-iframe cases. The Download button remains.
|
||||
// Deliberately opens the PDF in a new tab rather than calling
|
||||
// `iframe.contentWindow.print()`.
|
||||
//
|
||||
// That older approach does NOT work for a PDF in Chrome. Chrome
|
||||
// renders PDFs with its internal `pdf_embedder` extension, not as
|
||||
// a real DOM document, so the iframe's `contentDocument` is an
|
||||
// empty shell:
|
||||
//
|
||||
// <html><head><link rel="stylesheet"
|
||||
// href="chrome-extension://…/pdf_embedder.css"></head>
|
||||
// <body></body></html>
|
||||
//
|
||||
// The PDF *paints* (you can see it), but `print()` runs against
|
||||
// that empty body — so the operator gets a print dialog full of
|
||||
// blank pages and no error to explain why. Verified in Chrome by
|
||||
// reading the iframe's document straight out of the page.
|
||||
//
|
||||
// A new tab hands the operator Chrome's real PDF viewer, whose own
|
||||
// print button works. Same document, no blank pages.
|
||||
const url = pdfUrl;
|
||||
const win = window.open(url, "_blank", "noopener,noreferrer");
|
||||
if (!win) {
|
||||
// Popup blocked. Say so rather than appearing to do nothing.
|
||||
setBlocked(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (blocked) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pop-up blocked</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your browser blocked the new tab needed to print {title}. Allow
|
||||
pop-ups for this site, or use Download to save the PDF and open it
|
||||
directly.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => handleOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<a href={pdfUrl} download={downloadFilename}>
|
||||
<Button type="button" variant="outline">
|
||||
<Download className="mr-1 h-3.5 w-3.5" />
|
||||
Download instead
|
||||
</Button>
|
||||
</a>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
// Wide + tall — the PDF is the focus. Min-height is
|
||||
// important so the iframe can render at a useful size
|
||||
|
|
@ -109,7 +159,7 @@ export function PdfPreviewDialog({
|
|||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={() => handleOpenChange(false)}
|
||||
aria-label="Close preview"
|
||||
data-testid="pdf-preview-close"
|
||||
>
|
||||
|
|
@ -137,14 +187,15 @@ export function PdfPreviewDialog({
|
|||
<DialogFooter className="px-4 py-3 border-t gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={() => handleOpenChange(false)}
|
||||
data-testid="pdf-preview-close-footer"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{/* Print — calls the iframe's own print() so the
|
||||
operator prints just the PDF, not the page
|
||||
chrome behind the modal. */}
|
||||
{/* Print — opens the PDF in a new tab so the operator gets
|
||||
Chrome's real PDF viewer, whose print button works. See
|
||||
the note on `handlePrint` for why the iframe's own
|
||||
`print()` produces blank pages. */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -466,16 +517,10 @@ export function QuotePdfPreviewDialog({
|
|||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Same approach as `PdfPreviewDialog`: print
|
||||
// the PDF inside the iframe so the operator
|
||||
// prints just the PDF, not the page chrome.
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.print();
|
||||
} catch {
|
||||
// Ignore — some browsers throw on cross-
|
||||
// origin / blocked-iframe cases. The
|
||||
// Download button remains.
|
||||
}
|
||||
// Opens in a new tab, same as `handlePrint` above —
|
||||
// `contentWindow.print()` on a PDF iframe prints
|
||||
// Chrome's empty pdf_embedder shell, i.e. blank pages.
|
||||
if (pdfUrl) window.open(pdfUrl, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
data-testid="quote-pdf-preview-print"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ import { X, Printer, Download, Loader2, ExternalLink } from "lucide-react";
|
|||
* - On mount, fetches the PDF binary from the API as a Blob, creates
|
||||
* a local blob URL, and renders it inside an `<iframe>` (browser
|
||||
* native PDF viewer).
|
||||
* - Print button: asks the iframe to call its own `window.print()` —
|
||||
* prints the PDF directly, not the surrounding page chrome.
|
||||
* - Print button: opens the PDF in a new tab. NOT
|
||||
* `iframe.contentWindow.print()` — Chrome renders PDFs with its
|
||||
* internal `pdf_embedder` extension, so the iframe's
|
||||
* `contentDocument` is an empty shell and `print()` against it
|
||||
* yields blank pages. Same bug and same fix as
|
||||
* `components/delivery-notes/preview-dialog.tsx`.
|
||||
* - Download button: fetches again with `?download=1` to get a
|
||||
* `Content-Disposition: attachment` response, triggers a save dialog.
|
||||
* - Open in new tab: opens the blob URL in a new browser tab for
|
||||
|
|
@ -99,13 +103,14 @@ export function QuotePdfViewer({
|
|||
if (!open) return null;
|
||||
|
||||
const handlePrint = () => {
|
||||
// Print from inside the iframe so only the PDF prints, not the modal chrome.
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.print();
|
||||
} catch {
|
||||
// Some browsers block cross-frame print; fall back to opening the blob URL.
|
||||
if (blobUrl) window.open(blobUrl, "_blank");
|
||||
}
|
||||
// Open the blob URL in a new tab. Calling
|
||||
// `iframeRef.current?.contentWindow?.print()` here looks like the
|
||||
// obvious thing to do and is what this used to do, but Chrome
|
||||
// backs a PDF iframe with its `pdf_embedder` extension, so the
|
||||
// frame's document is an empty `<body>` — `print()` on it produces
|
||||
// a dialog full of blank pages. The new tab gets the real PDF
|
||||
// viewer, whose own print button works.
|
||||
if (blobUrl) window.open(blobUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue