/* ===================================================================== WAS GETIPPT UND NOCH NICHT GESPEICHERT IST, BLEIBT STEHEN (08.10.2026) Filipe: „wenn ich texte eingebe oder videos und kurz was anderes mache und es nicht gespeichert hab löscht es sich von selbst. es soll bleiben bis ich fertig bin und speichern drücke. oder aus der seite gehe aber nicht solange wie ich noch da bin." --------------------------------------------------------------------- WARUM ES SICH GELÖSCHT HAT Die Regie zeichnet sich bei jedem Ereignis neu — jemand kommt dazu, der Chat bekommt eine Zeile, das Video springt eine Sekunde weiter. Beim Neuzeichnen werden Listen von Grund auf gebaut (`kasten.textContent = ''`), und jedes Eingabefeld darin entsteht neu, gefüllt mit dem Wert vom Server. Was jemand hineingetippt und noch nicht gespeichert hatte, war damit weg — ohne Meldung, ohne Spur, oft mitten im Satz. Für die festen Felder gab es einen Notbehelf: `if (document.activeElement !== feld)` — also „überschreib es nicht, solange der Finger drinsteht". Das ist genau zu kurz gedacht, und Filipe beschreibt den Fall wörtlich: „und kurz was anderes mache". Wer tippt und dann woanders hinklickt, verliert den Fokus — und beim nächsten Takt auch seinen Text. --------------------------------------------------------------------- DIE FRAGE IST NICHT „HAT ES DEN FOKUS", SONDERN „IST ES SAUBER" Ein Feld ist SAUBER, solange darin genau das steht, was der Server zuletzt geliefert hat. Dann darf eine neue Auskunft es überschreiben — sie ist aktueller. Weicht es ab, hat ein Mensch etwas hineingetan, das noch nirgends steht; dann gewinnt der Mensch. Das ist dieselbe Unterscheidung, die eine Textverarbeitung macht, und sie hat zwei angenehme Nebenwirkungen: * Wer NICHTS angefasst hat, sieht Änderungen anderer sofort — VanVan trägt einen Titel ein, DogFather sieht ihn. * Wer etwas angefasst hat, verliert es nicht, auch wenn dieselbe Stelle gerade von jemand anderem geändert wurde. Er sieht dann beim Speichern, was passiert ist, statt es vorher wortlos zu verlieren. --------------------------------------------------------------------- UND BIS WANN? „bis ich fertig bin und speichern drücke. oder aus der seite gehe." Also: über jedes Neuzeichnen hinweg, aber NICHT über das Verlassen der Seite. Deshalb liegt das Gemerkte in einer Map im Speicher und NICHT in `localStorage`. Ein halber Satz, der drei Tage später wieder auftaucht, ist keine Hilfe — man weiß dann nicht mehr, ob er gelten soll, und löscht ihn zur Sicherheit. Was Filipe beschreibt, ist die Sitzung, nicht das Archiv. --------------------------------------------------------------------- SO WIRD ES BENUTZT Getippt.binde(feld, 'warte-wann-7', wertVomServer); -> füllt das Feld: mit dem Gemerkten, wenn etwas offen ist, sonst mit dem Serverwert. Hängt sich selbst an `input`. Getippt.gespeichert('warte-wann-7'); -> nach einem erfolgreichen Speichern. Ab da ist das Feld wieder sauber. Getippt.offeneZahl() -> wie viele Felder ungespeichert sind Getippt.istOffen(schl) -> ist genau dieses offen? DER SCHLÜSSEL MUSS STABIL SEIN und darf nicht die Position in der Liste enthalten. Steht er auf „Zeile 3" und jemand schiebt eine Zeile nach oben, landet der getippte Text in der falschen. Deshalb immer die Kennung des Gegenstands nehmen (`warte-wann-`), nie den Zähler der Schleife. ===================================================================== */ (() => { 'use strict'; /** Schlüssel -> der getippte, noch nicht gespeicherte Wert. */ const offen = new Map(); /* Welche Felder gerade im Dokument hängen. Wird gebraucht, damit `offeneZahl()` nicht Felder mitzählt, die es längst nicht mehr gibt — etwa eine Spendenzeile, die inzwischen erledigt ist. Eine Zahl, die Gespenster zählt, ist schlimmer als keine. */ const felder = new Map(); const alsText = (w) => (w === null || w === undefined ? '' : String(w)); function markiere(feld, istOffen) { feld.dataset.ungespeichert = istOffen ? 'ja' : 'nein'; } /** * Ein Eingabefeld an seinen Serverwert binden. * * @param {HTMLInputElement|HTMLTextAreaElement} feld * @param {string} schluessel stabil, ohne Listenposition * @param {string} vomServer was der Server zuletzt geliefert hat */ function binde(feld, schluessel, vomServer) { if (!feld || !schluessel) return; const server = alsText(vomServer); feld.dataset.schluessel = schluessel; feld.dataset.vomServer = server; felder.set(schluessel, feld); if (offen.has(schluessel)) { const getippt = offen.get(schluessel); /* ==== DER SELTENE, ABER WICHTIGE FALL ====================== Jemand tippt genau das, was inzwischen auch auf dem Server steht. Dann ist nichts mehr offen -- und es waere falsch, das Feld weiter als „ungespeichert" zu markieren: Der Mensch sucht sonst einen Knopf, der nichts mehr zu tun hat. */ if (getippt === server) { offen.delete(schluessel); feld.value = server; markiere(feld, false); } else { feld.value = getippt; markiere(feld, true); } } else { feld.value = server; markiere(feld, false); } /* `input` und nicht `change`: `change` kommt erst beim Verlassen des Feldes -- und genau dazwischen liegt das Neuzeichnen, das den Text verschluckt hat. Gemerkt wird bei jedem Zeichen. EINMAL JE FELD. Ein Feld, das bei jedem Neuzeichnen neu entsteht, bekommt den Zuhoerer ohnehin frisch; eines, das stehen bleibt (`p-titel`), bekaeme ihn sonst bei jedem Takt noch einmal -- nach einer Stunde waeren das hunderte. */ if (feld.dataset.getipptBereit !== 'ja') { feld.dataset.getipptBereit = 'ja'; feld.addEventListener('input', () => { const schl = feld.dataset.schluessel; if (!schl) return; if (feld.value === feld.dataset.vomServer) { offen.delete(schl); markiere(feld, false); } else { offen.set(schl, feld.value); markiere(feld, true); } melde(); }); } melde(); } /** * Nach dem Speichern: Das Feld ist wieder sauber. * * Endet der Schlüssel auf `*`, gilt es für alle, die so anfangen -- * gedacht für ein Formular, das mehrere Felder auf einmal sichert * (`Getippt.gespeichert('stufe-3-*')`). */ function gespeichert(schluessel) { if (!schluessel) return; if (schluessel.endsWith('*')) { const anfang = schluessel.slice(0, -1); for (const s of [...offen.keys()]) if (s.startsWith(anfang)) sauber(s); } else { sauber(schluessel); } melde(); } function sauber(schluessel) { offen.delete(schluessel); const feld = felder.get(schluessel); if (feld && feld.isConnected) { feld.dataset.vomServer = feld.value; markiere(feld, false); } } /** Alles vergessen -- etwa, wenn eine Sendung beendet wurde und die * ganze Tafel einen neuen Gegenstand hat. */ function vergissAlles() { offen.clear(); for (const feld of felder.values()) { if (feld && feld.isConnected) markiere(feld, false); } felder.clear(); melde(); } const istOffen = (schluessel) => offen.has(schluessel); /** Wie viele Felder gerade ungespeichert sind -- und zwar nur die, * die noch im Dokument stehen. */ function offeneZahl() { let n = 0; for (const s of offen.keys()) { const feld = felder.get(s); if (feld && feld.isConnected) n++; } return n; } /* Wer die Zahl anzeigen will, hoert hier zu, statt sie zu pollen. */ function melde() { document.dispatchEvent(new CustomEvent('getippt', { detail: { offen: offeneZahl() }, })); } window.Getippt = { binde, gespeichert, vergissAlles, istOffen, offeneZahl }; })();