chore: complete update
This commit is contained in:
136
resources/js/core/ClickManager.js
Normal file
136
resources/js/core/ClickManager.js
Normal 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');
|
||||
}
|
||||
49
resources/js/core/EventManager.js
Normal file
49
resources/js/core/EventManager.js
Normal 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
|
||||
})));
|
||||
}
|
||||
};
|
||||
73
resources/js/core/PerformanceMonitor.js
Normal file
73
resources/js/core/PerformanceMonitor.js
Normal 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);
|
||||
}
|
||||
}
|
||||
46
resources/js/core/events.js
Normal file
46
resources/js/core/events.js
Normal 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 }));
|
||||
}
|
||||
83
resources/js/core/frameloop.js
Normal file
83
resources/js/core/frameloop.js
Normal 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
|
||||
}
|
||||
2
resources/js/core/index.js
Normal file
2
resources/js/core/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './logger.js';
|
||||
export * from './useEvent';
|
||||
125
resources/js/core/init.js
Normal file
125
resources/js/core/init.js
Normal 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');*/
|
||||
}
|
||||
277
resources/js/core/logger-next.js
Normal file
277
resources/js/core/logger-next.js
Normal 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();
|
||||
37
resources/js/core/logger.js
Normal file
37
resources/js/core/logger.js
Normal 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)
|
||||
|
||||
}
|
||||
}
|
||||
51
resources/js/core/navigateTo.js
Normal file
51
resources/js/core/navigateTo.js
Normal 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();
|
||||
}
|
||||
}
|
||||
14
resources/js/core/removeModules.js
Normal file
14
resources/js/core/removeModules.js
Normal 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
138
resources/js/core/router.js
Normal 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 };
|
||||
99
resources/js/core/secureLogger.js
Normal file
99
resources/js/core/secureLogger.js
Normal 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
213
resources/js/core/state.js
Normal 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);
|
||||
});
|
||||
}
|
||||
23
resources/js/core/useEvent.js
Normal file
23
resources/js/core/useEvent.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user