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.
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
/* =====================================================================
|
||||
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();
|
||||
});
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user