/* Finanzmanager – Frontend-Hilfen. * * Bewusst schlank und ohne Framework. * * Bestätigungen hängen an
und nicht als onsubmit * im HTML: Die Content-Security-Policy erlaubt keine Inline-Skripte, ein * onsubmit-Attribut würde stillschweigend verworfen – und dann löschte der * Knopf ohne Rückfrage. */ (function () { 'use strict'; // Die Marke klappt die Seitenleiste auf Symbolbreite zusammen und wieder // auf. Der Zustand steht in einem Cookie und nicht im localStorage: So // rendert der Server die Leiste schon in der richtigen Breite, statt sie // bei jedem Seitenwechsel kurz breit aufblitzen zu lassen. const schalter = document.querySelector('[data-seitenleiste-schalter]'); const rahmen = document.querySelector('.rahmen'); if (schalter && rahmen) { schalter.addEventListener('click', function () { const eingeklappt = rahmen.classList.toggle('eingeklappt'); schalter.setAttribute('aria-expanded', eingeklappt ? 'false' : 'true'); schalter.title = eingeklappt ? 'Seitenleiste ausklappen' : 'Seitenleiste einklappen'; document.cookie = 'seitenleiste=' + (eingeklappt ? 'eingeklappt' : 'offen') + '; path=/; max-age=31536000; samesite=lax'; }); } // Formulare mit data-bestaetigen fragen vor dem Absenden nach. // In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein // anderer Handler das Ereignis unterwegs stoppt. document.addEventListener('submit', function (ereignis) { const formular = ereignis.target; if (!(formular instanceof HTMLFormElement)) { return; } const frage = formular.dataset.bestaetigen; if (frage && !window.confirm(frage)) { ereignis.preventDefault(); ereignis.stopPropagation(); } }, true); // Kategoriebaum im Budgetplaner auf- und zuklappen. Die Zeilen kennen ihre // Elternkategorie über data-eltern; von dort aus lässt sich der Teilbaum // einsammeln, ohne ihn im HTML zu verschachteln (in einer Tabelle ginge das // ohnehin nicht). document.querySelectorAll('.klappknopf').forEach(function (knopf) { knopf.setAttribute('aria-expanded', 'true'); knopf.addEventListener('click', function () { const wurzel = knopf.dataset.klappt; const offen = knopf.getAttribute('aria-expanded') !== 'false'; knopf.setAttribute('aria-expanded', offen ? 'false' : 'true'); const zeilen = Array.from(document.querySelectorAll('tr.kategoriezeile')); const betroffen = new Set([wurzel]); zeilen.forEach(function (zeile) { const eltern = zeile.dataset.eltern; if (eltern && betroffen.has(eltern)) { betroffen.add(zeile.dataset.kategorie); zeile.classList.toggle('zugeklappt', offen); } }); }); }); // Zahnrad im Budgetplaner: verteilt den eingegebenen Wert über die Zeile, // wahlweise auf das ganze oder nur auf das restliche Jahr. Gearbeitet wird // auf den Eingabefeldern, nicht auf der Datenbank – so ist das Ergebnis vor // dem Speichern zu sehen und geht mit demselben Knopf weg wie alles andere. // // Das Menü gibt es nur einmal und wandert in die angeklickte Zelle: Ein // eigenes Menü je Feld wären zwölf Stück pro Kategoriezeile, und ein frei // positioniertes bräuchte ein style-Attribut, das die // Content-Security-Policy verwirft. const planer = document.querySelector('table.budgetplaner'); if (planer) { const menue = document.createElement('div'); menue.className = 'zahnradmenue'; let feld = null; let zahnrad = null; function schliessen() { if (menue.parentNode) { menue.parentNode.removeChild(menue); } if (zahnrad) { zahnrad.setAttribute('aria-expanded', 'false'); } feld = null; zahnrad = null; } function uebertragen(abMonat) { const zeile = feld.closest('tr'); const wert = feld.value; zeile.querySelectorAll('input.betragsfeld').forEach(function (anderes) { if (Number(anderes.dataset.monat) >= abMonat) { anderes.value = wert; } }); schliessen(); } [ { text: 'Ganzes Jahr', ab: function () { return 1; } }, { text: 'Restliches Jahr', ab: function (monat) { return monat + 1; } } ].forEach(function (eintrag) { const knopf = document.createElement('button'); knopf.type = 'button'; knopf.className = 'menueeintrag'; knopf.textContent = eintrag.text; knopf.addEventListener('click', function () { if (feld) { uebertragen(eintrag.ab(Number(feld.dataset.monat))); } }); menue.appendChild(knopf); }); planer.addEventListener('click', function (ereignis) { const knopf = ereignis.target.closest('.zahnrad'); if (!knopf) { return; } const zelle = knopf.closest('td'); const warOffen = menue.parentNode === zelle; schliessen(); if (warOffen) { return; } feld = zelle.querySelector('input.betragsfeld'); zahnrad = knopf; knopf.setAttribute('aria-expanded', 'true'); zelle.appendChild(menue); menue.firstChild.focus(); }); // Klick daneben oder Escape schließt das Menü wieder. document.addEventListener('click', function (ereignis) { if (!(ereignis.target instanceof Element)) { return; } if (!ereignis.target.closest('.zahnrad, .zahnradmenue')) { schliessen(); } }); document.addEventListener('keydown', function (ereignis) { if (ereignis.key === 'Escape') { schliessen(); } }); } // Empfängerauswahl im Zahlungsverkehr: Die Adresse füllt Name, IBAN und BIC. // Geändert werden darf danach alles – die Auswahl ist eine Abkürzung, keine // Bindung. const adressauswahl = document.querySelector('[data-adressauswahl]'); if (adressauswahl) { adressauswahl.addEventListener('change', function () { const gewaehlt = adressauswahl.selectedOptions[0]; if (!gewaehlt || !gewaehlt.value) { return; } const felder = { empfaenger: gewaehlt.dataset.name, iban: gewaehlt.dataset.iban, bic: gewaehlt.dataset.bic }; Object.keys(felder).forEach(function (name) { const feld = document.getElementById(name); if (feld && felder[name]) { feld.value = felder[name]; } }); }); } // Kontenzuordnung einer Bankverbindung: Die Felder für ein neues Konto // stehen in einer eigenen Tabellenzeile und werden erst sichtbar, wenn // „neues Konto anlegen“ gewählt ist. Ausgeblendet wird hier und nicht im // HTML – ohne JavaScript sollen die Felder da sein, sonst ließe sich kein // Konto anlegen. document.querySelectorAll('select[data-neues-konto]').forEach(function (auswahl) { const zeile = document.getElementById(auswahl.dataset.neuesKonto); if (!zeile) { return; } const anpassen = function () { zeile.hidden = auswahl.value !== 'neu'; }; auswahl.addEventListener('change', anpassen); anpassen(); }); // Mehrfachauswahl in Listen: Das Ankreuzfeld in der Titelzeile schaltet alle // angezeigten Zeilen, der Knopf im zugehörigen Formular zählt die Auswahl // mit und bleibt ohne Auswahl gesperrt. Ohne JavaScript funktioniert alles // weiter – dann ist der Knopf offen und der Controller weist eine leere // Auswahl ab. document.querySelectorAll('.alle-waehlen').forEach(function (alle) { const formular = document.getElementById(alle.dataset.waehlt || ''); if (!formular) { return; } const kaestchen = Array.from( document.querySelectorAll('input[type="checkbox"][form="' + formular.id + '"]') ); const knopf = formular.querySelector('button[type="submit"]'); const beschriftung = knopf ? knopf.textContent : ''; const anpassen = function () { const anzahl = kaestchen.filter(function (k) { return k.checked; }).length; alle.checked = anzahl > 0 && anzahl === kaestchen.length; alle.indeterminate = anzahl > 0 && anzahl < kaestchen.length; if (knopf) { knopf.disabled = anzahl === 0; knopf.textContent = anzahl === 0 ? beschriftung : beschriftung + ' (' + anzahl + ')'; } // Die Rückfrage nennt die Anzahl; ihr Wortlaut steht in der Ansicht. const vorlage = anzahl === 1 ? formular.dataset.frageEine : formular.dataset.frageMehrere; if (vorlage) { formular.dataset.bestaetigen = vorlage.replace('{anzahl}', String(anzahl)); } }; alle.addEventListener('change', function () { kaestchen.forEach(function (k) { k.checked = alle.checked; }); anpassen(); }); kaestchen.forEach(function (k) { k.addEventListener('change', anpassen); }); anpassen(); }); // Formatwahl von Import und Export. Die Kachel in der Verwaltung führt zwei // Knöpfe; welches Dateiformat gemeint ist, fragt dieser Dialog ab und hängt // es dann an die Zieladresse. Die Karten liefert die Ansicht mit – hier // stehen nur die Ziele. // // Kennt der Browser nicht, bleibt der Knopf ein gewöhnlicher Link // und führt auf dieselbe Auswahl als eigene Seite. const formatdialog = document.getElementById('formatwahl'); if (formatdialog && typeof formatdialog.showModal === 'function') { const titel = formatdialog.querySelector('[data-formatdialog-titel]'); document.querySelectorAll('[data-formatwahl]').forEach(function (knopf) { knopf.addEventListener('click', function (ereignis) { ereignis.preventDefault(); const ziel = knopf.getAttribute('href'); if (titel) { titel.textContent = knopf.dataset.formatwahl + ': Format wählen'; } formatdialog.querySelectorAll('.formatkarte').forEach(function (karte) { karte.setAttribute('href', ziel + '?format=' + karte.dataset.format); }); formatdialog.showModal(); }); }); } // Meldungen ausblenden. Über eine Klasse, nicht über element.style: Die // Content-Security-Policy (style-src 'self') verwirft auch per JavaScript // gesetzte Inline-Styles, die Meldung bliebe sonst einfach stehen. const ausblenden = function (element) { element.classList.add('verblasst'); window.setTimeout(function () { element.remove(); }, 400); }; // Erfolgsmeldungen gehen von selbst; Fehler und Warnungen bleiben stehen, // bis sie weggeklickt werden. Sie liegen über der Ansicht – ohne diesen Weg // verdeckten sie die Werkzeugleiste bis zum nächsten Seitenwechsel. const einrichten = function (element) { element.title = 'Zum Ausblenden anklicken'; element.addEventListener('click', function () { ausblenden(element); }); if (element.classList.contains('meldung-erfolg')) { window.setTimeout(function () { ausblenden(element); }, 5000); } }; document.querySelectorAll('.meldung').forEach(einrichten); // Für Ansichten, die eine Meldung nachträglich zeigen, ohne die Seite neu // zu laden – etwa das Kontoblatt nach einer Schnellzuordnung // (kontoblatt.js). Den Meldungsbereich selbst gibt es im HTML nur, wenn // der Server ihn beim Laden der Seite mitgab; hier entsteht er bei Bedarf. window.FmMeldungen = { zeigen: function (text, typ) { let bereich = document.querySelector('.meldungen'); if (!bereich) { bereich = document.createElement('div'); bereich.className = 'meldungen'; document.body.appendChild(bereich); } const element = document.createElement('div'); element.className = 'meldung meldung-' + (typ || 'erfolg'); element.setAttribute('role', 'status'); element.textContent = text; bereich.appendChild(element); einrichten(element); } }; })();