diff --git a/assets/app.js b/assets/app.js index c949347..6ced00b 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,4 +1,6 @@ import './bootstrap.js' +import './loading.js' + import './styles/app.css' import htmx from 'htmx.org' diff --git a/assets/controllers/loading_controller.js b/assets/controllers/loading_controller.js deleted file mode 100644 index d990500..0000000 --- a/assets/controllers/loading_controller.js +++ /dev/null @@ -1,99 +0,0 @@ -import {Controller} from '@hotwired/stimulus' - -export default class extends Controller { - - static targets = ['indicator'] - static classes = ['hidden'] - static values = { - visible: { - type: Boolean, - default: false - } - } - - connect() { - this.debounceTimeout = null - - // Bind event handlers to preserve context - this.boundHandleBeforeRequest = this.handleBeforeRequest.bind(this) - this.boundHandleAfterRequest = this.handleAfterRequest.bind(this) - this.boundHandleTimeout = this.handleTimeout.bind(this) - this.boundHandlePageShow = this.handlePageShow.bind(this) - this.boundHandleHistoryRestore = this.handleHistoryRestore.bind(this) - - // Listen to HTMX events - document.body.addEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest) - document.body.addEventListener('htmx:afterRequest', this.boundHandleAfterRequest) - document.body.addEventListener('htmx:timeout', this.boundHandleTimeout) - document.body.addEventListener('htmx:historyRestore', this.boundHandleHistoryRestore) - - // Listen for browser back/forward navigation - window.addEventListener('pageshow', this.boundHandlePageShow) - } - - disconnect() { - // Clean up event listeners - document.body.removeEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest) - document.body.removeEventListener('htmx:afterRequest', this.boundHandleAfterRequest) - document.body.removeEventListener('htmx:timeout', this.boundHandleTimeout) - document.body.removeEventListener('htmx:historyRestore', this.boundHandleHistoryRestore) - window.removeEventListener('pageshow', this.boundHandlePageShow) - - // Clear any pending timeout - if (this.debounceTimeout) { - clearTimeout(this.debounceTimeout) - } - } - - handleBeforeRequest() { - // Don't start a new debounce if already visible - if (true === this.visibleValue) { - return - } - - // Show indicator after 200ms delay for field refreshes - this.debounceTimeout = setTimeout(() => { - this.show() - }, 200) - } - - handleAfterRequest() { - // Clear debounce timer if request completes before 200ms - if (this.debounceTimeout) { - clearTimeout(this.debounceTimeout) - this.debounceTimeout = null - } - - this.hide() - } - - handleTimeout() { - // Hide loading indicator - this.hide() - - // Show user-friendly error message - alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.') - } - - handlePageShow(event) { - // event.persisted is true when page is restored from bfcache (back/forward navigation) - if (true === event.persisted) { - this.hide() - } - } - - handleHistoryRestore() { - // HTMX history restore - hide loading indicator - this.hide() - } - - show() { - this.visibleValue = true - this.indicatorTarget.classList.remove(this.hiddenClass) - } - - hide() { - this.visibleValue = false - this.indicatorTarget.classList.add(this.hiddenClass) - } -} diff --git a/assets/loading.js b/assets/loading.js new file mode 100644 index 0000000..c72b7fc --- /dev/null +++ b/assets/loading.js @@ -0,0 +1,78 @@ +const INDICATOR_ID = 'loading-indicator' +const HIDDEN_CLASS = 'invisible' +const GRACE_PERIOD_MS = 500 +const DEBOUNCE_MS = 200 + +let debounceTimeout = null +let historyRestoreGracePeriod = false + +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() { + // Ignore requests triggered during history restore grace period + if (true === historyRestoreGracePeriod) { + return + } + + // Don't start a new debounce if already pending + if (debounceTimeout) { + return + } + + // Show indicator after delay + debounceTimeout = setTimeout(() => { + show() + }, DEBOUNCE_MS) +} + +function handleHistoryRestore() { + hide() + + // Set grace period to ignore change events triggered by history restore + historyRestoreGracePeriod = true + setTimeout(() => { + historyRestoreGracePeriod = false + }, GRACE_PERIOD_MS) +} + +function handleTimeout() { + hide() + 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 + hide() +} + +// Initialize event listeners +document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest) +document.body.addEventListener('htmx:afterRequest', hide) +document.body.addEventListener('htmx:timeout', handleTimeout) +document.body.addEventListener('htmx:historyRestore', handleHistoryRestore) +document.body.addEventListener('htmx:sendError', hide) +document.body.addEventListener('htmx:responseError', hide) +document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave) + +window.addEventListener('pageshow', hide) +window.addEventListener('popstate', handleHistoryRestore) \ No newline at end of file diff --git a/templates/_partials/_loading_indicator.html.twig b/templates/_partials/_loading_indicator.html.twig index 4425364..72b6643 100644 --- a/templates/_partials/_loading_indicator.html.twig +++ b/templates/_partials/_loading_indicator.html.twig @@ -1,5 +1,5 @@ -