fix(admin): live search not firing on text input + spinner always visible
hx-trigger bug:
"from:find input" in hx-trigger attaches the event listener to the
first <input> found in the form — which is the hidden CSRF token input.
Typing in the visible search field never fires the listener on that
element. Result: only Enter (form submit) triggered HTMX.
Fix: drop "from:find input" so the listener is on the form itself,
where input/change events from all children bubble naturally.
Spinner visibility bug:
.search-spinner { opacity: 0 } relied on our compiled output.css.
HTMX ships its own built-in CSS for .htmx-indicator (opacity:0 →
opacity:1 on htmx-request). Using class="htmx-indicator search-spinner"
delegates hide/show to HTMX's own stylesheet with no dependency on
whether output.css has been rebuilt. Our .search-spinner only handles
positioning and the spin animation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
<form class="flex flex-wrap gap-3 items-end"
|
<form class="flex flex-wrap gap-3 items-end"
|
||||||
hx-get="{{ url_for('admin.article_results') }}"
|
hx-get="{{ url_for('admin.article_results') }}"
|
||||||
hx-target="#article-results"
|
hx-target="#article-results"
|
||||||
hx-trigger="change, input delay:300ms from:find input"
|
hx-trigger="change, input delay:300ms"
|
||||||
hx-indicator="#articles-loading">
|
hx-indicator="#articles-loading">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg id="articles-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="articles-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<form class="flex flex-wrap gap-3 items-end"
|
<form class="flex flex-wrap gap-3 items-end"
|
||||||
hx-get="{{ url_for('admin.email_results') }}"
|
hx-get="{{ url_for('admin.email_results') }}"
|
||||||
hx-target="#email-results"
|
hx-target="#email-results"
|
||||||
hx-trigger="change, input delay:300ms from:find input"
|
hx-trigger="change, input delay:300ms"
|
||||||
hx-indicator="#emails-loading">
|
hx-indicator="#emails-loading">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<input type="text" name="search" value="{{ current_search }}" class="form-input" placeholder="Email or subject..." style="min-width:180px">
|
<input type="text" name="search" value="{{ current_search }}" class="form-input" placeholder="Email or subject..." style="min-width:180px">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg id="emails-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="emails-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<form class="flex flex-wrap gap-3 items-end"
|
<form class="flex flex-wrap gap-3 items-end"
|
||||||
hx-get="{{ url_for('admin.lead_results') }}"
|
hx-get="{{ url_for('admin.lead_results') }}"
|
||||||
hx-target="#lead-results"
|
hx-target="#lead-results"
|
||||||
hx-trigger="change, input delay:300ms from:find input"
|
hx-trigger="change, input delay:300ms"
|
||||||
hx-indicator="#leads-loading">
|
hx-indicator="#leads-loading">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg id="leads-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="leads-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<form class="card mb-4 flex flex-wrap gap-3 items-end"
|
<form class="card mb-4 flex flex-wrap gap-3 items-end"
|
||||||
hx-get="{{ url_for('admin.scenario_results') }}"
|
hx-get="{{ url_for('admin.scenario_results') }}"
|
||||||
hx-target="#scenario-results"
|
hx-target="#scenario-results"
|
||||||
hx-trigger="change, input delay:300ms from:find input"
|
hx-trigger="change, input delay:300ms"
|
||||||
hx-indicator="#scenario-loading">
|
hx-indicator="#scenario-loading">
|
||||||
|
|
||||||
<div class="flex-1 min-w-48">
|
<div class="flex-1 min-w-48">
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<button type="button" class="btn-outline"
|
<button type="button" class="btn-outline"
|
||||||
onclick="this.closest('form').reset(); htmx.trigger(this.closest('form'), 'change')">Clear</button>
|
onclick="this.closest('form').reset(); htmx.trigger(this.closest('form'), 'change')">Clear</button>
|
||||||
<svg id="scenario-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="scenario-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<form class="flex flex-wrap gap-3 items-end"
|
<form class="flex flex-wrap gap-3 items-end"
|
||||||
hx-get="{{ url_for('admin.supplier_results') }}"
|
hx-get="{{ url_for('admin.supplier_results') }}"
|
||||||
hx-target="#supplier-results"
|
hx-target="#supplier-results"
|
||||||
hx-trigger="change, input delay:300ms from:find input"
|
hx-trigger="change, input delay:300ms"
|
||||||
hx-indicator="#suppliers-loading">
|
hx-indicator="#suppliers-loading">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg id="suppliers-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="suppliers-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
<form class="flex gap-3 items-center"
|
<form class="flex gap-3 items-center"
|
||||||
hx-get="{{ url_for('admin.user_results') }}"
|
hx-get="{{ url_for('admin.user_results') }}"
|
||||||
hx-target="#user-results"
|
hx-target="#user-results"
|
||||||
hx-trigger="input delay:300ms from:find input"
|
hx-trigger="input delay:300ms"
|
||||||
hx-indicator="#user-loading">
|
hx-indicator="#user-loading">
|
||||||
<div class="flex-1 max-w-sm">
|
<div class="flex-1 max-w-sm">
|
||||||
<input type="search" name="search" class="form-input w-full"
|
<input type="search" name="search" class="form-input w-full"
|
||||||
placeholder="Search by email…" value="{{ search }}">
|
placeholder="Search by email…" value="{{ search }}">
|
||||||
</div>
|
</div>
|
||||||
<svg id="user-loading" class="search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg id="user-loading" class="htmx-indicator search-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
<circle cx="12" cy="12" r="10" stroke="#CBD5E1" stroke-width="3"/>
|
||||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
<path d="M12 2a10 10 0 0 1 10 10" stroke="#0EA5E9" stroke-width="3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -569,14 +569,14 @@
|
|||||||
@apply px-4 pb-4 text-slate-dark;
|
@apply px-4 pb-4 text-slate-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inline HTMX loading indicator for search forms */
|
/* Inline HTMX loading indicator for search forms.
|
||||||
|
Opacity is handled by HTMX's built-in .htmx-indicator CSS.
|
||||||
|
This class only adds positioning and the spin animation. */
|
||||||
.search-spinner {
|
.search-spinner {
|
||||||
opacity: 0;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
.search-spinner.htmx-request {
|
.search-spinner.htmx-request {
|
||||||
opacity: 1;
|
|
||||||
animation: spin-icon 0.9s linear infinite;
|
animation: spin-icon 0.9s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user