diff --git a/templates/rides/partials/search_script.html b/templates/rides/partials/search_script.html index 401ad061..26ede763 100644 --- a/templates/rides/partials/search_script.html +++ b/templates/rides/partials/search_script.html @@ -27,15 +27,53 @@ document.addEventListener('alpine:init', () => { this.showError('An error occurred while searching. Please try again.'); }); - // Bind to popstate for browser navigation - window.addEventListener('popstate', () => { + // Store bound handlers for cleanup + this.boundHandlers = new Map(); + + // Create handler functions + const popstateHandler = () => { const urlParams = new URLSearchParams(window.location.search); this.searchQuery = urlParams.get('search') || ''; this.syncFormWithUrl(); - }); + }; + this.boundHandlers.set('popstate', popstateHandler); + + const errorHandler = (evt) => { + console.error('HTMX Error:', evt.detail.error); + this.showError('An error occurred while searching. Please try again.'); + }; + this.boundHandlers.set('htmx:error', errorHandler); + + // Bind event listeners + window.addEventListener('popstate', popstateHandler); + document.body.addEventListener('htmx:error', errorHandler); // Restore filters from localStorage if no URL params exist const savedFilters = localStorage.getItem('rideFilters'); + + // Set up destruction handler + this.$cleanup = () => { + // Remove all bound event listeners + this.boundHandlers.forEach((handler, event) => { + if (event === 'popstate') { + window.removeEventListener(event, handler); + } else { + document.body.removeEventListener(event, handler); + } + }); + this.boundHandlers.clear(); + + // Cancel any pending requests + if (this.currentRequest) { + this.currentRequest.abort(); + this.currentRequest = null; + } + + // Clear any pending timeouts + if (this.suggestionTimeout) { + clearTimeout(this.suggestionTimeout); + } + }; if (savedFilters) { const filters = JSON.parse(savedFilters); Object.entries(filters).forEach(([key, value]) => { @@ -77,21 +115,37 @@ document.addEventListener('alpine:init', () => { document.querySelector('form').dispatchEvent(new Event('change')); }, - // Get search suggestions + // Get search suggestions with request tracking + lastRequestId: 0, + currentRequest: null, + async getSearchSuggestions() { if (this.searchQuery.length < 2) { this.showSuggestions = false; return; } + // Cancel any pending request + if (this.currentRequest) { + this.currentRequest.abort(); + } + + const requestId = ++this.lastRequestId; const controller = new AbortController(); + this.currentRequest = controller; + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout try { const parkSlug = document.querySelector('input[name="park_slug"]')?.value; const url = `/rides/search-suggestions/?q=${encodeURIComponent(this.searchQuery)}${parkSlug ? '&park_slug=' + parkSlug : ''}`; - const response = await fetch(url, { signal: controller.signal }); + const response = await fetch(url, { + signal: controller.signal, + headers: { + 'X-Request-ID': requestId.toString() + } + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -99,25 +153,42 @@ document.addEventListener('alpine:init', () => { const html = await response.text(); - // Ensure the response is for the current query - if (this.searchQuery === document.getElementById('search').value) { - document.getElementById('search-suggestions').innerHTML = html; + // Only update if this is still the most recent request + if (requestId === this.lastRequestId && this.searchQuery === document.getElementById('search').value) { + const suggestionsEl = document.getElementById('search-suggestions'); + suggestionsEl.innerHTML = html; this.showSuggestions = html.trim() ? true : false; + + // Set proper ARIA attributes + const searchInput = document.getElementById('search'); + searchInput.setAttribute('aria-expanded', this.showSuggestions.toString()); + searchInput.setAttribute('aria-controls', 'search-suggestions'); + if (this.showSuggestions) { + suggestionsEl.setAttribute('role', 'listbox'); + suggestionsEl.querySelectorAll('button').forEach(btn => { + btn.setAttribute('role', 'option'); + }); + } } } catch (error) { if (error.name === 'AbortError') { - console.warn('Search suggestion request timed out'); + console.warn('Search suggestion request timed out or cancelled'); } else { console.error('Error fetching suggestions:', error); - // Show error state in UI - document.getElementById('search-suggestions').innerHTML = ` -