diff --git a/frontend/js/main.js b/frontend/js/main.js index 1913b55..9162bde 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -26,8 +26,23 @@ function persistNavStack() { try { localStorage.setItem("baldav_nav_stack", JSON.stringify(navStack)); } catch {} } -// Flag: modal popHandler already handled this popstate, global handler should skip -let _modalPopHandled = false; +// --------------------------------------------------------------------------- +// Window stack — single global stack for ALL overlays (modals, fullscreen). +// Every open = push. Every back = pop top. One popstate handler. +// --------------------------------------------------------------------------- +const winStack = []; // { type, close() } +let _skipPops = 0; // counter: how many popstate events to skip (from closeModal's history.go) + +function winPush(type, closeFn) { + winStack.push({ type, close: closeFn }); + history.pushState({ win: type }, ""); +} + +function winPopTop() { + if (winStack.length === 0) return false; + winStack.pop().close(); + return true; +} // --------------------------------------------------------------------------- // Boot @@ -766,24 +781,17 @@ async function renderStatsCharts(container, days, foodTotals, bmr) { return; } - // Bind chart fullscreen toggle for stats charts + // Bind chart fullscreen toggle for stats charts (Today screen, no modal) container.querySelectorAll(".chart-wrap").forEach(wrap => { wrap.onclick = () => { if (wrap.classList.contains("chart-full")) return; wrap.classList.add("chart-full"); - history.pushState({ chartFull: true }, ""); const closeBtn = document.createElement("div"); closeBtn.className = "chart-close-btn"; closeBtn.innerHTML = "✕"; - function collapseChart() { - wrap.classList.remove("chart-full"); - closeBtn.remove(); - window.removeEventListener("popstate", chartPopHandler); - } - closeBtn.onclick = (e) => { e.stopPropagation(); collapseChart(); history.back(); }; - const chartPopHandler = () => { collapseChart(); }; - window.addEventListener("popstate", chartPopHandler, { once: true }); + closeBtn.onclick = (e) => { e.stopPropagation(); history.back(); }; wrap.appendChild(closeBtn); + winPush("fsChart", () => { wrap.classList.remove("chart-full"); closeBtn.remove(); }); }; }); } @@ -808,62 +816,16 @@ function showModal(html, onSubmit) { `; document.body.appendChild(overlay); - // Simple history stack: each open = pushState, each back = popState pops it. - // Modal = 1 entry. Fullscreen map/chart inside modal = 1 more entry each. - // Back always pops the top entry. No re-pushing, no flags. - let _historyDepth = 1; // how many entries this modal owns - history.pushState({ modal: true }, ""); - - const popHandler = () => { - // Skip if another modal's closeModal triggered this popstate - if (_modalPopHandled) return; - // If this overlay was already removed — skip - if (!overlay.parentNode) return; - _historyDepth--; - - // Check what's on top and collapse it - 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 = "⛶"; - try { - const mapObj = fsMap._leafletMap; - if (mapObj) { - mapObj.dragging.disable(); - mapObj.scrollWheelZoom.disable(); - setTimeout(() => { mapObj.invalidateSize(); if (fsMap._bounds) mapObj.fitBounds(fsMap._bounds); }, 100); - } - } catch {} - return; // stay in modal, depth already decremented - } - - const fsChart = overlay.querySelector(".chart-full"); - if (fsChart) { - fsChart.classList.remove("chart-full"); - const btn = fsChart.querySelector(".chart-close-btn"); - if (btn) btn.remove(); - return; // stay in modal - } - - // Nothing fullscreen — close modal - _modalPopHandled = true; - window.removeEventListener("popstate", popHandler); - overlay.remove(); - }; - window.addEventListener("popstate", popHandler); - - // Called when fullscreen opens inside this modal — push 1 entry - overlay._pushFullscreen = function() { - _historyDepth++; - history.pushState({ modalFs: true }, ""); - }; + const stackBase = winStack.length; // remember stack depth before our push + winPush("modal", () => overlay.remove()); function closeModal() { - window.removeEventListener("popstate", popHandler); - overlay.remove(); - _modalPopHandled = true; - history.back(); + // Pop all entries this modal added (fullscreen children + modal itself) + const count = winStack.length - stackBase; + for (let i = 0; i < count; i++) winStack.pop().close(); + // Jump history back by that many steps; skip the resulting popstate events + _skipPops = count; + if (count > 0) history.go(-count); } overlay.querySelector("#modal-back").onclick = () => closeModal(); @@ -872,7 +834,6 @@ function showModal(html, onSubmit) { const ok = await onSubmit(overlay); if (ok !== false) { closeModal(); if (ok !== "norender") renderScreen(state.screen); } }; - setTimeout(() => overlay.querySelector("input")?.focus(), 50); } function showWeightModal() { @@ -1428,18 +1389,25 @@ async function showRecipeDetailModal(recipeId) { logBtn.onclick = async () => { await DB.addFoodFromRecipe(state.date, nowTime(), recipeId); - history.back(); // popHandler closes modal + if (winStack.length > 0) winStack.pop().close(); + _skipPops = 1; + history.back(); renderScreen(state.screen); }; editBtn.onclick = () => { - history.back(); // popHandler closes modal - setTimeout(() => showRecipeEditorModal(recipeId), 50); + // Close this modal via winStack, then open editor + if (winStack.length > 0) winStack.pop().close(); + _skipPops = 1; + history.back(); + showRecipeEditorModal(recipeId); }; delBtn.onclick = async () => { await DB.deleteRecipe(recipeId); - history.back(); // popHandler closes modal + if (winStack.length > 0) winStack.pop().close(); + _skipPops = 1; + history.back(); renderScreen("recipes"); }; } @@ -2554,7 +2522,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 + mapEl._leafletMap = map; // save ref for winStack collapse // Ctrl+wheel zoom on desktop mapEl.addEventListener("wheel", (e) => { if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); } @@ -2616,7 +2584,7 @@ async function showSessionDetail(sessionId) { // Fullscreen map toggle function toggleMapFs() { if (mapEl.classList.contains("sd-map-full")) { - // Collapse via history.back() — popHandler will handle the CSS + // Collapse via history.back() — winStack close handler will reset CSS history.back(); } else { // Expand — push to history stack @@ -2625,9 +2593,16 @@ async function showSessionDetail(sessionId) { map.dragging.enable(); map.scrollWheelZoom.enable(); setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100); - // Push fullscreen entry — back will pop it - const modal = mapEl.closest(".modal-overlay"); - if (modal && modal._pushFullscreen) modal._pushFullscreen(); + // Push fullscreen map onto winStack + const parentOverlay = mapEl.closest(".modal-overlay"); + winPush("fsMap", () => { + mapEl.classList.remove("sd-map-full"); + fsDiv.innerHTML = "⛶"; + map.dragging.disable(); + map.scrollWheelZoom.disable(); + setTimeout(() => { map.invalidateSize(); if (mapEl._bounds) map.fitBounds(mapEl._bounds); }, 100); + }); + winStack[winStack.length - 1].overlay = parentOverlay; } } @@ -2794,22 +2769,22 @@ async function showSessionDetail(sessionId) { const exp = document.querySelector("#sd-export"); if (exp) exp.onclick = () => GPS.exportGPX(sessionId); - // Chart fullscreen toggle — tap to expand, tap X or back to collapse - // Chart fullscreen in modal — pushState so back collapses it + // Chart fullscreen in modal — push to winStack, back collapses document.querySelectorAll(".chart-wrap").forEach(wrap => { wrap.onclick = () => { if (wrap.classList.contains("chart-full")) return; wrap.classList.add("chart-full"); - const modal = wrap.closest(".modal-overlay"); - if (modal && modal._pushFullscreen) modal._pushFullscreen(); const closeBtn = document.createElement("div"); closeBtn.className = "chart-close-btn"; closeBtn.innerHTML = "✕"; - closeBtn.onclick = (e) => { - e.stopPropagation(); - history.back(); // popHandler will collapse - }; + closeBtn.onclick = (e) => { e.stopPropagation(); history.back(); }; wrap.appendChild(closeBtn); + const parentOverlay = wrap.closest(".modal-overlay"); + winPush("fsChart", () => { + wrap.classList.remove("chart-full"); + closeBtn.remove(); + }); + winStack[winStack.length - 1].overlay = parentOverlay; }; }); @@ -2864,10 +2839,10 @@ async function showSessionDetail(sessionId) { const del = document.querySelector("#sd-delete"); if (del) del.onclick = async () => { await DB.deleteGpsSession(sessionId); - document.querySelector(".modal-overlay")?.remove(); - // Remove modal popstate handler, go to gps screen - history.replaceState({ screen: "gps" }, ""); - renderScreen("gps", true); + if (winStack.length > 0) winStack.pop().close(); + _skipPops = 1; + history.back(); + renderScreen("gps"); }; }, 100); @@ -4021,31 +3996,25 @@ function esc(str) { // Start // --------------------------------------------------------------------------- -// Global back navigation between screens +// Single global popstate handler window.addEventListener("popstate", (e) => { - // Modal popHandler already handled this event — skip - if (_modalPopHandled) { _modalPopHandled = false; return; } - // If a modal is open, don't handle here (modal has its own handler) - if (document.querySelector(".modal-overlay")) return; - // Chart fullscreen on screen (not in modal) — collapse it - const fsChart = document.querySelector(".chart-full"); - if (fsChart) { - fsChart.classList.remove("chart-full"); - const btn = fsChart.querySelector(".chart-close-btn"); - if (btn) btn.remove(); - return; - } - // Stale modal/chart entries left after app restart — skip them - if (e.state && (e.state.modal || e.state.chartFull)) { + // closeModal used history.go(-N) — skip N popstate events + if (_skipPops > 0) { _skipPops--; return; } + + // winStack has entries — pop the top one (close modal/fullscreen) + if (winPopTop()) return; + + // Stale entries from previous app session — skip + if (e.state && (e.state.modal || e.state.chartFull || e.state.modalFs || e.state.win)) { history.back(); return; } - // Pop our nav stack and navigate to previous screen + + // Navigate between screens via navStack if (navStack.length > 1) { navStack.pop(); persistNavStack(); - const prev = navStack[navStack.length - 1]; - renderScreen(prev, true); + renderScreen(navStack[navStack.length - 1], true); } else if (e.state && e.state.screen) { renderScreen(e.state.screen, true); }