source: Klonkt/src/assets/js/mod/read.js@ b67edbd

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

Leesweergave: /read, en fragmenten in de richting van de scroll

De vorige opzet sprong naar het andere uiteinde van de scroll en schoof het
bericht daarna "vloeiend" in beeld -- de truc van PrutCMS, en een die je voelt.
Nu groeit de stroom gewoon: een schildwacht boven en onder het laatste bericht
haalt het volgende of vorige fragment op en zet het erbij. Bij invoegen BOVEN
wordt de ingevoegde hoogte gemeten en met scrollBy gecompenseerd, anders
verspringt de tekst onder je duim.

De grens tussen berichten is CSS, geen JavaScript: scroll-snap-type: y proximity
op de stroom, scroll-snap-align: start per bericht. Proximity en niet mandatory,
want binnen een lang bericht moet je vrij kunnen scrollen.

En het heet read, niet lees: een pad is geen plek voor een Nederlandse
werkwoordsvorm.

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

  • Property mode set to 100644
File size: 5.5 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
16// Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet
17// sneller dan je scrolt, en meer betekent alleen meer verkeer.
18const MARGE = '1px 0px 100% 0px';
19
20const stroom = () => document.getElementById('read-stream');
21const geladen = new Set(); // slugs die al in de stroom staan
22let bezigOnder = false;
23let bezigBoven = false;
24let waarnemer = null;
25let laatsteY = 0;
26
27function chrome(verbergen) {
28 document.body.classList.toggle('read-chrome-weg', !!verbergen);
29}
30
31/** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */
32function inBeeld() {
33 const posts = [...document.querySelectorAll('.read-post')];
34 const midden = window.innerHeight / 2;
35 return posts.find((p) => {
36 const r = p.getBoundingClientRect();
37 return r.top <= midden && r.bottom >= midden;
38 }) || posts[0];
39}
40
41/** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */
42function volgAdres() {
43 const p = inBeeld();
44 if (!p) return;
45 const slug = p.dataset.slug;
46 const basis = p.dataset.base || '';
47 if (location.pathname === `${basis}/read/${slug}`) return;
48 history.replaceState(null, '', `${basis}/read/${slug}`);
49 if (p.dataset.title) document.title = p.dataset.title;
50}
51
52async function haal(slug, basis) {
53 const r = await fetch(`${basis}/read/${encodeURIComponent(slug)}?partial=1`, {
54 headers: { 'HX-Request': 'true' },
55 credentials: 'same-origin',
56 });
57 if (!r.ok) throw new Error('HTTP ' + r.status);
58 const doos = document.createElement('div');
59 doos.innerHTML = await r.text();
60 const art = doos.querySelector('.read-post');
61 if (!art) throw new Error('geen bericht in het antwoord');
62 return art;
63}
64
65/** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */
66async function vulAan() {
67 const s = stroom();
68 if (!s || bezigOnder) return;
69 const laatste = s.querySelector('.read-post:last-of-type');
70 const slug = laatste && laatste.dataset.older;
71 if (!slug) return klaar(s);
72 if (geladen.has(slug)) return;
73 bezigOnder = true;
74 const wacht = melding(s, 'read-laden', '…');
75 try {
76 const art = await haal(slug, laatste.dataset.base || '');
77 geladen.add(slug);
78 wacht.remove();
79 s.appendChild(art);
80 kijk();
81 } catch (e) {
82 wacht.textContent = 'Kon het volgende bericht niet laden.';
83 console.warn('[read] onderaan:', e && e.message);
84 } finally { bezigOnder = false; }
85}
86
87/**
88 * Bovenaan aanvullen. Dit is het enige plek waar we de scrollpositie WEL
89 * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie
90 * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen
91 * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.
92 */
93async function vulAanBoven() {
94 const s = stroom();
95 if (!s || bezigBoven) return;
96 const eerste = s.querySelector('.read-post');
97 const slug = eerste && eerste.dataset.newer;
98 if (!slug || geladen.has(slug)) return;
99 bezigBoven = true;
100 try {
101 const art = await haal(slug, eerste.dataset.base || '');
102 geladen.add(slug);
103 const voor = s.scrollHeight;
104 s.insertBefore(art, eerste);
105 const erbij = s.scrollHeight - voor;
106 window.scrollBy(0, erbij);
107 kijk();
108 } catch (e) {
109 console.warn('[read] bovenaan:', e && e.message);
110 } finally { bezigBoven = false; }
111}
112
113function melding(s, klasse, tekst) {
114 const d = document.createElement('div');
115 d.className = klasse;
116 d.textContent = tekst;
117 s.appendChild(d);
118 return d;
119}
120
121function klaar(s) {
122 if (s.querySelector('.read-eind')) return;
123 melding(s, 'read-eind', 'Je bent bij het begin van het archief.');
124}
125
126/** (Her)richt de waarnemer op de huidige eerste en laatste post. */
127function kijk() {
128 if (waarnemer) waarnemer.disconnect();
129 waarnemer = new IntersectionObserver((entries) => {
130 for (const e of entries) {
131 if (!e.isIntersecting) continue;
132 if (e.target.matches('.read-post:last-of-type')) vulAan();
133 else if (e.target.matches('.read-post:first-of-type')) vulAanBoven();
134 }
135 }, { rootMargin: MARGE });
136 const eerste = stroom() && stroom().querySelector('.read-post');
137 const laatste = stroom() && stroom().querySelector('.read-post:last-of-type');
138 if (eerste) waarnemer.observe(eerste);
139 if (laatste && laatste !== eerste) waarnemer.observe(laatste);
140}
141
142function bijScroll() {
143 const y = window.scrollY || 0;
144 if (y < 8) chrome(false);
145 else if (y > laatsteY + 4) chrome(true);
146 else if (y < laatsteY - 24) chrome(false);
147 laatsteY = y;
148 volgAdres();
149}
150
151export function init() {
152 const s = stroom();
153 if (!s) return;
154 s.querySelectorAll('.read-post').forEach((p) => geladen.add(p.dataset.slug));
155 laatsteY = window.scrollY || 0;
156 window.addEventListener('scroll', bijScroll, { passive: true });
157 window.addEventListener('resize', kijk, { passive: true });
158 kijk();
159}
160
161export default { init };
Note: See TracBrowser for help on using the repository browser.