Files
michaelschiemer/resources/js/core/state.js

214 lines
5.7 KiB
JavaScript

// 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);
});
}