/* ===================================================================== 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 und kein eigenes
: 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); }, }; }; })();