| 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 | const stroom = () => document.getElementById('read-stream');
|
|---|
| 21 | const geladen = new Set(); // slugs die al in de stroom staan
|
|---|
| 22 | let bezigOnder = false;
|
|---|
| 23 | let bezigBoven = false;
|
|---|
| 24 | let waarnemer = null;
|
|---|
| 25 | let laatsteY = 0;
|
|---|
| 26 |
|
|---|
| 27 | function chrome(verbergen) {
|
|---|
| 28 | document.body.classList.toggle('read-chrome-weg', !!verbergen);
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | /** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */
|
|---|
| 32 | function inBeeld() {
|
|---|
| 33 | const posts = [...document.querySelectorAll('.read-post')];
|
|---|
| 34 | const midden = window.innerHeight / 2;
|
|---|
| 35 | return posts.find((p) => {
|
|---|
| 36 | const r = p.getBoundingClientRect();
|
|---|
| 37 | return r.top <= midden && r.bottom >= midden;
|
|---|
| 38 | }) || posts[0];
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | /** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */
|
|---|
| 42 | function volgAdres() {
|
|---|
| 43 | const p = inBeeld();
|
|---|
| 44 | if (!p) return;
|
|---|
| 45 | const slug = p.dataset.slug;
|
|---|
| 46 | const basis = p.dataset.base || '';
|
|---|
| 47 | if (location.pathname === `${basis}/read/${slug}`) return;
|
|---|
| 48 | history.replaceState(null, '', `${basis}/read/${slug}`);
|
|---|
| 49 | if (p.dataset.title) document.title = p.dataset.title;
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | async function haal(slug, basis) {
|
|---|
| 53 | const r = await fetch(`${basis}/read/${encodeURIComponent(slug)}?partial=1`, {
|
|---|
| 54 | headers: { 'HX-Request': 'true' },
|
|---|
| 55 | credentials: 'same-origin',
|
|---|
| 56 | });
|
|---|
| 57 | if (!r.ok) throw new Error('HTTP ' + r.status);
|
|---|
| 58 | const doos = document.createElement('div');
|
|---|
| 59 | doos.innerHTML = await r.text();
|
|---|
| 60 | const art = doos.querySelector('.read-post');
|
|---|
| 61 | if (!art) throw new Error('geen bericht in het antwoord');
|
|---|
| 62 | return art;
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | /** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */
|
|---|
| 66 | async function vulAan() {
|
|---|
| 67 | const s = stroom();
|
|---|
| 68 | if (!s || bezigOnder) return;
|
|---|
| 69 | const laatste = s.querySelector('.read-post:last-of-type');
|
|---|
| 70 | const slug = laatste && laatste.dataset.older;
|
|---|
| 71 | if (!slug) return klaar(s);
|
|---|
| 72 | if (geladen.has(slug)) return;
|
|---|
| 73 | bezigOnder = true;
|
|---|
| 74 | const wacht = melding(s, 'read-laden', '…');
|
|---|
| 75 | try {
|
|---|
| 76 | const art = await haal(slug, laatste.dataset.base || '');
|
|---|
| 77 | geladen.add(slug);
|
|---|
| 78 | wacht.remove();
|
|---|
| 79 | s.appendChild(art);
|
|---|
| 80 | kijk();
|
|---|
| 81 | } catch (e) {
|
|---|
| 82 | wacht.textContent = 'Kon het volgende bericht niet laden.';
|
|---|
| 83 | console.warn('[read] onderaan:', e && e.message);
|
|---|
| 84 | } finally { bezigOnder = false; }
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | /**
|
|---|
| 88 | * Bovenaan aanvullen. Dit is het enige plek waar we de scrollpositie WEL
|
|---|
| 89 | * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie
|
|---|
| 90 | * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen
|
|---|
| 91 | * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.
|
|---|
| 92 | */
|
|---|
| 93 | async function vulAanBoven() {
|
|---|
| 94 | const s = stroom();
|
|---|
| 95 | if (!s || bezigBoven) return;
|
|---|
| 96 | const eerste = s.querySelector('.read-post');
|
|---|
| 97 | const slug = eerste && eerste.dataset.newer;
|
|---|
| 98 | if (!slug || geladen.has(slug)) return;
|
|---|
| 99 | bezigBoven = true;
|
|---|
| 100 | try {
|
|---|
| 101 | const art = await haal(slug, eerste.dataset.base || '');
|
|---|
| 102 | geladen.add(slug);
|
|---|
| 103 | const voor = s.scrollHeight;
|
|---|
| 104 | s.insertBefore(art, eerste);
|
|---|
| 105 | const erbij = s.scrollHeight - voor;
|
|---|
| 106 | window.scrollBy(0, erbij);
|
|---|
| 107 | kijk();
|
|---|
| 108 | } catch (e) {
|
|---|
| 109 | console.warn('[read] bovenaan:', e && e.message);
|
|---|
| 110 | } finally { bezigBoven = false; }
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | function melding(s, klasse, tekst) {
|
|---|
| 114 | const d = document.createElement('div');
|
|---|
| 115 | d.className = klasse;
|
|---|
| 116 | d.textContent = tekst;
|
|---|
| 117 | s.appendChild(d);
|
|---|
| 118 | return d;
|
|---|
| 119 | }
|
|---|
| 120 |
|
|---|
| 121 | function klaar(s) {
|
|---|
| 122 | if (s.querySelector('.read-eind')) return;
|
|---|
| 123 | melding(s, 'read-eind', 'Je bent bij het begin van het archief.');
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | /** (Her)richt de waarnemer op de huidige eerste en laatste post. */
|
|---|
| 127 | function kijk() {
|
|---|
| 128 | if (waarnemer) waarnemer.disconnect();
|
|---|
| 129 | waarnemer = new IntersectionObserver((entries) => {
|
|---|
| 130 | for (const e of entries) {
|
|---|
| 131 | if (!e.isIntersecting) continue;
|
|---|
| 132 | if (e.target.matches('.read-post:last-of-type')) vulAan();
|
|---|
| 133 | else if (e.target.matches('.read-post:first-of-type')) vulAanBoven();
|
|---|
| 134 | }
|
|---|
| 135 | }, { rootMargin: MARGE });
|
|---|
| 136 | const eerste = stroom() && stroom().querySelector('.read-post');
|
|---|
| 137 | const laatste = stroom() && stroom().querySelector('.read-post:last-of-type');
|
|---|
| 138 | if (eerste) waarnemer.observe(eerste);
|
|---|
| 139 | if (laatste && laatste !== eerste) waarnemer.observe(laatste);
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | function bijScroll() {
|
|---|
| 143 | const y = window.scrollY || 0;
|
|---|
| 144 | if (y < 8) chrome(false);
|
|---|
| 145 | else if (y > laatsteY + 4) chrome(true);
|
|---|
| 146 | else if (y < laatsteY - 24) chrome(false);
|
|---|
| 147 | laatsteY = y;
|
|---|
| 148 | volgAdres();
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | export function init() {
|
|---|
| 152 | const s = stroom();
|
|---|
| 153 | if (!s) return;
|
|---|
| 154 | s.querySelectorAll('.read-post').forEach((p) => geladen.add(p.dataset.slug));
|
|---|
| 155 | laatsteY = window.scrollY || 0;
|
|---|
| 156 | window.addEventListener('scroll', bijScroll, { passive: true });
|
|---|
| 157 | window.addEventListener('resize', kijk, { passive: true });
|
|---|
| 158 | kijk();
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | export default { init };
|
|---|