chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,136 @@
// modules/core/click-manager.js
import { Logger } from './logger.js';
import { useEvent } from './useEvent.js';
import {navigateTo} from "./navigateTo";
import {SimpleCache} from "../utils/cache";
let callback = null;
let unsubscribes = [];
let cleanupInterval = null;
const prefetchCache = new SimpleCache(20, 60000); //new Map();
const maxCacheSize = 20; // max. Anzahl gecachter Seiten
const cacheTTL = 60000; // Lebensdauer in ms (60s)
function isInternal(link) {
return link.origin === location.origin;
}
function handleClick(e) {
const link = e.target.closest('a');
if (!link || e.defaultPrevented) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('#')) return;
// Skip conditions
if (
link.target === '_blank' ||
link.hasAttribute('download') ||
link.getAttribute('rel')?.includes('external') ||
link.hasAttribute('data-skip')
) {
Logger.info(`[click-manager] skipped: ${href}`);
return;
}
if (isInternal(link)) {
e.preventDefault();
const cached = prefetchCache.get(href);
const valid = cached && Date.now() - cached.timestamp < cacheTTL;
const options = {
viewTransition: link.hasAttribute('data-view-transition'),
replace: link.hasAttribute('data-replace'),
modal: link.hasAttribute('data-modal'),
prefetched: valid,
data: valid ? cached.data : null,
};
Logger.info(`[click-manager] internal: ${href}`, options);
if(options.modal) {
callback?.(href, link, options);
} else {
navigateTo(href, options);
}
}
}
let prefetchTimeout;
function handleMouseOver(e) {
clearTimeout(prefetchTimeout);
const link = e.target.closest('a[href]');
if (!link || !isInternal(link)) return;
const href = link.getAttribute('href');
if (!href || prefetchCache.has(href)) return;
// optional: Wait 150ms to reduce noise
prefetchTimeout = setTimeout(() => prefetch(href), 150);
}
function prefetch(href) {
Logger.info(`[click-manager] prefetching: ${href}`);
fetch(href)
.then(res => res.text())
.then(html => {
if (prefetchCache.cache.size >= maxCacheSize) {
const oldestKey = [...prefetchCache.cache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
prefetchCache.cache.delete(oldestKey);
}
prefetchCache.set(href, {
data: html,
timestamp: Date.now(),
});
})
.catch(err => {
Logger.warn(`[click-manager] prefetch failed for ${href}`, err);
});
}
function handlePopState() {
const href = location.pathname;
Logger.info(`[click-manager] popstate: ${href}`);
navigateTo(href, { replace: true });
}
export function getPrefetched(href) {
const cached = prefetchCache.get(href);
const valid = cached && Date.now() - cached.timestamp < cacheTTL;
return valid ? cached.data : null;
}
export function prefetchHref(href) {
if (!href || prefetchCache.has(href)) return;
prefetch(href);
}
export function init(onNavigate) {
callback = onNavigate;
unsubscribes = [
useEvent(document, 'click', handleClick),
useEvent(document, 'mouseover', handleMouseOver),
useEvent(window, 'popstate', handlePopState),
]
cleanupInterval = setInterval(() => {
for (const [key, val] of prefetchCache.cache.entries()) {
if (Date.now() - val.timestamp > prefetchCache.ttl) {
prefetchCache.cache.delete(key);
}
}
}, 120000);
Logger.info('[click-manager] ready');
}
export function destroy() {
callback = null;
prefetchCache.clear();
unsubscribes.forEach(unsub => unsub());
unsubscribes = [];
Logger.info('[click-manager] destroyed');
}

View File

@@ -0,0 +1,49 @@
// modules/core/EventManager.js
const registry = new Map();
export const EventManager = {
/**
* Fügt einen EventListener hinzu und speichert ihn im Modul-Kontext
*/
add(target, type, handler, { module = 'global', options = false } = {}) {
target.addEventListener(type, handler, options);
if (!registry.has(module)) registry.set(module, []);
registry.get(module).push([target, type, handler, options]);
},
/**
* Entfernt alle Listener, die für ein bestimmtes Modul registriert wurden
*/
removeModule(module) {
const entries = registry.get(module);
if (!entries) return;
entries.forEach(([target, type, handler, options]) => {
target.removeEventListener(type, handler, options);
});
registry.delete(module);
},
/**
* Entfernt alle Event-Listener aus allen Modulen
*/
clearAll() {
for (const [module, entries] of registry.entries()) {
entries.forEach(([target, type, handler, options]) => {
target.removeEventListener(type, handler, options);
});
}
registry.clear();
},
/**
* Debug: Gibt die aktuelle Registry in der Konsole aus
*/
debug() {
console.table([...registry.entries()].map(([module, events]) => ({
module,
listeners: events.length
})));
}
};

View File

@@ -0,0 +1,73 @@
// modules/core/PerformanceMonitor.js
export class PerformanceMonitor {
constructor({ fps = true } = {}) {
this.fpsEnabled = fps;
this.fps = 0;
this.frameCount = 0;
this.lastTime = performance.now();
this.visible = false;
this.taskTimings = new Map();
this.logs = [];
this.container = document.createElement('div');
this.container.style.position = 'fixed';
this.container.style.bottom = '0';
this.container.style.left = '0';
this.container.style.font = '12px Consolas, monospace';
this.container.style.color = '#0f0';
this.container.style.background = 'rgba(0,0,0,0.75)';
this.container.style.padding = '0.5rem';
this.container.style.zIndex = '9999';
this.container.style.pointerEvents = 'none';
this.container.style.lineHeight = '1.4';
this.container.style.whiteSpace = 'pre';
this.container.style.display = 'none';
document.body.appendChild(this.container);
window.addEventListener('keydown', (e) => {
if (e.key === '§') {
this.visible = !this.visible;
this.container.style.display = this.visible ? 'block' : 'none';
}
});
}
log(message) {
const timestamp = new Date().toLocaleTimeString();
this.logs.push(`[${timestamp}] ${message}`);
if (this.logs.length > 5) this.logs.shift();
}
update(taskMap = new Map()) {
this.frameCount++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastTime = now;
const timings = [];
for (const [id, duration] of this.taskTimings.entries()) {
timings.push(`${id}: ${duration.toFixed(2)}ms`);
}
const barWidth = Math.min(this.fps * 2, 100);
const logOutput = this.logs.slice().reverse().join('\n');
this.container.innerHTML = `
FPS: ${this.fps} | Tasks: ${taskMap.size}
${timings.join('\n')}
<div style="width:${barWidth}%;height:4px;background:#0f0;margin-top:4px;"></div>
${logOutput ? '\nLogs:\n' + logOutput : ''}
`;
}
}
trackTask(id, callback) {
const start = performance.now();
callback();
const duration = performance.now() - start;
this.taskTimings.set(id, duration);
}
}

View File

@@ -0,0 +1,46 @@
// src/core/events.js
// --- 1. Globaler EventBus ---
const listeners = new Map();
/**
* Abonniert ein benanntes Event
*/
export function on(eventName, callback) {
if (!listeners.has(eventName)) listeners.set(eventName, []);
listeners.get(eventName).push(callback);
// Unsubscribe
return () => {
const arr = listeners.get(eventName);
if (arr) listeners.set(eventName, arr.filter(fn => fn !== callback));
};
}
/**
* Sendet ein benanntes Event mit Payload
*/
export function emit(eventName, payload) {
const arr = listeners.get(eventName);
if (arr) arr.forEach(fn => fn(payload));
}
// --- 2. ActionDispatcher ---
const actionListeners = new Set();
/**
* Action registrieren (globaler Listener für alle Aktionen)
*/
export function registerActionListener(callback) {
actionListeners.add(callback);
return () => actionListeners.delete(callback);
}
/**
* Aktion ausführen
*/
export function dispatchAction(type, payload = {}) {
actionListeners.forEach(fn => fn({ type, payload }));
}

View File

@@ -0,0 +1,83 @@
// modules/core/frameloop.js
import {Logger} from "./logger";
const tasks = new Map();
let running = false;
let showDebug = false;
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
const debugOverlay = document.createElement('div');
debugOverlay.style.position = 'fixed';
debugOverlay.style.bottom = '0';
debugOverlay.style.left = '0';
debugOverlay.style.font = '12px monospace';
debugOverlay.style.color = '#0f0';
debugOverlay.style.background = 'rgba(0,0,0,0.75)';
debugOverlay.style.padding = '0.25rem 0.5rem';
debugOverlay.style.zIndex = '9999';
debugOverlay.style.pointerEvents = 'none';
debugOverlay.style.display = 'none';
const barWidth = Math.min(fps * 2, 100);
const bar = `<div style="width:${barWidth}%;height:4px;background:#0f0;margin-top:4px;"></div>`;
debugOverlay.innerHTML += bar;
debugOverlay.style.lineHeight = '1.4';
document.body.appendChild(debugOverlay);
import { PerformanceMonitor } from './PerformanceMonitor.js';
export const monitor = new PerformanceMonitor();
window.addEventListener('keydown', (e) => {
if (e.key === '§') {
showDebug = !showDebug;
debugOverlay.style.display = showDebug ? 'block' : 'none';
}
});
export function registerFrameTask(id, callback, options = {}) {
tasks.set(id, callback);
if (options.autoStart && !running) startFrameLoop();
}
export function unregisterFrameTask(id) {
tasks.delete(id);
}
export function clearFrameTasks() {
tasks.clear();
}
export function startFrameLoop() {
if (running) return;
running = true;
function loop() {
for (const [id, task] of tasks) {
try {
if (showDebug) {
monitor.trackTask(id, task);
} else {
task();
}
} catch (err) {
Logger.warn(`[Frameloop] Fehler in Task:`, err);
}
}
if (showDebug) {
monitor.update(tasks);
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
export function stopFrameLoop() {
running = false;
// Achtung: Loop läuft weiter, solange nicht aktiv gestoppt
}

View File

@@ -0,0 +1,2 @@
export * from './logger.js';
export * from './useEvent';

125
resources/js/core/init.js Normal file
View File

@@ -0,0 +1,125 @@
/*
const menu = document.getElementById("sidebar-menu");
const closeBtn = menu.querySelector(".close-btn");
closeBtn.addEventListener("click", () => {
menu.hidePopover();
});
*/
import {registerModules} from "../modules";
import {useEvent} from "./useEvent";
/*import { createTrigger, destroyTrigger, destroyAllTriggers } from './scrollfx/index.js';
createTrigger({
element: 'section',
target: '.fade',
start: 'top 80%',
end: 'bottom 30%',
scrub: true,
onUpdate: (() => {
const progressMap = new WeakMap();
return (el, progress) => {
if (!el) return;
let current = progressMap.get(el) || 0;
current += (progress - current) * 0.1;
progressMap.set(el, current);
el.style.opacity = current;
el.style.transform = `translateY(${30 - 30 * current}px)`;
};
})(),
onEnter: el => el.classList.add('entered'),
onLeave: el => {
el.classList.remove('entered');
el.style.opacity = 0;
el.style.transform = 'translateY(30px)';
}
});*/
/*
let lastScrollY = window.scrollY;
const fadeElements = document.querySelectorAll('.fade-in-on-scroll');
// Observer 1: Einblenden beim Runterscrollen
const fadeInObserver = new IntersectionObserver((entries) => {
const scrollingDown = window.scrollY > lastScrollY;
lastScrollY = window.scrollY;
entries.forEach(entry => {
if (entry.isIntersecting && scrollingDown) {
entry.target.classList.add('visible');
}
});
}, {
threshold: 0.4,
rootMargin: '0px 0px -10% 0px'
});
// Observer 2: Ausblenden beim Hochscrollen
const fadeOutObserver = new IntersectionObserver((entries) => {
const scrollingUp = window.scrollY < lastScrollY;
lastScrollY = window.scrollY;
entries.forEach(entry => {
if (!entry.isIntersecting && scrollingUp) {
entry.target.classList.remove('visible');
}
});
}, {
threshold: 0.5,
rootMargin: '0% 0px 50% 0px' // früher triggern beim Hochscrollen
});
// Alle Elemente mit beiden Observern beobachten
fadeElements.forEach(el => {
fadeInObserver.observe(el);
fadeOutObserver.observe(el);
});
*/
/*
const newContent = '<h1>Neue Seite</h1>'; // dein AJAX-Inhalt z.B.
const container = document.querySelector('main');
document.startViewTransition(() => {
container.innerHTML = newContent;
});*/
import { fadeScrollTrigger, zoomScrollTrigger, fixedZoomScrollTrigger } from '../modules/scrollfx/Tween.js';
import {autoLoadResponsiveVideos} from "../utils/autoLoadResponsiveVideo";
export async function initApp() {
await registerModules();
autoLoadResponsiveVideos();
/*initNoiseToggle({
selector: '.noise-overlay',
toggleKey: 'g', // Taste zum Umschalten
className: 'grainy', // Klasse auf <body>
enableTransition: true // Smooth fade
});
fadeScrollTrigger('.fade');
zoomScrollTrigger('.zoomed');
fixedZoomScrollTrigger('h1');*/
}

View File

@@ -0,0 +1,277 @@
/**
* Erweiterter Logger mit Processor-Architektur, Request-ID-Unterstützung und Server-Kommunikation.
*/
export class Logger {
/**
* Konfiguration des Loggers
*/
static config = {
enabled: true,
apiEndpoint: '/api/log',
consoleEnabled: true,
serverEnabled: true,
minLevel: 'debug',
};
/**
* Liste der registrierten Processors
*/
static processors = [];
/**
* Registrierte Handler
*/
static handlers = [];
/**
* Aktive RequestID
*/
static requestId = null;
/**
* Logger initialisieren
*/
static initialize(config = {}) {
// Konfiguration überschreiben
this.config = { ...this.config, ...config };
// Standard-Processors registrieren
this.registerProcessor(this.requestIdProcessor);
this.registerProcessor(this.timestampProcessor);
// Standard-Handler registrieren
if (this.config.consoleEnabled) {
this.registerHandler(this.consoleHandler);
}
if (this.config.serverEnabled) {
this.registerHandler(this.serverHandler);
}
// Request-ID aus dem Document laden, wenn vorhanden
if (typeof document !== 'undefined') {
this.initFromDocument();
}
// Unhandled Errors abfangen
this.setupErrorHandling();
}
/**
* Debug-Nachricht loggen
*/
static debug(...args) {
this.log('debug', ...args);
}
/**
* Info-Nachricht loggen
*/
static info(...args) {
this.log('info', ...args);
}
/**
* Warnungs-Nachricht loggen
*/
static warn(...args) {
this.log('warn', ...args);
}
/**
* Fehler-Nachricht loggen
*/
static error(...args) {
this.log('error', ...args);
}
/**
* Log-Nachricht mit beliebigem Level erstellen
*/
static log(level, ...args) {
if (!this.config.enabled) return;
// Level-Validierung
const validLevels = ['debug', 'info', 'warn', 'error'];
if (!validLevels.includes(level)) {
level = 'info';
}
// Nachricht und Kontext extrahieren
const message = this.formatMessage(args);
const context = args.find(arg => typeof arg === 'object' && arg !== null && !(arg instanceof Error)) || {};
// Exception extrahieren, falls vorhanden
const error = args.find(arg => arg instanceof Error);
if (error) {
context.exception = error;
}
// Log-Record erstellen
let record = {
level,
message,
context,
timestamp: new Date(),
extra: {},
};
// Alle Processors durchlaufen
this.processors.forEach(processor => {
record = processor(record);
});
// Log-Level-Prüfung (nach Processors, da sie das Level ändern könnten)
const levelPriority = {
debug: 100,
info: 200,
warn: 300,
error: 400,
};
if (levelPriority[record.level] < levelPriority[this.config.minLevel]) {
return;
}
// Alle Handler durchlaufen
this.handlers.forEach(handler => {
handler(record);
});
}
/**
* Nachricht aus verschiedenen Argumenten formatieren
*/
static formatMessage(args) {
return args
.filter(arg => !(arg instanceof Error) && (typeof arg !== 'object' || arg === null))
.map(arg => String(arg))
.join(' ');
}
/**
* Processor registrieren
*/
static registerProcessor(processor) {
if (typeof processor !== 'function') return;
this.processors.push(processor);
}
/**
* Handler registrieren
*/
static registerHandler(handler) {
if (typeof handler !== 'function') return;
this.handlers.push(handler);
}
/**
* Error-Handling-Setup
*/
static setupErrorHandling() {
if (typeof window !== 'undefined') {
// Unbehandelte Fehler abfangen
window.addEventListener('error', (event) => {
this.error('Unbehandelter Fehler:', event.error || event.message);
});
// Unbehandelte Promise-Rejects abfangen
window.addEventListener('unhandledrejection', (event) => {
this.error('Unbehandelte Promise-Ablehnung:', event.reason);
});
}
}
/**
* Request-ID aus dem Document laden
*/
static initFromDocument() {
const meta = document.querySelector('meta[name="request-id"]');
if (meta) {
const fullRequestId = meta.getAttribute('content');
// Nur den ID-Teil ohne Signatur verwenden
this.requestId = fullRequestId.split('.')[0] || null;
}
}
/*** STANDARD-PROCESSORS ***/
/**
* Processor für Request-ID
*/
static requestIdProcessor(record) {
if (Logger.requestId) {
record.extra.request_id = Logger.requestId;
}
return record;
}
/**
* Processor für Timestamp-Formatierung
*/
static timestampProcessor(record) {
record.formattedTimestamp = record.timestamp.toLocaleTimeString('de-DE');
return record;
}
/*** STANDARD-HANDLERS ***/
/**
* Handler für Console-Ausgabe
*/
static consoleHandler(record) {
const levelColors = {
debug: 'color: gray',
info: 'color: green',
warn: 'color: orange',
error: 'color: red',
};
const color = levelColors[record.level] || 'color: black';
const requestIdStr = record.extra.request_id ? `[${record.extra.request_id}] ` : '';
const formattedMessage = `[${record.formattedTimestamp}] [${record.level.toUpperCase()}] ${requestIdStr}${record.message}`;
// Farbige Ausgabe in der Konsole
console[record.level](
`%c${formattedMessage}`,
color,
...(record.context ? [record.context] : [])
);
}
/**
* Handler für Server-Kommunikation
*/
static serverHandler(record) {
fetch(Logger.config.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-ID': Logger.requestId || ''
},
body: JSON.stringify({
level: record.level,
message: record.message,
context: record.context || {}
})
})
.then(response => {
// Request-ID aus dem Header extrahieren
const requestId = response.headers.get('X-Request-ID');
if (requestId) {
// Nur den ID-Teil ohne Signatur speichern
const idPart = requestId.split('.')[0];
if (idPart) {
Logger.requestId = idPart;
}
}
return response.json();
})
.catch(() => {
// Fehler beim Senden des Logs ignorieren (keine rekursive Fehlerbehandlung)
});
}
}
// Standard-Initialisierung
Logger.initialize();

View File

@@ -0,0 +1,37 @@
import {monitor} from "./frameloop";
export class Logger {
static enabled = true //import.meta.env.MODE !== 'production';
static log(...args) {
this._write('log', '[LOG]', args);
}
static warn(...args) {
this._write('warn', '[WARN]', args)
}
static info(...args) {
this._write('info', '[INFO]', args);
}
static error(...args) {
this._write('error', '[ERROR]', args);
}
static _write(consoleMethod, prefix, args) {
if(!this.enabled) return;
const date = new Date();
const timestamp = date.toLocaleTimeString('de-DE');
const msg = `${prefix} [${timestamp}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
if(typeof console[consoleMethod] === 'function') {
console[consoleMethod](msg);
}
monitor?.log(msg)
}
}

View File

@@ -0,0 +1,51 @@
// modules/core/navigateTo.js
import { getPrefetched } from './ClickManager.js';
/**
* Funktion für SPA-Navigation mit optionaler ViewTransition
* Diese Funktion kann als generische Utility überall eingebunden werden!
*
* @param {string} href - Die Ziel-URL
* @param {object} options - Steueroptionen
* @param {boolean} [options.replace=false] - history.replaceState statt push
* @param {boolean} [options.viewTransition=false] - ViewTransition API verwenden
* @param {Function} [options.onUpdate] Wird nach Laden des HTML aufgerufen (html)
* @param {Function} [options.getPrefetched] Funktion zum Abrufen gecachter Daten (optional)
*/
export async function navigateTo(href, options = {}) {
const {
replace = false,
viewTransition = false,
onUpdate = html => {},
getPrefetched = null
/*onUpdate = (html) => {
const container = document.querySelector('main');
if (container) container.innerHTML = html;
}*/
} = options;
const fetchHtml = async () => {
let html = '';
if(getPrefetched) {
html = getPrefetched(href) || '';
}
if(!html) {
html = await fetch(href).then(r => r.text());
}
onUpdate(html);
if(replace) {
history.replaceState(null, '', href);
} else {
history.pushState(null, '', href);
}
};
if (viewTransition && document.startViewTransition) {
document.startViewTransition(fetchHtml);
} else {
await fetchHtml();
}
}

View File

@@ -0,0 +1,14 @@
// modules/core/removeModules.js
import { EventManager } from './EventManager.js';
/**
* Entfernt alle bekannten Events zu einem Array von Modulen
* Optional: Logging oder Cleanup-Hooks ergänzbar
*/
export function removeModules(moduleList) {
if (!Array.isArray(moduleList)) return;
moduleList.forEach((module) => {
EventManager.removeModule(module);
});
}

138
resources/js/core/router.js Normal file
View File

@@ -0,0 +1,138 @@
// modules/core/router.js
import { init as initClickManager } from './ClickManager.js';
import { navigateTo } from './navigateTo.js';
const routes = new Map();
const wildcards = [];
const guards = new Map();
let currentRoute = null;
let layoutCallback = null;
let metaCallback = null;
/**
* Registriert eine neue Route mit optionalem Handler
* @param {string} path - Pfad der Route
* @param {Function} handler - Callback bei Treffer
*/
export function defineRoute(path, handler) {
if (path.includes('*')) {
wildcards.push({ pattern: new RegExp('^' + path.replace('*', '.*') + '$'), handler });
} else {
routes.set(path, handler);
}
}
/**
* Definiert einen Guard für eine Route
* @param {string} path - Pfad
* @param {Function} guard - Guard-Funktion (return false = block)
*/
export function guardRoute(path, guard) {
guards.set(path, guard);
}
/**
* Gibt die aktuell aktive Route zurück
*/
export function getRouteContext() {
return currentRoute;
}
/**
* Setzt eine Callback-Funktion für dynamische Layout-Switches
*/
export function onLayoutSwitch(fn) {
layoutCallback = fn;
}
/**
* Setzt eine Callback-Funktion für Meta-Daten (z.B. title, theme)
*/
export function onMetaUpdate(fn) {
metaCallback = fn;
}
function matchRoute(href) {
if (routes.has(href)) return routes.get(href);
for (const entry of wildcards) {
if (entry.pattern.test(href)) return entry.handler;
}
return null;
}
function runGuard(href) {
const guard = guards.get(href);
return guard ? guard(href) !== false : true;
}
function extractMetaFromHTML(html) {
const temp = document.createElement('div');
temp.innerHTML = html;
const metaTags = {};
temp.querySelectorAll('[data-meta]').forEach(el => {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-meta-')) {
const key = attr.name.replace('data-meta-', '');
metaTags[key] = attr.value;
}
}
})
//const title = temp.querySelector('[data-meta-title]')?.getAttribute('data-meta-title');
//const theme = temp.querySelector('[data-meta-theme]')?.getAttribute('data-meta-theme');
return { metaTags };
}
function animateLayoutSwitch(type) {
document.body.dataset.layout = type;
document.body.classList.add('layout-transition');
setTimeout(() => document.body.classList.remove('layout-transition'), 300);
}
/**
* Startet den Router
*/
export function startRouter() {
initClickManager((href, link, options) => {
if (!runGuard(href)) return;
if (options.modal) {
const handler = matchRoute(href);
currentRoute = { href, modal: true, link, options };
handler?.(currentRoute);
layoutCallback?.(currentRoute);
metaCallback?.(currentRoute);
} else {
navigateTo(href, {
...options,
onUpdate: (html) => {
const container = document.querySelector('main');
if (container) container.innerHTML = html;
const routeHandler = matchRoute(href);
currentRoute = { href, html, modal: false };
const meta = extractMetaFromHTML(html);
if (meta.title) document.title = meta.title;
if (meta.theme) document.documentElement.style.setProperty('--theme-color', meta.theme);
routeHandler?.(currentRoute);
layoutCallback?.(currentRoute);
metaCallback?.(currentRoute);
}
});
}
});
// Bei Seitenstart erste Route prüfen
window.addEventListener('DOMContentLoaded', () => {
const href = location.pathname;
const routeHandler = matchRoute(href);
currentRoute = { href, modal: false };
routeHandler?.(currentRoute);
layoutCallback?.(currentRoute);
metaCallback?.(currentRoute);
});
}
export { animateLayoutSwitch, extractMetaFromHTML };

View File

@@ -0,0 +1,99 @@
/**
* Erweiterter Logger mit Request-ID-Unterstützung und Server-Kommunikation.
*/
export class SecureLogger {
static enabled = true;
static apiEndpoint = '/api/log';
static requestId = null;
static log(...args) {
this._write('log', args);
}
static warn(...args) {
this._write('warn', args)
}
static info(...args) {
this._write('info', args);
}
static error(...args) {
this._write('error', args);
}
static _write(level, args) {
if(!this.enabled) return;
const date = new Date();
const timestamp = date.toLocaleTimeString('de-DE');
const requestIdStr = this.requestId ? `[${this.requestId}] ` : '';
const message = args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ');
const formattedMessage = `[${timestamp}] ${requestIdStr}[${level.toUpperCase()}] ${message}`;
// Lokales Logging in der Konsole
console[level](formattedMessage);
// An den Server senden (wenn nicht in Produktion)
this._sendToServer(level, message, args.find(a => typeof a === 'object'));
}
static _sendToServer(level, message, context) {
fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-ID': this.requestId || ''
},
body: JSON.stringify({
level,
message,
context
})
})
.then(response => {
// Request-ID aus dem Header extrahieren
const requestId = response.headers.get('X-Request-ID');
if (requestId) {
// Nur den ID-Teil ohne Signatur speichern
const idPart = requestId.split('.')[0];
if (idPart) {
this.requestId = idPart;
}
}
return response.json();
})
.catch(err => {
console.error('Fehler beim Senden des Logs:', err);
});
}
/**
* Request-ID aus dem Document laden
*/
static initFromDocument() {
// Versuche die Request-ID aus einem Meta-Tag zu lesen
const meta = document.querySelector('meta[name="request-id"]');
if (meta) {
const fullRequestId = meta.getAttribute('content');
// Nur den ID-Teil ohne Signatur verwenden
this.requestId = fullRequestId.split('.')[0] || null;
}
}
}
// Request-ID initialisieren, wenn das DOM geladen ist
document.addEventListener('DOMContentLoaded', () => {
SecureLogger.initFromDocument();
// Abfangen aller unbehandelten Fehler und Logging
window.addEventListener('error', (event) => {
SecureLogger.error('Unbehandelter Fehler:', event.error || event.message);
});
// Abfangen aller unbehandelten Promise-Rejects
window.addEventListener('unhandledrejection', (event) => {
SecureLogger.error('Unbehandelte Promise-Ablehnung:', event.reason);
});
});

213
resources/js/core/state.js Normal file
View File

@@ -0,0 +1,213 @@
// 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);
});
}

View File

@@ -0,0 +1,23 @@
// modules/core/useEvent.js
import { EventManager } from './EventManager.js';
/**
* Vereinfachte Kurzform zur Registrierung eines EventListeners
*
* @param {EventTarget} target - Das Element, auf das der Event gehört
* @param {string} type - Der Eventtyp (z.B. 'click')
* @param {Function} handler - Die Callback-Funktion
* @param {object} meta - Optionen oder automatisch aus `import.meta` (optional)
* @param {Object|boolean} options - EventListener-Optionen
*/
export function useEvent(target, type, handler, meta = import.meta, options = false) {
const module = typeof meta === 'string'
? meta
: (meta.url?.split('/').slice(-2, -1)[0] || 'unknown');
EventManager.add(target, type, handler, { module, options });
return () => {
EventManager.removeModule(module);
}
}