From 9a398f37548bcf01b0b568dcdea567492e16682e Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 10 Mar 2026 19:01:48 +0100 Subject: [PATCH] [fix](trx-frontend-http): map fullscreen fallback for mobile Safari Mobile Safari (iOS) blocks requestFullscreen() on non-video elements, so the Fullscreen button silently did nothing. Add a CSS-based fake fullscreen path: - mapEnterFakeFullscreen() adds .map-fake-fullscreen to #map-stage (position:fixed; inset:0; z-index:9000; height:100dvh) and map-fake-fullscreen-active to (overflow:hidden). - toggleMapFullscreen() tries native fullscreen first; catches the thrown NotAllowedError (or any other error) and falls back to the CSS path. Also handles the case where requestFullscreen is absent. - mapIsFullscreen() checks for the CSS class in addition to the native fullscreen element references. - mapExitFakeFullscreen() removes both classes on exit. - Escape key exits CSS fake fullscreen (native handles its own Escape). - sizeAprsMapToViewport() uses window.innerHeight for the fake path since clientHeight may not reflect fixed layout synchronously. - sizeAprsMapToViewport() is called via requestAnimationFrame after toggling so layout is settled before the Leaflet invalidateSize(). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 68 +++++++++++++++---- .../trx-frontend-http/assets/web/style.css | 19 +++++- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 47ae486..509778e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -4272,7 +4272,23 @@ function mapStageEl() { function mapIsFullscreen() { const stage = mapStageEl(); if (!stage) return false; - return document.fullscreenElement === stage || document.webkitFullscreenElement === stage; + return document.fullscreenElement === stage + || document.webkitFullscreenElement === stage + || stage.classList.contains("map-fake-fullscreen"); +} + +function mapExitFakeFullscreen() { + const stage = mapStageEl(); + if (!stage) return; + stage.classList.remove("map-fake-fullscreen"); + document.body.classList.remove("map-fake-fullscreen-active"); +} + +function mapEnterFakeFullscreen() { + const stage = mapStageEl(); + if (!stage) return; + stage.classList.add("map-fake-fullscreen"); + document.body.classList.add("map-fake-fullscreen-active"); } function updateMapFullscreenButton() { @@ -4285,25 +4301,47 @@ async function toggleMapFullscreen() { const stage = mapStageEl(); if (!stage) return; try { - if (mapIsFullscreen()) { - if (document.exitFullscreen) { - await document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - await document.webkitExitFullscreen(); + const isNative = document.fullscreenElement === stage || document.webkitFullscreenElement === stage; + const isFake = stage.classList.contains("map-fake-fullscreen"); + if (isNative) { + if (document.exitFullscreen) await document.exitFullscreen(); + else if (document.webkitExitFullscreen) await document.webkitExitFullscreen(); + } else if (isFake) { + mapExitFakeFullscreen(); + } else { + // Try native fullscreen; fall back to CSS fake fullscreen when the + // API is unavailable or blocked (e.g. mobile Safari). + const nativeFn = stage.requestFullscreen || stage.webkitRequestFullscreen; + if (nativeFn) { + try { + await nativeFn.call(stage); + } catch (_) { + mapEnterFakeFullscreen(); + } + } else { + mapEnterFakeFullscreen(); } - } else if (stage.requestFullscreen) { - await stage.requestFullscreen(); - } else if (stage.webkitRequestFullscreen) { - await stage.webkitRequestFullscreen(); } } catch (err) { console.error("Map fullscreen toggle failed", err); } finally { updateMapFullscreenButton(); - sizeAprsMapToViewport(); + requestAnimationFrame(() => sizeAprsMapToViewport()); } } +// Allow Escape to exit CSS fake fullscreen (native fullscreen handles its own Escape). +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + const stage = mapStageEl(); + if (stage && stage.classList.contains("map-fake-fullscreen")) { + mapExitFakeFullscreen(); + updateMapFullscreenButton(); + requestAnimationFrame(() => sizeAprsMapToViewport()); + } + } +}); + function initAprsMap() { const mapEl = document.getElementById("aprs-map"); if (!mapEl) return; @@ -4506,7 +4544,13 @@ function sizeAprsMapToViewport() { if (!mapEl) return; const stage = mapStageEl(); if (mapIsFullscreen() && stage) { - const stageHeight = stage.clientHeight || stage.getBoundingClientRect().height; + // For CSS fake fullscreen use window.innerHeight directly — clientHeight + // may not yet reflect the fixed layout when called synchronously after + // adding the class. + const isFake = stage.classList.contains("map-fake-fullscreen"); + const stageHeight = isFake + ? window.innerHeight + : (stage.clientHeight || stage.getBoundingClientRect().height); const target = Math.max(260, Math.floor(stageHeight)); mapEl.style.height = `${target}px`; if (aprsMap) aprsMap.invalidateSize(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 916699a..78cbcb2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -1174,16 +1174,31 @@ small { color: var(--text-muted); } color: var(--text-heading); } #map-stage:fullscreen, -#map-stage:-webkit-full-screen { +#map-stage:-webkit-full-screen, +#map-stage.map-fake-fullscreen { background: var(--bg); width: 100%; height: 100%; padding: 0; box-sizing: border-box; } +/* CSS-based fake fullscreen for browsers that block the Fullscreen API + (notably mobile Safari, which only allows fullscreen for