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 versuche es erneut. Falls das Problem weiterhin besteht, kontaktiere 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)