Files
dogfather-universe/workspace/assets/js/wahl.js
T
DogFatherGitandClaude Opus 5 e8478c9cae Spicy sieht DogFathers Sachen nicht mehr -- und sechs Fehler dazu
VIER DINGE AUS DEN BILDSCHIRMFOTOS, jedes ein echter Fehler:

1. SPICY MEDIA SAH DOGFATHERS DATEN. Beim ersten Anlauf bekam die Rolle
   dieselbe Regel wie DogFather (`1=1`) -- damit stimmte der Ueberblick
   ueber Manager, Scouts und Creator, und nebenbei standen seine eigenen
   Termine, Aufgaben, Dateien und Eintraege mit drin. Jetzt gibt es
   ohneDogFather() als EINE Stelle dafuer: Zwei Spalten werden geprueft,
   `creator_id` (um wen geht es) und `erstellt_von` (wer hat es
   geschrieben) -- ein Termin, den er sich selbst anlegt, haengt nur an
   der zweiten. `IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
   `NULL NOT IN (...)` weder wahr noch falsch, die Zeile fiele
   stillschweigend heraus.

2. "PERSOENLICHER ZUGANGSCODE · undefined" auf der Anmeldeseite. Eine
   zweite Namensliste im Browser, die beim Hinzufuegen der Rolle
   niemand gepflegt hat. Ein unbekannter Schluessel faellt jetzt auf
   sich selbst zurueck statt auf `undefined` -- haesslich, aber
   sichtbar.

3. SPICY KAM NICHT IN DIE PERSONENVERWALTUNG. Der Server liess sie
   herein, das Skript warf sie wieder hinaus: zwei Listen fuer dieselbe
   Frage, gepflegt wurde nur die erste.

4. DIE ANMELDEKARTE WAR ZU KLEIN FUER FUENF ROLLEN. Der Kommentar an
   genau dieser Stelle warnt woertlich davor -- und ich habe getan,
   wovor er warnt: eine Rolle eingefuegt und die Zahl daneben nicht
   angefasst. Gemessen: Inhalt 573 px in einer 526 px hohen Tafel, die
   Fusszeile stand unter dem Rahmen. Schrift kleiner half nicht (sie lag
   schon auf dem Anschlag), also sind die Abstaende an neun Stellen
   enger. Nachgemessen auf fuenf Groessen: passt ueberall.

DIE ZEIT WAR WIRKLICH FALSCH. Nicht nur in meinem Satz: `datum()` in
personen.js schnitt die ISO-Zeichenkette ab -- und die ist UTC. Im
Protokoll stand 03:00, wo 05:00 war. Das Tueckische daran ist, dass es
nie kaputt aussieht: Eine Uhrzeit ist immer plausibel.

PROFILBILDER WERDEN JETZT GEZEICHNET. Der Server lieferte sie seit
gestern mit, gezeichnet wurden sie nirgends -- deshalb aenderte sich
nichts. Jetzt in der Personenauswahl (Aufgaben, Termine, Dateien,
Bereiche), in der Gespraechsliste und an jeder Nachricht. Der
Anfangsbuchstabe bleibt als Unterlage LIEGEN: Faellt das Bild aus, steht
dort weiter etwas Sinnvolles.

DER CHAT: Gesichter mit Rollenfarbe, Blasen mit Richtung (die erste
einer Folge eckig, die naechsten rund -- so sieht man, wo ein Gedanke
anfaengt), das offene Gespraech mit Schiene, Ungelesenes hervorgehoben,
und das Eingabefeld in einer eigenen Leiste, die sich beim Schreiben
hebt.

ZWEI EIGENE PATZER, beide von Pruefungen gefunden:
  - `o is not defined`: Mein Suchmuster hat die zwei Zeilen fuer das
    Bild ans DATEIENDE gesetzt statt in die Schleife -- und in
    kalender.js an einen <span> statt ans <option>. Gemeldet von
    pruef-sicht, das die Browserkonsole mitliest.
  - Ein Kommentar mit `bild` in schraegen Anfuehrungszeichen stand INNEN
    in einer Vorlagenzeichenkette und hat sie geschlossen. Die halbe SQL
    wurde zu Programmtext.
Dazu: `.chat-neu` gibt es nicht (heisst `.chat__eingabe`), und der
Rollenknopf hatte fuer den Manager keinen sichtbaren Fokus -- ein
box-shadow wird von `overflow: hidden` abgeschnitten, ein outline nicht.

Gruen: spicy (43), creator-anlegen (29), personen-liste (33), sicht (48),
chat (48), chat-optik (24), buehne (38), struktur (32), css-klassen (15),
handy (59), breiten (23), formulare (19), start-ansicht (136),
lesbarkeit (14), barrierefrei (18), tempo (8), kopf-messen (3),
glocke (26), haerte (20), manager-sicht (43), alle-wege (19),
aufgabenbrett (44), kalender (84).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 05:46:19 +02:00

463 lines
19 KiB
JavaScript

/* ===================================================================
Auswahlfelder, die man gestalten kann.
WARUM ES DAS GIBT:
Ein aufgeklapptes <select> wird vom Betriebssystem gezeichnet, nicht
von der Seite. In Windows kommt es weiss mit blauer Markierung --
mitten in einer dunklen Oberflaeche, und kein CSS der Welt aendert
daran etwas. Genau das hat gestoert.
WIE ES ARBEITET:
Das echte <select> bleibt im Dokument und behaelt seinen Wert. Es
wird nur unsichtbar gemacht; darueber liegt ein eigener Knopf mit
eigener Liste. Damit funktioniert alles weiter, was vorher schon da
war -- jedes `feld.value`, jedes `change`-Ereignis, jedes Formular.
Faellt dieses Skript aus, ist wieder das gewohnte Systemmenue da.
Es erfasst auch Felder, die erst spaeter entstehen (MutationObserver)
und Eintraege, die erst nachgeladen werden. Deshalb musste ich keine
dreizehn Seiten einzeln umbauen -- und die naechste ist automatisch
dabei.
Die Liste haengt an `position: fixed`, nicht am Elternelement: In
einer Karte mit `overflow: hidden` waere sie sonst abgeschnitten.
---------------------------------------------------------------------
SUCHEN UND SELBST EINTRAGEN (02.09.2026)
Wunsch: "ich will da auch sachen selber noch eintragen können, also
aussuchen und selbst eintragen."
Zwei Dinge kamen dafuer dazu, und beide sitzen HIER und nicht in einem
zweiten Bauteil daneben. Der erste Anlauf war genau das -- eine eigene
Personenwahl neben dieser hier. Sie hat sich prompt mit ihr gebissen:
Zwei Bauteile haben denselben <select> 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
<select> ist leer -- entweder eine Person ODER ein Name, nie
beides. Die Seiten schicken beides getrennt zum Server.
Ohne Erlaubnis bleibt alles beim Alten: filtern ja, erfinden nein.
=================================================================== */
(() => {
'use strict';
const OFFEN = new WeakMap();
let aktiv = null; // gerade offene Liste
/* Ab wann ein Suchfeld erscheint. Bei drei Eintraegen waere es nur im
Weg, bei siebzehn ist Scrollen die falsche Bedienung. */
const SUCHE_AB = 8;
const EXTERN_MAX = 80;
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
/* ---------- Ein Feld aufwerten ----------------------------------------- */
function werteAuf(feld) {
if (feld.multiple || feld.dataset.wahl === 'aus' || OFFEN.has(feld)) return;
const huelle = el('div', 'wahl2');
const knopf = el('button', 'wahl2__knopf');
knopf.type = 'button';
knopf.setAttribute('aria-haspopup', 'listbox');
knopf.setAttribute('aria-expanded', 'false');
const text = el('span', 'wahl2__text');
const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
pfeil.setAttribute('viewBox', '0 0 24 24');
pfeil.setAttribute('class', 'wahl2__pfeil');
pfeil.setAttribute('aria-hidden', 'true');
pfeil.innerHTML = '<path d="m6 9.5 6 6 6-6"/>';
knopf.append(text, pfeil);
/* Beschriftung uebernehmen, damit der Knopf denselben Namen traegt
wie vorher das Feld. */
const schild = feld.id && document.querySelector(`label[for="${feld.id}"]`);
if (schild) knopf.setAttribute('aria-label', schild.textContent.trim());
else if (feld.getAttribute('aria-label')) knopf.setAttribute('aria-label', feld.getAttribute('aria-label'));
feld.parentNode.insertBefore(huelle, feld);
huelle.append(feld, knopf);
feld.classList.add('wahl2__echt');
const zustand = { feld, knopf, text, liste: null };
OFFEN.set(feld, zustand);
knopf.addEventListener('click', () => (aktiv === zustand ? schliessen() : oeffnen(zustand)));
knopf.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); oeffnen(zustand); }
});
/* Aendert die Seite den Wert selbst, muss der Knopf mitziehen. */
feld.addEventListener('change', () => zeige(zustand));
/* Eintraege werden oft erst nachgeladen. Nach jeder Aenderung neu
zeichnen -- im naechsten Bild, damit ein direkt danach gesetztes
`feld.value` noch mitgenommen wird. */
new MutationObserver(() => requestAnimationFrame(() => zeige(zustand)))
.observe(feld, { childList: true, subtree: true });
zeige(zustand);
}
function zeige(z) {
const extern = z.feld.dataset.extern || '';
const gewaehlt = z.feld.selectedOptions[0];
/* Ein frei eingetragener Name schlaegt die Auswahl -- das <select>
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();
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;
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(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;
}
/* DAS GESICHT ZUM NAMEN (07.09.2026).
Wunsch Filipe: "jeder soll auch immer das profilbild von denen
sehen mit denen sie verbunden sind ... ich seh als
manager/betreuer von tili ihr profilbild immer noch nicht."
Der Server liefert die Bildadresse seit heute mit
(/api/personen). Gezeichnet wurde sie nirgends -- deshalb
aenderte sich fuer ihn nichts. Hier ist der wirksamste Ort:
Diese Liste ist die Personenauswahl in Aufgaben, Terminen,
Dateien, Bereichen und Calls. Ein Bild macht aus einer Liste
von Namen eine Liste von Menschen, und man trifft den
richtigen, ohne zu lesen.
Der ANFANGSBUCHSTABE bleibt als Unterlage LIEGEN und wird
nicht ersetzt: Laedt das Bild nicht (Verbindung weg, Datei
geloescht), steht dort weiterhin etwas Sinnvolles statt eines
kaputten Bildsymbols. Genau so macht es die Kopfleiste auch.
Wo es kein `data-bild` gibt (Kategorien, Saeulen, Takte),
bleibt die Zeile wie bisher -- die Auswahl weiss nicht, dass
sie manchmal Personen zeigt, und muss es auch nicht. */
const zeile = el('button', 'wahl2__eintrag');
if (o.dataset.bild || o.dataset.person === 'ja') {
const p = el('span', 'wahl2__bild');
p.setAttribute('aria-hidden', 'true');
p.textContent = (o.textContent || '?').trim().charAt(0).toUpperCase();
if (o.dataset.bild) {
const img = document.createElement('img');
img.src = o.dataset.bild;
img.alt = '';
img.addEventListener('error', () => img.remove());
p.append(img);
}
zeile.append(p);
}
zeile.append(el('span', 'wahl2__name', 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;
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
sie nach oben -- sonst steht sie halb ausserhalb des Fensters. */
function stelle(z) {
const k = z.knopf.getBoundingClientRect();
const liste = z.liste;
liste.style.minWidth = k.width + 'px';
liste.style.left = Math.min(k.left, innerWidth - liste.offsetWidth - 12) + 'px';
const platzUnten = innerHeight - k.bottom;
if (platzUnten < 220 && k.top > platzUnten) {
liste.style.top = Math.max(12, k.top - liste.offsetHeight - 6) + 'px';
} else {
liste.style.top = (k.bottom + 6) + 'px';
liste.style.maxHeight = Math.max(140, platzUnten - 18) + 'px';
}
}
function schliessen() {
if (!aktiv) return;
aktiv.liste?.remove();
aktiv.knopf.setAttribute('aria-expanded', 'false');
aktiv.liste = null;
aktiv = null;
}
/* ---------- Bedienung ---------------------------------------------------- */
document.addEventListener('keydown', (e) => {
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();
/* 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)) {
/* Tippen springt zum naechsten passenden Eintrag -- so wie man es
von einem Auswahlfeld kennt. */
const b = e.key.toLowerCase();
const ab = eintraege.slice(wo + 1).concat(eintraege.slice(0, wo + 1));
ab.find((x) => x.textContent.trim().toLowerCase().startsWith(b))?.focus();
}
});
document.addEventListener('mousedown', (e) => {
if (!aktiv) return;
if (aktiv.liste.contains(e.target) || aktiv.knopf.contains(e.target)) return;
schliessen();
});
addEventListener('resize', schliessen);
/* SCROLLEN IN DER LISTE DARF SIE NICHT SCHLIESSEN (04.09.2026, von Filipe gemeldet:
"wenn ich da scollen will geht das immer zu").
Hier stand `addEventListener('scroll', schliessen, true)`. Das dritte Argument
bedeutet: in der EINFANGPHASE lauschen -- und damit auf JEDES Scrollen im ganzen
Dokument, auch auf das in der Liste selbst. Wer die Namensliste herunterrollen
wollte, schloss sie damit im selben Moment. Bei acht Eintraegen faellt das kaum
auf, beim ganzen Team ist die Liste unbedienbar.
Zumachen muss sie trotzdem, wenn die SEITE scrollt: Sie haengt position:fixed am
Knopf und schwaemme sonst davon. Deshalb wird jetzt unterschieden, WO gescrollt
wurde. Ein Scroll-Ereignis der Seite hat document als Ziel -- das ist nicht in der
Liste enthalten, also schliesst sie wie bisher. */
addEventListener('scroll', (e) => {
if (!aktiv) return;
const ziel = e.target;
if (ziel instanceof Node && aktiv.liste?.contains(ziel)) return;
schliessen();
}, true);
/* Ein Formular-reset() leert das <select>, kennt aber den frei
eingetragenen Namen nicht -- ohne das hier stuende er nach dem
Abbrechen noch da und behauptete eine Zuordnung, die es nicht mehr
gibt. */
document.addEventListener('reset', (e) => {
const felder = e.target?.querySelectorAll?.('select[data-extern]');
if (!felder) return;
setTimeout(() => {
for (const f of felder) {
f.dataset.extern = '';
const z = OFFEN.get(f);
if (z) zeige(z);
}
}, 0);
});
/* ---------- Aufwerten, jetzt und spaeter --------------------------------- */
const alle = () => document.querySelectorAll('select:not(.wahl2__echt)');
const laufen = () => { for (const f of alle()) werteAuf(f); };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', laufen);
} else {
laufen();
}
new MutationObserver(() => laufen()).observe(document.documentElement, {
childList: true, subtree: true,
});
/* ---------- Freie Namen erlauben -----------------------------------------
Die Seiten rufen das auf, nachdem sie ihre Personen geladen haben:
window.personenwahl?.('f-creator', { hinweis: '…' });
Danach nimmt genau dieses Feld auch einen getippten Namen an. Der
Hinweis steht unter dem Feld, sobald wirklich einer eingetragen ist.
Als Attribute am Feld und nicht als Zustand hier drinnen: So
ueberlebt die Einstellung, wenn die Seite das Feld spaeter neu
aufbaut -- und man sieht ihr im Browser-Werkzeug an, dass sie
gesetzt ist. */
window.personenwahl = function (feld, optionen = {}) {
const f = typeof feld === 'string' ? document.getElementById(feld) : feld;
if (!f) return null;
f.dataset.frei = optionen.frei === false ? 'nein' : 'ja';
if (optionen.hinweis) f.dataset.freiHinweis = optionen.hinweis;
if (!f.dataset.extern) f.dataset.extern = '';
/* Falls das Feld schon aufgewertet ist, den Knopf neu beschriften --
"disabled" haengt jetzt an einer anderen Bedingung. */
const z = OFFEN.get(f);
if (z) zeige(z);
return {
wert: () => f.value,
extern: () => f.dataset.extern || '',
zuruecksetzen() {
f.value = '';
f.dataset.extern = '';
const zz = OFFEN.get(f);
if (zz) zeige(zz);
},
};
};
})();