| 1 | /**
|
|---|
| 2 | * De leesweergave: een stroom van hele berichten, één per beeld.
|
|---|
| 3 | *
|
|---|
| 4 | * NIET de truc van bartoverkamp.nl. Daar springt de pagina bij de rand naar de
|
|---|
| 5 | * andere kant en wordt het nieuwe artikel "vloeiend" in beeld gezet -- een naad
|
|---|
| 6 | * die verborgen wordt. Hier groeit de stroom in de richting waarin je leest:
|
|---|
| 7 | * bij de onderrand komt de oudere buur eronder te staan, bij de bovenrand de
|
|---|
| 8 | * nieuwere erboven. De browser snapt naar de berichtgrenzen (CSS, proximity).
|
|---|
| 9 | * Er is dan geen naad, dus ook niets te verbergen.
|
|---|
| 10 | *
|
|---|
| 11 | * Elk artikel draagt zijn eigen buren in data-attributen. Wat er nog te halen
|
|---|
| 12 | * valt staat dus altijd op het stuk dat er al is, en deze module hoeft geen
|
|---|
| 13 | * lijst bij te houden die uit de pas kan lopen.
|
|---|
| 14 | */
|
|---|
| 15 |
|
|---|
| 16 | // Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet
|
|---|
| 17 | // sneller dan je scrolt, en meer betekent alleen meer verkeer.
|
|---|
| 18 | const MARGE = '1px 0px 100% 0px';
|
|---|
| 19 |
|
|---|
| 20 | // De teksten die dit script plaatst. Ze staan in data-i18n op de stroom, want
|
|---|
| 21 | // een zin die hier staat, staat in één taal -- en dat is nooit de taal van de
|
|---|
| 22 | // lezer. Zelfde afspraak als de topnav (mod/chrome.js).
|
|---|
| 23 | let woorden = {};
|
|---|
| 24 | function woord(sleutel) { return woorden[sleutel] || ''; }
|
|---|
| 25 |
|
|---|
| 26 | const stroom = () => document.getElementById('read-stream');
|
|---|
| 27 | const geladen = new Set(); // slugs die al in de stroom staan
|
|---|
| 28 | let bezigOnder = false;
|
|---|
| 29 | let bezigBoven = false;
|
|---|
| 30 | let waarnemer = null;
|
|---|
| 31 | let laatsteY = 0;
|
|---|
| 32 |
|
|---|
| 33 | function chrome(verbergen) {
|
|---|
| 34 | document.body.classList.toggle('read-chrome-hidden', !!verbergen);
|
|---|
| 35 | }
|
|---|
| 36 |
|
|---|
| 37 | /** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */
|
|---|
| 38 | function inBeeld() {
|
|---|
| 39 | const posts = [...document.querySelectorAll('.read-post')];
|
|---|
| 40 | const midden = window.innerHeight / 2;
|
|---|
| 41 | return posts.find((p) => {
|
|---|
| 42 | const r = p.getBoundingClientRect();
|
|---|
| 43 | return r.top <= midden && r.bottom >= midden;
|
|---|
| 44 | }) || posts[0];
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | /** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */
|
|---|
| 48 | function volgAdres() {
|
|---|
| 49 | const p = inBeeld();
|
|---|
| 50 | if (!p) return;
|
|---|
| 51 | const slug = p.dataset.slug;
|
|---|
| 52 | const basis = p.dataset.base || '';
|
|---|
| 53 | if (location.pathname === `${basis}/read/${slug}`) return;
|
|---|
| 54 | history.replaceState(null, '', `${basis}/read/${slug}`);
|
|---|
| 55 | if (p.dataset.title) document.title = p.dataset.title;
|
|---|
| 56 | }
|
|---|
| 57 |
|
|---|
| 58 | async function haal(slug, basis) {
|
|---|
| 59 | const r = await fetch(`${basis}/read/${encodeURIComponent(slug)}?partial=1`, {
|
|---|
| 60 | headers: { 'HX-Request': 'true' },
|
|---|
| 61 | credentials: 'same-origin',
|
|---|
| 62 | });
|
|---|
| 63 | if (!r.ok) throw new Error('HTTP ' + r.status);
|
|---|
| 64 | const doos = document.createElement('div');
|
|---|
| 65 | doos.innerHTML = await r.text();
|
|---|
| 66 | const art = doos.querySelector('.read-post');
|
|---|
| 67 | if (!art) throw new Error('geen bericht in het antwoord');
|
|---|
| 68 | return art;
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | /** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */
|
|---|
| 72 | async function vulAan() {
|
|---|
| 73 | const s = stroom();
|
|---|
| 74 | if (!s || bezigOnder) return;
|
|---|
| 75 | const laatste = s.querySelector('.read-post:last-of-type');
|
|---|
| 76 | const slug = laatste && laatste.dataset.older;
|
|---|
| 77 | if (!slug) return klaar(s);
|
|---|
| 78 | if (geladen.has(slug)) return;
|
|---|
| 79 | bezigOnder = true;
|
|---|
| 80 | const wacht = melding(s, 'read-loading', '…');
|
|---|
| 81 | try {
|
|---|
| 82 | const art = await haal(slug, laatste.dataset.base || '');
|
|---|
| 83 | geladen.add(slug);
|
|---|
| 84 | wacht.remove();
|
|---|
| 85 | s.appendChild(art);
|
|---|
| 86 | kijk();
|
|---|
| 87 | } catch (e) {
|
|---|
| 88 | wacht.textContent = woord('load_error');
|
|---|
| 89 | console.warn('[read] onderaan:', e && e.message);
|
|---|
| 90 | } finally { bezigOnder = false; }
|
|---|
| 91 | }
|
|---|
| 92 |
|
|---|
| 93 | /**
|
|---|
| 94 | * Bovenaan aanvullen. Dit is het enige plek waar we de scrollpositie WEL
|
|---|
| 95 | * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie
|
|---|
| 96 | * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen
|
|---|
| 97 | * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.
|
|---|
| 98 | */
|
|---|
| 99 | async function vulAanBoven() {
|
|---|
| 100 | const s = stroom();
|
|---|
| 101 | if (!s || bezigBoven) return;
|
|---|
| 102 | const eerste = s.querySelector('.read-post');
|
|---|
| 103 | const slug = eerste && eerste.dataset.newer;
|
|---|
| 104 | if (!slug || geladen.has(slug)) return;
|
|---|
| 105 | bezigBoven = true;
|
|---|
| 106 | try {
|
|---|
| 107 | const art = await haal(slug, eerste.dataset.base || '');
|
|---|
| 108 | geladen.add(slug);
|
|---|
| 109 | const voor = s.scrollHeight;
|
|---|
| 110 | s.insertBefore(art, eerste);
|
|---|
| 111 | const erbij = s.scrollHeight - voor;
|
|---|
| 112 | window.scrollBy(0, erbij);
|
|---|
| 113 | kijk();
|
|---|
| 114 | } catch (e) {
|
|---|
| 115 | console.warn('[read] bovenaan:', e && e.message);
|
|---|
| 116 | } finally { bezigBoven = false; }
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | function melding(s, klasse, tekst) {
|
|---|
| 120 | const d = document.createElement('div');
|
|---|
| 121 | d.className = klasse;
|
|---|
| 122 | d.textContent = tekst;
|
|---|
| 123 | s.appendChild(d);
|
|---|
| 124 | return d;
|
|---|
| 125 | }
|
|---|
| 126 |
|
|---|
| 127 | function klaar(s) {
|
|---|
| 128 | if (s.querySelector('.read-end')) return;
|
|---|
| 129 | melding(s, 'read-end', woord('end'));
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | /** (Her)richt de waarnemer op de huidige eerste en laatste post. */
|
|---|
| 133 | function kijk() {
|
|---|
| 134 | if (waarnemer) waarnemer.disconnect();
|
|---|
| 135 | waarnemer = new IntersectionObserver((entries) => {
|
|---|
| 136 | for (const e of entries) {
|
|---|
| 137 | if (!e.isIntersecting) continue;
|
|---|
| 138 | if (e.target.matches('.read-post:last-of-type')) vulAan();
|
|---|
| 139 | else if (e.target.matches('.read-post:first-of-type')) vulAanBoven();
|
|---|
| 140 | }
|
|---|
| 141 | }, { rootMargin: MARGE });
|
|---|
| 142 | const eerste = stroom() && stroom().querySelector('.read-post');
|
|---|
| 143 | const laatste = stroom() && stroom().querySelector('.read-post:last-of-type');
|
|---|
| 144 | if (eerste) waarnemer.observe(eerste);
|
|---|
| 145 | if (laatste && laatste !== eerste) waarnemer.observe(laatste);
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | function bijScroll() {
|
|---|
| 149 | const y = window.scrollY || 0;
|
|---|
| 150 | if (y < 8) chrome(false);
|
|---|
| 151 | else if (y > laatsteY + 4) chrome(true);
|
|---|
| 152 | else if (y < laatsteY - 24) chrome(false);
|
|---|
| 153 | laatsteY = y;
|
|---|
| 154 | volgAdres();
|
|---|
| 155 | }
|
|---|
| 156 |
|
|---|
| 157 | export function init() {
|
|---|
| 158 | const s = stroom();
|
|---|
| 159 | if (!s) return;
|
|---|
| 160 | try { woorden = JSON.parse(s.getAttribute('data-i18n') || '{}'); } catch (e) { woorden = {}; }
|
|---|
| 161 | s.querySelectorAll('.read-post').forEach((p) => geladen.add(p.dataset.slug));
|
|---|
| 162 | laatsteY = window.scrollY || 0;
|
|---|
| 163 | window.addEventListener('scroll', bijScroll, { passive: true });
|
|---|
| 164 | window.addEventListener('resize', kijk, { passive: true });
|
|---|
| 165 | kijk();
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | export default { init };
|
|---|