Changes in src/assets/js/mod/read.js [88838bc:77019fa] in Klonkt
- File:
-
- 1 edited
-
src/assets/js/mod/read.js (modified) (13 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
r88838bc r77019fa 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 … … 143 282 */ 144 283 const OP_DESKTOP = window.matchMedia('(hover: hover) and (pointer: fine)'); 284 // PROEF (20-8): Lenis ook op touch. Robin meldde op mobiel drie dingen -- de 285 // header niet zichtbaar bij laden, snappen dat soms te ver gaat, en horizontaal 286 // kunnen scrollen. De eerste twee komen van de NATIVE snap: die vangt al bij het 287 // laden, en zijn uitloop vliegt bij een flick over een grens heen. Lenis zet de 288 // native snap uit en doet het zelf, dus die twee kunnen ermee verdwijnen. 289 // 290 // De prijs is echt: syncTouch betekent dat Lenis het VINGERSCROLLEN overneemt 291 // van iOS -- momentum, rubber-band en het wegschuiven van de adresbalk worden 292 // dan een benadering in JavaScript. De makers waarschuwen daar zelf voor op 293 // iOS < 16. Dit staat er dus als proef, niet als besluit: voelt het niet goed, 294 // dan is OP_TOUCH weer false zetten de hele terugweg. 295 const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)'); 145 296 const VENDOR_V = 1; // ophogen als de bestanden in /assets/js/vendor wijzigen 146 297 … … 149 300 150 301 async function startLenis() { 151 if (lenis || !OP_DESKTOP.matches) return; 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. 306 if (lenis || !(OP_DESKTOP.matches || OP_TOUCH.matches)) return; 152 307 const [L, S] = await Promise.all([ 153 308 import(`/assets/vendor/lenis.mjs?v=${VENDOR_V}`), … … 155 310 ]); 156 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')), 157 329 // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in 158 330 // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en … … 165 337 smoothWheel: true, 166 338 lerp: 0.2, 167 syncTouch: false, // op touch blijft alles van het systeem 339 // Alleen op touch overneemt hij het vingerscrollen; op desktop hoeft dat niet. 340 syncTouch: OP_TOUCH.matches, 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. 344 // 345 // touchInertiaExponent hoe VER de flick draagt (standaard 1.7) 346 // syncTouchLerp hoe hard hij REMT (standaard 0.075) 347 // 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. 351 // 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 } : {}), 168 357 autoRaf: true, 169 358 }); 170 359 snap = new S.default(lenis, { 171 360 type: 'proximity', 172 distanceThreshold: '12%', // de vangzone, hier WEL instelbaar 173 debounce: 60, // niet de standaard 500: dat voelt als te laat 174 duration: 0.4, // in totaal onder de halve seconde 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%', 368 // Hoe lang na de laatste scrollbeweging hij mag vangen. De standaard is 500 369 // en dat voelt als te laat. Op touch nog korter dan op desktop, want daar 370 // eindigt een veeg in een lange, trage staart -- en juist dan wil je dat de 371 // snap er snel bij is in plaats van te wachten tot de laatste pixel stil ligt. 372 // 373 // LET OP DE SPANNING: een vloeiendere uitloop (lagere syncTouchLerp) maakt 374 // die staart langer, en stelt de snap dus uit. Deze twee getallen houden 375 // elkaar in evenwicht; draai je aan de een, kijk dan ook naar de ander. 376 // OP TOUCH JUIST LANG, en dat is het omgekeerde van wat ik vorige ronde deed. 377 // Tussen je vinger loslaten en het op gang komen van de uitloop zit een korte 378 // stilte in de scrollbeweging. Met 25ms viel de snap precies in dat gat: hij 379 // greep op het moment van loslaten en knipte de veeg af -- wat aanvoelt als 380 // "stroef bij het loslaten", en het werd er dan ook erger van. 200ms laat de 381 // uitloop eerst zijn werk doen. 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, 385 // Op touch korter: een telefoon vraagt om directer antwoord dan een muis, en 386 // de snap komt daar aan het eind van een lange uitloop -- dan mag hij kort. 387 duration: OP_TOUCH.matches ? 0.18 : 0.4, 175 388 // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de 176 389 // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit. … … 179 392 easing: (t) => 1 - Math.pow(1 - t, 4), 180 393 }); 181 // Het snappunt is de bovenkant van elk bericht. Het laatste doet niet mee: 182 // zijn bovenkant is niet te bereiken, er zit te weinig pagina onder. 394 // Het snappunt is de bovenkant van elk bericht -- ook van het laatste. Dat 395 // deed eerder niet mee omdat zijn bovenkant onbereikbaar was; sinds het 396 // laatste bericht in CSS een volle schermhoogte krijgt, kan hij wel. 183 397 const berichten = [...document.querySelectorAll('.feed-reader .read-post')]; 184 berichten.slice(0, -1).forEach((a) => snap.addElement(a, { align: 'start' })); 398 berichten.forEach((a) => snap.addElement(a, { align: 'start' })); 399 400 // ALLEEN VOORUIT VANGEN. Dit hoort bij de ruime zone hierboven en is niet 401 // optioneel: lenis/snap kiest het DICHTSTBIJZIJNDE punt en zijn drempel geldt 402 // naar twee kanten. Scroll je een lang bericht in en stop je 300 pixels onder 403 // de bovenkant, dan is die bovenkant het dichtstbij -- en met een zone van 400 404 // wordt je teruggetrokken. Dat is precies de klacht "ik kan niet scrollen 405 // binnen een lang bericht", en met de oude zone van 86 pixels viel het alleen 406 // niet op. 407 // 408 // Dus: een doel ACHTER je slaan we over. Ligt er een punt voor je binnen de 409 // zone, dan gaan we daarheen; anders gebeurt er niets en scrol je vrij door. 410 // Zo verruimt de zone alleen de kant waar hij bedoeld is. 411 // 412 // Waarom niet gewoon de drempel? Omdat lenis/snap alleen de dichtstbijzijnde 413 // kandidaat beoordeelt: zonder deze omleiding houdt de zone vooruit op bij de 414 // helft van de afstand tussen twee berichten, hoe groot je de drempel ook zet. 415 const echtGaNaar = snap.goTo.bind(snap); 416 snap.goTo = (index) => { 417 const punten = snap.computeSnaps(); // zelfde volgorde als goTo intern gebruikt 418 const nu = lenis.scroll; 419 const doel = punten[index]; 420 if (doel && doel.value > nu + 2) { echtGaNaar(index); return; } 421 let vooruit = -1; 422 punten.forEach((punt, i) => { 423 if (punt.value > nu + 2 && (vooruit < 0 || punt.value < punten[vooruit].value)) vooruit = i; 424 }); 425 if (vooruit < 0) return; 426 if (punten[vooruit].value - nu <= snap.distanceThreshold) echtGaNaar(vooruit); 427 }; 185 428 // Native snappen uit: twee mechanismen op dezelfde scroller vechten. 186 429 document.documentElement.style.scrollSnapType = 'none'; … … 199 442 function zetSnappen(aan) { 200 443 if (snap) { if (aan) snap.start(); else snap.stop(); return; } 444 // Zonder Lenis (mobiel) zetten we de CSS-eigenschap om. Hij staat bij het 445 // laden UIT -- dat is de standaardstand van deze variabele -- zodat de browser 446 // niet meteen naar het eerste bericht springt en de header wegvalt. Pas je 447 // eerste gebaar naar beneden zet hem aan. 201 448 const el = document.documentElement; 202 449 const wil = aan ? '' : 'none'; … … 241 488 242 489 export function init() { 490 // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. 491 // 492 // Dit stond andersom, en dat brak het scrollen op de HELE site: navigeerde je 493 // van Lezen naar het beheer, dan viel init() bij de ontbrekende leesstroom 494 // meteen terug -- en bleef Lenis leven EN gestopt (lenis.stop() vangt wiel en 495 // vinger af). Daarna scrolde niets meer, ook niet op pagina's die met Lezen 496 // niets te maken hebben. Robins melding (20-8): "scrollen werkt nu nergens ook 497 // niet in admin panels". 498 // 499 // Opruimen hoort dus bij het VERLATEN van de weergave, niet bij het opzetten 500 // ervan. init() draait bij elke paginawissel, dus dit is de plek. 501 stopLenis(); 502 document.body.classList.remove('is-paged'); 503 document.querySelectorAll('[data-lenis-prevent].read-post') 504 .forEach((a) => a.removeAttribute('data-lenis-prevent')); 505 243 506 const s = document.getElementById('read-stream'); 244 507 if (!s) return; … … 253 516 // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet 254 517 // hij niets, maar hij springt niet in beeld bij het laden. 255 knop = document.getElementById('read-top'); 256 if (knop) { 257 knop.onclick = naarBoven; 258 toonKnop(knop); 259 } 518 // De terug-naar-boven-knop staat er tijdelijk uit (Robin, 20-8) -- in 519 // paginamodus doet de bovenbalk dat werk al. De code blijft staan zodat hij 520 // met een regel terug is. 521 knop = null; 522 const oudeKnop = document.getElementById('read-top'); 523 if (oudeKnop) oudeKnop.hidden = true; 260 524 261 525 if (opScroll) { … … 270 534 // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar 271 535 // levend; pas als het stil blijft mag een nieuwe richting gelden. 272 opScroll = () => { if (knop) toonKnop(knop); planRust(); }; 536 opScroll = () => { 537 if (knop) toonKnop(knop); 538 planRust(); 539 if (document.body.classList.contains('is-paged')) zetBalken(); 540 }; 273 541 window.addEventListener('scroll', opScroll, { passive: true }); 274 542 window.addEventListener('scrollend', rustNu); … … 279 547 window.addEventListener('keydown', opToets); 280 548 549 const balkBoven = document.getElementById('read-prev'); 550 const balkOnder = document.getElementById('read-next-nav'); 551 if (balkBoven) balkBoven.onclick = gaOmhoog; 552 if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1); 553 281 554 // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw 282 555 // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en 283 556 // wegnavigeren hoort hem op te ruimen. 284 stopLenis(); 285 if (document.body.dataset.feedView === 'reader') startLenis(); 557 if (document.body.dataset.feedView === 'reader') startLenis().then(pasPaginaModusToe); 558 else pasPaginaModusToe(); 559 560 286 561 } 287 562
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)