feat: add autocomplete dropdown support for filter fields

- Add fetchFieldValues helper function for API calls
- Add fetchAuthorValues for author autocomplete
- Add fetchGenreValues for genre autocomplete
- Add fetchSeriesValues for series autocomplete
- Add fetchLanguageValues for language autocomplete
- Functions use native DOM manipulation to populate datalist elements
- No Alpine.js reactive state (simple pattern, not reactive)
- Functions registered as methods in Alpine.data("bookshelf") component
- Triggers on input with 300ms debounce after 2 characters minimum
- Updates include count in option text (e.g., "Asimov, Isaac (47)")

Uses /api/media-items/search with field-specific params (author=value, genre=value, etc.).
This commit is contained in:
2026-03-23 22:38:03 -04:00
parent 2e24ce00cf
commit d3783eca8b
2 changed files with 82 additions and 1 deletions
+81
View File
@@ -227,6 +227,87 @@ Alpine.data("bookshelf", () => ({
showToast("Error saving filter", "error");
}
},
// Autocomplete helper function
async fetchFieldValues(
field: string,
search: string,
datalistId: string,
): Promise<void> {
const token = localStorage.getItem("token");
if (!token) {
console.error("Not authenticated");
return;
}
if (search.length < 2) return;
const currentLibraryId = (
document.getElementById("library-select") as HTMLSelectElement
)?.value;
if (!currentLibraryId) {
console.error("No library selected");
return;
}
try {
const response = await fetch(
`/api/media-items/search?${field}=${encodeURIComponent(
search,
)}&library_id=${currentLibraryId}&limit=50`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) {
console.error("Failed to fetch field values");
return;
}
const data = await response.json();
// Update datalist via DOM manipulation
const datalist = document.getElementById(datalistId);
if (!datalist) {
console.error(`Datalist ${datalistId} not found`);
return;
}
// Clear existing options
datalist.innerHTML = "";
// Add new options
data.results.forEach((item: { value: string; count: number }) => {
const option = document.createElement("option");
option.value = item.value;
option.textContent = `${item.value} (${item.count})`;
datalist.appendChild(option);
});
} catch (error) {
console.error("Error fetching field values:", error);
}
},
// Fetch author values for autocomplete
async fetchAuthorValues(input: HTMLInputElement): Promise<void> {
await this.fetchFieldValues("author", input.value, "author-datalist");
},
// Fetch genre values for autocomplete
async fetchGenreValues(input: HTMLInputElement): Promise<void> {
await this.fetchFieldValues("genre", input.value, "genre-datalist");
},
// Fetch series values for autocomplete
async fetchSeriesValues(input: HTMLInputElement): Promise<void> {
await this.fetchFieldValues("series", input.value, "series-datalist");
},
// Fetch language values for autocomplete
async fetchLanguageValues(input: HTMLInputElement): Promise<void> {
await this.fetchFieldValues("language", input.value, "language-datalist");
},
}));
// Standalone function - initialize the bookshelf (NOT async, like dashboard.ts)
+1 -1
View File
File diff suppressed because one or more lines are too long