Files
dogfather-universe/workspace/assets/js/nachfrage.js
T
DogFatherGitandClaude Opus 5 018d02c3b8 Vorlagen anpassen: Frist, dauerhaft und eine Anmerkung
VanVan im Support, Meldung #6, zweite Haelfte -- der Rest, der heute
frueh ausdruecklich offen stehen blieb: „Bei den Vorlagen laesst sich
weder die Frist anpassen noch ‚dauerhaft' einstellen, und eine
Anmerkung fehlt auch."

EIN ZWEITER KNOPF, NICHT EIN FENSTER FUER ALLE

„An alle" verteilt zwoelf Aufgaben mit einem Druck. Haette jedes
Uebernehmen jetzt ein Fenster geoeffnet, waere der haeufige Weg
langsamer geworden, um den seltenen moeglich zu machen. Neben
„Uebernehmen" steht deshalb „Anpassen …" -- an den Creator-Vorlagen
und am Katalog, aus derselben Funktion.

DAS FENSTER IST DAS DES HAUSES

`frageNach` kann seit heute ein DATUM und einen HAKEN, die Anmerkung
konnte es als `grund` schon. Ein eigener kleiner Dialog in
vorlagenbrett.js waere der zweite im Haus gewesen -- und der, in dem
beim naechsten Mal Esc, Fokusfalle oder der Abbruch fehlen. Beide
Felder sind standardmaessig aus; fuer die dreissig anderen Aufrufe
aendert sich nichts.

DREI ENTSCHEIDUNGEN, DIE NICHT NAHELIEGEND WAREN

 1. DIE ANMERKUNG WIRD EINE NOTIZ (`aufgaben_notizen`), kein Anhang
    an der Beschreibung. Sie traegt damit, von wem sie stammt, und
    die Aufgabe zeigt sie ohnehin an. In die Beschreibung geschrieben
    waere sie von der Vorlage nicht mehr zu unterscheiden -- und
    dieselbe Vorlage haette beim naechsten Mal einen anderen Text.

 2. EINE DAUERHAFTE AUFGABE BEKOMMT KEINE FRIST, auch wenn eine
    mitgeschickt wird. Sie waere ab dem naechsten Tag fuer immer
    ueberfaellig, und eine Warnung, die immer kommt, ist keine mehr.
    Dieselbe Regel steht seit Langem im Aenderungsweg; haette sie
    hier gefehlt, gaebe es zwei Antworten auf dieselbe Frage.
    Im Fenster wird das Datum deshalb GRAU, sobald der Haken sitzt --
    ein Datum, das dasteht und nicht gilt, ist schlimmer als keins.

 3. „DAUERHAFT" DARF NUR, WER VERTEILEN DARF. Eine dauerhafte Aufgabe
    laesst sich nicht abhaken (Commit von heute frueh); wer sie sich
    selbst anlegen koennte, haette etwas, das er nie wieder loswird.
    Der Haken fehlt deshalb im Fenster eines Creators -- und
    abgelehnt wird trotzdem am Server, nicht nur ausgeblendet.

EIN FUND, DEN DIE PRUEFUNG GEMACHT HAT

`Number(tage) || 7` -- die Untergrenze des Datumsfeldes lag dadurch
sieben Tage in der Zukunft statt heute, weil Null in JavaScript
unwahr ist. Die VORGABE („in 1 Tag") lag damit UNTER der erlaubten
Grenze: Wer das Fenster oeffnete und einfach „Uebernehmen" drueckte,
bekam eine Absage. `Number.isFinite` fragt, ob eine Zahl da ist, und
nicht, ob sie wahr ist -- derselbe Unterschied wie bei `kill -0`.

NEBENBEI BEHOBEN: Ein gerades Anfuehrungszeichen in einem deutschen
Fehlersatz (gemeldet von pruef-struktur).

GEPRUEFT

  pruef-aufgaben-vorlagen   46 -> 60 ok
    eigene Frist kommt an (und die Vorlage saehe 5 Tage vor -- die
    Angabe hat also wirklich gewirkt), Anmerkung wird woertlich zur
    Notiz mit Namen, ohne Angabe bleibt alles wie bisher, Scout 403
    und es entsteht auch nichts, DogFather 200 und KEINE Frist,
    „morgen" 400, der 45.13. 400, Vergangenheit 400, und als
    Gegenprobe dieselbe Vorlage ohne Frist 200.
    Im Browser: der Knopf, das Fenster, die Vorgabe, kein Haken fuer
    einen Creator, die Aufgabe traegt danach genau die eingetragene
    Frist samt Notiz -- und Abbrechen legt nichts an.
  pruef-nachfrage           74 -> 89 ok
    Datum mit Vorgabe und Untergrenze, Haken 44 px hoch mit 22-px-
    Kaestchen (nicht ueber die ganze Zeile), Sperre und Gegenprobe,
    beide Schranken fuer ein zu fruehes Datum einzeln.
  pruef-struktur 102 · pruef-css-klassen · pruef-modi-katalog 150 ·
  pruef-aufgabenbrett · pruef-vorlagen 24 ·
  pruef-bewerbung-aufgaben 164 · pruef-zuteilung

NUR DAS AGENTURHAUS. Vorlagenbrett und Aufgaben liegen unter
`/workspace`; am Crew-Haus aendert sich keine Zeile.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-02 23:57:41 +02:00

819 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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;
var datumfeld = null, hakenfeld = null, hakenSperrtDatum = false;
/* 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 DATUM UND EIN HAKEN (02.10.2026) ==================
VanVan im Support, Meldung #6, zweite Haelfte: „Bei den
Vorlagen laesst sich weder die Frist anpassen noch
‚dauerhaft' einstellen, und eine Anmerkung fehlt auch."
DREI WUENSCHE, DREI FELDER -- und zwei davon gab es hier
noch nicht. Die Anmerkung ist `grund` (steht schon oben),
die Frist braucht ein Datum, „dauerhaft" einen Haken.
SIE STEHEN HIER UND NICHT IN vorlagenbrett.js. Ein eigener
kleiner Dialog dort waere der zweite im Haus, der dasselbe
tut -- und der, in dem beim naechsten Mal die Esc-Taste
fehlt, der Fokus nicht gefangen ist oder der Abbruch etwas
anderes zurueckgibt. Genau dafuer gibt es diese Datei.
VORGABE AUS: Ohne `datum` bzw. `haken` in den Angaben
aendert sich fuer keinen der dreissig anderen Aufrufe im
Haus irgendetwas. */
+ '<div class="nachfrage__zeile" data-teil="datumzeile" hidden>'
+ '<label class="feld-schild" data-teil="datumschild" for="nachfrage-datum"></label>'
+ '<input id="nachfrage-datum" type="date" />'
+ '</div>'
/* DER HAKEN STEHT MIT SEINER BESCHRIFTUNG IN EINER ZEILE --
anders als die Felder darueber, bei denen die Beschriftung
oben steht. Ein Kaestchen unter seinem Text sieht aus wie
ein verrutschtes Feld; daneben liest es sich als Satz. */
+ '<div class="nachfrage__zeile nachfrage__zeile--haken"'
+ ' data-teil="hakenzeile" hidden>'
+ '<label class="nachfrage__haken">'
+ '<input id="nachfrage-haken" type="checkbox" />'
+ '<span data-teil="hakenschild"></span>'
+ '</label>'
+ '<p class="nachfrage__hinweis" data-teil="hakenhinweis"></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');
datumfeld = dlg.querySelector('#nachfrage-datum');
hakenfeld = dlg.querySelector('#nachfrage-haken');
/* ==== EIN DATUM, DAS NICHT GILT, IST SCHLIMMER ALS KEINS ======
Gilt fuer die eine Kombination, in der sich die beiden Felder
widersprechen: Eine DAUERHAFTE Aufgabe hat keine Frist (sie
waere ab dem naechsten Tag fuer immer ueberfaellig -- und eine
Warnung, die immer kommt, ist keine mehr). Setzt jemand den
Haken, waere das Datum darueber eine Angabe, die der Server
verwirft, ohne es zu sagen.
Deshalb wird es GRAU und nicht etwa heimlich ignoriert. Dass
der Dialog diese Verbindung ueberhaupt kennt, kostet eine
Angabe (`hakenSperrtDatum`) -- und ist billiger als ein
zweiter Dialog an der einen Stelle, die sie braucht. */
hakenfeld.addEventListener('change', datumSperre);
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 datumSperre() {
if (!dlg) return;
var aus = hakenSperrtDatum && hakenfeld.checked;
datumfeld.disabled = aus;
teil('datumzeile').dataset.aus = aus ? 'ja' : '';
}
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);
/* EIN DATUM (Beschriftung, leer heisst: kein Datumsfeld) und ein
HAKEN. `datumWert` ist die Vorgabe -- sie steht schon drin,
damit niemand die uebliche Frist von Hand tippen muss; genau
das war VanVans Punkt, nicht „ein leeres Feld mehr". */
var datum = o.datum || '';
var datumWert = o.datumWert || '';
var datumMin = o.datumMin || '';
var haken = o.haken || '';
var hakenAn = !!o.hakenAn;
var hakenHinweis = o.hakenHinweis || '';
hakenSperrtDatum = !!o.hakenSperrtDatum;
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 = [];
/* IM NOTAUSGANG BLEIBEN DATUM UND HAKEN AUF DER VORGABE.
`window.prompt` kann kein Datum und keinen Haken; was man
hier zurueckgibt, muss trotzdem BRAUCHBAR sein -- also das,
was auch ohne Zutun gegolten haette. Ein `null` wuerde die
aufrufende Stelle zwingen, denselben Vorgabewert ein zweites
Mal zu kennen. */
if (datum) erg.datum = datumWert;
if (haken) erg.haken = hakenAn;
return Promise.resolve((grund || zahl || eingabe || bild || datum || haken)
? 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('datumzeile').hidden = !datum;
if (datum) {
teil('datumschild').textContent = datum;
datumfeld.value = datumWert;
if (datumMin) datumfeld.min = datumMin; else datumfeld.removeAttribute('min');
}
teil('hakenzeile').hidden = !haken;
if (haken) {
teil('hakenschild').textContent = haken;
teil('hakenhinweis').textContent = hakenHinweis;
hakenfeld.checked = hakenAn;
}
datumSperre();
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. */
/* DAS DATUM WIRD GEPRUEFT, BEVOR DER DIALOG ZUGEHT. Ein
leeres Feld ist zulaessig (dann gilt die Vorgabe des
Servers); ein Datum in der Vergangenheit nicht -- eine
Frist, die schon vorbei ist, macht die Aufgabe im selben
Moment rot, in dem sie entsteht. */
if (datum && !datumfeld.disabled && datumfeld.value && datumMin
&& datumfeld.value < datumMin) {
fehlerZeile.textContent = 'Dieses Datum liegt in der Vergangenheit.';
datumfeld.focus(); return;
}
/* GESPERRT HEISST LEER. Sonst schickte der Browser ein
Datum mit, das der Server wegwirft -- und beim naechsten
Lesen stuende in der Antwort etwas anderes als in der
Datenbank. */
if (datum) {
erg2.datum = (hakenSperrtDatum && hakenfeld.checked)
? '' : (datumfeld.value || datumWert);
}
if (haken) erg2.haken = hakenfeld.checked;
if (bild) erg2.bilder = gewaehlteBilder.slice();
antwort = (grund || zahl || eingabe || bild || datum || haken) ? 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();
}
})();