const INDICATOR_ID = 'loading-indicator' const HIDDEN_CLASS = 'invisible' const GRACE_PERIOD_MS = 500 const DEBOUNCE_MS = 200 let debounceTimeout = null let historyRestoreGracePeriod = false let savedScrollPosition = null let activeRequests = 0 function getIndicator() { return document.getElementById(INDICATOR_ID) } function show() { const indicator = getIndicator() if (indicator) { indicator.classList.remove(HIDDEN_CLASS) } } function hide() { if (debounceTimeout) { clearTimeout(debounceTimeout) debounceTimeout = null } const indicator = getIndicator() if (indicator) { indicator.classList.add(HIDDEN_CLASS) } } function handleBeforeRequest(event) { // Ignore requests triggered during history restore grace period if (true === historyRestoreGracePeriod) { return } activeRequests++ // Save scroll position of the target container before HTMX request const target = event.detail?.target || document.getElementById('main-content') if (target && typeof target.scrollTop === 'number') { savedScrollPosition = { element: target, top: target.scrollTop } } // Don't start a new debounce if already pending if (debounceTimeout) { return } // Show indicator after delay debounceTimeout = setTimeout(() => { show() }, DEBOUNCE_MS) } function handleRequestEnd() { // Counter may already be 0 if a force-reset (history save, page show) fired // before this afterRequest — ignore the stale completion in that case if (0 === activeRequests) { return } activeRequests-- if (0 === activeRequests) { hide() } } function handleAfterSwap() { // Restore scroll position after HTMX swaps content if (savedScrollPosition !== null && savedScrollPosition.element) { savedScrollPosition.element.scrollTop = savedScrollPosition.top savedScrollPosition = null } } function handleHistoryRestore() { activeRequests = 0 hide() // Set grace period to ignore change events triggered by history restore historyRestoreGracePeriod = true setTimeout(() => { historyRestoreGracePeriod = false }, GRACE_PERIOD_MS) } function handleTimeout() { // Don't decrement here — htmx:afterRequest always fires alongside htmx:timeout // and handles the counter. This handler only shows the alert. alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.') } function handleBeforeHistorySave() { // Hide indicator before HTMX saves the page to history cache activeRequests = 0 hide() } function handlePageShow() { activeRequests = 0 hide() } // Initialize event listeners document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest) document.body.addEventListener('htmx:afterRequest', handleRequestEnd) document.body.addEventListener('htmx:afterSwap', handleAfterSwap) document.body.addEventListener('htmx:timeout', handleTimeout) document.body.addEventListener('htmx:historyRestore', handleHistoryRestore) document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave) window.addEventListener('pageshow', handlePageShow) window.addEventListener('popstate', handleHistoryRestore)