diff --git a/plugin-designs/my-upg-100-list/poster-wall.vue b/plugin-designs/my-upg-100-list/poster-wall.vue index 66b565cd6..f9f73a1a7 100644 --- a/plugin-designs/my-upg-100-list/poster-wall.vue +++ b/plugin-designs/my-upg-100-list/poster-wall.vue @@ -274,38 +274,69 @@ const { scrimRef, scrimStyle } = useViewportScrim(exportState) // all stay — printPoster below still uses posterUrl(), and all three print paths still // share the exportState HUD for progress/errors. -// ─── SHARED PRINT-WINDOW MECHANISM ──────────────────────────────────────────── +// ─── SHARED PRINT MECHANISM ─────────────────────────────────────────────────── // ONE mechanism, reused by all three "Print …" actions (poster / list / card -// deck) rather than three ad-hoc window.opens. Extracted from the poster path, -// which already carries the load-bearing fix documented below. -// -// ★ NO `noopener` HERE, AND THAT IS THE WHOLE FIX (2026-09-23). `noopener` makes window.open() -// return null BY SPECIFICATION — severing the opener relationship is exactly what the flag does, -// so there is no handle to hand back. With it, `w` was always null, the document.write preview -// below was UNREACHABLE FOR EVERY USER, and every click fell through to a raw-content fallback. -// MEASURED in a default (non-headless-flagged) browser: window.open('','_blank','noopener') -> null, -// the identical call without the flag -> a usable handle. I first attributed the null to this -// headless box; it is not environmental, it is what the flag means. -// The window we write into carries same-origin markup we generate ourselves, so the opener -// reference it regains is our own — there is nothing hostile for `noopener` to protect against here. +// deck) rather than three ad-hoc implementations. // // `bodyHtml` is the FULL document string (caller owns /' + + // A SQUARE on a PORTRAIT sheet must fit by its SHORTER axis. The old print rule + // (`max-height:none;width:100%`) scaled the square to the full page WIDTH, which on portrait + // letter is taller than the printable height -> the poster overflowed onto a second page. + // Constrain BOTH axes and let object-fit letterbox it inside the sheet instead. + '@media print{body{height:auto}' + + 'img{width:auto;max-width:100%;max-height:100%;object-fit:contain}}' + '' + title + '' + '') - if (!w) { // popup blocked → open the image tab + if (!w) { + // No longer "popup blocked" — an iframe cannot be blocked. This is now only reachable if the + // frame has no contentDocument at all (a sandboxed/detached host), so the honest fallback is + // the poster itself rather than a pop-up instruction the user cannot act on. window.open(url, '_blank', 'noopener') exportState.value = { ...exportState.value, phase: 'done', message: 'opened poster', pct: 100 } setTimeout(() => { exportState.value = null }, 900); return } - exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 } + exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 } setTimeout(() => { exportState.value = null }, 900) } catch (e) { exportState.value = { label: list.label, kind: 'Poster', phase: 'error', @@ -433,11 +477,13 @@ async function printList100(list) { 'Religion' + rowsHtml + '' + '', { autoprint: true }) if (!w) { + // Not a pop-up blocker any more (this prints via a same-page iframe). Only a host that + // denies us a contentDocument lands here, so say something the user can act on. exportState.value = { label: list.label, kind: 'UPG-100 List', phase: 'error', - message: 'Pop-up blocked — allow pop-ups for this site and try again.', pct: 0 } + message: 'Could not open the print dialog in this view — try the Download button.', pct: 0 } return } - exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 } + exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 } setTimeout(() => { exportState.value = null }, 900) } catch (e) { exportState.value = { label: list.label, kind: 'UPG-100 List', phase: 'error', @@ -505,11 +551,12 @@ async function printCardDeck(list) { '
' + cardsHtml + '
' + '', { autoprint: true }) if (!w) { + // See the note in the list path: an iframe cannot be pop-up blocked. exportState.value = { label: list.label, kind: 'Card Deck', phase: 'error', - message: 'Pop-up blocked — allow pop-ups for this site and try again.', pct: 0 } + message: 'Could not open the print dialog in this view — try the Download button.', pct: 0 } return } - exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 } + exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 } setTimeout(() => { exportState.value = null }, 900) } catch (e) { exportState.value = { label: list.label, kind: 'Card Deck', phase: 'error',