Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/mod/read.js

    r77019fa r88838bc  
    3030 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.
    3131 */
    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 }
    16932
    17033/**
     
    21073  const sel = window.getSelection && window.getSelection();
    21174  if (sel && String(sel).trim()) return;
    212 
    21375  const slug = art.dataset.slug;
    21476  if (!slug) return;
     
    253115
    254116let bezig = false;          // loopt er een gebaar of een uitloop?
    255 let ooitGescrold = false;   // snappen begint UIT, zie zetSnappen()
    256117let rustTimer = null;
    257118
     
    282143 */
    283144const 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)');
    296145const VENDOR_V = 1;   // ophogen als de bestanden in /assets/js/vendor wijzigen
    297146
     
    300149
    301150async 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;
    307152  const [L, S] = await Promise.all([
    308153    import(`/assets/vendor/lenis.mjs?v=${VENDOR_V}`),
     
    310155  ]);
    311156  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')),
    329157    // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in
    330158    // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en
     
    337165    smoothWheel: true,
    338166    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
    357168    autoRaf: true,
    358169  });
    359170  snap = new S.default(lenis, {
    360171    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
    388175    // Vlot weg, dan steeds langzamer aankomen (Robin, 20-8). easeOutQuart: op de
    389176    // helft van de tijd is 94% van de weg af, en de rest dempt zacht uit.
     
    392179    easing: (t) => 1 - Math.pow(1 - t, 4),
    393180  });
    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.
    397183  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' }));
    428185  // Native snappen uit: twee mechanismen op dezelfde scroller vechten.
    429186  document.documentElement.style.scrollSnapType = 'none';
     
    442199function zetSnappen(aan) {
    443200  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.
    448201  const el = document.documentElement;
    449202  const wil = aan ? '' : 'none';
     
    488241
    489242export 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 
    506243  const s = document.getElementById('read-stream');
    507244  if (!s) return;
     
    516253  // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet
    517254  // 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  }
    524260
    525261  if (opScroll) {
     
    534270  // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar
    535271  // 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(); };
    541273  window.addEventListener('scroll', opScroll, { passive: true });
    542274  window.addEventListener('scrollend', rustNu);
     
    547279  window.addEventListener('keydown', opToets);
    548280
    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 
    554281  // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw
    555282  // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en
    556283  // 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();
    561286}
    562287
Note: See TracChangeset for help on using the changeset viewer.