Rewrite fullscreen as history entries: each open=push, each back=pop
Single winStack[] array + single popstate handler replaces: - Per-modal popHandlers (removed) - _modalPopHandled flag (removed) - overlay._pushFullscreen (removed) - _suspended/resume mechanism (removed) How it works: - winPush(type, closeFn): push entry + history.pushState - winPopTop(): pop top entry, call close(), return true - closeModal(): pop all entries down to stackBase, history.go(-count) - _skipPops counter: closeModal's history.go fires popstate — skip N events Every open (modal, fullscreen map, fullscreen chart) = 1 winPush. Every back = 1 winPopTop via popstate. closeModal for buttons = pop N entries + history.go(-N) + skip N pops. Sub-modals work because stackBase tracks where each modal started. Ingredient modal pops only its own entry, recipe editor stays.
This commit is contained in:
parent
94ad61f5b9
commit
4960f2a863
1 changed files with 75 additions and 106 deletions
|
|
@ -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) {
|
|||
</div>`;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue