Changes in src/assets/js/mod/read.js [77019fa:88838bc] in Klonkt
- File:
-
- 1 edited
-
src/assets/js/mod/read.js (modified) (13 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/read.js
r77019fa r88838bc 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 dat38 * is de winst: in dit model DOET het scrollgevoel er niet toe. Er is geen39 * 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 en43 * vinger af (hij preventDefault't in onVirtualScroll), dus je kunt niet meer44 * tussen berichten door scrollen.45 * - data-lenis-prevent op elk paneel houdt de scroll BINNEN een bericht46 * ongemoeid. Dat werkt ook terwijl Lenis gestopt is, want die controle staat47 * in zijn bron vóór de gestopt-controle. Nagekeken in de dist, niet gehoopt.48 * - Bewegen tussen panelen gaat met lenis.scrollTo(..., { force: true }), want49 * 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 desktop52 * als de site dat instelt (reader_full_page). Buiten die modus verandert er53 * niets -- dan is Lenis gewoon aan en scrol je vrij.54 */55 /**56 * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen57 * 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 twee60 * balken om ertussen te navigeren en de stroomscroll vastgezet. Gebouwd omdat61 * het namaken van iOS-momentum niet lukte; in dit model doet het scrollgevoel er62 * namelijk niet toe. Mobiel is nu terug op systeemscroll met snap, en dat is wat63 * er ook stond voordat we dit probeerden.64 *65 * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan66 * 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 vinger100 * -- en dan is alles boven het eerste bericht onbereikbaar. Robin liep daar101 * tegenaan (20-8): "ik kan niet meer terug scrollen naar de header". De102 * 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 dan113 * geen bovenbalk. Buiten paginamodus staan ze allebei niet -- de CSS verbergt ze114 * 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 je121 // een paar pixels scrolde, en dekte dan de avatar, de omschrijving en de122 // weergaveknoppen af (Robins schermafbeelding, 20-8). Nu komt hij pas als het123 // eerste bericht de bovenrand van het scherm heeft bereikt -- dan is de header124 // 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 zweeft136 * hij: soms een kier boven de speler, soms er half achter. De tabbalk en de137 * mini-speler hebben allebei een eigen hoogte, ze stapelen op mobiel, en de138 * speler komt en gaat. Dus meten in plaats van gokken: hoe ver ligt de BOVENKANT139 * 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 (Robins144 // 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 }169 32 170 33 /** … … 210 73 const sel = window.getSelection && window.getSelection(); 211 74 if (sel && String(sel).trim()) return; 212 213 75 const slug = art.dataset.slug; 214 76 if (!slug) return; … … 253 115 254 116 let bezig = false; // loopt er een gebaar of een uitloop? 255 let ooitGescrold = false; // snappen begint UIT, zie zetSnappen()256 117 let rustTimer = null; 257 118 … … 282 143 */ 283 144 const OP_DESKTOP = window.matchMedia('(hover: hover) and (pointer: fine)'); 284 // PROEF (20-8): Lenis ook op touch. Robin meldde op mobiel drie dingen -- de285 // header niet zichtbaar bij laden, snappen dat soms te ver gaat, en horizontaal286 // kunnen scrollen. De eerste twee komen van de NATIVE snap: die vangt al bij het287 // laden, en zijn uitloop vliegt bij een flick over een grens heen. Lenis zet de288 // 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 overneemt291 // van iOS -- momentum, rubber-band en het wegschuiven van de adresbalk worden292 // dan een benadering in JavaScript. De makers waarschuwen daar zelf voor op293 // 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)');296 145 const VENDOR_V = 1; // ophogen als de bestanden in /assets/js/vendor wijzigen 297 146 … … 300 149 301 150 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. 306 if (lenis || !(OP_DESKTOP.matches || OP_TOUCH.matches)) return; 151 if (lenis || !OP_DESKTOP.matches) return; 307 152 const [L, S] = await Promise.all([ 308 153 import(`/assets/vendor/lenis.mjs?v=${VENDOR_V}`), … … 310 155 ]); 311 156 lenis = new L.default({ 312 /**313 * Wat Lenis MET RUST LAAT.314 *315 * In paginamodus staat Lenis stil en scrolt alleen het paneel van het316 * bericht zelf. Dat regelde ik eerst met een data-lenis-prevent-attribuut317 * dat de module op elk paneel zette -- maar dan hangt het scrollen af van of318 * dat attribuut op tijd en op elk (ook later bijgeladen) paneel staat, en319 * Robin kon binnen een lang bericht niet scrollen.320 *321 * Deze functie stelt dezelfde vraag zonder die afhankelijkheid: is dit een322 * bericht-paneel? Dan bemoeit Lenis zich er niet mee en scrolt de browser323 * het zelf -- wat op een telefoon precies is wat je wilt, want dat is de324 * systeemscroll.325 */326 prevent: (node) => !!(node && node.classList327 && node.classList.contains('read-post')328 && document.body.classList.contains('is-paged')),329 157 // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in 330 158 // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en … … 337 165 smoothWheel: true, 338 166 lerp: 0.2, 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 } : {}), 167 syncTouch: false, // op touch blijft alles van het systeem 357 168 autoRaf: true, 358 169 }); 359 170 snap = new S.default(lenis, { 360 171 type: 'proximity', 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, 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 388 175 // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de 389 176 // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit. … … 392 179 easing: (t) => 1 - Math.pow(1 - t, 4), 393 180 }); 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. 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. 397 183 const berichten = [...document.querySelectorAll('.feed-reader .read-post')]; 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 }; 184 berichten.slice(0, -1).forEach((a) => snap.addElement(a, { align: 'start' })); 428 185 // Native snappen uit: twee mechanismen op dezelfde scroller vechten. 429 186 document.documentElement.style.scrollSnapType = 'none'; … … 442 199 function zetSnappen(aan) { 443 200 if (snap) { if (aan) snap.start(); else snap.stop(); return; } 444 // Zonder Lenis (mobiel) zetten we de CSS-eigenschap om. Hij staat bij het445 // laden UIT -- dat is de standaardstand van deze variabele -- zodat de browser446 // niet meteen naar het eerste bericht springt en de header wegvalt. Pas je447 // eerste gebaar naar beneden zet hem aan.448 201 const el = document.documentElement; 449 202 const wil = aan ? '' : 'none'; … … 488 241 489 242 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 je493 // van Lezen naar het beheer, dan viel init() bij de ontbrekende leesstroom494 // meteen terug -- en bleef Lenis leven EN gestopt (lenis.stop() vangt wiel en495 // vinger af). Daarna scrolde niets meer, ook niet op pagina's die met Lezen496 // niets te maken hebben. Robins melding (20-8): "scrollen werkt nu nergens ook497 // niet in admin panels".498 //499 // Opruimen hoort dus bij het VERLATEN van de weergave, niet bij het opzetten500 // 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 506 243 const s = document.getElementById('read-stream'); 507 244 if (!s) return; … … 516 253 // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet 517 254 // hij niets, maar hij springt niet in beeld bij het laden. 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; 255 knop = document.getElementById('read-top'); 256 if (knop) { 257 knop.onclick = naarBoven; 258 toonKnop(knop); 259 } 524 260 525 261 if (opScroll) { … … 534 270 // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar 535 271 // levend; pas als het stil blijft mag een nieuwe richting gelden. 536 opScroll = () => { 537 if (knop) toonKnop(knop); 538 planRust(); 539 if (document.body.classList.contains('is-paged')) zetBalken(); 540 }; 272 opScroll = () => { if (knop) toonKnop(knop); planRust(); }; 541 273 window.addEventListener('scroll', opScroll, { passive: true }); 542 274 window.addEventListener('scrollend', rustNu); … … 547 279 window.addEventListener('keydown', opToets); 548 280 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 554 281 // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw 555 282 // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en 556 283 // wegnavigeren hoort hem op te ruimen. 557 if (document.body.dataset.feedView === 'reader') startLenis().then(pasPaginaModusToe); 558 else pasPaginaModusToe(); 559 560 284 stopLenis(); 285 if (document.body.dataset.feedView === 'reader') startLenis(); 561 286 } 562 287
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)