Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
241 lines
9.6 KiB
JavaScript
241 lines
9.6 KiB
JavaScript
/* =====================================================================
|
||
HOCHLADEN, DAS MAN SEHEN KANN — window.hochladen()
|
||
|
||
Filipe, 19.09.2026: „Ladezustände … Man soll auf die Seite kommen,
|
||
sofort verstehen, was man machen kann."
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM `fetch` HIER NICHT REICHT
|
||
|
||
Alle fünf Hochlade-Wege im Haus (Dateien, Chat-Anhang, Wissen-PDF,
|
||
Aufgaben-Anhang, Profilbild) benutzten `fetch`. Das funktioniert --
|
||
aber `fetch` kann beim SENDEN nicht sagen, wie weit es ist. Es gibt
|
||
`response.body` zum Lesen, nichts zum Schreiben.
|
||
|
||
Sichtbar war deshalb nur „wird hochgeladen …", und zwar von der
|
||
ersten bis zur letzten Sekunde gleich. Bei einer 40-MB-Datei im
|
||
Mobilfunknetz sind das zwei Minuten, in denen nichts darauf
|
||
hindeutet, dass überhaupt etwas passiert. Wer das sieht, drückt ein
|
||
zweites Mal -- und lädt dieselbe Datei doppelt hoch.
|
||
|
||
`XMLHttpRequest` ist älter und kann genau das eine, was hier fehlt:
|
||
`upload.onprogress`. Deshalb steht es hier, und nur hier.
|
||
|
||
---------------------------------------------------------------------
|
||
UND ABBRECHEN GEHÖRT DAZU
|
||
|
||
Ein Fortschrittsbalken ohne Abbruch ist eine Zumutung: Wer merkt,
|
||
dass er die falsche Datei erwischt hat, kann bis heute nur die Seite
|
||
neu laden -- und weiß dann nicht, ob die Datei trotzdem angekommen
|
||
ist. `xhr.abort()` bricht die Verbindung wirklich ab; was schon
|
||
gesendet wurde, verwirft der Server, weil er den Körper nie
|
||
vollständig bekommt.
|
||
|
||
---------------------------------------------------------------------
|
||
DREI AUSGÄNGE
|
||
|
||
fertig -> { ok: true, antwort, daten }
|
||
abgebrochen -> { ok: false, abgebrochen: true }
|
||
schiefgegangen -> { ok: false, status, daten } (mit Grund)
|
||
|
||
Der mittlere ist wichtig: Ein Abbruch ist KEIN Fehler und darf keine
|
||
rote Meldung erzeugen. Wer selbst abbricht, weiß, was er getan hat.
|
||
|
||
401 wird behandelt wie überall im Haus -- zurück zur Anmeldung.
|
||
|
||
---------------------------------------------------------------------
|
||
AUFRUF
|
||
|
||
const lauf = hochladen('/workspace/api/dateien', datei, {
|
||
kopf: { 'X-Dateiname': encodeURIComponent(datei.name) },
|
||
beiFortschritt: (anteil, gesendet, gesamt) => { ... },
|
||
});
|
||
abbruchKnopf.onclick = () => lauf.abbrechen();
|
||
const ergebnis = await lauf.fertig; // NICHT `e` -- in einem
|
||
// Ereignis-Handler wuerde das den Parameter verdecken, und
|
||
// `e.target.value` zeigte danach auf das Hochlade-Ergebnis.
|
||
===================================================================== */
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
/** Bytes als Text, den ein Mensch liest. Dieselbe Rechnung wie in
|
||
* `dateien.js` -- damit nicht an zwei Stellen zwei Zahlen stehen. */
|
||
function groesse(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
||
return (bytes / 1024 / 1024).toFixed(1).replace('.', ',') + ' MB';
|
||
}
|
||
window.groesseInWorten = groesse;
|
||
|
||
window.hochladen = function hochladen(pfad, daten, o) {
|
||
o = o || {};
|
||
const xhr = new XMLHttpRequest();
|
||
let abgebrochen = false;
|
||
|
||
const fertig = new Promise((liefere) => {
|
||
xhr.open(o.art || 'POST', pfad, true);
|
||
xhr.withCredentials = true;
|
||
for (const [k, v] of Object.entries(o.kopf || {})) xhr.setRequestHeader(k, v);
|
||
|
||
/* DER FORTSCHRITT. `lengthComputable` ist nicht selbstverständlich:
|
||
Bei einem Strom ohne bekannte Länge kann der Browser nichts
|
||
ausrechnen. Dann bleibt der Anteil `null` -- und der Aufrufer
|
||
zeigt einen laufenden Balken statt einer falschen Zahl. Eine
|
||
erfundene Prozentzahl wäre schlimmer als gar keine. */
|
||
xhr.upload.addEventListener('progress', (e) => {
|
||
if (!o.beiFortschritt) return;
|
||
const anteil = e.lengthComputable && e.total > 0 ? e.loaded / e.total : null;
|
||
o.beiFortschritt(anteil, e.loaded, e.total);
|
||
});
|
||
|
||
/* Wenn alles gesendet ist, der Server aber noch rechnet (bei einem
|
||
PDF wird geprüft, bei einem Bild verkleinert), steht der Balken
|
||
auf 100 % und es passiert scheinbar nichts mehr. Deshalb sagt
|
||
der Aufrufer ab hier "wird verarbeitet". */
|
||
xhr.upload.addEventListener('load', () => {
|
||
if (o.beiVerarbeitung) o.beiVerarbeitung();
|
||
});
|
||
|
||
xhr.addEventListener('load', () => {
|
||
if (xhr.status === 401) { location.assign('/workspace/'); return; }
|
||
let inhalt = null;
|
||
try { inhalt = JSON.parse(xhr.responseText); } catch { /* kein JSON */ }
|
||
liefere(xhr.status >= 200 && xhr.status < 300
|
||
? { ok: true, status: xhr.status, daten: inhalt }
|
||
: { ok: false, status: xhr.status, daten: inhalt });
|
||
});
|
||
|
||
/* ABBRUCH IST KEIN FEHLER. Ohne diese Unterscheidung bekäme der
|
||
Nutzer nach seinem eigenen Klick eine rote Meldung. */
|
||
xhr.addEventListener('abort', () => {
|
||
liefere({ ok: false, abgebrochen: true });
|
||
});
|
||
xhr.addEventListener('error', () => {
|
||
liefere({ ok: false, status: 0, netz: true });
|
||
});
|
||
xhr.addEventListener('timeout', () => {
|
||
liefere({ ok: false, status: 0, zeit: true });
|
||
});
|
||
|
||
/* Keine Frist von Hand: Eine große Datei in einem langsamen Netz
|
||
braucht ihre Zeit, und eine feste Zahl wäre wieder "eine
|
||
Rechnung von gestern". Der Browser bricht von selbst ab, wenn
|
||
die Verbindung wirklich weg ist -- das meldet `error`. */
|
||
xhr.send(daten);
|
||
});
|
||
|
||
return {
|
||
fertig,
|
||
abbrechen() {
|
||
if (abgebrochen) return;
|
||
abgebrochen = true;
|
||
try { xhr.abort(); } catch { /* war schon fertig */ }
|
||
},
|
||
get laeuft() { return xhr.readyState > 0 && xhr.readyState < 4; },
|
||
};
|
||
};
|
||
})();
|
||
|
||
/* =====================================================================
|
||
UND DIE ANZEIGE DAZU — window.hochladeAnzeige()
|
||
|
||
Die Mechanik oben nützt nichts, solange jede der fünf Stellen ihren
|
||
eigenen Balken baut. Dann sieht Hochladen im Chat anders aus als in
|
||
den Dateien, und die Abbruchtaste sitzt woanders -- genau das, was
|
||
Filipe mit „weniger Chaos, mehr Struktur" meint.
|
||
|
||
Deshalb baut diese Funktion die Anzeige EINMAL, in ein vorhandenes
|
||
Element hinein, und gibt drei Handgriffe zurück.
|
||
|
||
DREI ZUSTÄNDE, weil es drei gibt:
|
||
senden -> Balken mit Prozent, Abbrechen möglich
|
||
verarbeiten -> Balken voll, kein Prozent mehr, kein Abbruch
|
||
(der Server prüft/verkleinert -- abbrechen würde
|
||
jetzt nur eine halbe Datei hinterlassen)
|
||
weg -> alles verschwindet
|
||
|
||
Der mittlere Zustand ist der, den man am leichtesten vergisst: Der
|
||
Balken steht auf 100 %, und für zwei, drei Sekunden passiert nichts.
|
||
Ohne ein Wort dafür sieht das aus wie ein Hänger.
|
||
===================================================================== */
|
||
(function () {
|
||
'use strict';
|
||
|
||
window.hochladeAnzeige = function hochladeAnzeige(ziel, dateiname, beiAbbruch) {
|
||
if (!ziel) return { fortschritt() {}, verarbeiten() {}, weg() {} };
|
||
ziel.hidden = false;
|
||
ziel.textContent = '';
|
||
ziel.classList.add('hochlade');
|
||
|
||
const kopf = document.createElement('div');
|
||
kopf.className = 'hochlade__kopf';
|
||
const text = document.createElement('span');
|
||
text.className = 'hochlade__text';
|
||
text.textContent = `„${dateiname}“ wird hochgeladen …`;
|
||
kopf.append(text);
|
||
|
||
let abbruch = null;
|
||
if (beiAbbruch) {
|
||
abbruch = document.createElement('button');
|
||
abbruch.type = 'button';
|
||
abbruch.className = 'hochlade__abbruch';
|
||
abbruch.textContent = 'Abbrechen';
|
||
abbruch.addEventListener('click', () => {
|
||
abbruch.disabled = true;
|
||
text.textContent = 'wird abgebrochen …';
|
||
beiAbbruch();
|
||
});
|
||
kopf.append(abbruch);
|
||
}
|
||
|
||
/* Ein <progress> und kein eigenes <div>: Vorleseprogramme kennen
|
||
es, es sagt von selbst „Fortschritt 40 Prozent", und ohne
|
||
`value` zeigt es von allein den unbestimmten Lauf -- genau der
|
||
Fall, in dem der Browser die Gesamtgröße nicht kennt. */
|
||
const balken = document.createElement('progress');
|
||
balken.className = 'hochlade__balken';
|
||
balken.max = 1;
|
||
balken.removeAttribute('value');
|
||
|
||
ziel.append(kopf, balken);
|
||
|
||
return {
|
||
fortschritt(anteil, gesendet, gesamt) {
|
||
if (anteil === null || anteil === undefined) {
|
||
balken.removeAttribute('value');
|
||
text.textContent = `„${dateiname}“ wird hochgeladen …`;
|
||
return;
|
||
}
|
||
balken.value = anteil;
|
||
const g = window.groesseInWorten;
|
||
text.textContent = `„${dateiname}“ – ${Math.round(anteil * 100)} %`
|
||
+ (gesamt ? ` von ${g(gesamt)}` : '');
|
||
},
|
||
verarbeiten() {
|
||
balken.value = 1;
|
||
text.textContent = 'ist da – wird noch geprüft …';
|
||
if (abbruch) abbruch.remove();
|
||
},
|
||
weg() {
|
||
ziel.hidden = true;
|
||
ziel.textContent = '';
|
||
ziel.classList.remove('hochlade', 'hochlade--schluss');
|
||
},
|
||
/** Ein letzter, NEUTRALER Satz -- fuer den selbst ausgeloesten
|
||
* Abbruch. Er gehoert nicht in die rote Fehlerzeile: Wer selbst
|
||
* abbricht, hat keinen Fehler gemacht, und eine rote Meldung
|
||
* dafuer lehrt nur, rote Meldungen zu ignorieren.
|
||
* Verschwindet von allein -- ein Hinweis, den man wegklicken
|
||
* muss, ist ein Hindernis. */
|
||
abschluss(satz) {
|
||
balken.remove();
|
||
if (abbruch) abbruch.remove();
|
||
text.textContent = satz;
|
||
ziel.classList.add('hochlade--schluss');
|
||
setTimeout(() => { if (text.textContent === satz) this.weg(); }, 4000);
|
||
},
|
||
};
|
||
};
|
||
})();
|