/** * 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 * 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 (an 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 timing). * - afterprint, or 60 s after the frame has LOADED, reclaims the frame, so repeated prints never * leak nodes. A 10-minute ceiling reclaims a frame whose load never ends. * * 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( '