frontend: GUI-Zustand und Editortext im Browser speichern

Ohne Backend bleiben Einstellungen und Text ueber ein Neuladen erhalten
(localStorage). Gespeichert: Editortext (werkbaum-src) und GUI-Zustand
(werkbaum-ui: Modus, verworfene, Split-Zustand inkl. freier Drag-Groesse
--col/--drow, Zoom, Vollbild); Sprache bleibt in werkbaum-lang.

saveUI()/saveSrc() haengen an allen Zustandsaenderungen (Layout, Toggle,
Fenster-Buttons, Splitter-Drag, Zoom, Vollbild, Texteingabe);
restoreState() stellt beim Laden alles wieder her und unterdrueckt
waehrenddessen ueber restoring=true das Zurueckschreiben. Fehlender Text
faellt auf das Beispiel (INITIAL) zurueck, ein leerer bleibt leer.

Verifiziert (headless): Text, Modus, verworfene, Zoom, Vollbild,
Custom-Split (--col) und Minimier-Zustand ueberstehen ein Reload;
keine Konsolenfehler. Keine SPEC-Aenderung (Notation unberuehrt);
localStorage-Schluessel in frontend/CLAUDE.md dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-21 13:42:27 +02:00
co-authored by Claude Opus 4.8
parent 4031dba4bd
commit 7c39da7b44
2 changed files with 68 additions and 2 deletions
+7
View File
@@ -24,3 +24,10 @@ Diagramm rechts, Toggles für transponierte Ansicht und verworfene Elemente).
Status → URL → Größe → Tags (sonst kollidiert `@` in URLs). Status → URL → Größe → Tags (sonst kollidiert `@` in URLs).
- „verworfen" ist per Default ausgeblendet; Filterlogik steckt in - „verworfen" ist per Default ausgeblendet; Filterlogik steckt in
`visibleChildren()` und muss bei Renderer-Umbauten erhalten bleiben. `visibleChildren()` und muss bei Renderer-Umbauten erhalten bleiben.
- Zustand wird im `localStorage` gehalten (noch kein Backend): `werkbaum-lang`
(Sprache), `werkbaum-src` (Editortext), `werkbaum-ui` (JSON: Modus,
verworfene, Split-Zustand inkl. `--col`/`--drow`, Zoom, Vollbild). Neue
GUI-Einstellungen in `saveUI()`/`restoreState()` mitführen; `saveUI` liefert
während `restoring===true` nichts, damit das Wiederherstellen nicht sofort
zurückschreibt. Fehlender `werkbaum-src` fällt auf `INITIAL` zurück, ein
leerer String bleibt jedoch leer.
+61 -2
View File
@@ -900,10 +900,12 @@ src.addEventListener('keydown', e => {
src.value = value.slice(0, s) + ' ' + value.slice(eEnd); src.value = value.slice(0, s) + ' ' + value.slice(eEnd);
src.selectionStart = src.selectionEnd = s + 2; src.selectionStart = src.selectionEnd = s + 2;
render(); render();
saveSrc();
} }
}); });
src.addEventListener('input', render); src.addEventListener('input', render);
src.addEventListener('input', saveSrc);
const app = document.getElementById('app'); const app = document.getElementById('app');
function applyLayout(mode){ function applyLayout(mode){
@@ -939,6 +941,7 @@ function applySplit(){
diagramPanel.classList.toggle('collapsed', splitState==='a'); diagramPanel.classList.toggle('collapsed', splitState==='a');
editorPanel.classList.toggle('collapsed', splitState==='b'); editorPanel.classList.toggle('collapsed', splitState==='b');
document.querySelectorAll('.winbtn').forEach(b => b.classList.toggle('active', b.dataset.state===splitState)); document.querySelectorAll('.winbtn').forEach(b => b.classList.toggle('active', b.dataset.state===splitState));
saveUI();
} }
/* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */ /* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */
function snapTo(state){ if(splitState!==state){ splitState = state; applySplit(); } } function snapTo(state){ if(splitState!==state){ splitState = state; applySplit(); } }
@@ -1001,6 +1004,7 @@ function endDrag(e){
gutter.classList.remove('dragging'); gutter.classList.remove('dragging');
document.body.style.userSelect = ''; document.body.style.userSelect = '';
try{ gutter.releasePointerCapture(e.pointerId); }catch(_){} try{ gutter.releasePointerCapture(e.pointerId); }catch(_){}
saveUI(); /* freie Drag-Groesse (--col/--drow) sichern */
} }
gutter.addEventListener('pointerup', endDrag); gutter.addEventListener('pointerup', endDrag);
gutter.addEventListener('pointercancel', endDrag); gutter.addEventListener('pointercancel', endDrag);
@@ -1016,6 +1020,7 @@ function applyZoom(){
zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100)); zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100));
out.style.zoom = zoom; out.style.zoom = zoom;
document.getElementById('zoomReset').textContent = Math.round(zoom * 100) + ' %'; document.getElementById('zoomReset').textContent = Math.round(zoom * 100) + ' %';
saveUI();
} }
document.getElementById('zoomIn').addEventListener('click', () => { zoom += ZSTEP; applyZoom(); }); document.getElementById('zoomIn').addEventListener('click', () => { zoom += ZSTEP; applyZoom(); });
document.getElementById('zoomOut').addEventListener('click', () => { zoom -= ZSTEP; applyZoom(); }); document.getElementById('zoomOut').addEventListener('click', () => { zoom -= ZSTEP; applyZoom(); });
@@ -1029,6 +1034,7 @@ diagramEl.addEventListener('wheel', e => {
const showc = document.getElementById('showc'); const showc = document.getElementById('showc');
showc.addEventListener('change', render); showc.addEventListener('change', render);
showc.addEventListener('change', saveUI);
/* ---------- In die Zwischenablage kopieren ---------- */ /* ---------- In die Zwischenablage kopieren ---------- */
async function writeClipboard(text){ async function writeClipboard(text){
@@ -1422,10 +1428,63 @@ fsBtn.addEventListener('click', () => {
const on = document.body.classList.toggle('fullscreen'); const on = document.body.classList.toggle('fullscreen');
fsBtn.classList.toggle('active', on); fsBtn.classList.toggle('active', on);
fsBtn.setAttribute('aria-pressed', String(on)); fsBtn.setAttribute('aria-pressed', String(on));
saveUI();
}); });
applySplit(); /* Startzustand: normal, markiert die normal-Buttons aktiv */ /* ---------- GUI-Zustand + Editortext im Browser sichern ----------
src.value = INITIAL; Noch kein Backend: Einstellungen (Modus, verworfene, Aufteilung, Zoom,
Vollbild) und der Editortext bleiben per localStorage über Neuladen
erhalten. Sprache liegt weiterhin in 'werkbaum-lang'. */
const LS_UI = 'werkbaum-ui', LS_SRC = 'werkbaum-src';
let restoring = false; /* unterdrückt Speichern während des Wiederherstellens */
function saveSrc(){
if(restoring) return;
try{ localStorage.setItem(LS_SRC, src.value); }catch(_){}
}
function saveUI(){
if(restoring) return;
try{
const modeEl = document.querySelector('input[name="layout"]:checked');
localStorage.setItem(LS_UI, JSON.stringify({
mode: modeEl ? modeEl.value : 'horizontal',
discarded: showc.checked,
split: splitState,
col: app.style.getPropertyValue('--col') || null,
drow: app.style.getPropertyValue('--drow') || null,
zoom: zoom,
fullscreen: document.body.classList.contains('fullscreen')
}));
}catch(_){}
}
function restoreState(){
restoring = true;
let savedSrc = null, ui = null;
try{ savedSrc = localStorage.getItem(LS_SRC); }catch(_){}
try{ ui = JSON.parse(localStorage.getItem(LS_UI) || 'null'); }catch(_){}
src.value = (savedSrc !== null) ? savedSrc : INITIAL; /* leerer String bleibt leer */
const mode = (ui && ui.mode) || 'horizontal';
const modeEl = document.querySelector('input[name="layout"][value="' + mode + '"]');
if(modeEl) modeEl.checked = true;
if(ui){
if(typeof ui.discarded === 'boolean') showc.checked = ui.discarded;
if(typeof ui.zoom === 'number') zoom = ui.zoom;
if(ui.split) splitState = ui.split;
if(ui.fullscreen){
document.body.classList.add('fullscreen');
fsBtn.classList.add('active');
fsBtn.setAttribute('aria-pressed', 'true');
}
}
applyLayout(mode); /* setzt Ausrichtung + ruft applySplit() */
if(splitState === 'custom'){ /* freie Drag-Größen nach applySplit wieder setzen */
if(ui && ui.col) app.style.setProperty('--col', ui.col);
if(ui && ui.drow) app.style.setProperty('--drow', ui.drow);
}
applyZoom();
restoring = false;
}
restoreState(); /* Editortext + GUI-Zustand aus dem Browser wiederherstellen */
let startLang = 'de'; let startLang = 'de';
try{ startLang = localStorage.getItem('werkbaum-lang') || 'de'; }catch(_){} try{ startLang = localStorage.getItem('werkbaum-lang') || 'de'; }catch(_){}
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */ applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */