VanVan im Support, Meldung #11, VIERMAL gemeldet: „Hier im Supportbereich kann man immer nur ein Bild hinzufuegen bei einer Meldung. 2-3 waeren besser." Und in der zweiten Runde der Satz, auf den es ankommt: „wenn man es nacheinander versucht hinzuzufuegen wird das Bild immer nur ersetzt." EINE TABELLE STATT NEUER SPALTEN `support_bilder` haelt ab jetzt JEDES Supportbild -- das der Meldung (`runde_nr` NULL) und das einer Antwort (`runde_nr` = Runde). Die Alternative waere `bild2_datei`, `bild3_datei` gewesen, und beim vierten Bild wieder. Eine Zeile je Bild kennt keine Obergrenze im Schema; die Grenze steht an EINER Stelle im Code (`BILDER_MAX = 3`) und kommt von dort in die Oberflaeche, statt dort ein zweites Mal zu stehen. DIE ACHT VORHANDENEN BILDER WANDERN MIT. Ohne diesen Schritt haette die neue Tabelle ab heute recht und die alten Bilder waeren unsichtbar -- ohne Fehler, ohne rote Zeile, nur acht leere Karten. Der Umzug steht NACH der Spaltennachruestung: Er liest `urteil_bild_datei`, und die gibt es in einer bestehenden Datenbank erst, nachdem sie ergaenzt wurde. Stuende er davor, scheiterte er genau dort, wo es darauf ankommt -- live, waehrend lokal alles gruen bliebe, weil jede Pruefung ihre Datenbank frisch anlegt. DREI BILDER IN EINER ANFRAGE `x-bilder: 20481,15320` sagt, wo zu schneiden ist, der Rumpf ist die Aneinanderreihung. `multipart/form-data` haette einen Zerleger gebraucht, den dieses Haus nicht hat; drei Anfragen nacheinander haetten den Zustand „Meldung da, Bild zwei laedt noch" erzeugt -- genau den, gegen den die Kommentare an dieser Route schon vorher argumentieren. Die Summe muss auf das Byte stimmen, und jedes Stueck wird einzeln an seinen ersten Bytes erkannt: Wer falsch schneidet, bekommt eine Absage, kein verfaelschtes Bild. Ohne den Kopf gilt der ganze Rumpf als ein Bild -- derselbe Satz mit einer Laenge, damit eine Seite aus dem Zwischenspeicher weiterlaeuft. EINE ROUTE STATT DREI. `/:id/bild` und `/:id/runde/:nr/bild` sind weg; es gibt `/:id/bild/:bid`. Wohin ein Bild gehoert, steht in seiner Zeile -- der Weg muss es nicht wiederholen. Die Meldungsnummer bleibt trotzdem im Pfad: Sie ist die Sichtbarkeitsfrage, und beides muss zusammenpassen (gemessen). ZWEI FUNDE, DIE DIE PRUEFUNG GEMACHT HAT UND NICHT ICH 1. UEBER DIE SEITE KAM GAR KEIN BILD MEHR AN. Beim Melden stand kein `Content-Type`. Das ging gut, solange der Rumpf eine einzelne Datei war -- ein `File` bringt seinen Typ mit. Ein `Blob` aus mehreren hat keinen, `fetch` schickt die Zeile dann gar nicht, `express.raw` fuehlt sich nicht zustaendig, und der Server bekam einen leeren Rumpf. Die Meldung waere durchgegangen, der Text angekommen, die Bilder weg -- ohne Fehlermeldung. Alle Pruefungen am Server waren dabei gruen; gefunden hat es erst der echte Browser. 2. DAS KREUZ DES DRITTEN BILDES LAG AUF DEM ZWEITEN. Der Entfernen-Knopf ist 44 px breit und absolut gesetzt, der Kasten aber nur so breit wie sein Bild. Bei einem schmalen Bild ragt er darueber hinaus -- wer „das zweite weg" antippt, loescht das dritte. `min-width`/`min-height` loesen das an der Ursache: Ein Kasten ist nie schmaler als der Knopf in ihm. WAS ICH FALSCH ANGENOMMEN HATTE: Ich hatte eingebaut, dass ein Nachtrag in derselben Runde die Bilder ersetzt. Die Pruefung dazu wurde rot -- zu Recht: Eine zweite Antwort in derselben Runde kann es nicht geben, die erste verlaesst den Stand „wartet". Der Code waere nie gelaufen und damit nie pruefbar gewesen. Er ist weg; an seiner Stelle steht der Beweis, dass er nicht fehlt. DREI WEITERE ROTE ZEILEN, DIE NICHT ZU DIESEM UMBAU GEHOERTEN * `manager-ziele.js` hatte einen ZWEITEN Notnagel (`frageNach ? … : confirm(…)`). `nachfrage.js` hat denselben laengst, und zwar mit dem vollstaendigen Text; der hiesige war der kuerzere und haette gewonnen. Zwei Antworten auf dieselbe Frage -- gemeldet von `pruef-nachfrage`. * Zwei Mittelpunkte in `reaktion.css` standen woertlich im `content`. Sie liegen im Latin-1-Block, wo `pruef-zeichen` die Truemmer einer verunglueckten Kodierung sucht. Jetzt als Escape -- im Browser nachgemessen, es steht Zeichen fuer Zeichen dasselbe da. * Das Aufraeumen nach 90 Tagen loeschte nur das EINE Bild der Meldung; die Bilder aus den Antwortrunden blieben ohne Zeile auf der Platte liegen. Die Liste kommt jetzt aus einer Abfrage statt aus einer Spalte und kann deshalb nicht wieder unvollstaendig sein. GEPRUEFT pruef-support 78 -> 104 ok darunter: der Umzug der alten Bilder auf einer eigenen Wegwerf-Datenbank -- zweimal und dreimal gestartet, nichts verdoppelt, Datum von damals erhalten pruef-support-bilder NEU, 36 ok (echter Browser) dreimal nacheinander waehlen ergibt drei, das vierte wird mit einem Satz abgelehnt, dasselbe zaehlt nicht doppelt, einzeln entfernen laesst die anderen stehen, alle drei laden wirklich (naturalWidth), Kreuze 44x44 und keines verdeckt (mit Gegenprobe per Deckel), nichts ragt auf 390 px heraus pruef-nachfrage 69 -> 74 ok pruef-struktur 102 ok, 413 Routen (vorher 414: zwei weg, eine neu) pruef-zeichen 7 ok (vorher 1 Fehler) pruef-aufbewahrung 45 ok pruef-manager-ziele 216 ok pruef-ports 10 ok · pruef-portnummern 41 ok (die neue Pruefdatei verschiebt die abgeleiteten Nummern) NUR DAS AGENTURHAUS IST BETROFFEN. Die Supportseite liegt unter `/workspace`; am Crew-Haus aendert sich keine Zeile. Co-Authored-By: Claude Opus 5 <[email protected]>
708 lines
30 KiB
JavaScript
708 lines
30 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;
|
||
/* Die gewaehlten Bilder. Sie leben hier und nicht im Aufruf:
|
||
`bauen()` haengt die Zuhoerer einmal an, und die muessen auch
|
||
beim naechsten Oeffnen noch dasselbe meinen.
|
||
|
||
EINE LISTE SEIT DEM 02.10.2026 (vorher eine einzelne Variable).
|
||
VanVan im Support, Meldung #11: „2-3 wären besser." `bildMax`
|
||
sagt, wie viele diese eine Frage zulaesst -- die Vorgabe 1 haelt
|
||
jeden anderen Aufruf im Haus bei genau dem Verhalten von vorher. */
|
||
var bildfeld = null, gewaehlteBilder = [], bildMax = 1;
|
||
|
||
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>'
|
||
/* ==== EIN BILD, WENN DIE FRAGE EINES BRAUCHT (02.10.2026) ====
|
||
|
||
VanVan (Support): „Wenn man hier im Support auf deine Frage
|
||
'geht es wieder' reagiert und antwortet, kann man auch kein
|
||
Bild hinzufügen."
|
||
|
||
Beim MELDEN gab es das längst, beim ANTWORTEN nicht -- und
|
||
gerade dort ist es am meisten wert: „geht noch nicht" mit
|
||
Bildschirmfoto sagt in einer Sekunde, was drei Sätze nicht
|
||
treffen.
|
||
|
||
Es steht hier und nicht in support.js, weil der Grund und
|
||
das Bild in DENSELBEN Kasten gehören. Zwei Dialoge
|
||
nacheinander hießen: Wer beim zweiten abbricht, hat den
|
||
ersten umsonst getippt -- genau die Begründung, aus der die
|
||
Zahl weiter oben hier gelandet ist.
|
||
|
||
VORGABE AUS. Ohne `bild` in den Angaben ändert sich für die
|
||
zehn anderen Aufrufer im Haus nichts. */
|
||
+ '<div class="nachfrage__zeile" data-teil="bildzeile" hidden>'
|
||
+ '<span class="feld-schild" data-teil="bildschild"></span>'
|
||
+ '<input id="nachfrage-bild" type="file" accept="image/*" multiple hidden />'
|
||
+ '<div class="nachfrage__bild">'
|
||
+ '<button type="button" class="knopf knopf--klein" data-teil="bildwaehlen">'
|
||
+ 'Bild wählen</button>'
|
||
+ '<span class="nachfrage__bildname" data-teil="bildname"></span>'
|
||
+ '<button type="button" class="abmelden" data-teil="bildweg" hidden>'
|
||
+ 'Entfernen</button>'
|
||
+ '</div>'
|
||
+ '<div class="nachfrage__vorschauen" data-teil="bildvorschauen" hidden></div>'
|
||
+ '</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');
|
||
bildfeld = dlg.querySelector('#nachfrage-bild');
|
||
fehlerZeile = dlg.querySelector('[data-teil="fehler"]');
|
||
|
||
/* DIE DREI ZUHOERER NUR EINMAL. `bauen()` laeuft genau einmal --
|
||
die uebrigen Zuhoerer dieses Dialogs haengen je Oeffnung dran
|
||
und werden danach wieder abgenommen. Hier ist beides moeglich;
|
||
einmal ist weniger, was schiefgehen kann. */
|
||
teil('bildwaehlen').addEventListener('click', function () { bildfeld.click(); });
|
||
teil('bildweg').addEventListener('click', function () {
|
||
bildfeld.value = '';
|
||
gewaehlteBilder = [];
|
||
bilderZeichnen();
|
||
});
|
||
bildfeld.addEventListener('change', function (e) {
|
||
bilderDazu(e.target.files ? Array.prototype.slice.call(e.target.files) : []);
|
||
/* DAS FELD WIRD GELEERT, die Liste nicht. Sonst meldet der
|
||
Browser beim zweiten Antippen DESSELBEN Bildes kein
|
||
`change` -- er sieht keine Aenderung -- und es saehe aus, als
|
||
waere der Knopf kaputt. */
|
||
e.target.value = '';
|
||
});
|
||
return dlg;
|
||
}
|
||
|
||
/** Neu Gewaehltes DAZULEGEN statt ersetzen.
|
||
*
|
||
* VanVan, Meldung #11, zweite Runde: „wenn man es nacheinander
|
||
* versucht hinzuzufügen wird das Bild immer nur ersetzt." Auch mit
|
||
* `multiple` ersetzt jede neue Auswahl die `files`-Liste komplett;
|
||
* auf dem Handy tippt man aber dreimal statt einmal. */
|
||
function bilderDazu(neue) {
|
||
var meldung = '';
|
||
/* ==== BEI GENAU EINEM PLATZ IST EINE NEUE WAHL EINE KORREKTUR ==
|
||
|
||
Zwei Faelle, eine Regel: Mehr als `bildMax` geht nie.
|
||
|
||
Ist `bildMax` 1, gibt es nichts, wozu man etwas DAZULEGEN
|
||
koennte -- wer dann noch einmal waehlt, meint „doch lieber
|
||
dieses". Ihn erst auf „Entfernen" zu schicken waere ein
|
||
Klick, der nichts klaert. Das ist genau das Verhalten, das
|
||
dieser Dialog seit dem 02.10. frueh hatte, und es bleibt.
|
||
|
||
Ab zwei Plaetzen wird gefuellt und das Ueberzaehlige
|
||
ABGELEHNT -- dort waere „ersetzen" der Fehler, den VanVan
|
||
gemeldet hat: Das zuerst Gewaehlte verschwaende unbemerkt. */
|
||
if (bildMax === 1 && neue.length) gewaehlteBilder = [];
|
||
for (var i = 0; i < neue.length; i++) {
|
||
var datei = neue[i];
|
||
if (gewaehlteBilder.length >= bildMax) {
|
||
meldung = bildMax === 1
|
||
? 'Hier geht nur ein Bild.'
|
||
: 'Mehr als ' + bildMax + ' Bilder gehen nicht.';
|
||
break;
|
||
}
|
||
/* NUR BILDER -- derselbe Satz wie beim Melden. Der Server sagt
|
||
es auch, aber erst nach dem Hochladen; hier steht es, bevor
|
||
jemand wartet. */
|
||
if (datei.type && datei.type.indexOf('image/') !== 0) {
|
||
meldung = 'Als Anhang geht ein Foto – PNG, JPEG, WebP oder GIF.';
|
||
continue;
|
||
}
|
||
var schon = gewaehlteBilder.some(function (d) {
|
||
return d.name === datei.name && d.size === datei.size
|
||
&& d.lastModified === datei.lastModified;
|
||
});
|
||
if (!schon) gewaehlteBilder.push(datei);
|
||
}
|
||
fehlerZeile.textContent = meldung;
|
||
bilderZeichnen();
|
||
}
|
||
|
||
/** Was gewaehlt ist -- auf dem Bildschirm.
|
||
*
|
||
* DIE VORSCHAU IST DER PUNKT. Wer sieht, was er anhaengt, haengt
|
||
* seltener das falsche Bild an; dieselbe Ueberlegung wie auf der
|
||
* Supportseite beim Melden.
|
||
*
|
||
* JEDES BILD HAT SEIN EIGENES KREUZ, seit es mehrere sein duerfen:
|
||
* Wer das dritte falsch erwischt hat, soll nicht alle drei neu
|
||
* waehlen muessen.
|
||
*
|
||
* DIE ADRESSEN WERDEN WIEDER FREIGEGEBEN -- sonst haelt jede
|
||
* Vorschau ihr Bild bis zum naechsten Laden der Seite im
|
||
* Arbeitsspeicher fest. */
|
||
function bilderZeichnen() {
|
||
var name = teil('bildname');
|
||
var weg = teil('bildweg');
|
||
var schau = teil('bildvorschauen');
|
||
|
||
var alte = schau.querySelectorAll('img[data-url]');
|
||
for (var a = 0; a < alte.length; a++) {
|
||
try { URL.revokeObjectURL(alte[a].dataset.url); } catch (e) { /* egal */ }
|
||
}
|
||
schau.textContent = '';
|
||
|
||
if (!gewaehlteBilder.length) {
|
||
name.textContent = 'Noch keins gewählt';
|
||
weg.hidden = true;
|
||
schau.hidden = true;
|
||
return;
|
||
}
|
||
name.textContent = gewaehlteBilder.length === 1
|
||
? gewaehlteBilder[0].name + ' · ' + Math.round(gewaehlteBilder[0].size / 1024) + ' KB'
|
||
: gewaehlteBilder.length + ' von ' + bildMax + ' Bildern';
|
||
weg.hidden = false;
|
||
|
||
gewaehlteBilder.forEach(function (datei, i) {
|
||
var kasten = document.createElement('div');
|
||
kasten.className = 'nachfrage__vorschau';
|
||
var url = URL.createObjectURL(datei);
|
||
var img = document.createElement('img');
|
||
img.dataset.url = url;
|
||
img.src = url;
|
||
img.alt = 'Vorschau von ' + datei.name;
|
||
kasten.appendChild(img);
|
||
var raus = document.createElement('button');
|
||
raus.type = 'button';
|
||
raus.className = 'nachfrage__vorschau-weg';
|
||
raus.textContent = '✕';
|
||
raus.title = datei.name + ' wieder entfernen';
|
||
raus.setAttribute('aria-label', datei.name + ' wieder entfernen');
|
||
raus.addEventListener('click', function () {
|
||
gewaehlteBilder.splice(i, 1);
|
||
bilderZeichnen();
|
||
});
|
||
kasten.appendChild(raus);
|
||
schau.appendChild(kasten);
|
||
});
|
||
schau.hidden = false;
|
||
}
|
||
|
||
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;
|
||
/* WIE LANG MUSS DER GRUND SEIN? (24.09.2026)
|
||
Hier stand eine feste 3 -- an ZWEI Stellen (Dialog und
|
||
Notausgang ueber window.prompt). Der Server verlangt beim
|
||
Entfernen eines fremden Beitrags aber 10 (GRUND_MIN, DSA
|
||
Art. 17). Wer vier Zeichen tippte, kam durch den Dialog und
|
||
bekam danach eine Absage vom Server -- mit einer Meldung, die
|
||
den Grund nicht nannte.
|
||
Jetzt sagt die aufrufende Stelle, wie lang es sein muss; die 3
|
||
bleibt Vorgabe, damit sich fuer alle anderen Nachfragen nichts
|
||
aendert. */
|
||
var grundMin = Number(o.grundMin) > 0 ? Number(o.grundMin) : 3;
|
||
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 || '';
|
||
/* BILDER DAZU -- die Beschriftung der Zeile, leer heisst: kein
|
||
Bildfeld. Nie Pflicht: Ein Bildschirmfoto hilft, aber wer
|
||
keines hat, soll trotzdem antworten koennen.
|
||
`bildMax` sagt, wie viele. Vorgabe 1 -- ohne diese Angabe
|
||
verhaelt sich der Dialog wie vor dem 02.10.2026. */
|
||
var bild = o.bild || '';
|
||
bildMax = Math.max(1, Number(o.bildMax) || 1);
|
||
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 < grundMin) 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);
|
||
}
|
||
/* IM NOTAUSGANG GIBT ES KEIN BILD, und das wird gesagt statt
|
||
verschwiegen: `window.prompt` kann keine Datei. Wer hier
|
||
landet, hat einen Browser ohne `<dialog>` -- er kann
|
||
antworten, nur eben ohne Anhang. Die LEERE LISTE sorgt
|
||
dafuer, dass die aufrufende Stelle nicht raten muss -- und
|
||
dass sie dieselbe Form bekommt wie im Normalfall, statt
|
||
einmal `null` und einmal ein Feld. */
|
||
if (bild) erg.bilder = [];
|
||
return Promise.resolve((grund || zahl || eingabe || bild) ? 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;
|
||
}
|
||
teil('bildzeile').hidden = !bild;
|
||
if (bild) {
|
||
teil('bildschild').textContent = bild;
|
||
bildfeld.value = '';
|
||
/* JEDES OEFFNEN FAENGT LEER AN. Ohne diese Zeile haengt beim
|
||
zweiten Mal noch das Bild vom ersten dran -- und niemand
|
||
sieht es, weil der Dialog oben schon zu war. */
|
||
gewaehlteBilder = [];
|
||
bilderZeichnen();
|
||
}
|
||
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 < grundMin) {
|
||
fehlerZeile.textContent = grundMin > 3
|
||
? 'Bitte etwas ausführlicher – mindestens ' + grundMin + ' Zeichen.'
|
||
: '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;
|
||
}
|
||
/* IMMER EINE LISTE, auch wenn sie leer ist -- und nicht
|
||
`undefined`. Die aufrufende Stelle schleift darueber und
|
||
muss weder wissen, ob das Feld angeboten wurde, noch den
|
||
Sonderfall „genau eines" kennen. */
|
||
if (bild) erg2.bilder = gewaehlteBilder.slice();
|
||
antwort = (grund || zahl || eingabe || bild) ? 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();
|
||
});
|
||
};
|
||
})();
|
||
|
||
/* =====================================================================
|
||
EINE EINGABE GEHT NICHT WORTLOS VERLOREN — window.verwurfWache()
|
||
|
||
Filipe, 19.09.2026: „Was passiert, wenn jemand eine Funktion
|
||
abbricht?"
|
||
|
||
---------------------------------------------------------------------
|
||
WAS GEMESSEN WURDE
|
||
|
||
Acht Dialoge im Workspace enthalten Eingabefelder -- „Aufgabe
|
||
bearbeiten" (10 Felder), der Tageseintrag in der Leistung (8), Ziele,
|
||
Netzwerk, Import, ein neuer Chat-Raum, ein Creator.
|
||
|
||
Bei allen gilt bis heute dasselbe: Esc drücken, daneben klicken oder
|
||
auf „Abbrechen" tippen wirft ALLES weg, wortlos. Wer zehn Felder
|
||
ausgefüllt hat und mit dem Daumen den Rand trifft, fängt von vorn an.
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM BEIDE WEGE ABGEFANGEN WERDEN MÜSSEN
|
||
|
||
Ein <dialog> geht auf ZWEI Arten zu, und sie laufen verschieden:
|
||
|
||
Esc / Klick daneben -> löst `cancel` aus, DANN schliesst der
|
||
Browser selbst. Die Methode `close()`
|
||
wird dabei NICHT aufgerufen.
|
||
„Abbrechen"-Knopf -> ruft `close()` auf, ohne `cancel`.
|
||
|
||
Wer nur `cancel` abfängt, lässt den Knopf durch. Wer nur `close`
|
||
ersetzt, lässt Esc durch. Beides einzeln wäre eine halbe Sicherung
|
||
-- und eine halbe Sicherung ist schlimmer als keine, weil man ihr
|
||
vertraut.
|
||
|
||
---------------------------------------------------------------------
|
||
UND SIE DARF NICHT IMMER FRAGEN
|
||
|
||
„Eine Warnung, die immer kommt, ist keine Warnung mehr." Gefragt
|
||
wird nur, wenn sich WIRKLICH etwas geändert hat: Beim Öffnen wird
|
||
ein Abbild der Felder genommen, beim Schliessen verglichen. Wer
|
||
einen Dialog aufmacht und gleich wieder zu, merkt nichts davon.
|
||
|
||
AUFRUF (einmal je Dialog, beim Aufbau der Seite):
|
||
verwurfWache(document.getElementById('bearbeiten-dialog'));
|
||
===================================================================== */
|
||
(function () {
|
||
'use strict';
|
||
|
||
/** Der Inhalt aller Felder als eine Zeichenkette. Reicht zum
|
||
* Vergleichen -- es geht nur um „gleich oder nicht". */
|
||
function abbild(dlg) {
|
||
return [...dlg.querySelectorAll('input, textarea, select')]
|
||
.map((f) => (f.type === 'checkbox' || f.type === 'radio')
|
||
? (f.checked ? '1' : '0')
|
||
: String(f.value ?? ''))
|
||
.join('\u0000');
|
||
}
|
||
|
||
window.verwurfWache = function verwurfWache(dlg, o) {
|
||
if (!dlg || dlg.dataset.wache === 'ja') return;
|
||
o = o || {};
|
||
dlg.dataset.wache = 'ja';
|
||
|
||
let beimOeffnen = null;
|
||
|
||
/* WANN GEHT ER AUF? Das weiss nur der Browser -- die Seite ruft
|
||
`showModal()` an zwanzig Stellen. Ein Beobachter auf dem
|
||
`open`-Merkmal braucht dafuer keine einzige Zeile in der Seite. */
|
||
if (window.MutationObserver) {
|
||
new MutationObserver(() => {
|
||
if (dlg.open) beimOeffnen = abbild(dlg);
|
||
}).observe(dlg, { attributes: true, attributeFilter: ['open'] });
|
||
}
|
||
if (dlg.open) beimOeffnen = abbild(dlg);
|
||
|
||
/* NOCH EINMAL IM NAECHSTEN BILD.
|
||
----------------------------------------------------------------
|
||
Nachgemessen am 20.09.2026: Alle acht Dialoge fuellen ihre
|
||
Felder VOR `showModal()`. Der Schnappschuss oben stimmt also.
|
||
|
||
Er stimmt aber nur, solange das so bleibt. Fuellt jemand morgen
|
||
ein Feld erst NACH dem Oeffnen, waere der Schnappschuss leer,
|
||
und die Wache fragte bei JEDEM Schliessen. „Eine Warnung, die
|
||
immer kommt, ist keine Warnung mehr."
|
||
|
||
ERSTER ANLAUF WAR EIN NEUER SCHNAPPSCHUSS BEIM HINEINKLICKEN
|
||
(`focusin`) -- und der hat die Wache still ausgeschaltet.
|
||
Gemessen: Der Schnappschuss entstand NACH dem Tippen, weil
|
||
Fokus und Werteingabe im selben Atemzug passieren koennen.
|
||
`beimOeffnen` war danach gleich dem getippten Text, `geaendert()`
|
||
falsch, und der Dialog ging wortlos zu -- die Sicherung sah
|
||
eingebaut aus und tat nichts.
|
||
|
||
Jetzt wird im NAECHSTEN BILD noch einmal abgebildet. Was das
|
||
Skript beim Oeffnen nachtraegt, ist dann drin; getippt haben
|
||
kann in derselben Sechzehntelsekunde niemand. */
|
||
const nachtragen = () => {
|
||
if (!dlg.open) return;
|
||
requestAnimationFrame(() => { if (dlg.open) beimOeffnen = abbild(dlg); });
|
||
};
|
||
nachtragen();
|
||
if (window.MutationObserver) {
|
||
new MutationObserver(nachtragen)
|
||
.observe(dlg, { attributes: true, attributeFilter: ['open'] });
|
||
}
|
||
|
||
const geaendert = () => beimOeffnen !== null && abbild(dlg) !== beimOeffnen;
|
||
|
||
function fragen(dann) {
|
||
frageNach({
|
||
titel: o.titel || 'Eingaben verwerfen?',
|
||
was: o.was || 'Was du hier eingetragen hast, geht verloren.',
|
||
bleibt: o.bleibt || 'Der Eintrag selbst bleibt unverändert – '
|
||
+ 'es wird nur nichts übernommen.',
|
||
ja: o.ja || 'Verwerfen',
|
||
nein: 'Weiter bearbeiten',
|
||
}).then((ja) => { if (ja) { beimOeffnen = null; dann(); } });
|
||
}
|
||
|
||
/* Weg 1: Esc und der Klick daneben. `cancel` ist abbrechbar --
|
||
das ist der einzige Griff, den der Browser dafuer anbietet. */
|
||
dlg.addEventListener('cancel', (e) => {
|
||
if (!geaendert()) return;
|
||
e.preventDefault();
|
||
fragen(() => dlg.close());
|
||
});
|
||
|
||
/* Weg 2: jeder Aufruf von close() -- also die „Abbrechen"-Knoepfe
|
||
und alles, was die Seite sonst tut. Ersetzt wird NUR an diesem
|
||
einen Dialog, nicht am Prototyp: Ein globaler Eingriff traefe
|
||
auch den Nachfrage-Dialog selbst, und der schlösse sich dann nie.
|
||
|
||
Bei unveraendertem Inhalt bleibt es synchron wie vorher -- wer
|
||
nichts getippt hat, merkt von der Wache nichts. */
|
||
const echtesSchliessen = dlg.close.bind(dlg);
|
||
dlg.close = function (...rest) {
|
||
if (!geaendert()) { beimOeffnen = null; return echtesSchliessen(...rest); }
|
||
fragen(() => echtesSchliessen(...rest));
|
||
return undefined;
|
||
};
|
||
|
||
/* Nach dem Speichern ist nichts mehr zu retten -- dann darf der
|
||
Dialog ohne Nachfrage zugehen. Die Seite meldet das, indem sie
|
||
`vergessen()` aufruft; ohne Meldung fragt die Wache weiter. */
|
||
dlg.vergessen = () => { beimOeffnen = abbild(dlg); };
|
||
};
|
||
|
||
/* =====================================================================
|
||
UND SIE WIRD NICHT VON HAND VERTEILT
|
||
|
||
Jeder Dialog mit Eingabefeldern bekommt die Wache -- abgeleitet,
|
||
nicht aufgezaehlt. Wer morgen einen neunten baut, hat sie, ohne
|
||
daran zu denken; und genau daran scheitern gepflegte Listen.
|
||
|
||
AUSGENOMMEN IST DER NACHFRAGE-DIALOG SELBST. Er hat ein Textfeld
|
||
(den Grund), und wuerde er sich selbst bewachen, fragte er beim
|
||
Schliessen nach sich selbst -- und zwar in sich selbst. Der
|
||
zweite Dialog kann nicht aufgehen, solange der erste offen ist:
|
||
Er haenge fuer immer.
|
||
===================================================================== */
|
||
function alleBewachen() {
|
||
for (const dlg of document.querySelectorAll('dialog:not(.nachfrage)')) {
|
||
if (!dlg.querySelector('input, textarea, select')) continue;
|
||
window.verwurfWache(dlg);
|
||
}
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', alleBewachen);
|
||
} else {
|
||
alleBewachen();
|
||
}
|
||
})();
|