From 8df6594432d16cafd59bc888ffdada5d57702993 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 28 Aug 2026 19:35:07 +0000 Subject: [PATCH] Fix cube transition direction on forward history navigation popstate always passed back=true, so going forward after back still rotated the cube backwards. Track an incrementing idx in history.state and only mirror the transition when the target entry is actually behind; replaceState calls now preserve the state object instead of wiping it. --- frontend/src/AnalyticsView.vue | 2 +- frontend/src/pagerite.js | 22 +++++++++++++++++----- frontend/src/swapdoc.js | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/src/AnalyticsView.vue b/frontend/src/AnalyticsView.vue index d7ed696..594eafb 100644 --- a/frontend/src/AnalyticsView.vue +++ b/frontend/src/AnalyticsView.vue @@ -126,7 +126,7 @@ const readStats = computed(() => calcReadStats(visits.value)) watch(range, (r) => { const url = new URL(location.href) url.hash = r - history.replaceState(null, '', url) + history.replaceState(history.state, '', url) }) const clients = computed(() => data.value?.clients || {}) diff --git a/frontend/src/pagerite.js b/frontend/src/pagerite.js index 7569fdf..3895671 100644 --- a/frontend/src/pagerite.js +++ b/frontend/src/pagerite.js @@ -365,6 +365,13 @@ import "overlayscrollbars/overlayscrollbars.css"; currentPath = location.pathname; }); + // History position marker: every entry we create carries an incrementing + // idx so popstate can tell forward navigation from back (needed for the + // mirrored cube transition). replaceState calls below must preserve this + // state object instead of passing null. + let historyIdx = history.state?.idx ?? 0; + history.replaceState({ idx: historyIdx }, ""); + // --- Banner parallax ---------------------------------------------------- // The banner artwork stays windowed in place while its contents drift // against the scroll. The --pry scroll parameter is also available to @@ -420,7 +427,7 @@ import "overlayscrollbars/overlayscrollbars.css"; const scrollable = document.documentElement.scrollHeight > innerHeight; const want = !scrollable || scrollY === 0 || !current ? "" : `#${current.id}`; if (want !== location.hash) { - history.replaceState(null, "", want || location.pathname + location.search); + history.replaceState(history.state, "", want || location.pathname + location.search); } }); }, { passive: true }); @@ -696,7 +703,7 @@ import "overlayscrollbars/overlayscrollbars.css"; doit(); } currentPath = new URL(finalUrl, location.href).pathname; - if (push) history.pushState(null, "", finalUrl); + if (push) history.pushState({ idx: ++historyIdx }, "", finalUrl); // The open editor follows the URL: retarget the per-page tabs to the // navigated-to page (unsaved text of the previous page is discarded — // the article it previewed into is gone). @@ -773,7 +780,7 @@ import "overlayscrollbars/overlayscrollbars.css"; // drop the section hash — not a navigation, no analytics ping. if (a.getAttribute("href") === "" && url.pathname === location.pathname) { ev.preventDefault(); - history.replaceState(null, "", location.pathname + location.search); + history.replaceState(history.state, "", location.pathname + location.search); scrollTo({ top: 0, behavior: reduceMotion.matches ? "instant" : "smooth" }); return; } @@ -792,10 +799,15 @@ import "overlayscrollbars/overlayscrollbars.css"; }); }); - addEventListener("popstate", () => { + addEventListener("popstate", (ev) => { // Hash-only history entries are not navigations. if (location.pathname === currentPath) return; - load(location.href, false, true); + // Direction from the entry idx: forward navigation animates like an + // ordinary navigation; only actually going back mirrors the cube. + const target = ev.state?.idx ?? historyIdx - 1; + const back = target < historyIdx; + historyIdx = target; + load(location.href, false, back); }); // --- Task-list checkboxes ------------------------------------------------ diff --git a/frontend/src/swapdoc.js b/frontend/src/swapdoc.js index 3c98b79..acede29 100644 --- a/frontend/src/swapdoc.js +++ b/frontend/src/swapdoc.js @@ -126,7 +126,7 @@ export async function loadPlain(p) { } catch { return null } if (!doc.getElementById('main')) return null swapRegions(doc) - history.replaceState(null, '', finalUrl) + history.replaceState(history.state, '', finalUrl) runScripts(document.getElementById('page-banner')) runScripts(document.getElementById('main')) // Keep pagerite.js's in-memory page cache in sync with the fresh copy.