doxa-maps/library/composables/printInPlace.js
doxa-maps-88655c70 04a55c66b2
Some checks are pending
build-gate / build (push) Waiting to run
Prayer Walk Planner: full-page dev mount + agent-triggerable in-place print
Two halves of the Driver's directive:

1. FULL PAGE (BUG-PRAYER-WALK-PLANNER-MAP-DOES-NOT-FILL-THE-PAGE).
   The BUILT leaf was already full-viewport; the DEV staging page boxed
   the element in a centred 640px card -- measured 781x584 at y=206 on
   a 1440x900 viewport, exactly 'the map is not filling the full
   page'. The dev harness now mounts fixed inset:0 like the leaf
   (two-host rule).

2. AGENT-OPERABLE PRINT. PrayerWalkPlanner.print() used window.open,
   which is pop-up-blocked without a user gesture -- an agent calling
   the documented API got silence. The poster-wall's in-place
   hidden-iframe mechanism (e957cbe) graduates VERBATIM to
   @map/composables/printInPlace.js (library bar: 2+ consumers,
   parameterized) and the planner uses it; @page pins letter portrait
   (the 839b0c4 ruling).

Verified end-to-end as an agent would: registry -> addPolygon ->
selectPolygon -> print() => {opened:true,count:1}, hidden iframe
print() fired once, portrait pinned, zero pop-ups.
2026-09-24 00:58:10 +00:00

47 lines
2.4 KiB
JavaScript

/**
* printInPlace.js — SHARED (@map/composables/printInPlace.js).
*
* ★ THE ONE print mechanism, graduated VERBATIM from my-upg-100-list/poster-wall.vue
* openPrintWindow() (commit e957cbe "Print the poster in place, on a portrait sheet") on the
* Driver's ruling that killed the pop-up path: print must happen IN PLACE — no new browser
* window, no pop-up blocker, the dialog rises over the page the user is looking at.
*
* WHY IT GRADUATES (library high bar: reusable + parameterized, 2+ consumers): the
* prayer-walk-planner's print still used window.open(), and that is not just the rejected UX --
* it is AGENT-HOSTILE. window.open() without a user gesture is pop-up-blocked, so
* PrayerWalkPlanner.print() called from an agent (the Driver: "the agent can also create
* polygons and trigger the print function") silently did nothing. An iframe cannot be blocked
* and needs no gesture, which is what makes the same call work for a human click AND an agent.
*
* Mechanics preserved from the poster-wall original, each for a measured reason:
* - OFF-SCREEN 1x1, not display:none — a display:none iframe does not lay out in every
* engine, and a zero-height layout can print a blank first page.
* - autoprint appends the print bootstrap AFTER the caller's markup so it runs once the
* document is parsed (matches <img onload> timing).
* - afterprint + a 60s floor reclaim the frame, so repeated prints never leak nodes.
*
* Returns the frame's contentWindow, or null when the host is sandboxed/detached (caller
* keeps its own fallback).
*/
export function openPrintFrame(bodyHtml, { autoprint = false } = {}) {
const frame = document.createElement('iframe')
frame.setAttribute('aria-hidden', 'true')
frame.style.cssText = 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0'
document.body.appendChild(frame)
const doc = frame.contentDocument
if (!doc) { frame.remove(); return null }
doc.open()
doc.write(bodyHtml)
if (autoprint) {
doc.write(
'<script>window.addEventListener("load",function(){setTimeout(function(){window.focus();window.print()},150)})</' + 'script>')
}
doc.close()
const drop = () => { if (frame.parentNode) frame.remove() }
try { frame.contentWindow.addEventListener('afterprint', () => setTimeout(drop, 0)) } catch { /* cross-doc guard */ }
setTimeout(drop, 60000)
return frame.contentWindow
}