Reaktionen: Vorschlaege, ganzer Katalog, eigene Favoriten
Filipe: "soll viel besser und geiler sein und man soll auch auswaehlen
koennen, also paar als vorschlaege mit der moeglichkeit andere
auszuwaehlen und als favoriten zu speichern."
Aus einem Streifen mit sechs Zeichen ist eine Karte geworden:
Vorschlaege oben, Suchfeld, darunter der Katalog in sechs Gruppen --
82 Zeichen, jedes mit deutschen Suchwoertern. Wer "feuer" tippt, muss
nicht scrollen.
DREI LISTEN, DREI FRAGEN, und sie werden gern verwechselt:
KATALOG Was gibt es? fest, workspace-reaktionen.js
VORSCHLAG Was steht vorn, solange fest -- bis jemand eigene
ich nichts gewaehlt habe? Favoriten hat
FAVORITEN Was hat DIESER Mensch in der Datenbank
sich gemerkt?
Nur der Katalog entscheidet, was ANGENOMMEN wird. Vorschlag und
Favoriten sind Reihenfolge, keine Erlaubnis -- sonst koennte man sich
ueber einen Favoriten etwas erlauben, das im Katalog nicht steht. Die
erlaubte Menge wird aus dem Katalog ABGELEITET; eine zweite Liste
daneben waere die, in der ein Zeichen fehlt, das die Oberflaeche
anbietet.
FAVORITEN HAENGEN AM MENSCHEN, NICHT AM GERAET. Im Browser abgelegt
waeren sie am Handy andere als am Rechner und beim naechsten Loeschen
der Seitendaten weg.
DER STERN SCHALTET EINEN MODUS, statt an jedem Zeichen einen zweiten
Knopf zu haben: Am Handy gibt es kein "daneben zeigen". Im Merk-Modus
faerbt sich die ganze Tafel, damit niemand reagiert, wenn er merken
wollte. Die Tafel bleibt dabei offen -- acht Favoriten waeren sonst
acht Mal Aufmachen -- und die Vorschlagszeile aendert sich sofort mit.
DIE GRENZE SAGT BESCHEID, statt still den aeltesten wegzuwerfen: Wer
einen neunten merken will, bekommt "nimm erst einen weg". Ein Favorit,
der ohne Ansage verschwindet, sieht wie ein Fehler aus.
Eine Selbstpruefung beim Laden meldet doppelte Zeichen im Katalog und
Vorschlaege, die nicht darin stehen -- beides waere in der Tafel still.
pruef-chat-aufloesen 85 -> 115. Gemessen wird, was PASSIERT, nicht dass
es Elemente gibt: dass die Suche wirklich eingrenzt (1 statt 82) und
das Richtige findet, dass ein Fehlgriff "Nichts zu ... gefunden" sagt
statt eine leere Flaeche zu zeigen, und vor allem, dass im Merk-Modus
KEINE Reaktion entsteht -- an der Zahl der Reaktionen gemessen, nicht
an der Absicht.
NEBENBEFUND AUS DER EIGENEN PRUEFUNG: pruef-css-klassen hat meine
Gruppenueberschrift mit 10,56 px abgelehnt (Grenze 11,5). Richtig so --
eine gesperrte Versalzeile ist ohnehin schwerer zu lesen, die darf
nicht auch noch die kleinste sein. Auf .72rem angehoben, statt die
Grundlinie zu verschieben.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+140
-19
@@ -440,48 +440,169 @@
|
||||
keinen Ersatz aus dem Aermel: Dann erscheint die Auswahl nicht,
|
||||
statt Zeichen anzubieten, die abgewiesen werden.
|
||||
=================================================================== */
|
||||
let reaktionsZeichen = null;
|
||||
let auswahlDaten = null; /* vom Server: Vorschlag, Katalog, Favoriten */
|
||||
let offeneAuswahl = null;
|
||||
let favoritenModus = false;
|
||||
|
||||
async function zeichenHolen() {
|
||||
if (reaktionsZeichen) return reaktionsZeichen;
|
||||
async function auswahlHolen(neuLaden = false) {
|
||||
if (auswahlDaten && !neuLaden) return auswahlDaten;
|
||||
try {
|
||||
const a = await hole('/workspace/api/chat/reaktionen/auswahl');
|
||||
if (!a.ok) return null;
|
||||
const d = await a.json();
|
||||
reaktionsZeichen = Array.isArray(d.zeichen) && d.zeichen.length ? d.zeichen : null;
|
||||
return reaktionsZeichen;
|
||||
if (!Array.isArray(d.zeichen) || !d.zeichen.length) return null;
|
||||
auswahlDaten = d;
|
||||
return d;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function auswahlSchliessen() {
|
||||
if (offeneAuswahl) { offeneAuswahl.remove(); offeneAuswahl = null; }
|
||||
favoritenModus = false;
|
||||
}
|
||||
|
||||
/** Ein Zeichen in der Tafel. Was ein Klick tut, haengt am Modus --
|
||||
* reagieren oder merken. */
|
||||
function zeichenKnopf(z, nachrichtId) {
|
||||
const b = el('button', 'reagieren-tafel__zeichen', z);
|
||||
b.type = 'button';
|
||||
b.dataset.zeichen = z;
|
||||
if (auswahlDaten?.favoriten?.includes(z)) b.dataset.favorit = 'ja';
|
||||
b.setAttribute('aria-label', `Mit ${z} reagieren`);
|
||||
b.addEventListener('click', async () => {
|
||||
if (favoritenModus) { await favoritSetzen(z); return; }
|
||||
auswahlSchliessen();
|
||||
reagieren(nachrichtId, z);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
async function favoritSetzen(zeichen) {
|
||||
try {
|
||||
const a = await hole('/workspace/api/chat/reaktionen/favorit', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ zeichen }),
|
||||
});
|
||||
const d = await a.json().catch(() => ({}));
|
||||
const h = offeneAuswahl?.querySelector('.reagieren-tafel__hinweis');
|
||||
if (!a.ok) {
|
||||
/* DIE ABSAGE STEHT IN DER TAFEL, nicht irgendwo anders: Wer
|
||||
gerade Favoriten festlegt, schaut genau dorthin. */
|
||||
if (h) h.textContent = d.fehler || 'Ging nicht.';
|
||||
return;
|
||||
}
|
||||
auswahlDaten.favoriten = d.favoriten;
|
||||
auswahlDaten.zeichen = d.zeichen;
|
||||
if (h && favoritenModus) {
|
||||
h.textContent = `${d.favoriten.length} von ${auswahlDaten.favoriten_max} gemerkt.`;
|
||||
}
|
||||
/* Die Tafel bleibt offen und zeigt den neuen Stand. Sie zu
|
||||
schliessen waere bei acht Favoriten acht Mal Aufmachen. */
|
||||
for (const b of offeneAuswahl?.querySelectorAll('.reagieren-tafel__zeichen') || []) {
|
||||
if (d.favoriten.includes(b.dataset.zeichen)) b.dataset.favorit = 'ja';
|
||||
else delete b.dataset.favorit;
|
||||
}
|
||||
offeneAuswahl?._schnellBauen?.();
|
||||
} catch { /* stumm -- die Tafel bleibt, wie sie ist */ }
|
||||
}
|
||||
|
||||
/** Die Tafel. Oben die Vorschlagszeile, darunter Suche und Katalog.
|
||||
*
|
||||
* WARUM ALLES IN EINER TAFEL und nicht "sechs Zeichen, dann ein
|
||||
* Fenster fuer den Rest": Wer etwas anderes will, soll nicht erst
|
||||
* ein zweites Mal klicken. Die Vorschlagszeile ist die Abkuerzung,
|
||||
* nicht die Grenze. */
|
||||
async function auswahlZeigen(knopf, nachrichtId) {
|
||||
/* Ein zweiter Klick auf denselben Knopf schliesst wieder -- sonst
|
||||
staendig eine offene Tafel, die man wegklicken muss. */
|
||||
const schonOffen = offeneAuswahl && offeneAuswahl.dataset.fuer === String(nachrichtId);
|
||||
auswahlSchliessen();
|
||||
if (schonOffen) return;
|
||||
|
||||
const zeichen = await zeichenHolen();
|
||||
if (!zeichen) { melde('Die Auswahl ließ sich nicht laden.'); return; }
|
||||
const daten = await auswahlHolen();
|
||||
if (!daten) { melde('Die Auswahl ließ sich nicht laden.'); return; }
|
||||
|
||||
const tafel = el('div', 'reagieren-tafel');
|
||||
tafel.dataset.fuer = String(nachrichtId);
|
||||
tafel.setAttribute('role', 'group');
|
||||
tafel.setAttribute('aria-label', 'Mit einem Zeichen reagieren');
|
||||
for (const z of zeichen) {
|
||||
const b = el('button', 'reagieren-tafel__zeichen', z);
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-label', `Mit ${z} reagieren`);
|
||||
b.addEventListener('click', () => { auswahlSchliessen(); reagieren(nachrichtId, z); });
|
||||
tafel.append(b);
|
||||
}
|
||||
tafel.setAttribute('role', 'dialog');
|
||||
tafel.setAttribute('aria-label', 'Reagieren');
|
||||
|
||||
const hinweis = el('p', 'reagieren-tafel__hinweis');
|
||||
|
||||
/* ---- Kopf: Vorschlaege + Favoriten-Schalter --------------------- */
|
||||
const kopf = el('div', 'reagieren-tafel__kopf');
|
||||
const schnell = el('div', 'reagieren-tafel__schnell');
|
||||
const schnellBauen = () => {
|
||||
schnell.textContent = '';
|
||||
for (const z of auswahlDaten.zeichen) schnell.append(zeichenKnopf(z, nachrichtId));
|
||||
};
|
||||
schnellBauen();
|
||||
kopf.append(schnell);
|
||||
|
||||
/* Der Stern schaltet um: Klicken heisst dann MERKEN statt
|
||||
REAGIEREN. Ein Modus statt eines zweiten Knopfes an jedem
|
||||
Zeichen -- am Handy gibt es kein "daneben zeigen". */
|
||||
const stern = el('button', 'reagieren-tafel__stern', '★');
|
||||
stern.type = 'button';
|
||||
stern.title = 'Favoriten festlegen';
|
||||
stern.setAttribute('aria-pressed', 'false');
|
||||
stern.addEventListener('click', () => {
|
||||
favoritenModus = !favoritenModus;
|
||||
tafel.dataset.merken = favoritenModus ? 'ja' : '';
|
||||
stern.setAttribute('aria-pressed', favoritenModus ? 'true' : 'false');
|
||||
hinweis.textContent = favoritenModus
|
||||
? `Antippen merkt ein Zeichen (bis ${auswahlDaten.favoriten_max}). Noch einmal nimmt es weg.`
|
||||
: '';
|
||||
});
|
||||
kopf.append(stern);
|
||||
tafel.append(kopf);
|
||||
tafel.append(hinweis);
|
||||
|
||||
/* ---- Suche ------------------------------------------------------ */
|
||||
const suche = document.createElement('input');
|
||||
suche.type = 'search';
|
||||
suche.className = 'reagieren-tafel__suche';
|
||||
suche.placeholder = 'Suchen … z. B. feuer, herz, danke';
|
||||
suche.setAttribute('aria-label', 'Zeichen suchen');
|
||||
tafel.append(suche);
|
||||
|
||||
/* ---- Katalog ---------------------------------------------------- */
|
||||
const gitter = el('div', 'reagieren-tafel__katalog');
|
||||
tafel.append(gitter);
|
||||
|
||||
const katalogBauen = (suchwort) => {
|
||||
gitter.textContent = '';
|
||||
const w = String(suchwort || '').trim().toLowerCase();
|
||||
let gefunden = 0;
|
||||
for (const gruppe of auswahlDaten.katalog) {
|
||||
const treffer = gruppe.zeichen.filter(([z, woerter]) =>
|
||||
!w || woerter.includes(w) || z === w);
|
||||
if (!treffer.length) continue;
|
||||
gefunden += treffer.length;
|
||||
gitter.append(el('p', 'reagieren-tafel__gruppe', gruppe.name));
|
||||
const reihe = el('div', 'reagieren-tafel__reihe');
|
||||
for (const [z] of treffer) reihe.append(zeichenKnopf(z, nachrichtId));
|
||||
gitter.append(reihe);
|
||||
}
|
||||
if (!gefunden) {
|
||||
/* KEIN LEERES FELD. "Nichts gefunden" ist eine Auskunft, eine
|
||||
leere Flaeche sieht nach einem Fehler aus. */
|
||||
gitter.append(el('p', 'reagieren-tafel__leer',
|
||||
`Nichts zu „${suchwort}“ gefunden.`));
|
||||
}
|
||||
};
|
||||
katalogBauen('');
|
||||
let tippTakt = null;
|
||||
suche.addEventListener('input', () => {
|
||||
clearTimeout(tippTakt);
|
||||
tippTakt = setTimeout(() => katalogBauen(suche.value), 90);
|
||||
});
|
||||
|
||||
knopf.closest('.chat-nachricht')?.append(tafel);
|
||||
offeneAuswahl = tafel;
|
||||
tafel.querySelector('button')?.focus();
|
||||
/* Die Tafel merkt sich, wie sie ihre Schnellzeile neu baut -- nach
|
||||
einem Favoriten-Klick muss sie sich aendern, ohne dass man sie
|
||||
schliesst und wieder oeffnet. */
|
||||
tafel._schnellBauen = schnellBauen;
|
||||
}
|
||||
|
||||
/* Wegklicken und Escape schliessen die Tafel. Ohne das bleibt sie
|
||||
|
||||
Reference in New Issue
Block a user