Files
thrillwiki_django_no_react/templates/cotton/enhanced_search.html
pacnpal 12deafaa09 Refactor photo management and upload functionality to use HTMX for asynchronous requests
- Updated photo upload handling in `photo_manager.html` and `photo_upload.html` to utilize HTMX for file uploads, improving user experience and reducing reliance on Promises.
- Refactored caption update and primary photo toggle methods to leverage HTMX for state updates without full page reloads.
- Enhanced error handling and success notifications using HTMX events.
- Replaced fetch API calls with HTMX forms in various templates, including `homepage.html`, `park_form.html`, and `roadtrip_planner.html`, to streamline AJAX interactions.
- Improved search suggestion functionality in `search_script.html` by implementing HTMX for fetching suggestions, enhancing performance and user experience.
- Updated designer, manufacturer, and ride model forms to handle responses with HTMX, ensuring better integration and user feedback.
2025-09-26 10:18:56 -04:00

282 lines
13 KiB
HTML

{% comment %}
Enhanced Search Component - Django Cotton Version
Advanced search input with autocomplete suggestions, debouncing, and loading states.
Provides real-time search with suggestions and filtering integration.
Usage Examples:
<c-enhanced_search
placeholder="Search parks by name, location, or features..."
current_value=""
/>
<c-enhanced_search
placeholder="Find your perfect park..."
current_value="disney"
autocomplete_url="/parks/suggest/"
class="custom-class"
/>
Parameters:
- placeholder: Search input placeholder text (default: "Search parks...")
- current_value: Current search value (optional)
- autocomplete_url: URL for autocomplete suggestions (optional)
- debounce_delay: Debounce delay in milliseconds (default: 300)
- class: Additional CSS classes (optional)
Features:
- Real-time search with debouncing
- Autocomplete dropdown with suggestions
- Loading states and indicators
- HTMX integration for seamless search
- Keyboard navigation support
- Clear button functionality
{% endcomment %}
<c-vars
placeholder="Search parks..."
current_value=""
autocomplete_url=""
debounce_delay="300"
class=""
/>
<div class="relative w-full {{ class }}"
x-data="{
open: false,
search: '{{ current_value }}',
suggestions: [],
loading: false,
selectedIndex: -1,
clearSearch() {
this.search = '';
this.open = false;
this.suggestions = [];
this.selectedIndex = -1;
// Trigger search update if HTMX is available
if (typeof htmx !== 'undefined' && this.$refs.searchInput) {
htmx.trigger(this.$refs.searchInput, 'keyup');
}
},
selectSuggestion(suggestion) {
this.search = suggestion.name || suggestion;
this.open = false;
this.selectedIndex = -1;
// Trigger search update if HTMX is available
if (typeof htmx !== 'undefined' && this.$refs.searchInput) {
htmx.trigger(this.$refs.searchInput, 'keyup');
}
},
handleKeydown(event) {
if (!this.open) return;
switch(event.key) {
case 'ArrowDown':
event.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
break;
case 'Enter':
event.preventDefault();
if (this.selectedIndex >= 0 && this.suggestions[this.selectedIndex]) {
this.selectSuggestion(this.suggestions[this.selectedIndex]);
}
break;
case 'Escape':
this.open = false;
this.selectedIndex = -1;
break;
}
}
}"
@click.away="open = false">
<div class="relative">
<!-- Search Icon with ARIA -->
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" aria-hidden="true">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<!-- Search Input with Enhanced Accessibility -->
<input
x-ref="searchInput"
id="park-search"
type="text"
name="search"
x-model="search"
placeholder="{{ placeholder }}"
class="block w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-base sm:text-sm min-h-[44px] sm:min-h-0"
hx-get="{% url 'parks:park_list' %}"
hx-trigger="keyup changed delay:{{ debounce_delay }}ms"
hx-target="#park-results"
hx-include="[name='view_mode'], [name='status'], [name='operator'], [name='ordering']"
hx-indicator="#search-spinner"
hx-push-url="true"
@keydown="handleKeydown"
@input="
if (search.length >= 2) {
{% if autocomplete_url %}
loading = true;
// Create temporary form for HTMX request
const tempForm = document.createElement('form');
tempForm.setAttribute('hx-get', '{{ autocomplete_url }}');
tempForm.setAttribute('hx-vals', JSON.stringify({q: search}));
tempForm.setAttribute('hx-trigger', 'submit');
tempForm.setAttribute('hx-swap', 'none');
tempForm.addEventListener('htmx:afterRequest', (event) => {
try {
const data = JSON.parse(event.detail.xhr.responseText);
suggestions = data.suggestions || [];
open = suggestions.length > 0;
loading = false;
selectedIndex = -1;
} catch (error) {
loading = false;
open = false;
}
document.body.removeChild(tempForm);
});
document.body.appendChild(tempForm);
htmx.trigger(tempForm, 'submit');
{% endif %}
} else {
open = false;
suggestions = [];
selectedIndex = -1;
}
"
autocomplete="off"
role="combobox"
aria-expanded="false"
:aria-expanded="open"
aria-autocomplete="list"
aria-controls="search-suggestions"
aria-describedby="search-help-text search-live-region"
:aria-activedescendant="selectedIndex >= 0 ? `suggestion-${selectedIndex}` : null"
/>
<!-- Loading Spinner with ARIA -->
<div id="search-spinner" class="absolute inset-y-0 right-0 pr-3 flex items-center htmx-indicator" aria-hidden="true">
<svg class="animate-spin h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<!-- Clear Button with Enhanced Accessibility -->
<button
x-show="search.length > 0"
@click="clearSearch()"
type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors htmx-indicator:hidden min-w-[44px] min-h-[44px] justify-center"
aria-label="Clear search input"
title="Clear search"
tabindex="0"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Autocomplete Dropdown with ARIA -->
<div
x-show="open && suggestions.length > 0"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700 max-h-60 overflow-y-auto"
style="display: none;"
role="listbox"
aria-label="Search suggestions"
id="search-suggestions"
>
<div class="py-1">
<template x-for="(suggestion, index) in suggestions" :key="index">
<button
type="button"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 flex items-center justify-between min-h-[44px] focus:outline-none focus:ring-2 focus:ring-blue-500"
:class="{ 'bg-gray-100 dark:bg-gray-700': selectedIndex === index }"
@click="selectSuggestion(suggestion)"
@mouseenter="selectedIndex = index"
role="option"
:id="`suggestion-${index}`"
:aria-selected="selectedIndex === index"
:aria-label="`Select ${suggestion.name || suggestion}${suggestion.type ? ' - ' + suggestion.type : ''}`"
>
<span x-text="suggestion.name || suggestion"></span>
<template x-if="suggestion.type">
<span class="text-xs text-gray-500 dark:text-gray-400 capitalize" x-text="suggestion.type"></span>
</template>
</button>
</template>
</div>
<!-- Quick Filters -->
{% if autocomplete_url %}
<div class="border-t border-gray-200 dark:border-gray-700 p-2">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Quick Filters:</div>
<div class="flex flex-wrap gap-1">
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?has_coasters=True"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Parks with Coasters
</button>
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?min_rating=4"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Highly Rated
</button>
<button
type="button"
class="inline-flex items-center px-2 py-1 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-800/50"
hx-get="{% url 'parks:park_list' %}?park_type=disney"
hx-target="#park-results"
hx-push-url="true"
@click="open = false"
>
Disney Parks
</button>
</div>
</div>
{% endif %}
</div>
<!-- Screen Reader Support Elements -->
<div id="search-help-text" class="sr-only">
Type to search parks. Use arrow keys to navigate suggestions, Enter to select, or Escape to close.
</div>
<!-- Live Region for Screen Reader Announcements -->
<div id="search-live-region"
aria-live="polite"
aria-atomic="true"
class="sr-only"
x-text="open && suggestions.length > 0 ?
`${suggestions.length} suggestion${suggestions.length !== 1 ? 's' : ''} available. Use arrow keys to navigate.` :
(search.length >= 2 && !loading && suggestions.length === 0 ? 'No suggestions found.' : '')">
</div>
</div>