Files
datascape/assets/scroll-sync.js
T
luxickandClaude Opus 5.5 aa90a04f91 Boost page navigation with htmx
Links and GET forms to wiki pages swap the next page into .shell instead
of loading a new document: the server answers boosted requests and
history restores with a partial (title, page actions out-of-band, shell
content). The header stays, browsers without cross-document view
transitions (Firefox) no longer flash between pages, and the swap runs
as a same-document view transition. Files, the editor, internal
endpoints and POST forms still navigate normally.

Back/Forward refetch pages (no snapshots that could predate a move or
delete); the tree rail keeps its scroll position and the content column
returns to where it was. Mutation redirects swap their result in the
same way. Page scripts that assumed a fresh document now also run after
a swap (toc, contents, companion, scroll-sync, search actions) or
register once (fitness).

The e2e suite now also runs in Firefox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:29:08 +02:00

67 lines
3.1 KiB
JavaScript

// 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. Boosted
// navigation replaces .center and changes the page without a reload, so
// listen on document and resolve both when writing.
var pending = false;
document.addEventListener('scroll', function (e) {
if (pending || !e.target.classList || !e.target.classList.contains('center')) return;
pending = true;
requestAnimationFrame(function () {
pending = false;
center = document.querySelector('.center');
if (!center) return;
var m = maxScroll();
try {
sessionStorage.setItem('scrollsync:' + location.pathname,
JSON.stringify({ f: m > 0 ? center.scrollTop / m : 0, t: Date.now() }));
} catch (e) {}
});
}, { capture: true, passive: true });
})();