Files
dogfather-universe/workspace/assets/js/nachfrage.js
T
DogFatherGit c516aad4ed Nichts verschwindet mehr ohne eine Nachfrage, die sagt was passiert
Filipe: "Es darf vor allem keine Stellen geben, an denen ein Benutzer
etwas falsch machen kann, nur weil die Seite es nicht verstaendlich
genug erklaert."

Gemessen: 30 Stellen in 16 Dateien benutzten confirm() oder prompt().
Das Haus hatte die richtige Bauweise laengst -- einen <dialog>, in
aufgaben.html sogar ausfuehrlich begruendet -- aber sie stand IN EINER
SEITE. Wer anderswo etwas loeschen liess, hatte sie nicht.

confirm('Wirklich loeschen?') stellt die falsche Frage: Es fragt, ob
man sicher ist, und nennt nicht, WAS passiert, was BLEIBT und ob es
ZURUECK geht. Jetzt beantwortet jeder der 41 Dialoge alle drei.

Neu: workspace/assets/js/nachfrage.js -- window.frageNach() mit
Pflichtgrund, Zahlenfeld, einzeiliger Eingabe und Abtippsicherung.
Drei Ausgaenge: <dialog> / confirm()-Notnagel fuer Safari vor 15.4 /
Abbruch (Esc, Klick daneben, "Doch nicht" -- immer false).

DREIMAL DERSELBE FALLSTRICK, dreimal nachgemessen statt vermutet:
  .dialog stand in aufgaben.css und leistung.css -> auf dateien.html
    waere der Dialog ein weisser Systemkasten gewesen. 14 Regeln
    klammergenau nach module.css verschoben (Klammern gezaehlt, nicht
    per Muster geschnitten -- heute frueh hat ein nicht-gieriges
    Muster schon einmal CSS zerrissen).
  Das Formular trug .neu neu--blank -- und .neu gibt seine Abstaende
    nur in aufgaben.css. Gemessen: padding 0px, und die Felder
    verloren ihre height:44px. Jetzt steht alles unter
    .nachfrage__form in module.css; der Dialog borgt nichts mehr.
  Die erste Fassung der Pruefung zaehlte nachfrage.js SELBST als
    Nutzer -- damit war jede Seite trivialerweise "Nutzer" und die
    Pruefung gruen ohne Inhalt. Jetzt ausdruecklich ausgenommen.

ZWEI FUNDE NEBENBEI:
  hilfeAufraeumen() wird im Betrieb NIE aufgerufen. Der Kommentar
    behauptete "wird beim Start aufgerufen (siehe index.js)" -- das
    war nie wahr; einziger Aufrufer ist die eigene Pruefung. Folge:
    geschlossene vertrauliche Faelle bleiben unbegrenzt stehen. NICHT
    eingeschaltet (das loescht echte Daten und ist Filipes
    Entscheidung), sondern der Kommentar richtiggestellt.
  Einen Hilfe-Fall zu schliessen ist endgueltig -- es gibt keine
    Route, die ihn wieder oeffnet. Vorher stand darueber nur die
    Frage nach einem Schlusswort. Jetzt sagt der Dialog es.

pruef-struktur hat meine eigene Pruefung von heute Nachmittag
erwischt: Sie bildete ihr Datum aus UTC. Beim Beheben erst
heuteLokal(datum) genommen -- die Funktion nimmt gar kein Argument
und haette still "heute" statt "+3 Tage" geliefert. Jetzt tagLokal(3),
nachgerechnet: Abstand 3 Tage.

Am Bildschirm angesehen (Rechner 1280, Handy 390): passt rein, Esc
ergibt false, Fokus liegt auf dem harmlosen Knopf, Knoepfe 44px auf
Touch. Der Platzhalter im Abtippfeld zeigte den erwarteten Namen --
das sah aus wie ein schon ausgefuelltes Feld, entfernt.

Neu: server/pruef-nachfrage.mjs -- 17/0, mit sechs Gegenproben und
beiden Richtungen (wer fragt, laedt die Datei; wer nie fragt, laedt
sie nicht -- sonst truege die Anmeldewand 4,8 KB fuer nichts).

pruef-meldungen 8/0, pruef-css-klassen gruen, pruef-struktur gruen,
pruef-leistung gruen.
2026-09-19 19:57:43 +02:00

327 lines
14 KiB
JavaScript

/* =====================================================================
NACHFRAGEN, BEVOR ETWAS WEG IST — window.frageNach()
Filipe, 19.09.2026: „Es darf vor allem keine Stellen geben, an denen
ein Benutzer etwas falsch machen kann, nur weil die Seite es nicht
verstaendlich genug erklaert." Und: „Fehler moeglichst verhindern,
bevor sie entstehen."
---------------------------------------------------------------------
WARUM ES DIESE DATEI GIBT
Das Haus hatte die richtige Bauweise laengst -- in `aufgaben.html`
steht sie sogar begruendet: ein <dialog>, kein confirm(), weil
confirm die ganze Seite anhaelt, kein Textfeld aufnimmt und auf
jedem Rechner anders aussieht.
Nur stand diese Bauweise IN EINER SEITE. Wer auf einer anderen Seite
etwas loeschen liess, hatte sie nicht -- und griff zu confirm().
Gemessen am 19.09.2026: 30 Stellen im Workspace, in 16 Dateien.
Also wandert sie hierher, wo jede Seite sie hat.
Das ist dieselbe Lehre wie bei den Fehlermeldungen (`meldung.js`)
und bei den Kachelfarben: Was an 30 Stellen gepflegt werden muss,
wird an 30 Stellen unterschiedlich. Eine Stelle, die alle benutzen,
kann nicht auseinanderlaufen.
---------------------------------------------------------------------
WAS EIN GUTER NACHFRAGE-DIALOG SAGT
confirm('Wirklich loeschen?') stellt die falsche Frage. Es fragt, ob
man sicher ist -- und wer soll das wissen, wenn nicht dasteht, was
danach passiert? Der Dialog hier beantwortet drei Dinge:
1. WAS passiert -- "Die Datei wird geloescht."
2. Was NICHT passiert -- "Der Chat-Verlauf bleibt."
3. Ob es ZURUECK geht -- "Das laesst sich nicht rueckgaengig machen."
Punkt 3 ist der wichtigste und in confirm() gar nicht ausdrueckbar.
Deshalb traegt der Bestaetigungsknopf bei `endgueltig: true` eine
sichtbare Warnfarbe UND der Text sagt es noch einmal in Worten --
Farbe allein traegt keine Aussage (Rot-Gruen-Schwaeche, Kontrastmodus).
---------------------------------------------------------------------
DREI AUSGAENGE, wie ueberall im Haus
<dialog> vorhanden -> der richtige Dialog
<dialog> fehlt (alt) -> confirm() als Notnagel, mit DEMSELBEN
Text, damit der Nutzer dieselbe
Auskunft bekommt -- nur haesslicher
Nutzer bricht ab -> false, und zwar immer: bei Esc, beim
Klick daneben und beim "Doch nicht"
Der mittlere Ausgang ist nicht theoretisch: showModal gibt es erst ab
Safari 15.4. Ein iPhone, das nicht aktualisiert wurde, wuerde ohne
Notnagel gar nichts anzeigen -- und der Knopf taete stillschweigend
nichts. Das waere schlimmer als ein haesslicher Dialog.
---------------------------------------------------------------------
AUFRUF
if (!await frageNach({
titel: 'Datei loeschen?',
was: 'Die Datei wird vom Server entfernt.',
bleibt:'Die Nachricht im Chat bleibt stehen.',
endgueltig: true,
ja: 'Loeschen',
})) return;
Mit Grund (gibt dann { grund } statt true zurueck):
const a = await frageNach({ titel: '...', grund: 'Warum?',
grundPflicht: true });
if (!a) return; // abgebrochen
... a.grund ...
===================================================================== */
(function () {
'use strict';
/* Ein einziger Dialog fuer die ganze Seite. Er wird beim ersten
Gebrauch gebaut und danach wiederverwendet -- zwei offene Dialoge
gleichzeitig kann der Browser ohnehin nicht, und ein neuer je
Aufruf wuerde den Fokus verlieren. */
var dlg = null, feld = null, tippfeld = null, zahlfeld = null, fehlerZeile = null;
function bauen() {
if (dlg) return dlg;
dlg = document.createElement('dialog');
dlg.className = 'dialog nachfrage';
dlg.innerHTML =
'<form method="dialog" class="nachfrage__form">'
+ '<h2 class="dialog__titel" data-teil="titel"></h2>'
+ '<p class="dialog__unter" data-teil="was"></p>'
+ '<p class="dialog__unter dialog__unter--still" data-teil="bleibt"></p>'
+ '<p class="nachfrage__endgueltig" data-teil="endgueltig" hidden>'
+ 'Das lässt sich danach nicht mehr rückgängig machen.</p>'
+ '<div class="nachfrage__zeile" data-teil="tippzeile" hidden>'
+ '<label class="feld-schild" data-teil="tippschild" for="nachfrage-tipp"></label>'
+ '<input id="nachfrage-tipp" type="text" autocomplete="off"'
+ ' autocapitalize="off" spellcheck="false" />'
+ '</div>'
+ '<div class="nachfrage__zeile" data-teil="grundzeile" hidden>'
+ '<label class="feld-schild" data-teil="grundschild" for="nachfrage-grund"></label>'
+ '<textarea id="nachfrage-grund" rows="3" maxlength="500"></textarea>'
+ '</div>'
+ '<div class="nachfrage__zeile" data-teil="zahlzeile" hidden>'
+ '<label class="feld-schild" data-teil="zahlschild" for="nachfrage-zahl"></label>'
+ '<input id="nachfrage-zahl" type="number" min="1" inputmode="numeric" />'
+ '<p class="nachfrage__hinweis" data-teil="zahlhinweis"></p>'
+ '</div>'
+ '<p class="nachfrage__fehler" data-teil="fehler" role="alert"></p>'
+ '<div class="dialog__knoepfe">'
+ '<button type="submit" class="knopf knopf--klein" data-teil="ja"></button>'
+ '<button type="button" class="abmelden" data-teil="nein">Doch nicht</button>'
+ '</div></form>';
document.body.appendChild(dlg);
feld = dlg.querySelector('#nachfrage-grund');
tippfeld = dlg.querySelector('#nachfrage-tipp');
zahlfeld = dlg.querySelector('#nachfrage-zahl');
fehlerZeile = dlg.querySelector('[data-teil="fehler"]');
return dlg;
}
function teil(name) { return dlg.querySelector('[data-teil="' + name + '"]'); }
/** Setzt Text und blendet die Zeile aus, wenn es keinen gibt.
* Eine leere Zeile, die Platz einnimmt, sieht aus wie ein Fehler. */
function zeile(name, text) {
var e = teil(name);
e.textContent = text || '';
e.hidden = !text;
}
window.frageNach = function frageNach(o) {
o = o || {};
var titel = o.titel || 'Sicher?';
var was = o.was || '';
var bleibt = o.bleibt || '';
var ja = o.ja || 'Ja, machen';
var nein = o.nein || 'Doch nicht';
var endgueltig = !!o.endgueltig;
var grund = o.grund || '';
var grundPflicht = !!o.grundPflicht;
var platzhalter = o.platzhalter || '';
/* ABTIPPEN STATT ANKLICKEN. Fuer die Handgriffe, bei denen ein
Fehlklick nicht zu reparieren ist (eine Person loeschen). Wer den
Namen abtippt, hat ihn gelesen -- das ist der eigentliche Zweck,
nicht die Huerde. */
var tippen = o.tippen || '';
/* EIN EINZEILIGES FELD, wenn nach einem Namen gefragt wird und
nicht nach einem Grund. Ein Textfeld ueber drei Zeilen fuer einen
Vornamen sieht aus, als erwarte jemand einen Aufsatz. Benutzt
dasselbe Feld wie `tippen` -- beides gleichzeitig kommt nicht vor
und waere auch nicht sinnvoll. */
var eingabe = o.eingabe || '';
var eingabeWert = o.eingabeWert || '';
var eingabeMin = o.eingabeMin === undefined ? 2 : o.eingabeMin;
/* EINE ZAHL IM SELBEN DIALOG. Vorher fragte die Moderation zweimal
hintereinander (Grund, dann Dauer) -- zwei Kaesten nacheinander,
und wer beim zweiten abbrach, hatte den ersten umsonst getippt.
Reihenfolge bleibt: erst WARUM, dann WIE LANGE. Wer zuerst nach
der Dauer gefragt wird, hat die Strafe festgelegt, bevor er sie
begruendet hat. */
var zahl = o.zahl || '';
var zahlWert = o.zahlWert === undefined ? '' : String(o.zahlWert);
var zahlMax = o.zahlMax || 0;
var zahlLeer = !!o.zahlLeer;
var zahlHinweis = o.zahlHinweis || '';
var NIE_ZURUECK = 'Das lässt sich danach nicht mehr rückgängig machen.';
/* ---- Ausgang 2: kein <dialog> -> confirm() mit DEMSELBEN Text ----
Nicht "Sicher?" -- wer den Notnagel bekommt, soll dieselbe
Auskunft haben wie alle anderen. */
if (typeof HTMLDialogElement === 'undefined'
|| !HTMLDialogElement.prototype.showModal) {
var text = [titel, was, bleibt, endgueltig ? NIE_ZURUECK : '']
.filter(Boolean).join('\n\n');
if (!window.confirm(text)) return Promise.resolve(false);
if (tippen) {
var t0 = window.prompt('Zum Bestätigen genau eingeben: ' + tippen, '');
if (t0 === null || t0.trim() !== tippen) return Promise.resolve(false);
}
var erg = {};
if (eingabe) {
var e0 = window.prompt(eingabe, eingabeWert);
if (e0 === null || e0.trim().length < eingabeMin) return Promise.resolve(false);
erg.eingabe = e0.trim();
}
if (grund) {
var g0 = window.prompt(grund, '');
if (g0 === null) return Promise.resolve(false);
if (grundPflicht && g0.trim().length < 3) return Promise.resolve(false);
erg.grund = g0.trim();
}
if (zahl) {
var z0 = window.prompt(zahl + (zahlHinweis ? ' (' + zahlHinweis + ')' : ''),
zahlWert);
if (z0 === null) return Promise.resolve(false);
erg.zahl = z0.trim() === '' ? null : Number(z0);
}
return Promise.resolve((grund || zahl || eingabe) ? erg : true);
}
bauen();
teil('titel').textContent = titel;
zeile('was', was);
zeile('bleibt', bleibt);
teil('endgueltig').hidden = !endgueltig;
teil('ja').textContent = ja;
teil('nein').textContent = nein;
/* Warnfarbe NUR zusaetzlich zum Satz oben, nie statt seiner. */
teil('ja').classList.toggle('knopf--warn', endgueltig);
teil('tippzeile').hidden = !(tippen || eingabe);
if (eingabe) {
teil('tippschild').textContent = eingabe;
tippfeld.value = eingabeWert;
tippfeld.placeholder = '';
} else if (tippen) {
teil('tippschild').textContent = 'Zum Bestätigen genau eingeben: ' + tippen;
tippfeld.value = '';
/* KEIN Platzhalter mit dem erwarteten Wort. Er sieht in einem
leeren Feld aus wie ein bereits eingetragener Text -- und wer
glaubt, es stehe schon da, drueckt auf "Loeschen" und wundert
sich ueber die Fehlermeldung. Der Name steht ohnehin in der
Beschriftung direkt darueber. */
tippfeld.placeholder = '';
}
teil('grundzeile').hidden = !grund;
if (grund) {
teil('grundschild').textContent = grund;
feld.value = '';
feld.placeholder = platzhalter;
}
teil('zahlzeile').hidden = !zahl;
if (zahl) {
teil('zahlschild').textContent = zahl;
teil('zahlhinweis').textContent = zahlHinweis;
zahlfeld.value = zahlWert;
if (zahlMax) zahlfeld.max = String(zahlMax); else zahlfeld.removeAttribute('max');
zahlfeld.required = !zahlLeer;
}
fehlerZeile.textContent = '';
return new Promise(function (fertig) {
var antwort = false;
var form = dlg.querySelector('form');
function aufraeumen() {
dlg.removeEventListener('close', beiSchliessen);
form.removeEventListener('submit', beiJa);
teil('nein').removeEventListener('click', beiNein);
dlg.removeEventListener('click', beiDaneben);
}
function beiSchliessen() { aufraeumen(); fertig(antwort); }
function beiNein() { antwort = false; dlg.close(); }
function beiDaneben(e) {
/* Klick auf den abgedunkelten Rand = abbrechen. Das erwartet
jeder, und ohne es bleibt der Dialog am Handy gefuehlt
haengen, weil dort kein Esc zur Verfuegung steht. */
if (e.target === dlg) { antwort = false; dlg.close(); }
}
function beiJa(e) {
e.preventDefault();
if (eingabe && tippfeld.value.trim().length < eingabeMin) {
fehlerZeile.textContent = eingabeMin > 1
? 'Bitte mindestens ' + eingabeMin + ' Zeichen eintragen.'
: 'Bitte etwas eintragen.';
tippfeld.focus(); return;
}
if (tippen && tippfeld.value.trim() !== tippen) {
/* Sagt, WAS nicht stimmt -- "falsch" allein hilft niemandem,
der sich vertippt hat. */
fehlerZeile.textContent = 'Das stimmt noch nicht. Erwartet wird genau: '
+ tippen;
tippfeld.focus();
tippfeld.select();
return;
}
var erg2 = {};
if (eingabe) erg2.eingabe = tippfeld.value.trim();
if (grund) {
var g = feld.value.trim();
if (grundPflicht && g.length < 3) {
fehlerZeile.textContent = 'Bitte kurz sagen, warum.';
feld.focus();
return; /* NICHT schliessen -- sonst waere die Eingabe weg */
}
erg2.grund = g;
}
if (zahl) {
var roh = zahlfeld.value.trim();
if (roh === '' && !zahlLeer) {
fehlerZeile.textContent = 'Bitte eine Anzahl eintragen.';
zahlfeld.focus(); return;
}
var n = roh === '' ? null : Number(roh);
if (n !== null && !(n > 0)) {
fehlerZeile.textContent = 'Das ist keine gültige Anzahl.';
zahlfeld.focus(); return;
}
if (n !== null && zahlMax && n > zahlMax) {
fehlerZeile.textContent = 'Mehr als ' + zahlMax + ' geht hier nicht.';
zahlfeld.focus(); return;
}
erg2.zahl = n;
}
antwort = (grund || zahl || eingabe) ? erg2 : true;
dlg.close();
}
dlg.addEventListener('close', beiSchliessen);
form.addEventListener('submit', beiJa);
teil('nein').addEventListener('click', beiNein);
dlg.addEventListener('click', beiDaneben);
dlg.showModal();
/* Der Fokus liegt auf dem harmlosen Knopf, nicht auf "Loeschen".
Wer aus Gewohnheit Enter drueckt, bricht damit ab -- das ist
die richtige Richtung fuer einen Fehlgriff. Ausser es gibt ein
Textfeld, dann gehoert der Fokus dorthin. */
if (tippen || eingabe) tippfeld.focus();
else if (grund) feld.focus();
else teil('nein').focus();
});
};
})();