| [b67edbd] | 1 | /**
|
|---|
| [4989294] | 2 | * De leesweergave: tikken op een bericht opent dat bericht, en een knop om terug
|
|---|
| 3 | * naar boven te gaan.
|
|---|
| [b67edbd] | 4 | *
|
|---|
| [7cf8144] | 5 | * Dit was een heel scherm met een eigen route, dat zijn buren zelf ophaalde, de
|
|---|
| 6 | * scrollpositie corrigeerde bij invoegen en de balken wegschoof. Dat is allemaal
|
|---|
| [4989294] | 7 | * weg, en dat is winst: Lezen is nu een WEERGAVE van de feed
|
|---|
| [7cf8144] | 8 | * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de
|
|---|
| [19e28ce] | 9 | * berichten al en "meer laden" vult al aan.
|
|---|
| [b67edbd] | 10 | *
|
|---|
| [19e28ce] | 11 | * GEWOON SCROLLEN (Robin, 30-9: "Reader view normaal scrollen, Lenis mag helemaal
|
|---|
| 12 | * uit klonkt"). Hier stond Lenis: op desktop voor zacht scrollen en snappen naar
|
|---|
| 13 | * de bovenkant van een bericht, op mobiel nam hij de vinger over en bootsten we
|
|---|
| 14 | * het snappen na. Dat is er allemaal uit. De browser scrolt, zoals op elke andere
|
|---|
| 15 | * pagina, en er snapt niets meer. Waarom snappen niet native terugkwam, en hoe
|
|---|
| 16 | * dat alsnog zou kunnen, staat in de vault onder "Klonkt Reader Scroll".
|
|---|
| [e46ebaa] | 17 | *
|
|---|
| [4989294] | 18 | * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk
|
|---|
| 19 | * voor toetsenbord en schermlezer; de tik hieronder is er voor een duim.
|
|---|
| [e46ebaa] | 20 | *
|
|---|
| [4989294] | 21 | * Vier uitzonderingen op die tik, want een tik die je niet bedoelde is erger dan
|
|---|
| 22 | * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen
|
|---|
| 23 | * werking, een geselecteerde tekst is geen tik, een verschoven vinger is
|
|---|
| 24 | * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.
|
|---|
| [e46ebaa] | 25 | */
|
|---|
| 26 |
|
|---|
| [19e28ce] | 27 | const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)');
|
|---|
| [369aa60] | 28 |
|
|---|
| [22d2dba5] | 29 | // ── Paginamodus ─────────────────────────────────────────────────────────────
|
|---|
| 30 | /**
|
|---|
| 31 | * PAGINAMODUS STAAT UIT -- maar de code blijft staan (Robin, 20-8: "weghalen
|
|---|
| 32 | * maar bewaren, voor als ik het later weer wil").
|
|---|
| 33 | *
|
|---|
| [19e28ce] | 34 | * Wat het is: elk bericht een paneel van een scherm dat zelf scrollt, met twee
|
|---|
| 35 | * balken om ertussen te navigeren. Gebouwd omdat het namaken van iOS-momentum
|
|---|
| 36 | * niet lukte; in dit model doet het scrollgevoel er namelijk niet toe.
|
|---|
| 37 | *
|
|---|
| 38 | * Sinds Lenis weg is (30-9) zet de CSS de stroom vast in plaats van lenis.stop()
|
|---|
| 39 | * (html:has(body.is-paged) in style.css), en gaan de balken met native
|
|---|
| 40 | * scrollIntoView. Niet uitgeprobeerd sinds die omzetting, want de modus staat
|
|---|
| 41 | * uit: wie hem aanzet, kijkt dat eerst na.
|
|---|
| [22d2dba5] | 42 | *
|
|---|
| 43 | * Aanzetten: deze constante op true. Dan komen de balken terug (CSS hangt aan
|
|---|
| [19e28ce] | 44 | * body.is-paged) en scrollen de panelen zelf.
|
|---|
| [22d2dba5] | 45 | */
|
|---|
| 46 | const PAGINAMODUS = false;
|
|---|
| 47 |
|
|---|
| 48 | function paginaModus() {
|
|---|
| 49 | return PAGINAMODUS && OP_TOUCH.matches && document.body.dataset.feedView === 'reader';
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | function panelen() {
|
|---|
| 53 | return [...document.querySelectorAll('.feed-reader .read-post')];
|
|---|
| 54 | }
|
|---|
| 55 |
|
|---|
| 56 | /** Welk paneel vult nu het scherm? Het eerste waarvan de bovenkant niet voorbij is. */
|
|---|
| 57 | function huidigIndex() {
|
|---|
| 58 | const P = panelen();
|
|---|
| 59 | for (let i = 0; i < P.length; i++) {
|
|---|
| 60 | if (P[i].getBoundingClientRect().top > 8) return Math.max(0, i - 1);
|
|---|
| 61 | }
|
|---|
| 62 | return Math.max(0, P.length - 1);
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | function gaNaar(i) {
|
|---|
| 66 | const P = panelen();
|
|---|
| 67 | const doel = P[Math.max(0, Math.min(P.length - 1, i))];
|
|---|
| 68 | if (!doel) return;
|
|---|
| [19e28ce] | 69 | doel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|---|
| [22d2dba5] | 70 | setTimeout(zetBalken, 80);
|
|---|
| 71 | }
|
|---|
| 72 |
|
|---|
| 73 | /**
|
|---|
| 74 | * Omhoog vanaf het EERSTE bericht brengt je naar de header.
|
|---|
| 75 | *
|
|---|
| [19e28ce] | 76 | * In paginamodus scrolt de stroom niet met je vinger, dus dan is alles boven het
|
|---|
| 77 | * eerste bericht onbereikbaar. Robin liep daar tegenaan (20-8): "ik kan niet
|
|---|
| 78 | * meer terug scrollen naar de header". De omhoog-knop is daar de enige weg
|
|---|
| 79 | * naartoe, dus die krijgt er een trede bij.
|
|---|
| [22d2dba5] | 80 | */
|
|---|
| 81 | function gaOmhoog() {
|
|---|
| 82 | const i = huidigIndex();
|
|---|
| 83 | if (i > 0) { gaNaar(i - 1); return; }
|
|---|
| [19e28ce] | 84 | window.scrollTo({ top: 0, behavior: 'smooth' });
|
|---|
| [22d2dba5] | 85 | }
|
|---|
| 86 |
|
|---|
| 87 | /**
|
|---|
| 88 | * Welke balk hoort er te staan? Bovenaan de pagina is er niets boven je, dus dan
|
|---|
| 89 | * geen bovenbalk. Buiten paginamodus staan ze allebei niet -- de CSS verbergt ze
|
|---|
| 90 | * daar al, maar hidden houdt ze ook uit de toetsenbordvolgorde.
|
|---|
| 91 | */
|
|---|
| 92 | function zetBalken() {
|
|---|
| 93 | const boven = document.getElementById('read-prev');
|
|---|
| 94 | const onder = document.getElementById('read-next-nav');
|
|---|
| 95 | const aan = paginaModus();
|
|---|
| 96 | // De bovenbalk hoort NOOIT over de header te liggen. Hij verscheen al zodra je
|
|---|
| 97 | // een paar pixels scrolde, en dekte dan de avatar, de omschrijving en de
|
|---|
| 98 | // weergaveknoppen af (Robins schermafbeelding, 20-8). Nu komt hij pas als het
|
|---|
| 99 | // eerste bericht de bovenrand van het scherm heeft bereikt -- dan is de header
|
|---|
| 100 | // voorbij en is er ook echt iets om naar terug te gaan.
|
|---|
| 101 | const eerste = panelen()[0];
|
|---|
| 102 | const headerNogInBeeld = eerste ? eerste.getBoundingClientRect().top > 4 : true;
|
|---|
| 103 | if (boven) boven.hidden = !aan || headerNogInBeeld;
|
|---|
| 104 | if (onder) onder.hidden = !aan;
|
|---|
| 105 | if (onder && aan) onder.style.bottom = onderChroom() + 'px';
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | /**
|
|---|
| 109 | * Hoe hoog staat de onderrand van het scherm werkelijk vol?
|
|---|
| 110 | *
|
|---|
| 111 | * De onderbalk stond op een geraden 4.75rem boven de onderkant, en dan zweeft
|
|---|
| 112 | * hij: soms een kier boven de speler, soms er half achter. De tabbalk en de
|
|---|
| 113 | * mini-speler hebben allebei een eigen hoogte, ze stapelen op mobiel, en de
|
|---|
| 114 | * speler komt en gaat. Dus meten in plaats van gokken: hoe ver ligt de BOVENKANT
|
|---|
| 115 | * van het hoogste vaste element boven de onderrand van het venster?
|
|---|
| 116 | */
|
|---|
| 117 | function onderChroom() {
|
|---|
| 118 | // .bottom-tab-fab staat erbij omdat de Write-knop BOVEN de tabbalk uitsteekt:
|
|---|
| 119 | // meet je alleen de balk, dan legt onze balk zich over die knop heen (Robins
|
|---|
| 120 | // schermafbeelding, 20-8).
|
|---|
| 121 | const kandidaten = ['.bottom-tab', '.bottom-tab-fab', '#pcms-audio-player'];
|
|---|
| 122 | let hoogste = 0;
|
|---|
| 123 | kandidaten.forEach((sel) => {
|
|---|
| 124 | const el = document.querySelector(sel);
|
|---|
| 125 | if (!el) return;
|
|---|
| 126 | const st = getComputedStyle(el);
|
|---|
| 127 | if (st.display === 'none' || st.visibility === 'hidden') return;
|
|---|
| 128 | const r = el.getBoundingClientRect();
|
|---|
| 129 | if (r.height <= 0) return;
|
|---|
| 130 | hoogste = Math.max(hoogste, window.innerHeight - r.top);
|
|---|
| 131 | });
|
|---|
| 132 | return Math.round(hoogste);
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | function pasPaginaModusToe() {
|
|---|
| [19e28ce] | 136 | document.body.classList.toggle('is-paged', paginaModus());
|
|---|
| [22d2dba5] | 137 | zetBalken();
|
|---|
| 138 | }
|
|---|
| 139 |
|
|---|
| [e46ebaa] | 140 | /**
|
|---|
| [19e28ce] | 141 | * Terug naar boven, en bewust NIET meteen window.scrollTo(0).
|
|---|
| [e46ebaa] | 142 | *
|
|---|
| 143 | * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
|
|---|
| 144 | * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet
|
|---|
| [4989294] | 145 | * dus twee verschillende dingen -- dat scheelt op mobiel een hoop vegen.
|
|---|
| [e46ebaa] | 146 | */
|
|---|
| 147 | function naarBoven() {
|
|---|
| 148 | const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|---|
| 149 | const gedrag = zacht ? 'smooth' : 'auto';
|
|---|
| 150 | const posts = [...document.querySelectorAll('.feed-reader .read-post')];
|
|---|
| 151 | const huidig = posts.find((a) => {
|
|---|
| 152 | const r = a.getBoundingClientRect();
|
|---|
| 153 | return r.top <= 8 && r.bottom > 8;
|
|---|
| 154 | });
|
|---|
| 155 | // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop.
|
|---|
| 156 | if (huidig && huidig.getBoundingClientRect().top < -8) {
|
|---|
| 157 | huidig.scrollIntoView({ behavior: gedrag, block: 'start' });
|
|---|
| 158 | return;
|
|---|
| 159 | }
|
|---|
| 160 | window.scrollTo({ top: 0, behavior: gedrag });
|
|---|
| 161 | }
|
|---|
| 162 |
|
|---|
| 163 | /** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */
|
|---|
| 164 | function toonKnop(knop) {
|
|---|
| 165 | knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6);
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| [847775c] | 168 | let tapX = 0, tapY = 0;
|
|---|
| 169 | function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; }
|
|---|
| 170 |
|
|---|
| 171 | function onTap(e) {
|
|---|
| 172 | if (e.defaultPrevented || e.button !== 0) return;
|
|---|
| 173 | if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|---|
| 174 | const t = e.target;
|
|---|
| 175 | if (!t || typeof t.closest !== 'function') return;
|
|---|
| 176 | const art = t.closest('.read-post');
|
|---|
| 177 | if (!art) return;
|
|---|
| 178 | if (t.closest('a, button, input, textarea, select, label, summary, [role="button"]')) return;
|
|---|
| 179 | if (Math.abs(e.clientX - tapX) > 10 || Math.abs(e.clientY - tapY) > 10) return;
|
|---|
| 180 | const sel = window.getSelection && window.getSelection();
|
|---|
| 181 | if (sel && String(sel).trim()) return;
|
|---|
| [22d2dba5] | 182 |
|
|---|
| [847775c] | 183 | const slug = art.dataset.slug;
|
|---|
| 184 | if (!slug) return;
|
|---|
| 185 | location.href = (art.dataset.base || '') + '/' + encodeURIComponent(slug);
|
|---|
| 186 | }
|
|---|
| 187 |
|
|---|
| [e46ebaa] | 188 | let knop = null;
|
|---|
| 189 | let opScroll = null;
|
|---|
| 190 |
|
|---|
| [b67edbd] | 191 | export function init() {
|
|---|
| [19e28ce] | 192 | // EERST OPRUIMEN, en pas daarna terugvallen als er geen leesstroom is. init()
|
|---|
| 193 | // draait bij elke paginawissel, en wat de leesweergave aanzette hoort weg te
|
|---|
| 194 | // zijn zodra je hem verlaat -- ook op pagina's die met Lezen niets te maken
|
|---|
| 195 | // hebben (Robins melding van 20-8, toen dat misging: "scrollen werkt nu nergens
|
|---|
| 196 | // ook niet in admin panels").
|
|---|
| [22d2dba5] | 197 | document.body.classList.remove('is-paged');
|
|---|
| [19e28ce] | 198 | // Een tabblad dat nog met de vorige versie van deze module is geopend, heeft
|
|---|
| 199 | // het native snappen inline uitgezet. Na een htmx-wissel naar deze versie hoort
|
|---|
| 200 | // daar niets van over te blijven.
|
|---|
| 201 | document.documentElement.style.scrollSnapType = '';
|
|---|
| [22d2dba5] | 202 |
|
|---|
| [7cf8144] | 203 | const s = document.getElementById('read-stream');
|
|---|
| [b67edbd] | 204 | if (!s) return;
|
|---|
| [7cf8144] | 205 | // Op de stroom, niet per artikel: wat "meer laden" erbij zet doet vanzelf mee.
|
|---|
| 206 | // init() draait bij ELKE paginawissel, dus eerst losmaken -- anders stapelt
|
|---|
| 207 | // dezelfde afhandelaar zich op en vuurt hij twee keer.
|
|---|
| 208 | s.removeEventListener('pointerdown', onPointerDown);
|
|---|
| 209 | s.removeEventListener('click', onTap);
|
|---|
| [847775c] | 210 | s.addEventListener('pointerdown', onPointerDown, { passive: true });
|
|---|
| 211 | s.addEventListener('click', onTap);
|
|---|
| [e46ebaa] | 212 |
|
|---|
| [4989294] | 213 | // De knop staat in de HTML, zodat hij er ook is zonder deze module -- dan doet
|
|---|
| 214 | // hij niets, maar hij springt niet in beeld bij het laden.
|
|---|
| [22d2dba5] | 215 | // De terug-naar-boven-knop staat er tijdelijk uit (Robin, 20-8) -- in
|
|---|
| 216 | // paginamodus doet de bovenbalk dat werk al. De code blijft staan zodat hij
|
|---|
| 217 | // met een regel terug is.
|
|---|
| 218 | knop = null;
|
|---|
| 219 | const oudeKnop = document.getElementById('read-top');
|
|---|
| 220 | if (oudeKnop) oudeKnop.hidden = true;
|
|---|
| [e46ebaa] | 221 |
|
|---|
| 222 | if (opScroll) {
|
|---|
| 223 | window.removeEventListener('scroll', opScroll);
|
|---|
| 224 | window.removeEventListener('resize', opScroll);
|
|---|
| 225 | }
|
|---|
| [22d2dba5] | 226 | opScroll = () => {
|
|---|
| 227 | if (knop) toonKnop(knop);
|
|---|
| 228 | if (document.body.classList.contains('is-paged')) zetBalken();
|
|---|
| 229 | };
|
|---|
| [e46ebaa] | 230 | window.addEventListener('scroll', opScroll, { passive: true });
|
|---|
| 231 | window.addEventListener('resize', opScroll, { passive: true });
|
|---|
| [a505f31] | 232 |
|
|---|
| [22d2dba5] | 233 | const balkBoven = document.getElementById('read-prev');
|
|---|
| 234 | const balkOnder = document.getElementById('read-next-nav');
|
|---|
| 235 | if (balkBoven) balkBoven.onclick = gaOmhoog;
|
|---|
| 236 | if (balkOnder) balkOnder.onclick = () => gaNaar(huidigIndex() + 1);
|
|---|
| 237 |
|
|---|
| [19e28ce] | 238 | pasPaginaModusToe();
|
|---|
| [b67edbd] | 239 | }
|
|---|
| 240 |
|
|---|
| 241 | export default { init };
|
|---|