Changeset 7cf8144 in Klonkt for src/assets/js/mod/read.js


Ignore:
Timestamp:
08/18/2026 06:47:58 PM (3 weeks ago)
Author:
Bart <bart@…>
Branches:
main
Children:
9da8027
Parents:
2d2fd8c
git-author:
Bart <bart@…> (08/18/2026 05:28:31 AM)
git-committer:
Bart <bart@…> (08/18/2026 06:47:58 PM)
Message:

Lezen is een weergave van de feed, geen eigen plek

/read was een route met een eigen shell: eigen pagina, eigen chrome-gedrag, en
op een site zonder berichten een 404 -- een dode knop in de switcher. Nu is het
wat het altijd had moeten zijn: een derde waarde van body[data-feed-view], naast
timeline en grid. Dezelfde berichten, andere vorm.

De feed stuurt ze alledrie mee en CSS kiest, precies zoals timeline/grid dat al
deden. Het lijf loopt door PostAccessService, dus een gesloten poort levert ook
hier geen tekst op. "Meer laden" vult de leesstroom OOB mee aan, net als de
tegels.

DE LEGE STAAT zit in de :has(.feed-reader) op de verbergregel. Zonder berichten
rendert de leessectie niet, en dan blijft de tijdlijn staan -- want daar staat
"nog niets geschreven" met de knop om te beginnen. Zonder die voorwaarde was
Lezen op een verse site een wit scherm. Feeds zonder leessectie (cirkel,
archief) houden om dezelfde reden gewoon hun tijdlijn.

Wat weg kon, en dat is de winst:

  • de route, pages/read.ejs, en 'on-read' als page-class;
  • mod/read.js van 222 naar 51 regels. Het ophalen van buren, het corrigeren van de scrollpositie bij invoegen en het onderbreken van het snappen zijn niet opgelost maar OVERBODIG: de feed levert de berichten al. Wat blijft is de tik.
  • read.end en read.load_error: het script plaatst geen zinnen meer.

De balken schuiven niet meer weg. Dat was mooi op een eigen scherm, maar hier
zou het de switcher verbergen waarmee je net koos -- en dan kun je niet terug.

MOD_V naar 4, style.css naar v70.

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

File:
1 edited

Legend:

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

    r2d2fd8c r7cf8144  
    11/**
    2  * De leesweergave: een stroom van hele berichten, één per beeld.
     2 * De leesweergave: tikken op een bericht opent dat bericht.
    33 *
    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.
     4 * Dit was een heel scherm met een eigen route, dat zijn buren zelf ophaalde, de
     5 * scrollpositie corrigeerde bij invoegen en de balken wegschoof. Dat is allemaal
     6 * weg, en dat is winst: Lezen is nu een DERDE WEERGAVE van de feed
     7 * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de
     8 * berichten al, "meer laden" vult al aan, en het snappen naar berichtgrenzen
     9 * doet CSS. Wat overblijft is dit ene gemak.
    1010 *
    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.
     11 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk
     12 * voor toetsenbord en schermlezer; dit hier is er voor een duim.
    1413 *
    15  * De namen hieronder zijn Engels, zoals overal in assets/js/mod. Het commentaar
    16  * blijft Nederlands, zoals overal in deze repo.
     14 * Vier uitzonderingen, want een tik die je niet bedoelde is erger dan geen tik:
     15 * iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen werking,
     16 * een geselecteerde tekst is geen tik, een verschoven vinger is scrollen, en
     17 * cmd/ctrl-klik hoort de browser zelf af te handelen.
    1718 */
    1819
    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.
    21 const 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).
    26 let strings = {};
    27 function str(key) { return strings[key] || ''; }
    28 
    29 const stream = () => document.getElementById('read-stream');
    30 const loaded = new Set();       // slugs die al in de stroom staan
    31 let busyBelow = false;
    32 let busyAbove = false;
    33 let observer = null;
    34 let 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.
    38 function 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. */
    43 function 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. */
    53 function 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 
    63 async function fetchArticle(slug, base) {
    64   const r = await fetch(`${base}/read/${encodeURIComponent(slug)}?fragment=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. */
    77 async 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  */
    104 async 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 
    134 function 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 
    142 function 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. */
    148 function 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 
    168 /**
    169  * Tikken op een bericht opent dat bericht.
    170  *
    171  * De stroom toont de tekst; het gesprek -- reacties, waarderingen, boosts --
    172  * staat op de berichtpagina. De titel en de voetlink zijn echte <a>'s en doen
    173  * het werk voor toetsenbord en schermlezer; dit hier is het gemak voor een duim.
    174  *
    175  * Daarom vier uitzonderingen, want een tik die je niet bedoelde is erger dan
    176  * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen
    177  * werking, een geselecteerde tekst is geen tik, een verschoven vinger is
    178  * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.
    179  */
    18020let tapX = 0, tapY = 0;
    18121function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; }
     
    19737}
    19838
    199 function onScroll() {
    200   const y = window.scrollY || 0;
    201   if (y < 8) toggleChrome(false);
    202   else if (y > lastY + 4) toggleChrome(true);
    203   else if (y < lastY - 24) toggleChrome(false);
    204   lastY = y;
    205   syncAddress();
    206 }
    207 
    20839export function init() {
    209   const s = stream();
     40  const s = document.getElementById('read-stream');
    21041  if (!s) return;
    211   try { strings = JSON.parse(s.getAttribute('data-i18n') || '{}'); } catch (e) { strings = {}; }
    212   s.querySelectorAll('.read-post').forEach((p) => loaded.add(p.dataset.slug));
    213   lastY = window.scrollY || 0;
    214   window.addEventListener('scroll', onScroll, { passive: true });
    215   window.addEventListener('resize', watch, { passive: true });
    216   // Op de stroom, niet per artikel: aangevulde berichten doen zo vanzelf mee.
     42  // Op de stroom, niet per artikel: wat "meer laden" erbij zet doet vanzelf mee.
     43  // init() draait bij ELKE paginawissel, dus eerst losmaken -- anders stapelt
     44  // dezelfde afhandelaar zich op en vuurt hij twee keer.
     45  s.removeEventListener('pointerdown', onPointerDown);
     46  s.removeEventListener('click', onTap);
    21747  s.addEventListener('pointerdown', onPointerDown, { passive: true });
    21848  s.addEventListener('click', onTap);
    219   watch();
    22049}
    22150
Note: See TracChangeset for help on using the changeset viewer.