From e37dde1e400ef5b7eb3bf619be321e6abf8a138c Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 24 Jul 2026 15:24:40 +0200 Subject: [PATCH] preserve scroll position when entering page editor --- assets/layout.html | 1 + assets/scroll-sync.js | 61 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 assets/scroll-sync.js diff --git a/assets/layout.html b/assets/layout.html index 06dcb7c..b6ae07f 100644 --- a/assets/layout.html +++ b/assets/layout.html @@ -11,6 +11,7 @@ + diff --git a/assets/scroll-sync.js b/assets/scroll-sync.js new file mode 100644 index 0000000..b17326b --- /dev/null +++ b/assets/scroll-sync.js @@ -0,0 +1,61 @@ +// Roughly preserve reading position when entering the editor. +// +// While reading a page we record how far the center column is scrolled (as a +// fraction of its scrollable height, keyed by path). When the same path opens +// in the editor we restore that fraction, so you land near where you were +// reading instead of at the top. It is fraction-based — the rendered page and +// the raw-markdown editor have different heights — so the match is deliberately +// approximate. This only moves the viewport; the caret default (end of doc) is +// unchanged, so a click still decides where you actually edit. +(function () { + var center = document.querySelector('.center'); + if (!center) return; + + var key = 'scrollsync:' + location.pathname; + // Only restore a freshly-recorded position: a stale entry from earlier in + // the tab session would jump a direct ?edit open to the wrong place. + var MAX_AGE_MS = 5 * 60 * 1000; + var params = new URLSearchParams(location.search); + var inEditor = params.has('edit'); + // Section / insert edits load only a slice of the document into the editor, + // so a whole-page reading fraction does not map onto them — restore only for + // a full-page ?edit. + var fullPageEdit = inEditor && !params.has('section') && !params.has('insert_before'); + + function maxScroll() { return Math.max(0, center.scrollHeight - center.clientHeight); } + + if (inEditor) { + if (!fullPageEdit) return; + var raw = sessionStorage.getItem(key); + if (!raw) return; + var data; + try { data = JSON.parse(raw); } catch (e) { return; } + if (!data || typeof data.f !== 'number' || Date.now() - (data.t || 0) > MAX_AGE_MS) return; + // Wait for CodeMirror to mount and the column to reach full height, then + // map the fraction onto it. A double rAF after load lets CM's initial + // layout and focus scroll settle so ours wins. + window.addEventListener('load', function () { + requestAnimationFrame(function () { + requestAnimationFrame(function () { + center.scrollTop = data.f * maxScroll(); + }); + }); + }); + return; + } + + // Reading mode: keep the latest scroll fraction recorded (throttled to one + // write per frame) so it is ready the moment the editor opens. + var pending = false; + center.addEventListener('scroll', function () { + if (pending) return; + pending = true; + requestAnimationFrame(function () { + pending = false; + var m = maxScroll(); + try { + sessionStorage.setItem(key, JSON.stringify({ f: m > 0 ? center.scrollTop / m : 0, t: Date.now() })); + } catch (e) {} + }); + }, { passive: true }); +})();