source: Klonkt/src/assets/js/mod/messages.js@ 2bd31d6

main
Last change on this file since 2bd31d6 was 2bd31d6, checked in by Robin <roboburr@…>, 5 weeks ago

Zwaaien is een klik, geen paginawissel

Een zwaai is een seintje. Dat de hele pagina ervoor herlaadde -- met een
groene balk bovenaan en je scrollpositie kwijt -- gaf het meer gewicht dan
het heeft.

Nu onderschept mod/messages.js de verzending en stuurt hem met fetch, zoals
like/boost in de Krant al deden. De knoppen maken plaats voor een korte
bevestiging; mislukt het, dan komen ze terug met een schud.

DE VAL HIER, en hij verschilt van like/boost: deze formulieren hebben MEERDERE
verzendknoppen ("Wat leuk!", "Hoe gaat het?", "Terug"), elk met een eigen
name="text". FormData(f) neemt de aangeklikte knop NIET mee -- dat doet de
browser alleen bij een echte verzending. Zonder e.submitter vertrekt er dus
een zwaai zonder tekst. Er is een test die precies dat vastlegt: een verzoek
zonder text moet 400 geven en NIETS versturen.

Het formulier blijft een formulier: zonder JS post het en leidt de route om,
ongewijzigd. De route kiest op X-Requested-With welke van de twee.

Geen aan/uit-knop zoals bij een like: zwaaien valt niet terug te draaien, dus
een toggle zou liegen over wat er kan.

Eigen CSS voor de twee toestanden, want de bestaande klasse nudge bleek
alleen op authorize-interaction opmaak te hebben -- op Berichten deed hij niets
(en doet dat nog steeds niet voor de bookmarklet-knop daar).

5 tests erbij.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 7.9 KB
Line 
1// Berichten: filteren, zoeken, en gesprekken in- en uitklappen (shaer-bqr).
2//
3// Dit stond inline in pages/messages.ejs. Dat werkte alleen na een volledige
4// laadbeurt: kwam je hier via een link BINNEN de site, dan arriveerde het script
5// via htmx met een nonce die het document niet kent, en weigerde de CSP het
6// (shaer-0i6). Chips, zoeken en het inklappen deden dan niets, en de reply-editor
7// laadde niet -- dat was Barts melding.
8//
9// TWEE DINGEN VERANDERD bij de verhuizing, en ze zijn allebei nodig omdat een
10// module ANDERS leeft dan een inline script:
11//
12// NIETS VASTHOUDEN het inline script pakte .msg-list, #msg-q en .msg-nomatch
13// een keer bij het inladen. Een module wordt per document
14// maar EEN keer geimporteerd, dus als je Berichten verlaat
15// en terugkomt wijzen die naar elementen die er niet meer
16// zijn. Nu wordt er per keer opgezocht.
17// OPNIEUW INDEXEREN bij binnenkomst op deze pagina, niet alleen bij "meer
18// laden". Anders is de lijst na een navigatie niet
19// geindexeerd en filtert het zoeken op niets.
20//
21// Alles gedelegeerd op document, met een slot: de module kan een tweede keer
22// geladen worden en mag dan geen tweede stel luisteraars neerzetten.
23
24(function () {
25 'use strict';
26 if (window.__msgWired) return;
27 window.__msgWired = true;
28
29 var kind = 'all';
30 var items = [];
31
32 // Per keer opzoeken. Zie de kop: vasthouden overleeft een navigatie niet.
33 function list() { return document.querySelector('.msg-list'); }
34 function noMatch() { return document.querySelector('.msg-nomatch'); }
35 function query() { return document.getElementById('msg-q'); }
36
37 function indexItem(li) {
38 // Een draad indexeren op zijn eerste bubbel zou de rest onvindbaar maken,
39 // dus daar nemen we de hele tekst; losse regels blijven gericht geindexeerd.
40 if (li.classList.contains('msg-thread')) {
41 li._search = ((li.getAttribute('data-who') || '') + ' ' + li.textContent).toLowerCase();
42 return;
43 }
44 var body = li.querySelector('.msg-content');
45 var post = li.querySelector('.msg-post');
46 var poll = li.querySelector('.msg-poll');
47 li._search = ((li.getAttribute('data-who') || '') + ' ' +
48 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
49 (poll ? poll.textContent : '')).toLowerCase();
50 }
51
52 function reindex() {
53 var l = list();
54 items = l ? Array.prototype.slice.call(l.querySelectorAll(':scope > .msg-item')) : [];
55 items.forEach(function (li) { if (!li._search) indexItem(li); });
56 }
57
58 function apply() {
59 if (!list()) return;
60 var qEl = query();
61 var term = (qEl && qEl.value || '').trim().toLowerCase();
62 var shown = 0;
63 items.forEach(function (li) {
64 var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
65 (!term || li._search.indexOf(term) !== -1);
66 li.style.display = ok ? '' : 'none';
67 // Een treffer die in een dichtgeklapt gesprek zit, laat anders alleen de
68 // naam zien: je zoekt iets, het staat er, en je ziet het niet. Zolang er
69 // gezocht wordt gaat zo'n gesprek open; daarna keert hij terug naar de
70 // stand die de lezer zelf koos.
71 if (li.classList.contains('msg-thread')) {
72 li.classList.toggle('is-search-open', !!term && ok);
73 }
74 if (ok) shown++;
75 });
76 var nm = noMatch();
77 if (nm) nm.hidden = shown !== 0;
78 }
79
80 // In- en uitklappen door op de tegenpartij te tikken. De stand leeft alleen in
81 // deze pagina: standaard uitgeklapt, en na een herlading weer. Dat is bewust --
82 // een gesprek dat je gisteren dichtklapte stilhouden is niet hetzelfde als het
83 // opruimen van je scherm van nu.
84 document.addEventListener('click', function (e) {
85 var who = e.target.closest && e.target.closest('.msg-thread-who'); if (!who) return;
86 var li = who.closest('.msg-thread'); if (!li) return;
87 var collapsed = li.classList.toggle('is-collapsed');
88 who.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
89 });
90
91 document.addEventListener('click', function (e) {
92 var chip = e.target.closest && e.target.closest('.msg-chip'); if (!chip) return;
93 kind = chip.getAttribute('data-show');
94 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
95 apply();
96 });
97
98 // Gedelegeerd in plaats van op het invoerveld zelf: dat veld wordt bij een
99 // navigatie vervangen.
100 document.addEventListener('input', function (e) {
101 if (e.target && e.target.id === 'msg-q') apply();
102 });
103
104 document.body.addEventListener('htmx:afterSettle', function (e) {
105 // "Meer laden" voegt rijen toe aan de bestaande lijst; een navigatie brengt
106 // een hele nieuwe lijst. In beide gevallen opnieuw indexeren en de actieve
107 // chip toepassen.
108 if (e.target && (e.target.id === 'msg-list' || e.target.querySelector && e.target.querySelector('.msg-list'))) {
109 reindex();
110 apply();
111 }
112 });
113
114 // De bookmarklet-knop: bouwt zijn href pas in de browser, want hij heeft de
115 // origin nodig.
116 function wireBookmarklet() {
117 var a = document.getElementById('fedi-bm-btn');
118 if (!a || a.__wired) return;
119 a.__wired = true;
120 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
121 a.addEventListener('click', function (e) {
122 e.preventDefault();
123 a.classList.add('nudge');
124 setTimeout(function () { a.classList.remove('nudge'); }, 600);
125 });
126 }
127
128 // Zwaaien ter plekke: een seintje is geen paginawissel.
129 //
130 // Het formulier blijft een echt formulier -- zonder JS post het en leidt de
131 // route om, precies als vroeger. Hier onderscheppen we het en sturen het met
132 // fetch, zoals like/boost in de Krant het al deden.
133 //
134 // LET OP DE VERZENDKNOP. Deze formulieren hebben er MEERDERE ("Wat leuk!",
135 // "Hoe gaat het?", "Terug"), elk met een eigen name="text". FormData(f) neemt
136 // de aangeklikte knop NIET mee -- dat doet de browser alleen bij een echte
137 // verzending. Zonder submitter vertrekt er dus een zwaai zonder tekst, en die
138 // weigert de route. Vandaar submitter, met een terugval op de eerste knop.
139 document.addEventListener('submit', function (e) {
140 var f = e.target.closest && e.target.closest('.msg-wave, .msg-quickreply');
141 if (!f || f.dataset.sending) return;
142 e.preventDefault();
143 var knoppen = [].slice.call(f.querySelectorAll('button'));
144 var knop = e.submitter || knoppen[0];
145 if (!knop) return;
146 f.dataset.sending = '1';
147 knoppen.forEach(function (b) { b.disabled = true; });
148 var body = new URLSearchParams();
149 new FormData(f).forEach(function (v, k) { body.append(k, v); });
150 if (knop.name) body.set(knop.name, knop.value);
151 fetch(f.action, {
152 method: 'POST', body: body,
153 headers: { 'X-Requested-With': 'fetch' },
154 credentials: 'same-origin',
155 })
156 .then(function (r) { return r.ok ? r.json() : null; })
157 .then(function (j) {
158 if (!j || !j.ok) throw new Error('geweigerd');
159 // Gelukt: de knoppen maken plaats voor de bevestiging. Zwaaien is
160 // eenmalig -- anders dan een like valt het niet terug te draaien, dus
161 // een aan/uit-knop zou liegen over wat er kan.
162 var klaar = document.createElement('span');
163 klaar.className = 'msg-wave-done';
164 klaar.textContent = '👋 ' + (f.dataset.sent || '');
165 f.replaceChildren(klaar);
166 })
167 .catch(function () {
168 // Mislukt: alles terug zoals het was, zodat een tweede poging kan.
169 delete f.dataset.sending;
170 knoppen.forEach(function (b) { b.disabled = false; });
171 f.classList.add('is-failed');
172 setTimeout(function () { f.classList.remove('is-failed'); }, 600);
173 });
174 });
175
176 function start() { reindex(); apply(); wireBookmarklet(); }
177 start();
178 // Kom je hier via een link binnen de site, dan is de lijst er pas na de swap.
179 document.body.addEventListener('pcmsNav', start);
180})();
Note: See TracBrowser for help on using the repository browser.