fix: avoid loading indicator issues when using browser history for navigation
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import './bootstrap.js'
|
import './bootstrap.js'
|
||||||
|
import './loading.js'
|
||||||
|
|
||||||
import './styles/app.css'
|
import './styles/app.css'
|
||||||
|
|
||||||
import htmx from 'htmx.org'
|
import htmx from 'htmx.org'
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="invisible fixed top-0 left-0 inset-0 z-50 backdrop-blur-sm flex items-center justify-center transition-all duration-100"
|
<div id="loading-indicator"
|
||||||
{{ stimulus_target('loading', 'indicator') }}>
|
class="invisible fixed top-0 left-0 inset-0 z-50 backdrop-blur-sm flex items-center justify-center transition-all duration-100">
|
||||||
<svg class="text-primary-light w-32 h-32 lg:w-48 lg:h-48" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
<svg class="text-primary-light w-32 h-32 lg:w-48 lg:h-48" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
||||||
<circle cx="22" cy="22" r="1">
|
<circle cx="22" cy="22" r="1">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
Kontaktdaten
|
Kontaktdaten
|
||||||
</h2>
|
</h2>
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
{% include '_partials/_flashes.html.twig' %}
|
||||||
{{ form_start(personalDataForm, { 'attr': { 'data-action': 'loading#show' } }) }}
|
{{ form_start(personalDataForm) }}
|
||||||
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
||||||
<div>
|
<div>
|
||||||
{{ form_row(personalDataForm.street, { 'label_attr': { 'class': 'text-white' } }) }}
|
{{ form_row(personalDataForm.street, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="w-screen h-dvh {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
|
<div class="w-screen h-dvh {% block background %}bg-outer bg-outer--2{% endblock %}">
|
||||||
<div class="max-w-screen-xl h-dvh ml-auto flex flex-col overflow-hidden bg-inner">
|
<div class="max-w-screen-xl h-dvh ml-auto flex flex-col overflow-hidden bg-inner">
|
||||||
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="w-screen h-dvh {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
|
<div class="w-screen h-dvh {% block background %}bg-outer bg-outer--2{% endblock %}">
|
||||||
<div class="max-w-screen-xl h-dvh ml-auto flex flex-col overflow-hidden bg-inner">
|
<div class="max-w-screen-xl h-dvh ml-auto flex flex-col overflow-hidden bg-inner">
|
||||||
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
{% include '_partials/_flashes.html.twig' %}
|
||||||
<div class="pb-4">
|
<div class="pb-4">
|
||||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#show' } }) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.firstName, { 'label_attr': { 'class': 'text-white' } }) }}
|
{{ form_row(form.firstName, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||||
{{ form_row(form.name, { 'label_attr': { 'class': 'text-white' } }) }}
|
{{ form_row(form.name, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||||
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
{% include '_partials/_flashes.html.twig' %}
|
||||||
<div class="pb-4">
|
<div class="pb-4">
|
||||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#show' } }) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||||
<button type="submit" class="button button--primary">
|
<button type="submit" class="button button--primary">
|
||||||
Passwort zurücksetzen
|
Passwort zurücksetzen
|
||||||
|
|||||||
Reference in New Issue
Block a user