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.
327 lines
14 KiB
JavaScript
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();
|
|
});
|
|
};
|
|
})();
|