From 540b5d88389917be790d5a011ccc3a30a4bbbeaf Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 2 Sep 2026 20:10:09 +0200 Subject: [PATCH] Freie Namen, ein Call-Knopf, und drei Seiten, die fuer Scouts kaputt waren DREI SACHEN AUF EINMAL, alle aus derselben Sitzung. 1. AUSSUCHEN ODER SELBST EINTRAGEN Wunsch: "ich will da auch sachen selber noch eintragen koennen, also aussuchen und selbst eintragen." Jede Personenauswahl (Kalender, Aufgaben, Bereiche, Content, Dateien) nimmt jetzt auch einen getippten Namen an -- eine Agentur, eine Marke, einen Gast ohne Konto. Dazu bekommt JEDE Auswahl ab acht Eintraegen ein Suchfeld: tippen statt scrollen. Gebaut IM vorhandenen Auswahl-Bauteil (wahl.js), nicht daneben. Der erste Anlauf war ein zweites Bauteil -- es hat sich prompt mit dem ersten gebissen, beide haben denselben eingepackt, und das Feld stand + danach in doppelter Huelle. Ein zweites Bauteil haette ausserdem + anders ausgesehen und waere beim naechsten Umbau nur an einer von + beiden Stellen nachgezogen worden. + + 1. EIN SUCHFELD ueber der Liste, sobald es mehr als acht Eintraege + gibt. Tippen filtert. Gilt fuer JEDES Auswahlfeld -- bei + siebzehn Personen ist Scrollen die falsche Bedienung. + + 2. FREIE NAMEN, aber nur wo es ausdruecklich erlaubt ist + (`data-frei="ja"`). Dann nimmt die Liste den getippten Text als + eigenen Eintrag an: eine Agentur, eine Marke, ein Gast ohne + Konto. Der Text steht danach in `feld.dataset.extern`, und das + + steht dann auf "leer", und ohne diese Zeile zeigte der Knopf den + Platzhalter an, obwohl etwas eingetragen ist. */ + z.text.textContent = extern || (gewaehlt ? gewaehlt.textContent : ''); + z.knopf.dataset.leer = extern || (gewaehlt && gewaehlt.value) ? 'nein' : 'ja'; + z.knopf.dataset.extern = extern ? 'ja' : 'nein'; + /* Bei erlaubten freien Namen darf der Knopf NICHT abschalten, wenn + die Liste leer ist -- man kann ja trotzdem etwas eintragen. */ + z.knopf.disabled = z.feld.disabled + || (!z.feld.options.length && z.feld.dataset.frei !== 'ja'); + hinweisZeigen(z); + } + + /* Der Satz unter dem Feld erscheint NUR, wenn wirklich ein freier + Name drinsteht. Ein dauerhaft sichtbarer Warnsatz wird nach zwei + Tagen ueberlesen. */ + function hinweisZeigen(z) { + const text = z.feld.dataset.extern ? (z.feld.dataset.freiHinweis || '') : ''; + if (!text) { z.hinweis?.remove(); z.hinweis = null; return; } + if (!z.hinweis) { + z.hinweis = el('p', 'wahl2__hinweis'); + z.knopf.parentNode.append(z.hinweis); + } + z.hinweis.textContent = text; + } + + /** Einen frei eingetragenen Namen setzen (oder mit '' wieder loeschen). + * Loest `change` aus, damit die Seiten davon erfahren -- sie haengen + * alle daran. */ + function externSetzen(z, text) { + const sauber = String(text || '').replace(/\s+/g, ' ').trim().slice(0, EXTERN_MAX); + z.feld.dataset.extern = sauber; + if (sauber) z.feld.value = ''; + z.feld.dispatchEvent(new Event('change', { bubbles: true })); + zeige(z); } /* ---------- Liste ------------------------------------------------------- */ function oeffnen(z) { schliessen(); - if (z.feld.disabled || !z.feld.options.length) return; + const frei = z.feld.dataset.frei === 'ja'; + if (z.feld.disabled || (!z.feld.options.length && !frei)) return; const liste = el('div', 'wahl2__liste'); liste.setAttribute('role', 'listbox'); z.liste = liste; - let vorherigeGruppe = null; - for (const o of z.feld.options) { - /* Gruppen (optgroup) mitnehmen -- die Kategoriewahl lebt davon. */ - const gruppe = o.parentElement.tagName === 'OPTGROUP' ? o.parentElement.label : null; - if (gruppe && gruppe !== vorherigeGruppe) { - liste.append(el('p', 'wahl2__gruppe', gruppe)); - vorherigeGruppe = gruppe; - } - const zeile = el('button', 'wahl2__eintrag', o.textContent); - zeile.type = 'button'; - zeile.setAttribute('role', 'option'); - zeile.setAttribute('aria-selected', o.selected ? 'true' : 'false'); - if (o.selected) zeile.dataset.an = 'ja'; - if (o.disabled) { zeile.disabled = true; zeile.dataset.aus = 'ja'; } - zeile.addEventListener('click', () => { - if (o.disabled) return; - z.feld.value = o.value; - /* Von Hand ausloesen: Ein per Skript gesetzter Wert meldet sich - nicht von selbst, und die Seiten haengen an `change`. */ - z.feld.dispatchEvent(new Event('change', { bubbles: true })); - zeige(z); - schliessen(); - z.knopf.focus(); + const zeilen = el('div', 'wahl2__zeilen'); + + /* Ein Suchfeld gibt es, sobald die Liste lang wird -- oder immer, + wenn man auch selbst etwas eintragen darf. Bei drei Eintraegen + waere es nur im Weg. */ + let suchfeld = null; + if (frei || z.feld.options.length > SUCHE_AB) { + suchfeld = el('input', 'wahl2__suche'); + suchfeld.type = 'text'; + suchfeld.autocomplete = 'off'; + suchfeld.spellcheck = false; + suchfeld.placeholder = frei ? 'Suchen oder Namen eintippen …' : 'Suchen …'; + suchfeld.setAttribute('aria-label', suchfeld.placeholder); + suchfeld.value = z.feld.dataset.extern || ''; + suchfeld.addEventListener('input', () => fuellen(suchfeld.value)); + suchfeld.addEventListener('keydown', (e) => { + /* Enter nimmt den ersten Treffer -- oder, wenn es keinen gibt + und freie Namen erlaubt sind, den getippten Text. Ohne das + muesste man nach dem Tippen noch klicken. */ + if (e.key === 'Enter') { + e.preventDefault(); + zeilen.querySelector('.wahl2__eintrag:not([disabled])')?.click(); + } }); - liste.append(zeile); + liste.append(suchfeld); } + liste.append(zeilen); + + function waehle(wert) { + z.feld.dataset.extern = ''; + z.feld.value = wert; + /* Von Hand ausloesen: Ein per Skript gesetzter Wert meldet sich + nicht von selbst, und die Seiten haengen an `change`. */ + z.feld.dispatchEvent(new Event('change', { bubbles: true })); + zeige(z); + schliessen(); + z.knopf.focus(); + } + + function fuellen(suche = '') { + const s = suche.trim().toLowerCase(); + zeilen.replaceChildren(); + + /* Der getippte Text als eigener Eintrag -- ganz oben, damit man + ihn ohne Scrollen erreicht. Nur, wenn er nicht sowieso genau + auf einen vorhandenen Eintrag passt. */ + const genauSo = [...z.feld.options] + .some((o) => o.value && o.textContent.trim().toLowerCase() === s); + if (frei && s && !genauSo) { + const eigen = el('button', 'wahl2__eintrag wahl2__eintrag--eigen', + `„${suche.trim()}" eintragen`); + eigen.type = 'button'; + eigen.setAttribute('role', 'option'); + eigen.addEventListener('click', () => { + externSetzen(z, suche); + schliessen(); + z.knopf.focus(); + }); + zeilen.append(eigen); + } + + let vorherigeGruppe = null; + let treffer = 0; + for (const o of z.feld.options) { + if (s && !o.textContent.toLowerCase().includes(s)) continue; + treffer++; + /* Gruppen (optgroup) mitnehmen -- die Kategoriewahl lebt davon. */ + const gruppe = o.parentElement.tagName === 'OPTGROUP' ? o.parentElement.label : null; + if (gruppe && gruppe !== vorherigeGruppe) { + zeilen.append(el('p', 'wahl2__gruppe', gruppe)); + vorherigeGruppe = gruppe; + } + const zeile = el('button', 'wahl2__eintrag', o.textContent); + zeile.type = 'button'; + zeile.setAttribute('role', 'option'); + const an = o.selected && !z.feld.dataset.extern; + zeile.setAttribute('aria-selected', an ? 'true' : 'false'); + if (an) zeile.dataset.an = 'ja'; + if (o.disabled) { zeile.disabled = true; zeile.dataset.aus = 'ja'; } + zeile.addEventListener('click', () => { if (!o.disabled) waehle(o.value); }); + zeilen.append(zeile); + } + + /* Ein leeres Ergebnis muss etwas sagen. Eine wortlos leere Liste + sieht aus, als sei etwas kaputt. */ + if (!treffer && !(frei && s)) { + zeilen.append(el('p', 'wahl2__leer', + s ? 'Nichts gefunden.' : 'Nichts zur Auswahl.')); + } + } + + fuellen(suchfeld ? suchfeld.value : ''); document.body.append(liste); stelle(z); z.knopf.setAttribute('aria-expanded', 'true'); aktiv = z; - const erster = liste.querySelector('.wahl2__eintrag[data-an="ja"]') - || liste.querySelector('.wahl2__eintrag:not([disabled])'); - erster?.focus(); - erster?.scrollIntoView({ block: 'nearest' }); + if (suchfeld) { + suchfeld.focus(); + suchfeld.select(); + } else { + const erster = zeilen.querySelector('.wahl2__eintrag[data-an="ja"]') + || zeilen.querySelector('.wahl2__eintrag:not([disabled])'); + erster?.focus(); + erster?.scrollIntoView({ block: 'nearest' }); + } } /* Position berechnen. Passt die Liste nicht mehr nach unten, klappt @@ -167,9 +304,23 @@ if (!aktiv) return; const eintraege = [...aktiv.liste.querySelectorAll('.wahl2__eintrag:not([disabled])')]; const wo = eintraege.indexOf(document.activeElement); + /* Steht der Zeiger im Suchfeld, gehoeren die Tasten DORTHIN. Ohne + diese Unterscheidung haette der Sprung-zum-Buchstaben unten jeden + Tastendruck abgefangen: Man tippt ein "a", und der Zeiger springt + aus dem Suchfeld auf einen Eintrag -- Suchen waere unmoeglich. */ + const imSuchfeld = document.activeElement?.classList.contains('wahl2__suche'); + if (e.key === 'Escape') { e.preventDefault(); const k = aktiv.knopf; schliessen(); k.focus(); } else if (e.key === 'ArrowDown') { e.preventDefault(); eintraege[Math.min(wo + 1, eintraege.length - 1)]?.focus(); } - else if (e.key === 'ArrowUp') { e.preventDefault(); eintraege[Math.max(wo - 1, 0)]?.focus(); } + else if (e.key === 'ArrowUp') { + e.preventDefault(); + /* Vom ersten Eintrag zurueck ins Suchfeld -- sonst sitzt man + darin fest und kommt an den getippten Text nicht mehr heran. */ + if (wo === 0 && aktiv.liste.querySelector('.wahl2__suche')) { + aktiv.liste.querySelector('.wahl2__suche').focus(); + } else eintraege[Math.max(wo - 1, 0)]?.focus(); + } + else if (imSuchfeld) { /* alles Weitere gehoert dem Suchfeld */ } else if (e.key === 'Home') { e.preventDefault(); eintraege[0]?.focus(); } else if (e.key === 'End') { e.preventDefault(); eintraege.at(-1)?.focus(); } else if (e.key.length === 1 && /\S/.test(e.key)) { @@ -189,6 +340,22 @@ addEventListener('resize', schliessen); addEventListener('scroll', schliessen, true); + /* Ein Formular-reset() leert das + + + + +

Gespräche werden geladen …

- - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index b8690438..67473515 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -164,11 +164,11 @@ - - - - - - + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index b7356dd3..be85d9cb 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,9 +93,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index debe5dbd..ec253e70 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index e507f89a..9266e71e 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -178,9 +178,9 @@ - - - - + + + + diff --git a/workspace/personen.html b/workspace/personen.html index e4bb69fd..8a4ab33c 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -121,9 +121,9 @@ - - - - + + + + diff --git a/workspace/profil.html b/workspace/profil.html index ef6698ea..3fa208a1 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -193,10 +193,10 @@ - - - - - + + + + + diff --git a/workspace/report.html b/workspace/report.html index c2728341..07a1c9e0 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -91,10 +91,10 @@ - - - - - + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index df4cc667..b779e82a 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,9 +124,9 @@ - - - - + + + + diff --git a/workspace/start.html b/workspace/start.html index e39f54b5..4b87f67f 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -99,9 +99,9 @@

- - - - + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 055a87b3..b8c00536 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -70,10 +70,10 @@ - - - - - + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 05a8df7d..98032152 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 093080bb..c3255f61 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -57,9 +57,9 @@ - + - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 4dad148a..d169cfe7 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -175,9 +175,9 @@ - - - - + + + +