[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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user