source: Klonkt/src/assets/js/mod/read.js@ 48481cd

main
Last change on this file since 48481cd was e46ebaa, checked in by Robin <roboburr@…>, 3 weeks ago

Lezen: snappen dat je laat lezen, en geen lege schermen meer

Barts meldingen van 20 augustus, in een stuk of zes rondes bijgesteld. De
uitkomst is korter dan de weg ernaartoe, en die weg is het waard om vast te
leggen omdat ik er twee keer overheen ben geschoten.

SNAPPEN. De regel is nu in drie zinnen te zeggen:

omhoog scrollen nooit snappen -- je zoekt iets terug en bepaalt zelf

waar je stopt

omlaag, ver weg vrij scrollen
omlaag, laatste
150px van een
bericht vangt, en je landt op de bovenkant van het volgende

Er is geen top-zone: de bovenkant van het bericht waar je IN zit is geen
doelwit meer. Dat was de oorspronkelijke klacht -- je las een paar regels
verder en proximity trok je terug naar de bovenrand, waardoor het leek of
het scrollen vastliep. De grens die wel vangt is de onderkant van je huidige
bericht, en dat is hetzelfde punt als de bovenkant van het volgende.

Onderweg twee doodlopende wegen, allebei door Robin gecorrigeerd:

  • Eerst zette ik de snap UIT voor berichten langer dan het scherm. Op een telefoon is bijna elk bericht net iets langer dan de viewport, dus daar verdween het snappen overal: op 812px hoog kregen berichten van 1009 en zelfs 839 al geen snap meer.
  • Toen probeerde ik de vangzone aan de VOET van het bericht te hangen ("ben je de reacties voorbij"). Bij een kort bericht staat die voet middenin de lege ruimte die min-height maakte, dus de zone begon op een willekeurige plek. Gelukkig ving Robin dat voordat het uitgerold stond.

Het moest dus een getal in pixels zijn, en 150 is dat getal.

GEEN LEGE SCHERMEN MEER. min-height:100svh rekte elk bericht op tot een vol
scherm. Bij een kort bericht gaf dat een halve pagina leegte onder "Replies
and reactions" -- je keek naar niets en moest doorscrollen om te ontdekken
dat er nog iets kwam. Weg dus: berichten hebben hun eigen hoogte (gemeten:
360/1009/839/250 waar het eerst 888/888/888/888 was) en de stroom is gewoon
bericht na bericht. Het snappen hangt aan scroll-snap-align en niet aan die
hoogte, dus dat werkt onveranderd.

VLOEIEND: scroll-behavior:smooth, want zonder dat springt een snap er in een
frame naartoe en voelt het als een hik. Uit bij prefers-reduced-motion.

TERUG NAAR BOVEN, rechtsonder, in twee stappen: eerst naar het begin van DIT
bericht, daarna pas naar de kop van de pagina. Die knop is de enige die naar
een bovenkant mag springen, en hij heeft het snappen daar niet voor nodig --
scrollIntoView stuurt zelf. Nagemeten, juist omdat omhoog-snappen uit staat:
1706 -> 1206, exact de bovenkant.

COVERS nemen de volle kolombreedte met de verhouding intact. De ronde ervoor
haalde het opblazen eruit maar liet kleine afbeeldingen op ware grootte
staan, en dat was als postzegel in een brede kolom te klein.

DE CIRKEL. Lezen blijft daar dood (berichten van anderen), maar een tijdlijn
werkt er juist bij uitstek. Die dwong ik eerst af zodra de site geen
Lezen-site was, en dan kon je in de cirkel geen Grid meer kiezen. Nu is Grid
de landing en staat Tijdlijn ernaast: bij feed_alt_view=timeline altijd, bij
auto op desktop. De cirkel krijgt zijn eigen geheugen terug, met een eigen
sleutel zodat een keuze daar je thuisweergave niet aanraakt.

style.css naar v77 en MOD_V naar 13.

Suite 1165/1165.

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

  • Property mode set to 100644
File size: 8.4 KB
Line 
1/**
2 * De leesweergave: tikken op een bericht opent dat bericht.
3 *
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 *
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.
13 *
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.
18 */
19
20/**
21 * Snappen alleen waar het HELPT.
22 *
23 * Elk bericht heeft scroll-snap-align:start en min-height:100svh. Voor een kort
24 * bericht is dat precies goed: het vult het scherm en de volgende klikt netjes
25 * op zijn plek. Voor een LANG bericht is het een val -- je leest naar beneden en
26 * de browser trekt je terug naar de bovenrand, waardoor het scrollen lijkt te
27 * stoppen terwijl je gewoon doorscrollt. Barts melding (20-8).
28 *
29 * "Past dit bericht op het scherm?" is een vraag over gemeten hoogte, en die kan
30 * CSS niet stellen -- vandaar hier. Past het niet, dan gaat de snap eraf en lees
31 * je ononderbroken door tot het volgende bericht wel weer een snappunt is.
32 *
33 * De marge van 4px vangt afrondingsverschillen tussen svh en de echte hoogte;
34 * zonder die speling wipt een bericht dat toevallig exact past heen en weer.
35 */
36/**
37 * Wanneer is de bovenkant van een bericht een snappunt?
38 *
39 * Alleen zolang je er nog NIET voorbij bent. Zit de bovenrand op of onder de
40 * bovenkant van het scherm, dan kom je er nog aan en mag hij vangen. Is hij
41 * eenmaal voorbij -- je leest in het bericht -- dan gaat de snap eraf.
42 *
43 * Robins formulering (20-8), en die is preciezer dan wat ik er eerst van maakte:
44 * "in de post zelf mag er nooit gesnapt worden, ook niet bovenin; enkel de
45 * onderkant mag snappen naar de volgende post, dus daar de bovenkant van".
46 * Precies dat: de onderkant van bericht N is de bovenkant van N+1, en die is een
47 * doelwit omdat je hem nadert. De bovenkant van het bericht waar je IN zit is
48 * dat niet meer, want daar ben je voorbij.
49 *
50 * Zonder deze regel trok proximity je terug naar de bovenrand zodra je een paar
51 * regels verder scrolde, en leek het of het scrollen vastliep.
52 *
53 * De 4px is speling voor afronding: tijdens het snappen zelf loopt top naar 0 en
54 * mag hij niet halverwege afhaken.
55 */
56const SPELING = 4;
57
58/**
59 * En snappen doet alleen mee op de weg NAAR BENEDEN (Robin, 20-8).
60 *
61 * Omhoog scroll je om iets terug te zoeken, en dan is elke vangst een hindernis:
62 * je wilt zelf bepalen waar je stopt. Omlaag lees je door, en dan helpt de
63 * grens juist. Dus bij omhoog gaat de snap er overal af.
64 *
65 * De drempel van 2px is er tegen richtingsruis: tijdens een vloeiende snap
66 * schommelt scrollY een fractie, en zonder speling zou de richting dan heen en
67 * weer klappen -- precies midden in de beweging die net soepel moest zijn.
68 */
69let laatsteY = 0;
70let omlaag = true;
71
72/**
73 * Hoe DICHT bij de grens hij vangt: de onderste VANGZONE pixels van een bericht.
74 *
75 * In pixels, en bewust niet inhoudelijk. Ik probeerde het aan de voet van het
76 * bericht te hangen ("ben je de reacties voorbij"), maar bij een KORT bericht
77 * gaat dat mis: min-height rekt zo'n bericht tot een vol scherm, dus de voet
78 * staat middenin met lege ruimte eronder en de zone begint op een willekeurige
79 * plek. Robin ving dat (20-8) voordat het uitgerold stond.
80 *
81 * Eerst stond dit op 20px en dat was te krap om ooit te vangen. Honderdvijftig
82 * is ruwweg het laatste stukje van een bericht: kom je daarbinnen tot stilstand,
83 * dan trekt hij de streep recht naar de bovenkant van het volgende. Stop je
84 * eerder, dan blijf je gewoon staan.
85 *
86 * Eén getal, dus makkelijk bij te stellen als het onder een duim anders voelt.
87 */
88const VANGZONE = 150;
89
90function ijkEen(art) {
91 const top = art.getBoundingClientRect().top;
92 // Omhoog: nooit. Omlaag: alleen een grens die je NADERT en die binnen de zone
93 // ligt. De ondergrens blijft krap (SPELING): eenmaal voorbij wordt er niet
94 // teruggetrokken, ook niet over een paar pixels.
95 const wil = (!omlaag || top < -SPELING || top > VANGZONE) ? 'none' : '';
96 // Alleen aanraken als het echt verandert: elke stijlwijziging tijdens een
97 // vloeiende scroll is een kans op een hik, zeker op iOS.
98 if (art.style.scrollSnapAlign !== wil) art.style.scrollSnapAlign = wil;
99}
100
101function ijkSnappen() {
102 const y = window.scrollY;
103 if (Math.abs(y - laatsteY) > 2) omlaag = y > laatsteY;
104 laatsteY = y;
105 document.querySelectorAll('.feed-reader .read-post').forEach(ijkEen);
106}
107
108/**
109 * Terug naar boven, en bewust NIET window.scrollTo(0).
110 *
111 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
112 * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet
113 * dus twee verschillende dingen -- dat scheelt op mobiel een halve minuut vegen.
114 */
115function naarBoven() {
116 const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
117 const gedrag = zacht ? 'smooth' : 'auto';
118 const posts = [...document.querySelectorAll('.feed-reader .read-post')];
119 const huidig = posts.find((a) => {
120 const r = a.getBoundingClientRect();
121 return r.top <= 8 && r.bottom > 8;
122 });
123 // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop.
124 if (huidig && huidig.getBoundingClientRect().top < -8) {
125 huidig.scrollIntoView({ behavior: gedrag, block: 'start' });
126 return;
127 }
128 window.scrollTo({ top: 0, behavior: gedrag });
129}
130
131/** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */
132function toonKnop(knop) {
133 knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6);
134}
135
136let tapX = 0, tapY = 0;
137function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; }
138
139function onTap(e) {
140 if (e.defaultPrevented || e.button !== 0) return;
141 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
142 const t = e.target;
143 if (!t || typeof t.closest !== 'function') return;
144 const art = t.closest('.read-post');
145 if (!art) return;
146 if (t.closest('a, button, input, textarea, select, label, summary, [role="button"]')) return;
147 if (Math.abs(e.clientX - tapX) > 10 || Math.abs(e.clientY - tapY) > 10) return;
148 const sel = window.getSelection && window.getSelection();
149 if (sel && String(sel).trim()) return;
150 const slug = art.dataset.slug;
151 if (!slug) return;
152 location.href = (art.dataset.base || '') + '/' + encodeURIComponent(slug);
153}
154
155let knop = null;
156let opScroll = null;
157
158export function init() {
159 const s = document.getElementById('read-stream');
160 if (!s) return;
161 // Op de stroom, niet per artikel: wat "meer laden" erbij zet doet vanzelf mee.
162 // init() draait bij ELKE paginawissel, dus eerst losmaken -- anders stapelt
163 // dezelfde afhandelaar zich op en vuurt hij twee keer.
164 s.removeEventListener('pointerdown', onPointerDown);
165 s.removeEventListener('click', onTap);
166 s.addEventListener('pointerdown', onPointerDown, { passive: true });
167 s.addEventListener('click', onTap);
168
169 // De knop staat in de HTML (read-top.ejs), zodat hij er ook is zonder deze
170 // module -- dan doet hij niets, maar hij springt niet in beeld bij het laden.
171 knop = document.getElementById('read-top');
172 if (knop) {
173 knop.onclick = naarBoven;
174 toonKnop(knop);
175 }
176
177 // Eén luisteraar, en losmaken bij een volgende init(): deze module draait bij
178 // ELKE paginawissel en anders stapelen ze op.
179 if (opScroll) {
180 window.removeEventListener('scroll', opScroll);
181 window.removeEventListener('resize', opScroll);
182 }
183 // Eén keer per frame, niet per scroll-event: scroll vuurt tientallen keren per
184 // seconde en we raken hier de stijl van elk bericht aan.
185 let gepland = false;
186 opScroll = () => {
187 if (knop) toonKnop(knop);
188 if (gepland) return;
189 gepland = true;
190 requestAnimationFrame(() => { gepland = false; ijkSnappen(); });
191 };
192 window.addEventListener('scroll', opScroll, { passive: true });
193 window.addEventListener('resize', opScroll, { passive: true });
194 ijkSnappen(); // ook meteen bij binnenkomst, niet pas bij de eerste scroll
195}
196
197export default { init };
Note: See TracBrowser for help on using the repository browser.