- Timestamp:
- 08/18/2026 06:47:58 PM (3 weeks ago)
- 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)
- Location:
- src
- Files:
-
- 2 edited
-
assets/js/mod/read.js (modified) (2 diffs)
-
views/pages/read.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
r7421147 r100aad8 12 12 * valt staat dus altijd op het stuk dat er al is, en deze module hoeft geen 13 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. 14 17 */ 15 18 16 19 // Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet 17 20 // sneller dan je scrolt, en meer betekent alleen meer verkeer. 18 const MARGE= '1px 0px 100% 0px';21 const ROOT_MARGIN = '1px 0px 100% 0px'; 19 22 20 23 // De teksten die dit script plaatst. Ze staan in data-i18n op de stroom, want 21 24 // een zin die hier staat, staat in één taal -- en dat is nooit de taal van de 22 25 // lezer. Zelfde afspraak als de topnav (mod/chrome.js). 23 let woorden= {};24 function woord(sleutel) { return woorden[sleutel] || ''; }26 let strings = {}; 27 function str(key) { return strings[key] || ''; } 25 28 26 const str oom = () => document.getElementById('read-stream');27 const geladen = new Set();// slugs die al in de stroom staan28 let b ezigOnder= false;29 let b ezigBoven= false;30 let waarnemer = null;31 let la atsteY = 0;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; 32 35 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. 38 function toggleChrome(hide) { 39 document.body.classList.toggle('read-chrome-hidden', !!hide); 35 40 } 36 41 37 42 /** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */ 38 function in Beeld() {43 function inView() { 39 44 const posts = [...document.querySelectorAll('.read-post')]; 40 const midd en= window.innerHeight / 2;45 const middle = window.innerHeight / 2; 41 46 return posts.find((p) => { 42 47 const r = p.getBoundingClientRect(); 43 return r.top <= midd en && r.bottom >= midden;48 return r.top <= middle && r.bottom >= middle; 44 49 }) || posts[0]; 45 50 } 46 51 47 52 /** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */ 48 function volgAdres() {49 const p = in Beeld();53 function syncAddress() { 54 const p = inView(); 50 55 if (!p) return; 51 56 const slug = p.dataset.slug; 52 const bas is= p.dataset.base || '';53 if (location.pathname === `${bas is}/read/${slug}`) return;54 history.replaceState(null, '', `${bas is}/read/${slug}`);57 const base = p.dataset.base || ''; 58 if (location.pathname === `${base}/read/${slug}`) return; 59 history.replaceState(null, '', `${base}/read/${slug}`); 55 60 if (p.dataset.title) document.title = p.dataset.title; 56 61 } 57 62 58 async function haal(slug, basis) {59 const r = await fetch(`${bas is}/read/${encodeURIComponent(slug)}?partial=1`, {63 async function fetchArticle(slug, base) { 64 const r = await fetch(`${base}/read/${encodeURIComponent(slug)}?partial=1`, { 60 65 headers: { 'HX-Request': 'true' }, 61 66 credentials: 'same-origin', 62 67 }); 63 68 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'); 68 73 return art; 69 74 } 70 75 71 76 /** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */ 72 async function vulAan() {73 const s = str oom();74 if (!s || b ezigOnder) return;75 const la atste= s.querySelector('.read-post:last-of-type');76 const slug = la atste && laatste.dataset.older;77 if (!slug) return klaar(s);78 if ( geladen.has(slug)) return;79 b ezigOnder= true;80 const wacht = melding(s, 'read-loading', '…');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', '…'); 81 86 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(); 85 90 s.appendChild(art); 86 kijk();91 watch(); 87 92 } catch (e) { 88 wacht.textContent = woord('load_error');89 console.warn('[read] onderaan:', e && e.message);90 } finally { b ezigOnder= false; }93 pending.textContent = str('load_error'); 94 console.warn('[read] below:', e && e.message); 95 } finally { busyBelow = false; } 91 96 } 92 97 … … 97 102 * en tellen die bij de scrollpositie op -- geen animatie, geen sprong. 98 103 */ 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; 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; 106 112 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 = ''; })); 114 128 } 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; } 117 132 } 118 133 119 function melding(s, klasse, tekst) {134 function notice(s, className, text) { 120 135 const d = document.createElement('div'); 121 d.className = klasse;122 d.textContent = te kst;136 d.className = className; 137 d.textContent = text; 123 138 s.appendChild(d); 124 139 return d; 125 140 } 126 141 127 function klaar(s) {142 function markEnd(s) { 128 143 if (s.querySelector('.read-end')) return; 129 melding(s, 'read-end', woord('end'));144 notice(s, 'read-end', str('end')); 130 145 } 131 146 132 147 /** (Her)richt de waarnemer op de huidige eerste en laatste post. */ 133 function kijk() {134 if ( waarnemer) waarnemer.disconnect();135 waarnemer = new IntersectionObserver((entries) => {148 function watch() { 149 if (observer) observer.disconnect(); 150 observer = new IntersectionObserver((entries) => { 136 151 for (const e of entries) { 137 152 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(); 140 160 } 141 }, { rootMargin: MARGE});142 const eerste = stroom() && stroom().querySelector('.read-post');143 const la atste = stroom() && stroom().querySelector('.read-post:last-of-type');144 if ( eerste) waarnemer.observe(eerste);145 if (la atste && 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); 146 166 } 147 167 148 function bijScroll() {168 function onScroll() { 149 169 const y = window.scrollY || 0; 150 if (y < 8) chrome(false);151 else if (y > la atsteY + 4) chrome(true);152 else if (y < la atsteY - 24) chrome(false);153 la atsteY = 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(); 155 175 } 156 176 157 177 export function init() { 158 const s = str oom();178 const s = stream(); 159 179 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 la atsteY = 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(); 166 186 } 167 187 -
src/views/pages/read.ejs
r7421147 r100aad8 57 57 /* PROXIMITY, geen mandatory. Een bericht dat langer is dan het scherm moet je 58 58 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. */ 64 html: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. */ 71 html:has(body.on-read), .on-read .read-stream, .on-read .read-post { overflow-anchor: none; } 61 72 .on-read .read-post { 62 73 scroll-snap-align: start;
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)