Changeset 22d2dba5 in Klonkt for src/assets/js/mod/read.js


Ignore:
Timestamp:
08/20/2026 07:25:24 AM (3 weeks ago)
Author:
Claude (agent) <aiclaude@…>
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)
Message:

wip paginamodus

File:
1 edited

Legend:

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

    rc581e5e r22d2dba5  
    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 */
     68const PAGINAMODUS = false;
     69
     70function paginaModus() {
     71  return PAGINAMODUS && OP_TOUCH.matches && document.body.dataset.feedView === 'reader';
     72}
     73
     74function 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. */
     79function 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
     87function 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 */
     104function 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 */
     116function 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 */
     141function 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
     159function 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}
    32169
    33170/**
     
    73210  const sel = window.getSelection && window.getSelection();
    74211  if (sel && String(sel).trim()) return;
     212
    75213  const slug = art.dataset.slug;
    76214  if (!slug) return;
     
    115253
    116254let bezig = false;          // loopt er een gebaar of een uitloop?
     255let ooitGescrold = false;   // snappen begint UIT, zie zetSnappen()
    117256let rustTimer = null;
    118257
     
    161300
    162301async 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.
    163306  if (lenis || !(OP_DESKTOP.matches || OP_TOUCH.matches)) return;
    164307  const [L, S] = await Promise.all([
     
    167310  ]);
    168311  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')),
    169329    // Lenis tekent de scrollbeweging zelf, maar STEVIG GEDEMPT (lerp 0.2 in
    170330    // plaats van de standaard 0.1). Reden: een muiswiel scrollt in schokken en
     
    179339    // Alleen op touch overneemt hij het vingerscrollen; op desktop hoeft dat niet.
    180340    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.
    182344    //
    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)
    189347    //
    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.
    191351    //
    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 } : {}),
    225357    autoRaf: true,
    226358  });
    227359  snap = new S.default(lenis, {
    228360    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%',
    230368    // Hoe lang na de laatste scrollbeweging hij mag vangen. De standaard is 500
    231369    // en dat voelt als te laat. Op touch nog korter dan op desktop, want daar
     
    242380    // "stroef bij het loslaten", en het werd er dan ook erger van. 200ms laat de
    243381    // 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,
    245385    // Op touch korter: een telefoon vraagt om directer antwoord dan een muis, en
    246386    // de snap komt daar aan het eind van een lange uitloop -- dan mag hij kort.
     
    256396  const berichten = [...document.querySelectorAll('.feed-reader .read-post')];
    257397  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  };
    258427  // Native snappen uit: twee mechanismen op dezelfde scroller vechten.
    259428  document.documentElement.style.scrollSnapType = 'none';
     
    272441function zetSnappen(aan) {
    273442  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.
    274447  const el = document.documentElement;
    275448  const wil = aan ? '' : 'none';
     
    314487
    315488export 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
    316505  const s = document.getElementById('read-stream');
    317506  if (!s) return;
     
    326515  // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet
    327516  // 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;
    333523
    334524  if (opScroll) {
     
    343533  // Elke scroll -- van een vinger of van de browser zelf -- houdt het gebaar
    344534  // 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  };
    346540  window.addEventListener('scroll', opScroll, { passive: true });
    347541  window.addEventListener('scrollend', rustNu);
     
    352546  window.addEventListener('keydown', opToets);
    353547
     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
    354553  // Alleen in de leesweergave, en alleen op desktop. Bij elke init() opnieuw
    355554  // beoordelen: van Grid naar Lezen schakelen hoort hem aan te zetten, en
    356555  // 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
    359560}
    360561
Note: See TracChangeset for help on using the changeset viewer.