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

main
Last change on this file since ca260d1 was 19e28ce, checked in by Robin <roboburr@…>, 4 hours ago

Lezen scrolt gewoon, en Lenis is uit Klonkt

Robin, 30-9: "Reader view normaal scrollen, Lenis mag helemaal uit klonkt."

Lenis stond alleen in de leesweergave: op desktop voor zacht scrollen en
snappen naar de bovenkant van een bericht, op mobiel nam hij de vinger over en
bootste mod/read.js het snappen na. Dat is er allemaal uit, en de browser
scrolt nu zoals op elke andere pagina.

ER SNAPT OOK NIETS MEER, en dat is een keuze. Er lag nog een native route
klaar (scroll-snap-type, alleen bij naar beneden scrollen aangezet), maar die
had precies de problemen waarom Lenis er ooit kwam: bij het laden sprong hij
naar het eerste bericht zodat de header wegviel, en bij een harde veeg vloog hij
over een grens. "Normaal scrollen" is zonder snappen; de vault-notitie "Klonkt
Reader Scroll" zegt hoe het native terug kan als dat toch gewild is.

Wat met het snappen meeging:

  • de richtingsdetectie in read.js (wiel, vinger, toetsen, rust-timer): die bestond alleen om het snappen aan en uit te zetten;
  • de volle schermhoogte op het laatste bericht: die maakte zijn bovenkant bereikbaar als snappunt, en is zonder snappen een leeg scherm aan het eind;
  • de afgedwongen schermhoogte per bericht op mobiel: die hoorde bij vegen van bericht naar bericht, en gaf onder een kort bericht dezelfde leegte waar Robin op 20-8 op desktop over viel. De site-instelling "elk bericht een eigen scherm" geldt nu overal voor wie dat wil.

De bewaarde paginamodus (staat uit) leunde op lenis.stop() om de stroom vast te
zetten; dat doet nu een regel CSS op het document, en de balken gaan met native
scrollIntoView. Niet uitgeprobeerd sinds die omzetting, want de modus staat uit;
dat staat er ook bij.

Weg: src/assets/vendor/lenis.mjs en lenis-snap.mjs (57 KB samen, en twee
netwerkrondes voordat er iets werkte), en data-lenis-prevent op het zoekvak.
Scroll anchoring blijft uit: ook zonder snappen hoort wat je leest niet te
verspringen als Load more er berichten bij zet. init() zet een inline
scroll-snap-type van de vorige versie terug, voor een tabblad dat na een
htmx-wissel met de oude module was begonnen.

MOD_V 67 en style.css v111: anders draait een browser die er al was nog een jaar
de oude read.js met Lenis.

Volledige suite 1292 groen; er leunde geen toets op Lenis of het snappen.

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 10.0 KB
Line 
1/**
2 * De leesweergave: tikken op een bericht opent dat bericht, en een knop om terug
3 * naar boven te gaan.
4 *
5 * Dit was een heel scherm met een eigen route, dat zijn buren zelf ophaalde, de
6 * scrollpositie corrigeerde bij invoegen en de balken wegschoof. Dat is allemaal
7 * weg, en dat is winst: Lezen is nu een WEERGAVE van de feed
8 * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de
9 * berichten al en "meer laden" vult al aan.
10 *
11 * GEWOON SCROLLEN (Robin, 30-9: "Reader view normaal scrollen, Lenis mag helemaal
12 * uit klonkt"). Hier stond Lenis: op desktop voor zacht scrollen en snappen naar
13 * de bovenkant van een bericht, op mobiel nam hij de vinger over en bootsten we
14 * het snappen na. Dat is er allemaal uit. De browser scrolt, zoals op elke andere
15 * pagina, en er snapt niets meer. Waarom snappen niet native terugkwam, en hoe
16 * dat alsnog zou kunnen, staat in de vault onder "Klonkt Reader Scroll".
17 *
18 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk
19 * voor toetsenbord en schermlezer; de tik hieronder is er voor een duim.
20 *
21 * Vier uitzonderingen op die tik, want een tik die je niet bedoelde is erger dan
22 * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen
23 * werking, een geselecteerde tekst is geen tik, een verschoven vinger is
24 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.
25 */
26
27const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)');
28
29// ── Paginamodus ─────────────────────────────────────────────────────────────
30/**
31 * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen
32 * maar bewaren, voor als ik het later weer wil").
33 *
34 * Wat het is: elk bericht een paneel van een scherm dat zelf scrollt, met twee
35 * balken om ertussen te navigeren. Gebouwd omdat het namaken van iOS-momentum
36 * niet lukte; in dit model doet het scrollgevoel er namelijk niet toe.
37 *
38 * Sinds Lenis weg is (30-9) zet de CSS de stroom vast in plaats van lenis.stop()
39 * (html:has(body.is-paged) in style.css), en gaan de balken met native
40 * scrollIntoView. Niet uitgeprobeerd sinds die omzetting, want de modus staat
41 * uit: wie hem aanzet, kijkt dat eerst na.
42 *
43 * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan
44 * body.is-paged) en scrollen de panelen zelf.
45 */
46const PAGINAMODUS = false;
47
48function paginaModus() {
49 return PAGINAMODUS && OP_TOUCH.matches && document.body.dataset.feedView === 'reader';
50}
51
52function panelen() {
53 return [...document.querySelectorAll('.feed-reader .read-post')];
54}
55
56/** Welk paneel vult nu het scherm? Het eerste waarvan de bovenkant niet voorbij is. */
57function huidigIndex() {
58 const P = panelen();
59 for (let i = 0; i < P.length; i++) {
60 if (P[i].getBoundingClientRect().top > 8) return Math.max(0, i - 1);
61 }
62 return Math.max(0, P.length - 1);
63}
64
65function gaNaar(i) {
66 const P = panelen();
67 const doel = P[Math.max(0, Math.min(P.length - 1, i))];
68 if (!doel) return;
69 doel.scrollIntoView({ behavior: 'smooth', block: 'start' });
70 setTimeout(zetBalken, 80);
71}
72
73/**
74 * Omhoog vanaf het EERSTE bericht brengt je naar de header.
75 *
76 * In paginamodus scrolt de stroom niet met je vinger, dus dan is alles boven het
77 * eerste bericht onbereikbaar. Robin liep daar tegenaan (20-8): "ik kan niet
78 * meer terug scrollen naar de header". De omhoog-knop is daar de enige weg
79 * naartoe, dus die krijgt er een trede bij.
80 */
81function gaOmhoog() {
82 const i = huidigIndex();
83 if (i > 0) { gaNaar(i - 1); return; }
84 window.scrollTo({ top: 0, behavior: 'smooth' });
85}
86
87/**
88 * Welke balk hoort er te staan? Bovenaan de pagina is er niets boven je, dus dan
89 * geen bovenbalk. Buiten paginamodus staan ze allebei niet -- de CSS verbergt ze
90 * daar al, maar hidden houdt ze ook uit de toetsenbordvolgorde.
91 */
92function zetBalken() {
93 const boven = document.getElementById('read-prev');
94 const onder = document.getElementById('read-next-nav');
95 const aan = paginaModus();
96 // De bovenbalk hoort NOOIT over de header te liggen. Hij verscheen al zodra je
97 // een paar pixels scrolde, en dekte dan de avatar, de omschrijving en de
98 // weergaveknoppen af (Robins schermafbeelding, 20-8). Nu komt hij pas als het
99 // eerste bericht de bovenrand van het scherm heeft bereikt -- dan is de header
100 // voorbij en is er ook echt iets om naar terug te gaan.
101 const eerste = panelen()[0];
102 const headerNogInBeeld = eerste ? eerste.getBoundingClientRect().top > 4 : true;
103 if (boven) boven.hidden = !aan || headerNogInBeeld;
104 if (onder) onder.hidden = !aan;
105 if (onder && aan) onder.style.bottom = onderChroom() + 'px';
106}
107
108/**
109 * Hoe hoog staat de onderrand van het scherm werkelijk vol?
110 *
111 * De onderbalk stond op een geraden 4.75rem boven de onderkant, en dan zweeft
112 * hij: soms een kier boven de speler, soms er half achter. De tabbalk en de
113 * mini-speler hebben allebei een eigen hoogte, ze stapelen op mobiel, en de
114 * speler komt en gaat. Dus meten in plaats van gokken: hoe ver ligt de BOVENKANT
115 * van het hoogste vaste element boven de onderrand van het venster?
116 */
117function onderChroom() {
118 // .bottom-tab-fab staat erbij omdat de Write-knop BOVEN de tabbalk uitsteekt:
119 // meet je alleen de balk, dan legt onze balk zich over die knop heen (Robins
120 // schermafbeelding, 20-8).
121 const kandidaten = ['.bottom-tab', '.bottom-tab-fab', '#pcms-audio-player'];
122 let hoogste = 0;
123 kandidaten.forEach((sel) => {
124 const el = document.querySelector(sel);
125 if (!el) return;
126 const st = getComputedStyle(el);
127 if (st.display === 'none' || st.visibility === 'hidden') return;
128 const r = el.getBoundingClientRect();
129 if (r.height <= 0) return;
130 hoogste = Math.max(hoogste, window.innerHeight - r.top);
131 });
132 return Math.round(hoogste);
133}
134
135function pasPaginaModusToe() {
136 document.body.classList.toggle('is-paged', paginaModus());
137 zetBalken();
138}
139
140/**
141 * Terug naar boven, en bewust NIET meteen window.scrollTo(0).
142 *
143 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
144 * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet
145 * dus twee verschillende dingen -- dat scheelt op mobiel een hoop vegen.
146 */
147function naarBoven() {
148 const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
149 const gedrag = zacht ? 'smooth' : 'auto';
150 const posts = [...document.querySelectorAll('.feed-reader .read-post')];
151 const huidig = posts.find((a) => {
152 const r = a.getBoundingClientRect();
153 return r.top <= 8 && r.bottom > 8;
154 });
155 // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop.
156 if (huidig && huidig.getBoundingClientRect().top < -8) {
157 huidig.scrollIntoView({ behavior: gedrag, block: 'start' });
158 return;
159 }
160 window.scrollTo({ top: 0, behavior: gedrag });
161}
162
163/** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */
164function toonKnop(knop) {
165 knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6);
166}
167
168let tapX = 0, tapY = 0;
169function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; }
170
171function onTap(e) {
172 if (e.defaultPrevented || e.button !== 0) return;
173 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
174 const t = e.target;
175 if (!t || typeof t.closest !== 'function') return;
176 const art = t.closest('.read-post');
177 if (!art) return;
178 if (t.closest('a, button, input, textarea, select, label, summary, [role="button"]')) return;
179 if (Math.abs(e.clientX - tapX) > 10 || Math.abs(e.clientY - tapY) > 10) return;
180 const sel = window.getSelection && window.getSelection();
181 if (sel && String(sel).trim()) return;
182
183 const slug = art.dataset.slug;
184 if (!slug) return;
185 location.href = (art.dataset.base || '') + '/' + encodeURIComponent(slug);
186}
187
188let knop = null;
189let opScroll = null;
190
191export function init() {
192 // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. init()
193 // draait bij elke paginawissel, en wat de leesweergave aanzette hoort weg te
194 // zijn zodra je hem verlaat -- ook op pagina's die met Lezen niets te maken
195 // hebben (Robins melding van 20-8, toen dat misging: "scrollen werkt nu nergens
196 // ook niet in admin panels").
197 document.body.classList.remove('is-paged');
198 // Een tabblad dat nog met de vorige versie van deze module is geopend, heeft
199 // het native snappen inline uitgezet. Na een htmx-wissel naar deze versie hoort
200 // daar niets van over te blijven.
201 document.documentElement.style.scrollSnapType = '';
202
203 const s = document.getElementById('read-stream');
204 if (!s) return;
205 // Op de stroom, niet per artikel: wat "meer laden" erbij zet doet vanzelf mee.
206 // init() draait bij ELKE paginawissel, dus eerst losmaken -- anders stapelt
207 // dezelfde afhandelaar zich op en vuurt hij twee keer.
208 s.removeEventListener('pointerdown', onPointerDown);
209 s.removeEventListener('click', onTap);
210 s.addEventListener('pointerdown', onPointerDown, { passive: true });
211 s.addEventListener('click', onTap);
212
213 // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet
214 // hij niets, maar hij springt niet in beeld bij het laden.
215 // De terug-naar-boven-knop staat er tijdelijk uit (Robin, 20-8) -- in
216 // paginamodus doet de bovenbalk dat werk al. De code blijft staan zodat hij
217 // met een regel terug is.
218 knop = null;
219 const oudeKnop = document.getElementById('read-top');
220 if (oudeKnop) oudeKnop.hidden = true;
221
222 if (opScroll) {
223 window.removeEventListener('scroll', opScroll);
224 window.removeEventListener('resize', opScroll);
225 }
226 opScroll = () => {
227 if (knop) toonKnop(knop);
228 if (document.body.classList.contains('is-paged')) zetBalken();
229 };
230 window.addEventListener('scroll', opScroll, { passive: true });
231 window.addEventListener('resize', opScroll, { passive: true });
232
233 const balkBoven = document.getElementById('read-prev');
234 const balkOnder = document.getElementById('read-next-nav');
235 if (balkBoven) balkBoven.onclick = gaOmhoog;
236 if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1);
237
238 pasPaginaModusToe();
239}
240
241export default { init };
Note: See TracBrowser for help on using the repository browser.