/** * HTMX Non-Invasive Integration für MYP Platform * ============================================= * * Diese Implementierung stellt sicher, dass HTMX vollständig non-invasiv * geladen wird, ohne das Seitenladen oder bestehende JavaScript-Funktionalität * zu blockieren oder zu beeinträchtigen. * * Features: * - Lazy Loading von HTMX nur wenn benötigt * - Progressive Enhancement Pattern * - Keine Blockierung von kritischen Pfaden * - Graceful Fallback ohne HTMX * * Author: Till Tomczak - MYP Team * Zweck: Non-invasive HTMX Integration für dynamische UI-Updates */ (function() { 'use strict'; // Configuration const HTMX_CONFIG = { cdnUrl: 'https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js', localFallback: '/static/js/lib/htmx.min.js', loadTimeout: 5000, enableLogging: true, retryAttempts: 2 }; // State Management let htmxLoaded = false; let htmxLoading = false; let loadPromise = null; let pendingRequests = []; /** * Logger mit Namespace */ function log(level, message, data = null) { if (!HTMX_CONFIG.enableLogging) return; const prefix = '[HTMX-Integration]'; const styles = { info: 'color: #0073ce; font-weight: bold;', warn: 'color: #f59e0b; font-weight: bold;', error: 'color: #ef4444; font-weight: bold;', success: 'color: #10b981; font-weight: bold;' }; console[level === 'success' ? 'log' : level]( `%c${prefix} ${message}`, styles[level] || styles.info, data ); } /** * Prüft ob HTMX bereits verfügbar ist */ function isHtmxAvailable() { return typeof window.htmx !== 'undefined' && window.htmx; } /** * Lädt HTMX dynamisch und non-invasiv */ async function loadHtmx() { if (htmxLoaded || isHtmxAvailable()) { log('info', 'HTMX bereits verfügbar'); return Promise.resolve(); } if (htmxLoading) { log('info', 'HTMX wird bereits geladen, verwende existierendes Promise'); return loadPromise; } htmxLoading = true; log('info', 'Beginne HTMX-Laden...'); loadPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; script.defer = true; // Timeout Handler const timeout = setTimeout(() => { script.remove(); htmxLoading = false; reject(new Error('HTMX laden timeout')); }, HTMX_CONFIG.loadTimeout); // Success Handler script.onload = () => { clearTimeout(timeout); htmxLoaded = true; htmxLoading = false; log('success', 'HTMX erfolgreich geladen'); // HTMX konfigurieren configureHtmx(); // Pending Requests verarbeiten processPendingRequests(); resolve(); }; // Error Handler mit Fallback script.onerror = () => { clearTimeout(timeout); log('warn', 'CDN-Laden fehlgeschlagen, versuche lokalen Fallback...'); // Lokaler Fallback const fallbackScript = document.createElement('script'); fallbackScript.src = HTMX_CONFIG.localFallback; fallbackScript.async = true; fallbackScript.defer = true; fallbackScript.onload = () => { htmxLoaded = true; htmxLoading = false; log('success', 'HTMX über lokalen Fallback geladen'); configureHtmx(); processPendingRequests(); resolve(); }; fallbackScript.onerror = () => { htmxLoading = false; log('error', 'HTMX konnte nicht geladen werden (CDN + Fallback fehlgeschlagen)'); reject(new Error('HTMX nicht verfügbar')); }; document.head.appendChild(fallbackScript); script.remove(); }; // CDN laden script.src = HTMX_CONFIG.cdnUrl; document.head.appendChild(script); }); return loadPromise; } /** * Konfiguriert HTMX nach dem Laden */ function configureHtmx() { if (!isHtmxAvailable()) { log('warn', 'HTMX nicht verfügbar für Konfiguration'); return; } try { // HTMX Konfiguration htmx.config.selfRequestsOnly = true; htmx.config.allowEval = false; htmx.config.allowScriptTags = false; htmx.config.historyCacheSize = 10; htmx.config.timeout = 30000; htmx.config.withCredentials = true; htmx.config.defaultSwapStyle = 'outerHTML'; htmx.config.defaultSwapDelay = 0; htmx.config.defaultSettleDelay = 20; // CSRF Token Integration const csrfToken = document.querySelector('meta[name="csrf-token"]'); if (csrfToken) { htmx.config.requestClass = 'htmx-request'; // Event Listener für CSRF Token document.body.addEventListener('htmx:configRequest', function(evt) { const token = document.querySelector('meta[name="csrf-token"]'); if (token) { evt.detail.headers['X-CSRFToken'] = token.getAttribute('content'); } }); } // Error Handling document.body.addEventListener('htmx:responseError', function(evt) { log('error', 'HTMX Response Error', { status: evt.detail.xhr.status, response: evt.detail.xhr.responseText }); // Fallback zu Standard-Navigation bei schweren Fehlern if (evt.detail.xhr.status >= 500) { window.location.reload(); } }); // Success Handling document.body.addEventListener('htmx:afterRequest', function(evt) { log('info', 'HTMX Request completed', { method: evt.detail.requestConfig.verb, url: evt.detail.requestConfig.path, status: evt.detail.xhr.status }); }); // Loading States document.body.addEventListener('htmx:beforeRequest', function(evt) { addLoadingState(evt.detail.elt); }); document.body.addEventListener('htmx:afterRequest', function(evt) { removeLoadingState(evt.detail.elt); }); log('success', 'HTMX konfiguration abgeschlossen'); } catch (error) { log('error', 'Fehler bei HTMX-Konfiguration:', error); } } /** * Fügt Loading State zu Elementen hinzu */ function addLoadingState(element) { if (!element) return; element.classList.add('htmx-loading'); element.style.opacity = '0.7'; element.style.pointerEvents = 'none'; // Spinner hinzufügen wenn noch nicht vorhanden if (!element.querySelector('.htmx-spinner')) { const spinner = document.createElement('div'); spinner.className = 'htmx-spinner inline-block ml-2'; spinner.innerHTML = ''; element.appendChild(spinner); } } /** * Entfernt Loading State von Elementen */ function removeLoadingState(element) { if (!element) return; element.classList.remove('htmx-loading'); element.style.opacity = ''; element.style.pointerEvents = ''; // Spinner entfernen const spinner = element.querySelector('.htmx-spinner'); if (spinner) { spinner.remove(); } } /** * Verarbeitet wartende HTMX-Requests */ function processPendingRequests() { if (!isHtmxAvailable() || pendingRequests.length === 0) { return; } log('info', `Verarbeite ${pendingRequests.length} wartende HTMX-Requests`); pendingRequests.forEach(request => { try { request(); } catch (error) { log('error', 'Fehler beim Verarbeiten wartender Request:', error); } }); pendingRequests = []; } /** * Fügt HTMX-Attribute zu einem Element hinzu */ function enhanceElement(element, config) { if (!element || typeof config !== 'object') { log('warn', 'Ungültige Parameter für enhanceElement'); return; } const request = () => { Object.keys(config).forEach(attr => { const htmxAttr = `hx-${attr}`; element.setAttribute(htmxAttr, config[attr]); }); // HTMX Element verarbeiten lassen if (isHtmxAvailable()) { htmx.process(element); } }; if (isHtmxAvailable()) { request(); } else { pendingRequests.push(request); loadHtmx().catch(error => { log('error', 'HTMX-Laden für Element-Enhancement fehlgeschlagen:', error); }); } } /** * Erstellt ein HTMX-Enhanced Element */ function createEnhancedElement(tagName, config, content = '') { const element = document.createElement(tagName); element.innerHTML = content; enhanceElement(element, config); return element; } /** * Lazy Loading Trigger für HTMX */ function initializeLazyLoading() { // Intersection Observer für lazy loading const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const element = entry.target; if (element.hasAttribute('data-htmx-lazy')) { loadHtmx().then(() => { log('info', 'HTMX lazy-loaded für sichtbares Element'); }).catch(error => { log('error', 'HTMX lazy-loading fehlgeschlagen:', error); }); observer.unobserve(element); } } }); }, { threshold: 0.1 }); // Alle lazy-loading Elemente beobachten document.querySelectorAll('[data-htmx-lazy]').forEach(el => { observer.observe(el); }); } /** * Event-basiertes HTMX-Laden */ function initializeEventTriggers() { document.addEventListener('click', (event) => { const target = event.target.closest('[data-htmx-trigger]'); if (target) { event.preventDefault(); loadHtmx().then(() => { // Nach dem Laden HTMX-Attribute anwenden const config = JSON.parse(target.getAttribute('data-htmx-config') || '{}'); enhanceElement(target, config); // Original Click Event simulieren target.click(); }).catch(error => { log('error', 'HTMX-Laden über Event-Trigger fehlgeschlagen:', error); // Fallback zu Standard-Verhalten window.location.href = target.href || target.getAttribute('data-fallback-url'); }); } }); } /** * Auto-Detection für HTMX-Attribute */ function autoDetectHtmxElements() { const htmxAttributes = [ 'hx-get', 'hx-post', 'hx-put', 'hx-patch', 'hx-delete', 'hx-trigger', 'hx-target', 'hx-swap', 'hx-boost' ]; const hasHtmxElements = htmxAttributes.some(attr => document.querySelector(`[${attr}]`) ); if (hasHtmxElements) { log('info', 'HTMX-Elemente detectiert, lade HTMX...'); loadHtmx().catch(error => { log('error', 'Auto-Detection HTMX-Laden fehlgeschlagen:', error); }); } } /** * Public API */ window.MYP_HTMX = { load: loadHtmx, enhance: enhanceElement, create: createEnhancedElement, isLoaded: () => htmxLoaded, isAvailable: isHtmxAvailable, config: HTMX_CONFIG }; /** * Initialization */ function initialize() { log('info', 'Initialisiere HTMX Non-Invasive Integration...'); // Event Listeners registrieren if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initializeLazyLoading(); initializeEventTriggers(); autoDetectHtmxElements(); }); } else { initializeLazyLoading(); initializeEventTriggers(); autoDetectHtmxElements(); } log('success', 'HTMX Integration bereit'); } // Initialize immediately initialize(); })(); /** * CSS für HTMX Loading States */ const htmxStyles = ` .htmx-loading { position: relative; transition: opacity 0.2s ease; } .htmx-spinner { display: inline-block; animation: htmx-spin 1s linear infinite; } @keyframes htmx-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .htmx-indicator { opacity: 0; transition: opacity 200ms ease-in; } .htmx-request .htmx-indicator { opacity: 1; } .htmx-request.htmx-indicator { opacity: 1; } .htmx-swapping { opacity: 0; transition: opacity 200ms ease-out; } .htmx-settling { opacity: 1; transition: opacity 200ms ease-in; } `; // CSS dynamisch injizieren if (!document.getElementById('htmx-integration-styles')) { const style = document.createElement('style'); style.id = 'htmx-integration-styles'; style.textContent = htmxStyles; document.head.appendChild(style); }