Rewrite modal navigation: fix 5 back-gesture bugs

Root causes fixed:
1. closeModal had replaceState AFTER async history.back() — replaceState
   overwrote the current entry before back() fired, causing skip-to-today
2. Chart fullscreen in modals used pushState — each open/close cycle
   added extra history entries, exhausting the stack
3. Map collapse in popHandler didn't call invalidateSize/fitBounds

Changes:
- closeModal() simplified: just remove overlay + history.back()
  No more replaceState race condition
- Chart/map fullscreen inside modals: NO pushState, CSS-only
  popHandler detects .chart-full/.sd-map-full and collapses, re-pushes modal
- Map collapse: uses saved _leafletMap ref and _bounds for proper resize
- Init: renderScreen with fromPopstate=true to avoid navStack duplication
- Modal popHandler searches within overlay only (not global document)

Fixed user-reported bugs:
1. Map collapse keeps zoom → now calls invalidateSize + fitBounds
2. Chart open → back → back = app exit → now back closes chart, back closes modal
3. "<-" goes to Today instead of track list → fixed closeModal race
4. X on chart → back = correct → was already working
5. Chart open → back back = app exit → same fix as #2
This commit is contained in:
4aiman 2026-05-25 18:18:07 +03:00
commit 0701fbd723

View file

@ -94,8 +94,8 @@ async function init() {
// Add padding entries so first back doesn't exit
for (let i = 0; i < 3; i++) history.pushState({ screen: navStack[navStack.length - 1] }, "");
// Restore last screen
renderScreen(navStack[navStack.length - 1]);
// Restore last screen (fromPopstate=true to avoid duplicate navStack push)
renderScreen(navStack[navStack.length - 1], true);
}
// ---------------------------------------------------------------------------
@ -642,18 +642,29 @@ function showModal(html, onSubmit) {
history.pushState({ modal: true }, "");
const popHandler = () => {
// Fullscreen map inside modal? Collapse it first.
// Fullscreen map inside modal? Collapse it, stay in modal.
const fsMap = overlay.querySelector(".sd-map-full");
if (fsMap) {
fsMap.classList.remove("sd-map-full");
const expandBtn = fsMap.querySelector(".sd-map-expand-btn");
if (expandBtn) expandBtn.innerHTML = "&#x26F6;";
try { map.dragging.disable(); map.scrollWheelZoom.disable(); } catch {}
try {
const mapObj = fsMap._leafletMap;
if (mapObj) {
mapObj.dragging.disable();
mapObj.scrollWheelZoom.disable();
setTimeout(() => {
mapObj.invalidateSize();
const bounds = fsMap._bounds;
if (bounds) mapObj.fitBounds(bounds);
}, 100);
}
} catch {}
history.pushState({ modal: true }, "");
return;
}
// Fullscreen chart inside modal? Collapse it first.
const fsChart = document.querySelector(".chart-full");
// Fullscreen chart inside modal? Collapse it, stay in modal.
const fsChart = overlay.querySelector(".chart-full");
if (fsChart) {
fsChart.classList.remove("chart-full");
const btn = fsChart.querySelector(".chart-close-btn");
@ -663,23 +674,21 @@ function showModal(html, onSubmit) {
}
// Nothing fullscreen — close modal
window.removeEventListener("popstate", popHandler);
closeModal(false);
overlay.remove();
};
window.addEventListener("popstate", popHandler);
function closeModal(goBack) {
overlay.remove();
function closeModal() {
window.removeEventListener("popstate", popHandler);
if (goBack) history.back();
// Ensure history isn't empty after modal close (prevents app exit on next back)
history.replaceState({ screen: state.screen }, "");
overlay.remove();
history.back(); // pops {modal: true}, returns to previous screen
}
overlay.querySelector("#modal-back").onclick = () => closeModal(true);
overlay.querySelector("#modal-cancel").onclick = () => closeModal(true);
overlay.querySelector("#modal-back").onclick = () => closeModal();
overlay.querySelector("#modal-cancel").onclick = () => closeModal();
overlay.querySelector("#modal-ok").onclick = async () => {
const ok = await onSubmit(overlay);
if (ok !== false) { closeModal(true); renderScreen(state.screen); }
if (ok !== false) { closeModal(); renderScreen(state.screen); }
};
setTimeout(() => overlay.querySelector("input")?.focus(), 50);
}
@ -2182,6 +2191,7 @@ async function showSessionDetail(sessionId) {
L.Icon.Default.imagePath = "lib/";
const map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false, dragging: false, tap: false, touchZoom: true });
mapEl._leafletMap = map; // save for popHandler map collapse
// Ctrl+wheel zoom on desktop
mapEl.addEventListener("wheel", (e) => {
if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); }
@ -2225,6 +2235,7 @@ async function showSessionDetail(sessionId) {
}
const allCoords = sortedCoords;
mapEl._bounds = L.latLngBounds(sortedCoords).pad(0.1); // save for collapse fitBounds
// Fullscreen toggle button on map
// Fullscreen map toggle
@ -2417,12 +2428,11 @@ async function showSessionDetail(sessionId) {
if (exp) exp.onclick = () => GPS.exportGPX(sessionId);
// Chart fullscreen toggle — tap to expand, tap X or back to collapse
// Chart fullscreen in modal — NO pushState. Modal popHandler handles back.
document.querySelectorAll(".chart-wrap").forEach(wrap => {
wrap.onclick = () => {
if (wrap.classList.contains("chart-full")) return;
wrap.classList.add("chart-full");
// Push history so back gesture collapses chart, not modal
history.pushState({ chartFull: true }, "");
const closeBtn = document.createElement("div");
closeBtn.className = "chart-close-btn";
closeBtn.innerHTML = "&#x2715;";