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