Changeset 100aad8 in Klonkt


Ignore:
Timestamp:
08/18/2026 06:47:58 PM (3 weeks ago)
Author:
Bart <bart@…>
Branches:
main
Children:
85b51bf
Parents:
7421147
git-author:
Bart <bart@…> (08/18/2026 04:27:37 AM)
git-committer:
Bart <bart@…> (08/18/2026 06:47:58 PM)
Message:

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@…>

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/read.js

    r7421147 r100aad8  
    1212 * valt staat dus altijd op het stuk dat er al is, en deze module hoeft geen
    1313 * 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.
    1417 */
    1518
    1619// Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet
    1720// sneller dan je scrolt, en meer betekent alleen meer verkeer.
    18 const MARGE = '1px 0px 100% 0px';
     21const ROOT_MARGIN = '1px 0px 100% 0px';
    1922
    2023// De teksten die dit script plaatst. Ze staan in data-i18n op de stroom, want
    2124// een zin die hier staat, staat in één taal -- en dat is nooit de taal van de
    2225// lezer. Zelfde afspraak als de topnav (mod/chrome.js).
    23 let woorden = {};
    24 function woord(sleutel) { return woorden[sleutel] || ''; }
     26let strings = {};
     27function str(key) { return strings[key] || ''; }
    2528
    26 const stroom = () => document.getElementById('read-stream');
    27 const geladen = new Set();      // slugs die al in de stroom staan
    28 let bezigOnder = false;
    29 let bezigBoven = false;
    30 let waarnemer = null;
    31 let laatsteY = 0;
     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;
    3235
    33 function chrome(verbergen) {
    34   document.body.classList.toggle('read-chrome-hidden', !!verbergen);
     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);
    3540}
    3641
    3742/** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */
    38 function inBeeld() {
     43function inView() {
    3944  const posts = [...document.querySelectorAll('.read-post')];
    40   const midden = window.innerHeight / 2;
     45  const middle = window.innerHeight / 2;
    4146  return posts.find((p) => {
    4247    const r = p.getBoundingClientRect();
    43     return r.top <= midden && r.bottom >= midden;
     48    return r.top <= middle && r.bottom >= middle;
    4449  }) || posts[0];
    4550}
    4651
    4752/** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */
    48 function volgAdres() {
    49   const p = inBeeld();
     53function syncAddress() {
     54  const p = inView();
    5055  if (!p) return;
    5156  const slug = p.dataset.slug;
    52   const basis = p.dataset.base || '';
    53   if (location.pathname === `${basis}/read/${slug}`) return;
    54   history.replaceState(null, '', `${basis}/read/${slug}`);
     57  const base = p.dataset.base || '';
     58  if (location.pathname === `${base}/read/${slug}`) return;
     59  history.replaceState(null, '', `${base}/read/${slug}`);
    5560  if (p.dataset.title) document.title = p.dataset.title;
    5661}
    5762
    58 async function haal(slug, basis) {
    59   const r = await fetch(`${basis}/read/${encodeURIComponent(slug)}?partial=1`, {
     63async function fetchArticle(slug, base) {
     64  const r = await fetch(`${base}/read/${encodeURIComponent(slug)}?partial=1`, {
    6065    headers: { 'HX-Request': 'true' },
    6166    credentials: 'same-origin',
    6267  });
    6368  if (!r.ok) throw new Error('HTTP ' + r.status);
    64   const doos = document.createElement('div');
    65   doos.innerHTML = await r.text();
    66   const art = doos.querySelector('.read-post');
    67   if (!art) throw new Error('geen bericht in het antwoord');
     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');
    6873  return art;
    6974}
    7075
    7176/** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */
    72 async function vulAan() {
    73   const s = stroom();
    74   if (!s || bezigOnder) return;
    75   const laatste = s.querySelector('.read-post:last-of-type');
    76   const slug = laatste && laatste.dataset.older;
    77   if (!slug) return klaar(s);
    78   if (geladen.has(slug)) return;
    79   bezigOnder = true;
    80   const wacht = melding(s, 'read-loading', '…');
     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', '…');
    8186  try {
    82     const art = await haal(slug, laatste.dataset.base || '');
    83     geladen.add(slug);
    84     wacht.remove();
     87    const art = await fetchArticle(slug, last.dataset.base || '');
     88    loaded.add(slug);
     89    pending.remove();
    8590    s.appendChild(art);
    86     kijk();
     91    watch();
    8792  } catch (e) {
    88     wacht.textContent = woord('load_error');
    89     console.warn('[read] onderaan:', e && e.message);
    90   } finally { bezigOnder = false; }
     93    pending.textContent = str('load_error');
     94    console.warn('[read] below:', e && e.message);
     95  } finally { busyBelow = false; }
    9196}
    9297
     
    97102 * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.
    98103 */
    99 async function vulAanBoven() {
    100   const s = stroom();
    101   if (!s || bezigBoven) return;
    102   const eerste = s.querySelector('.read-post');
    103   const slug = eerste && eerste.dataset.newer;
    104   if (!slug || geladen.has(slug)) return;
    105   bezigBoven = true;
     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;
    106112  try {
    107     const art = await haal(slug, eerste.dataset.base || '');
    108     geladen.add(slug);
    109     const voor = s.scrollHeight;
    110     s.insertBefore(art, eerste);
    111     const erbij = s.scrollHeight - voor;
    112     window.scrollBy(0, erbij);
    113     kijk();
     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 = ''; }));
    114128  } catch (e) {
    115     console.warn('[read] bovenaan:', e && e.message);
    116   } finally { bezigBoven = false; }
     129    root.style.scrollSnapType = '';
     130    console.warn('[read] above:', e && e.message);
     131  } finally { busyAbove = false; }
    117132}
    118133
    119 function melding(s, klasse, tekst) {
     134function notice(s, className, text) {
    120135  const d = document.createElement('div');
    121   d.className = klasse;
    122   d.textContent = tekst;
     136  d.className = className;
     137  d.textContent = text;
    123138  s.appendChild(d);
    124139  return d;
    125140}
    126141
    127 function klaar(s) {
     142function markEnd(s) {
    128143  if (s.querySelector('.read-end')) return;
    129   melding(s, 'read-end', woord('end'));
     144  notice(s, 'read-end', str('end'));
    130145}
    131146
    132147/** (Her)richt de waarnemer op de huidige eerste en laatste post. */
    133 function kijk() {
    134   if (waarnemer) waarnemer.disconnect();
    135   waarnemer = new IntersectionObserver((entries) => {
     148function watch() {
     149  if (observer) observer.disconnect();
     150  observer = new IntersectionObserver((entries) => {
    136151    for (const e of entries) {
    137152      if (!e.isIntersecting) continue;
    138       if (e.target.matches('.read-post:last-of-type')) vulAan();
    139       else if (e.target.matches('.read-post:first-of-type')) vulAanBoven();
     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();
    140160    }
    141   }, { rootMargin: MARGE });
    142   const eerste = stroom() && stroom().querySelector('.read-post');
    143   const laatste = stroom() && stroom().querySelector('.read-post:last-of-type');
    144   if (eerste) waarnemer.observe(eerste);
    145   if (laatste && laatste !== eerste) waarnemer.observe(laatste);
     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);
    146166}
    147167
    148 function bijScroll() {
     168function onScroll() {
    149169  const y = window.scrollY || 0;
    150   if (y < 8) chrome(false);
    151   else if (y > laatsteY + 4) chrome(true);
    152   else if (y < laatsteY - 24) chrome(false);
    153   laatsteY = y;
    154   volgAdres();
     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();
    155175}
    156176
    157177export function init() {
    158   const s = stroom();
     178  const s = stream();
    159179  if (!s) return;
    160   try { woorden = JSON.parse(s.getAttribute('data-i18n') || '{}'); } catch (e) { woorden = {}; }
    161   s.querySelectorAll('.read-post').forEach((p) => geladen.add(p.dataset.slug));
    162   laatsteY = window.scrollY || 0;
    163   window.addEventListener('scroll', bijScroll, { passive: true });
    164   window.addEventListener('resize', kijk, { passive: true });
    165   kijk();
     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();
    166186}
    167187
  • src/views/pages/read.ejs

    r7421147 r100aad8  
    5757/* PROXIMITY, geen mandatory. Een bericht dat langer is dan het scherm moet je
    5858   rustig kunnen doorlezen; mandatory zou je bij elke pauze terugtrekken naar
    59    een grens. Zo snapt hij alleen als je in de buurt van een grens bent. */
    60 .on-read .read-stream { scroll-snap-type: y proximity; }
     59   een grens. Zo snapt hij alleen als je in de buurt van een grens bent.
     60
     61   OP <html>, en dat is geen smaakkwestie: snappen doet de SCROLLCONTAINER, en
     62   dat is hier het document -- .read-stream heeft overflow: visible en scrollt
     63   zelf niet. Op .read-stream stond de regel er wel, maar deed hij niets. */
     64html:has(body.on-read) { scroll-snap-type: y proximity; }
     65
     66/* De browser NIET ook laten compenseren. Chrome en Firefox schuiven bij
     67   invoegen bóven het beeld de scrollpositie zelf mee (scroll anchoring); samen
     68   met onze eigen correctie in mod/read.js is dat twee keer, en dan springt de
     69   tekst precies één bericht weg. Onze eigen correctie moet blijven, want Safari
     70   (en dus iOS) kent scroll anchoring helemaal niet. */
     71html:has(body.on-read), .on-read .read-stream, .on-read .read-post { overflow-anchor: none; }
    6172.on-read .read-post {
    6273  scroll-snap-align: start;
Note: See TracChangeset for help on using the changeset viewer.