[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 <body> (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 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-10 19:01:48 +01:00
parent 2e8f025b19
commit 9a398f3754
2 changed files with 73 additions and 14 deletions
@@ -4272,7 +4272,23 @@ function mapStageEl() {
function mapIsFullscreen() { function mapIsFullscreen() {
const stage = mapStageEl(); const stage = mapStageEl();
if (!stage) return false; 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() { function updateMapFullscreenButton() {
@@ -4285,25 +4301,47 @@ async function toggleMapFullscreen() {
const stage = mapStageEl(); const stage = mapStageEl();
if (!stage) return; if (!stage) return;
try { try {
if (mapIsFullscreen()) { const isNative = document.fullscreenElement === stage || document.webkitFullscreenElement === stage;
if (document.exitFullscreen) { const isFake = stage.classList.contains("map-fake-fullscreen");
await document.exitFullscreen(); if (isNative) {
} else if (document.webkitExitFullscreen) { if (document.exitFullscreen) await document.exitFullscreen();
await document.webkitExitFullscreen(); 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) { } catch (err) {
console.error("Map fullscreen toggle failed", err); console.error("Map fullscreen toggle failed", err);
} finally { } finally {
updateMapFullscreenButton(); 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() { function initAprsMap() {
const mapEl = document.getElementById("aprs-map"); const mapEl = document.getElementById("aprs-map");
if (!mapEl) return; if (!mapEl) return;
@@ -4506,7 +4544,13 @@ function sizeAprsMapToViewport() {
if (!mapEl) return; if (!mapEl) return;
const stage = mapStageEl(); const stage = mapStageEl();
if (mapIsFullscreen() && stage) { 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)); const target = Math.max(260, Math.floor(stageHeight));
mapEl.style.height = `${target}px`; mapEl.style.height = `${target}px`;
if (aprsMap) aprsMap.invalidateSize(); if (aprsMap) aprsMap.invalidateSize();
@@ -1174,16 +1174,31 @@ small { color: var(--text-muted); }
color: var(--text-heading); color: var(--text-heading);
} }
#map-stage:fullscreen, #map-stage:fullscreen,
#map-stage:-webkit-full-screen { #map-stage:-webkit-full-screen,
#map-stage.map-fake-fullscreen {
background: var(--bg); background: var(--bg);
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
/* CSS-based fake fullscreen for browsers that block the Fullscreen API
(notably mobile Safari, which only allows fullscreen for <video>). */
#map-stage.map-fake-fullscreen {
position: fixed;
inset: 0;
z-index: 9000;
height: 100dvh;
}
body.map-fake-fullscreen-active {
overflow: hidden;
touch-action: none;
}
#map-stage:fullscreen #aprs-map, #map-stage:fullscreen #aprs-map,
#map-stage:-webkit-full-screen #aprs-map { #map-stage:-webkit-full-screen #aprs-map,
#map-stage.map-fake-fullscreen #aprs-map {
border-radius: 0; border-radius: 0;
height: 100% !important;
} }
.aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; } .aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.aprs-controls > button.active, .aprs-controls > button.active,