// src/core/state.js const globalState = new Map(); /** * Normale State-Erstellung (nicht persistent) */ export function createState(key, initialValue) { if (!globalState.has(key)) globalState.set(key, initialValue); return { get() { return globalState.get(key); }, set(value) { globalState.set(key, value); dispatchEvent(new CustomEvent('statechange', { detail: { key, value } })); }, subscribe(callback) { const handler = (event) => { if (event.detail.key === key) { callback(event.detail.value); } }; addEventListener('statechange', handler); return () => removeEventListener('statechange', handler); } }; } /** * Persistent State via localStorage */ export function createPersistentState(key, defaultValue) { const stored = localStorage.getItem(`state:${key}`); const initial = stored !== null ? JSON.parse(stored) : defaultValue; const state = createState(key, initial); state.subscribe((val) => { localStorage.setItem(`state:${key}`, JSON.stringify(val)); }); return state; } /** * Zugriff auf alle States (intern) */ export function getRawState() { return globalState; } /** * Elementbindung: Text, Attribut, Klasse oder Custom-Callback */ export function bindStateToElement({ state, element, property = 'text', attributeName = null }) { const apply = (value) => { if (property === 'text') { element.textContent = value; } else if (property === 'attr' && attributeName) { element.setAttribute(attributeName, value); } else if (property === 'class') { element.className = value; } else if (typeof property === 'function') { property(element, value); } }; apply(state.get()); return state.subscribe(apply); } /** * Mehrere Elemente gleichzeitig binden */ export function bindStateToElements({ state, elements, ...rest }) { return elements.map(el => bindStateToElement({ state, element: el, ...rest })); } /** * Declarative Auto-Bindings via data-bind Attribute */ export function initStateBindings() { document.querySelectorAll('[data-bind]').forEach(el => { const key = el.getAttribute('data-bind'); const state = createState(key, ''); bindStateToElement({ state, element: el, property: 'text' }); }); document.querySelectorAll('[data-bind-attr]').forEach(el => { const key = el.getAttribute('data-bind-attr'); const attr = el.getAttribute('data-bind-attr-name') || 'value'; const state = createState(key, ''); bindStateToElement({ state, element: el, property: 'attr', attributeName: attr }); }); document.querySelectorAll('[data-bind-class]').forEach(el => { const key = el.getAttribute('data-bind-class'); const state = createState(key, ''); bindStateToElement({ state, element: el, property: 'class' }); }); document.querySelectorAll('[data-bind-input]').forEach(el => { const key = el.getAttribute('data-bind-input'); const persistent = el.hasAttribute('data-persistent'); const state = persistent ? createPersistentState(key, '') : createState(key, ''); bindInputToState(el, state); }); } /** * Zwei-Wege-Bindung für Formulareingaben */ export function bindInputToState(inputEl, state) { // Initialwert setzen if (inputEl.type === 'checkbox') { inputEl.checked = !!state.get(); } else { inputEl.value = state.get(); } // DOM → State inputEl.addEventListener('input', () => { if (inputEl.type === 'checkbox') { state.set(inputEl.checked); } else { state.set(inputEl.value); } }); // State → DOM return state.subscribe((val) => { if (inputEl.type === 'checkbox') { inputEl.checked = !!val; } else { inputEl.value = val; } }); } /** * Berechneter Zustand auf Basis anderer States */ export function createComputedState(dependencies, computeFn) { let value = computeFn(...dependencies.map(d => d.get())); const key = `computed:${Math.random().toString(36).slice(2)}`; const state = createState(key, value); dependencies.forEach(dep => { dep.subscribe(() => { const newValue = computeFn(...dependencies.map(d => d.get())); state.set(newValue); }); }); return state; } /** * Aktiviert Undo/Redo für einen State */ export function enableUndoRedoForState(state) { const history = []; let index = -1; let lock = false; const record = (val) => { if (lock) return; history.splice(index + 1); // zukünftige States verwerfen history.push(val); index++; }; record(state.get()); const unsubscribe = state.subscribe((val) => { if (!lock) record(val); }); return { undo() { if (index > 0) { lock = true; index--; state.set(history[index]); lock = false; } }, redo() { if (index < history.length - 1) { lock = true; index++; state.set(history[index]); lock = false; } }, destroy: unsubscribe }; } /** * Dev-Konsole zur Nachverfolgung von State-Änderungen */ export function enableStateLogging(state, label = 'state') { state.subscribe((val) => { console.debug(`[State Change: ${label}]`, val); }); }