Changeset 22d2dba5 in Klonkt for src/assets/js/mod/read.js
- Timestamp:
- 08/20/2026 07:25:24 AM (3 weeks ago)
- Branches:
- main
- Children:
- 77019fa
- Parents:
- c581e5e
- git-author:
- Robin <roboburr@…> (08/20/2026 03:33:49 AM)
- git-committer:
- Claude (agent) <aiclaude@…> (08/20/2026 07:25:24 AM)
- File:
-
- 1 edited
-
src/assets/js/mod/read.js (modified) (13 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
rc581e5e r22d2dba5 30 30 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen. 31 31 */ 32 33 // ── Paginamodus ───────────────────────────────────────────────────────────── 34 /** 35 * Elk bericht een paneel dat zelf scrollt; tussen berichten ga je met knoppen. 36 * 37 * Robins voorstel (20-8) nadat het namaken van iOS-momentum niet lukte, en dat 38 * is de winst: in dit model DOET het scrollgevoel er niet toe. Er is geen 39 * momentum om na te bouwen en geen snap die vangt. 40 * 41 * Hoe het werkt: 42 * - lenis.stop() zet het scrollen van de STROOM stil. Lenis vangt dan wiel en 43 * vinger af (hij preventDefault't in onVirtualScroll), dus je kunt niet meer 44 * tussen berichten door scrollen. 45 * - data-lenis-prevent op elk paneel houdt de scroll BINNEN een bericht 46 * ongemoeid. Dat werkt ook terwijl Lenis gestopt is, want die controle staat 47 * in zijn bron vóór de gestopt-controle. Nagekeken in de dist, niet gehoopt. 48 * - Bewegen tussen panelen gaat met lenis.scrollTo(..., { force: true }), want 49 * force is precies de uitzondering die een gestopte Lenis toch laat scrollen. 50 * 51 * Geldt waar een bericht een eigen scherm heeft: op mobiel altijd, op desktop 52 * als de site dat instelt (reader_full_page). Buiten die modus verandert er 53 * niets -- dan is Lenis gewoon aan en scrol je vrij. 54 */ 55 /** 56 * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen 57 * maar bewaren, voor als ik het later weer wil"). 58 * 59 * Wat het is: elk bericht een paneel van een scherm dat zelf scrolt, met twee 60 * balken om ertussen te navigeren en de stroomscroll vastgezet. Gebouwd omdat 61 * het namaken van iOS-momentum niet lukte; in dit model doet het scrollgevoel er 62 * namelijk niet toe. Mobiel is nu terug op systeemscroll met snap, en dat is wat 63 * er ook stond voordat we dit probeerden. 64 * 65 * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan 66 * body.is-paged), stopt Lenis de stroomscroll en scrollen de panelen zelf. 67 */ 68 const PAGINAMODUS = false; 69 70 function paginaModus() { 71 return PAGINAMODUS && OP_TOUCH.matches && document.body.dataset.feedView === 'reader'; 72 } 73 74 function panelen() { 75 return [...document.querySelectorAll('.feed-reader .read-post')]; 76 } 77 78 /** Welk paneel vult nu het scherm? Het eerste waarvan de bovenkant niet voorbij is. */ 79 function huidigIndex() { 80 const P = panelen(); 81 for (let i = 0; i < P.length; i++) { 82 if (P[i].getBoundingClientRect().top > 8) return Math.max(0, i - 1); 83 } 84 return Math.max(0, P.length - 1); 85 } 86 87 function gaNaar(i) { 88 const P = panelen(); 89 const doel = P[Math.max(0, Math.min(P.length - 1, i))]; 90 if (!doel) return; 91 if (lenis) lenis.scrollTo(doel, { force: true }); 92 else doel.scrollIntoView({ behavior: 'smooth', block: 'start' }); 93 setTimeout(zetBalken, 80); 94 } 95 96 /** 97 * Omhoog vanaf het EERSTE bericht brengt je naar de header. 98 * 99 * In paginamodus staat Lenis stil, dus de pagina scrolt niet meer met je vinger 100 * -- en dan is alles boven het eerste bericht onbereikbaar. Robin liep daar 101 * tegenaan (20-8): "ik kan niet meer terug scrollen naar de header". De 102 * omhoog-knop is daar de enige weg naartoe, dus die krijgt er een trede bij. 103 */ 104 function gaOmhoog() { 105 const i = huidigIndex(); 106 if (i > 0) { gaNaar(i - 1); return; } 107 if (lenis) lenis.scrollTo(0, { force: true }); 108 else window.scrollTo({ top: 0, behavior: 'smooth' }); 109 } 110 111 /** 112 * Welke balk hoort er te staan? Bovenaan de pagina is er niets boven je, dus dan 113 * geen bovenbalk. Buiten paginamodus staan ze allebei niet -- de CSS verbergt ze 114 * daar al, maar hidden houdt ze ook uit de toetsenbordvolgorde. 115 */ 116 function zetBalken() { 117 const boven = document.getElementById('read-prev'); 118 const onder = document.getElementById('read-next-nav'); 119 const aan = paginaModus(); 120 // De bovenbalk hoort NOOIT over de header te liggen. Hij verscheen al zodra je 121 // een paar pixels scrolde, en dekte dan de avatar, de omschrijving en de 122 // weergaveknoppen af (Robins schermafbeelding, 20-8). Nu komt hij pas als het 123 // eerste bericht de bovenrand van het scherm heeft bereikt -- dan is de header 124 // voorbij en is er ook echt iets om naar terug te gaan. 125 const eerste = panelen()[0]; 126 const headerNogInBeeld = eerste ? eerste.getBoundingClientRect().top > 4 : true; 127 if (boven) boven.hidden = !aan || headerNogInBeeld; 128 if (onder) onder.hidden = !aan; 129 if (onder && aan) onder.style.bottom = onderChroom() + 'px'; 130 } 131 132 /** 133 * Hoe hoog staat de onderrand van het scherm werkelijk vol? 134 * 135 * De onderbalk stond op een geraden 4.75rem boven de onderkant, en dan zweeft 136 * hij: soms een kier boven de speler, soms er half achter. De tabbalk en de 137 * mini-speler hebben allebei een eigen hoogte, ze stapelen op mobiel, en de 138 * speler komt en gaat. Dus meten in plaats van gokken: hoe ver ligt de BOVENKANT 139 * van het hoogste vaste element boven de onderrand van het venster? 140 */ 141 function onderChroom() { 142 // .bottom-tab-fab staat erbij omdat de Write-knop BOVEN de tabbalk uitsteekt: 143 // meet je alleen de balk, dan legt onze balk zich over die knop heen (Robins 144 // schermafbeelding, 20-8). 145 const kandidaten = ['.bottom-tab', '.bottom-tab-fab', '#pcms-audio-player']; 146 let hoogste = 0; 147 kandidaten.forEach((sel) => { 148 const el = document.querySelector(sel); 149 if (!el) return; 150 const st = getComputedStyle(el); 151 if (st.display === 'none' || st.visibility === 'hidden') return; 152 const r = el.getBoundingClientRect(); 153 if (r.height <= 0) return; 154 hoogste = Math.max(hoogste, window.innerHeight - r.top); 155 }); 156 return Math.round(hoogste); 157 } 158 159 function pasPaginaModusToe() { 160 const aan = paginaModus(); 161 document.body.classList.toggle('is-paged', aan); 162 panelen().forEach((a) => { 163 if (aan) a.setAttribute('data-lenis-prevent', ''); 164 else a.removeAttribute('data-lenis-prevent'); 165 }); 166 if (lenis) { if (aan) lenis.stop(); else lenis.start(); } 167 zetBalken(); 168 } 32 169 33 170 /** … … 73 210 const sel = window.getSelection && window.getSelection(); 74 211 if (sel && String(sel).trim()) return; 212 75 213 const slug = art.dataset.slug; 76 214 if (!slug) return; … … 115 253 116 254 let bezig = false; // loopt er een gebaar of een uitloop? 255 let ooitGescrold = false; // snappen begint UIT, zie zetSnappen() 117 256 let rustTimer = null; 118 257 … … 161 300 162 301 async function startLenis() { 302 // DESKTOP EN TOUCH ALLEBEI. Er heeft hier een tijd gestaan dat mobiel terug was 303 // op de systeemscroll; dat klopte niet meer met de regel hieronder, die 304 // syncTouch juist AANzet op touch. Reden dat Lenis ook op de telefoon meedraait: 305 // zijn snap heeft zijn scroll-gebeurtenissen nodig om te kunnen timen. 163 306 if (lenis || !(OP_DESKTOP.matches || OP_TOUCH.matches)) return; 164 307 const [L, S] = await Promise.all([ … … 167 310 ]); 168 311 lenis = new L.default({ 312 /** 313 * Wat Lenis MET RUST LAAT. 314 * 315 * In paginamodus staat Lenis stil en scrolt alleen het paneel van het 316 * bericht zelf. Dat regelde ik eerst met een data-lenis-prevent-attribuut 317 * dat de module op elk paneel zette -- maar dan hangt het scrollen af van of 318 * dat attribuut op tijd en op elk (ook later bijgeladen) paneel staat, en 319 * Robin kon binnen een lang bericht niet scrollen. 320 * 321 * Deze functie stelt dezelfde vraag zonder die afhankelijkheid: is dit een 322 * bericht-paneel? Dan bemoeit Lenis zich er niet mee en scrolt de browser 323 * het zelf -- wat op een telefoon precies is wat je wilt, want dat is de 324 * systeemscroll. 325 */ 326 prevent: (node) => !!(node && node.classList 327 && node.classList.contains('read-post') 328 && document.body.classList.contains('is-paged')), 169 329 // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in 170 330 // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en … … 179 339 // Alleen op touch overneemt hij het vingerscrollen; op desktop hoeft dat niet. 180 340 syncTouch: OP_TOUCH.matches, 181 // TOUCH-SPECIFIEK, want de standaardwaarden voelen op een telefoon traag. 341 // Twee touch-knoppen, en ze doen echt iets anders. Lenis rekent bij het 342 // LOSLATEN eenmalig een doel uit -- afstand = snelheid^touchInertiaExponent 343 // -- en kruipt daar dan naartoe met syncTouchLerp. 182 344 // 183 // Waar dat zit is precies aan te wijzen. In Lenis staat 184 // `lerp: d ? syncTouchLerp : 1`, waarbij d "de vinger is net losgelaten" 185 // betekent. Tijdens het SLEPEN is de lerp dus 1 -- de inhoud volgt je vinger 186 // exact, net als iOS, en daar is niets mis mee. De traagheid zit in de glijder 187 // NA het loslaten: die gebruikt syncTouchLerp, en dat staat standaard op 188 // 0.075. Dat dempt zo langzaam uit dat de pagina nog seconden naijlt. 345 // touchInertiaExponent hoe VER de flick draagt (standaard 1.7) 346 // syncTouchLerp hoe hard hij REMT (standaard 0.075) 189 347 // 190 // TWEE KNOPPEN, en ze doen iets anders -- dat verwarde ik eerst: 348 // Let op: `touchInertiaMultiplier` bestaat NIET in 1.3.26. Lenis slikt 349 // onbekende opties zonder fout, dus zo'n regel lijkt te werken en doet niets. 350 // Controleer een optienaam in src/assets/vendor/lenis.mjs voor je hem zet. 191 351 // 192 // touchInertiaMultiplier hoe VER een veeg je brengt 193 // syncTouchLerp hoe snel de glijder UITDEMPT 194 // 195 // Eerste ronde zette ik de multiplier op 25 (korter dan de standaard 35), 196 // omdat ik "te traag" las als "hij ijlt te lang na". Robin bedoelde het 197 // omgekeerde: een veeg moet je VERDER brengen, zoals op iOS -- daar draagt 198 // een flick een heel eind. Dus juist omhoog, ruim boven de standaard. 199 // 200 // De lerp gaat mee terug naar 0.1: tussen de trage standaard (0.075, dat 201 // seconden naijlt) en de kordate 0.15 in. Met een langere weg af te leggen 202 // mag de demping wat zachter, anders komt hij te abrupt tot stilstand. 203 // Waar we na een paar rondes proberen op uitkwamen, met de betekenis erbij 204 // omdat de tweede knop contra-intuitief is: 205 // 206 // touchInertiaMultiplier 70 hoe VER een veeg draagt (standaard 35). 207 // 25 en 45 waren allebei te kort. 208 // syncTouchLerp 0.05 hoe snel hij zijn doel BENADERT. Hoger = eerder 209 // aankomen en dus abrupt stoppen; LAGER = langer 210 // onderweg blijven. Robin: "te stroef bij het 211 // loslaten, mag echt een tijdje doorscrollen". 212 // Dus omlaag, niet omhoog -- 0.15 en 0.1 kapten 213 // de uitloop af. 214 // syncTouchLerp 0.25: VIER KEER de vorige waarde, en ruim boven de standaard 215 // van 0.075. Ik heb hem vier rondes lang de verkeerde kant op gedraaid omdat 216 // ik "stroef" las als "hij stopt te vroeg" -- maar het betekende STROPERIG. 217 // Een lage lerp benadert het doel langzaam en geeft dus een kruipende 218 // uitloop; hoog betekent er vlot naartoe. Met de multiplier op 70 blijft de 219 // AFSTAND groot, dus je gaat ver EN snel: dat is hoe een flick op iOS voelt. 220 // 221 // Kort samengevat, want dit is twee keer misgegaan: 222 // syncTouchLerp hoger = sneller weg, korter narollen 223 // multiplier hoger = verder komen 224 ...(OP_TOUCH.matches ? { syncTouchLerp: 0.25, touchInertiaMultiplier: 70 } : {}), 352 // Robin wilde langer en sneller uitrollen zonder remgevoel, dus de exponent 353 // gaat omhoog (een stevige flick draagt daarmee ruim vier tot zes keer zo 354 // ver) en de lerp gaat juist boven de standaard: hij legt die afstand vlot 355 // af in plaats van er stroperig naartoe te kruipen. 356 ...(OP_TOUCH.matches ? { syncTouchLerp: 0.09, touchInertiaExponent: 2.2 } : {}), 225 357 autoRaf: true, 226 358 }); 227 359 snap = new S.default(lenis, { 228 360 type: 'proximity', 229 distanceThreshold: '12%', // de vangzone, hier WEL instelbaar 361 // DE VANGZONE. Gemeten op dev: de voet van een bericht (de link naar de 362 // reacties) staat 138 tot 472 pixels boven de bovenkant van het volgende. 363 // Met de oude 12% was de zone 86 pixels op een venster van 720, dus hij hapte 364 // pas ruim onder die voet toe. 55% is ~400 pixels: de zone begint nu rond de 365 // voet, wat Robin vroeg (20-8) -- eerder al snappen, boven de reacties. 366 // Wil je het nog eerder, dan is dit het enige getal dat je hoeft te draaien. 367 distanceThreshold: '55%', 230 368 // Hoe lang na de laatste scrollbeweging hij mag vangen. De standaard is 500 231 369 // en dat voelt als te laat. Op touch nog korter dan op desktop, want daar … … 242 380 // "stroef bij het loslaten", en het werd er dan ook erger van. 200ms laat de 243 381 // uitloop eerst zijn werk doen. 244 debounce: OP_TOUCH.matches ? 200 : 60, 382 // Met een iOS-achtige uitloop duurt de staart lang; te kort wachten laat de 383 // snap midden in de vlucht ingrijpen. 150ms laat hem uitrollen en vangt dan. 384 debounce: OP_TOUCH.matches ? 150 : 60, 245 385 // Op touch korter: een telefoon vraagt om directer antwoord dan een muis, en 246 386 // de snap komt daar aan het eind van een lange uitloop -- dan mag hij kort. … … 256 396 const berichten = [...document.querySelectorAll('.feed-reader .read-post')]; 257 397 berichten.slice(0, -1).forEach((a) => snap.addElement(a, { align: 'start' })); 398 399 // ALLEEN VOORUIT VANGEN. Dit hoort bij de ruime zone hierboven en is niet 400 // optioneel: lenis/snap kiest het DICHTSTBIJZIJNDE punt en zijn drempel geldt 401 // naar twee kanten. Scroll je een lang bericht in en stop je 300 pixels onder 402 // de bovenkant, dan is die bovenkant het dichtstbij -- en met een zone van 400 403 // wordt je teruggetrokken. Dat is precies de klacht "ik kan niet scrollen 404 // binnen een lang bericht", en met de oude zone van 86 pixels viel het alleen 405 // niet op. 406 // 407 // Dus: een doel ACHTER je slaan we over. Ligt er een punt voor je binnen de 408 // zone, dan gaan we daarheen; anders gebeurt er niets en scrol je vrij door. 409 // Zo verruimt de zone alleen de kant waar hij bedoeld is. 410 // 411 // Waarom niet gewoon de drempel? Omdat lenis/snap alleen de dichtstbijzijnde 412 // kandidaat beoordeelt: zonder deze omleiding houdt de zone vooruit op bij de 413 // helft van de afstand tussen twee berichten, hoe groot je de drempel ook zet. 414 const echtGaNaar = snap.goTo.bind(snap); 415 snap.goTo = (index) => { 416 const punten = snap.computeSnaps(); // zelfde volgorde als goTo intern gebruikt 417 const nu = lenis.scroll; 418 const doel = punten[index]; 419 if (doel && doel.value > nu + 2) { echtGaNaar(index); return; } 420 let vooruit = -1; 421 punten.forEach((punt, i) => { 422 if (punt.value > nu + 2 && (vooruit < 0 || punt.value < punten[vooruit].value)) vooruit = i; 423 }); 424 if (vooruit < 0) return; 425 if (punten[vooruit].value - nu <= snap.distanceThreshold) echtGaNaar(vooruit); 426 }; 258 427 // Native snappen uit: twee mechanismen op dezelfde scroller vechten. 259 428 document.documentElement.style.scrollSnapType = 'none'; … … 272 441 function zetSnappen(aan) { 273 442 if (snap) { if (aan) snap.start(); else snap.stop(); return; } 443 // Zonder Lenis (mobiel) zetten we de CSS-eigenschap om. Hij staat bij het 444 // laden UIT -- dat is de standaardstand van deze variabele -- zodat de browser 445 // niet meteen naar het eerste bericht springt en de header wegvalt. Pas je 446 // eerste gebaar naar beneden zet hem aan. 274 447 const el = document.documentElement; 275 448 const wil = aan ? '' : 'none'; … … 314 487 315 488 export function init() { 489 // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. 490 // 491 // Dit stond andersom, en dat brak het scrollen op de HELE site: navigeerde je 492 // van Lezen naar het beheer, dan viel init() bij de ontbrekende leesstroom 493 // meteen terug -- en bleef Lenis leven EN gestopt (lenis.stop() vangt wiel en 494 // vinger af). Daarna scrolde niets meer, ook niet op pagina's die met Lezen 495 // niets te maken hebben. Robins melding (20-8): "scrollen werkt nu nergens ook 496 // niet in admin panels". 497 // 498 // Opruimen hoort dus bij het VERLATEN van de weergave, niet bij het opzetten 499 // ervan. init() draait bij elke paginawissel, dus dit is de plek. 500 stopLenis(); 501 document.body.classList.remove('is-paged'); 502 document.querySelectorAll('[data-lenis-prevent].read-post') 503 .forEach((a) => a.removeAttribute('data-lenis-prevent')); 504 316 505 const s = document.getElementById('read-stream'); 317 506 if (!s) return; … … 326 515 // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet 327 516 // hij niets, maar hij springt niet in beeld bij het laden. 328 knop = document.getElementById('read-top'); 329 if (knop) { 330 knop.onclick = naarBoven; 331 toonKnop(knop); 332 } 517 // De terug-naar-boven-knop staat er tijdelijk uit (Robin, 20-8) -- in 518 // paginamodus doet de bovenbalk dat werk al. De code blijft staan zodat hij 519 // met een regel terug is. 520 knop = null; 521 const oudeKnop = document.getElementById('read-top'); 522 if (oudeKnop) oudeKnop.hidden = true; 333 523 334 524 if (opScroll) { … … 343 533 // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar 344 534 // levend; pas als het stil blijft mag een nieuwe richting gelden. 345 opScroll = () => { if (knop) toonKnop(knop); planRust(); }; 535 opScroll = () => { 536 if (knop) toonKnop(knop); 537 planRust(); 538 if (document.body.classList.contains('is-paged')) zetBalken(); 539 }; 346 540 window.addEventListener('scroll', opScroll, { passive: true }); 347 541 window.addEventListener('scrollend', rustNu); … … 352 546 window.addEventListener('keydown', opToets); 353 547 548 const balkBoven = document.getElementById('read-prev'); 549 const balkOnder = document.getElementById('read-next-nav'); 550 if (balkBoven) balkBoven.onclick = gaOmhoog; 551 if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1); 552 354 553 // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw 355 554 // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en 356 555 // wegnavigeren hoort hem op te ruimen. 357 stopLenis(); 358 if (document.body.dataset.feedView === 'reader') startLenis(); 556 if (document.body.dataset.feedView === 'reader') startLenis().then(pasPaginaModusToe); 557 else pasPaginaModusToe(); 558 559 359 560 } 360 561
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)