Changeset 7cf8144 in Klonkt for src/assets/js/mod/read.js
- Timestamp:
- 08/18/2026 06:47:58 PM (3 weeks ago)
- 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)
- File:
-
- 1 edited
-
src/assets/js/mod/read.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
r2d2fd8c r7cf8144 1 1 /** 2 * De leesweergave: een stroom van hele berichten, één per beeld.2 * De leesweergave: tikken op een bericht opent dat bericht. 3 3 * 4 * NIET de truc van bartoverkamp.nl. Daar springt de pagina bij de rand naarde5 * andere kant en wordt het nieuwe artikel "vloeiend" in beeld gezet -- een naad6 * 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 bovenrandde8 * 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. 10 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. 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. 14 13 * 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. 17 18 */ 18 19 19 // Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet20 // 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, want24 // een zin die hier staat, staat in één taal -- en dat is nooit de taal van de25 // 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 staan31 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 die37 // 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 WEL100 * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie101 * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen102 * 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 precies116 // goed, maar een proximity-snap trekt hem daarna alsnog naar de grens van117 // het bericht dat er net bij kwam -- gemeten: een sprong ter grootte van118 // 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 pas126 // 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 is154 // tegelijk het eerste én het laatste. Met een else-if wint de eerste tak155 // altijd, en dan draait prependNewer() nooit: wie op het OUDSTE bericht156 // binnenkomt, krijgt "einde archief" te zien terwijl er zes nieuwere157 // 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 doen173 * 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 dan176 * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen177 * werking, een geselecteerde tekst is geen tik, een verschoven vinger is178 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.179 */180 20 let tapX = 0, tapY = 0; 181 21 function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; } … … 197 37 } 198 38 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 208 39 export function init() { 209 const s = stream();40 const s = document.getElementById('read-stream'); 210 41 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); 217 47 s.addEventListener('pointerdown', onPointerDown, { passive: true }); 218 48 s.addEventListener('click', onTap); 219 watch();220 49 } 221 50
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)