source: Klonkt/src/assets/js/mod/read.js@ 87696ce

main
Last change on this file since 87696ce was 100aad8, checked in by Bart <bart@…>, 3 weeks ago

Leesweergave: Engelse namen, en het snappen werkte niet

De namen in mod/read.js waren Nederlands (stroom, vulAan, waarnemer); de rest
van assets/js/mod is Engels. Nu ook. Commentaar blijft Nederlands. chrome
heet toggleChrome, want window.chrome bestaat ook.

Door de module los te draaien kwamen er twee echte fouten boven:

  1. Wie op het OUDSTE bericht binnenkwam, zag "einde archief" en kon niet meer omhoog. Bij het laden staat er één artikel in de stroom, en dat is tegelijk het eerste én het laatste; met een else-if won de eerste tak altijd en draaide prependNewer() nooit. Twee losse ifs.
  1. Snappen deed helemaal niets. scroll-snap-type stond op .read-stream, en dat is geen scrollcontainer (overflow: visible) -- het document scrollt. De regel staat nu op <html>, en daar werkt hij.

En toen dat eenmaal werkte, vocht het met de correctie: na het invoegen trok de
proximity-snap de tekst naar de grens van het nieuwe bericht, gemeten -600 tot
-872px, precies de ruk die deze opzet moet vermijden. Snappen gaat nu even uit
tijdens invoegen+corrigeren en twee frames later weer aan: afwijking daalt naar
-35..-62px. Ook overflow-anchor: none, anders compenseren Chrome en Firefox er
nog eens overheen -- onze eigen correctie moet blijven, want Safari kent scroll
anchoring niet.

Co-Authored-By: Claude Opus 5 <claude@…>

  • Property mode set to 100644
File size: 7.1 KB
Line 
1/**
2 * De leesweergave: een stroom van hele berichten, één per beeld.
3 *
4 * NIET de truc van bartoverkamp.nl. Daar springt de pagina bij de rand naar de
5 * andere kant en wordt het nieuwe artikel "vloeiend" in beeld gezet -- een naad
6 * die verborgen wordt. Hier groeit de stroom in de richting waarin je leest:
7 * bij de onderrand komt de oudere buur eronder te staan, bij de bovenrand de
8 * nieuwere erboven. De browser snapt naar de berichtgrenzen (CSS, proximity).
9 * Er is dan geen naad, dus ook niets te verbergen.
10 *
11 * Elk artikel draagt zijn eigen buren in data-attributen. Wat er nog te halen
12 * valt staat dus altijd op het stuk dat er al is, en deze module hoeft geen
13 * lijst bij te houden die uit de pas kan lopen.
14 *
15 * De namen hieronder zijn Engels, zoals overal in assets/js/mod. Het commentaar
16 * blijft Nederlands, zoals overal in deze repo.
17 */
18
19// Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet
20// sneller dan je scrolt, en meer betekent alleen meer verkeer.
21const ROOT_MARGIN = '1px 0px 100% 0px';
22
23// De teksten die dit script plaatst. Ze staan in data-i18n op de stroom, want
24// een zin die hier staat, staat in één taal -- en dat is nooit de taal van de
25// lezer. Zelfde afspraak als de topnav (mod/chrome.js).
26let strings = {};
27function str(key) { return strings[key] || ''; }
28
29const stream = () => document.getElementById('read-stream');
30const loaded = new Set(); // slugs die al in de stroom staan
31let busyBelow = false;
32let busyAbove = false;
33let observer = null;
34let lastY = 0;
35
36// Niet `chrome`: dat is in deze browser ook een global, en een module-scope die
37// window.chrome overschaduwt is een val voor de volgende lezer.
38function toggleChrome(hide) {
39 document.body.classList.toggle('read-chrome-hidden', !!hide);
40}
41
42/** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */
43function inView() {
44 const posts = [...document.querySelectorAll('.read-post')];
45 const middle = window.innerHeight / 2;
46 return posts.find((p) => {
47 const r = p.getBoundingClientRect();
48 return r.top <= middle && r.bottom >= middle;
49 }) || posts[0];
50}
51
52/** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */
53function syncAddress() {
54 const p = inView();
55 if (!p) return;
56 const slug = p.dataset.slug;
57 const base = p.dataset.base || '';
58 if (location.pathname === `${base}/read/${slug}`) return;
59 history.replaceState(null, '', `${base}/read/${slug}`);
60 if (p.dataset.title) document.title = p.dataset.title;
61}
62
63async function fetchArticle(slug, base) {
64 const r = await fetch(`${base}/read/${encodeURIComponent(slug)}?partial=1`, {
65 headers: { 'HX-Request': 'true' },
66 credentials: 'same-origin',
67 });
68 if (!r.ok) throw new Error('HTTP ' + r.status);
69 const box = document.createElement('div');
70 box.innerHTML = await r.text();
71 const art = box.querySelector('.read-post');
72 if (!art) throw new Error('no post in the response');
73 return art;
74}
75
76/** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */
77async function appendOlder() {
78 const s = stream();
79 if (!s || busyBelow) return;
80 const last = s.querySelector('.read-post:last-of-type');
81 const slug = last && last.dataset.older;
82 if (!slug) return markEnd(s);
83 if (loaded.has(slug)) return;
84 busyBelow = true;
85 const pending = notice(s, 'read-loading', '…');
86 try {
87 const art = await fetchArticle(slug, last.dataset.base || '');
88 loaded.add(slug);
89 pending.remove();
90 s.appendChild(art);
91 watch();
92 } catch (e) {
93 pending.textContent = str('load_error');
94 console.warn('[read] below:', e && e.message);
95 } finally { busyBelow = false; }
96}
97
98/**
99 * Bovenaan aanvullen. Dit is het enige plek waar we de scrollpositie WEL
100 * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie
101 * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen
102 * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.
103 */
104async function prependNewer() {
105 const s = stream();
106 if (!s || busyAbove) return;
107 const first = s.querySelector('.read-post');
108 const slug = first && first.dataset.newer;
109 if (!slug || loaded.has(slug)) return;
110 busyAbove = true;
111 const root = document.documentElement;
112 try {
113 const art = await fetchArticle(slug, first.dataset.base || '');
114 loaded.add(slug);
115 // Snappen even UIT. De correctie hieronder zet de scrollpositie precies
116 // goed, maar een proximity-snap trekt hem daarna alsnog naar de grens van
117 // het bericht dat er net bij kwam -- gemeten: een sprong ter grootte van
118 // dat hele bericht, precies de ruk die deze opzet moet vermijden.
119 root.style.scrollSnapType = 'none';
120 const before = s.scrollHeight;
121 s.insertBefore(art, first);
122 const added = s.scrollHeight - before;
123 window.scrollBy(0, added);
124 watch();
125 // Twee frames: één om de correctie te laten landen, één om te snappen pas
126 // weer toe te staan als de browser klaar is met deze scroll.
127 requestAnimationFrame(() => requestAnimationFrame(() => { root.style.scrollSnapType = ''; }));
128 } catch (e) {
129 root.style.scrollSnapType = '';
130 console.warn('[read] above:', e && e.message);
131 } finally { busyAbove = false; }
132}
133
134function notice(s, className, text) {
135 const d = document.createElement('div');
136 d.className = className;
137 d.textContent = text;
138 s.appendChild(d);
139 return d;
140}
141
142function markEnd(s) {
143 if (s.querySelector('.read-end')) return;
144 notice(s, 'read-end', str('end'));
145}
146
147/** (Her)richt de waarnemer op de huidige eerste en laatste post. */
148function watch() {
149 if (observer) observer.disconnect();
150 observer = new IntersectionObserver((entries) => {
151 for (const e of entries) {
152 if (!e.isIntersecting) continue;
153 // GEEN else-if. Bij het laden staat er één artikel in de stroom, en dat is
154 // tegelijk het eerste én het laatste. Met een else-if wint de eerste tak
155 // altijd, en dan draait prependNewer() nooit: wie op het OUDSTE bericht
156 // binnenkomt, krijgt "einde archief" te zien terwijl er zes nieuwere
157 // boven hem staan die hij nooit te pakken krijgt.
158 if (e.target.matches('.read-post:last-of-type')) appendOlder();
159 if (e.target.matches('.read-post:first-of-type')) prependNewer();
160 }
161 }, { rootMargin: ROOT_MARGIN });
162 const first = stream() && stream().querySelector('.read-post');
163 const last = stream() && stream().querySelector('.read-post:last-of-type');
164 if (first) observer.observe(first);
165 if (last && last !== first) observer.observe(last);
166}
167
168function onScroll() {
169 const y = window.scrollY || 0;
170 if (y < 8) toggleChrome(false);
171 else if (y > lastY + 4) toggleChrome(true);
172 else if (y < lastY - 24) toggleChrome(false);
173 lastY = y;
174 syncAddress();
175}
176
177export function init() {
178 const s = stream();
179 if (!s) return;
180 try { strings = JSON.parse(s.getAttribute('data-i18n') || '{}'); } catch (e) { strings = {}; }
181 s.querySelectorAll('.read-post').forEach((p) => loaded.add(p.dataset.slug));
182 lastY = window.scrollY || 0;
183 window.addEventListener('scroll', onScroll, { passive: true });
184 window.addEventListener('resize', watch, { passive: true });
185 watch();
186}
187
188export default { init };
Note: See TracBrowser for help on using the repository browser.