build(typescript): update compiled search.js from TypeScript source

- Compiled from web/src/search.ts
- Added proper type annotations
- Fixed null checks for DOM elements
- Added escapeHtml for library name display
- Updated onclick to use window.selectLibraryAndBook
This commit is contained in:
2026-02-18 16:42:05 -05:00
parent dfd9cbcde7
commit 9d2bed92f2
+55 -65
View File
@@ -1,16 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
let searchTimeout = null;
const SEARCH_DEBOUNCE_MS = 300;
const SEARCH_MIN_CHARS = 2;
function initializeSearch() {
const searchInput = document.getElementById('header-search');
if (!searchInput) {
console.warn('Search input not found');
return;
}
searchInput.addEventListener('input', handleSearchInput);
searchInput.addEventListener('keydown', handleSearchKeydown);
searchInput.addEventListener('focus', () => {
@@ -18,114 +16,116 @@ function initializeSearch() {
performSearch(searchInput.value);
}
});
document.addEventListener('click', (e) => {
const searchResults = document.getElementById('search-results');
const searchInput = document.getElementById('header-search');
if (searchResults && !searchResults.contains(e.target) && e.target !== searchInput) {
const searchInputEl = document.getElementById('header-search');
if (searchResults && !searchResults.contains(e.target) && e.target !== searchInputEl) {
hideSearchResults();
}
});
}
function handleSearchInput(e) {
const query = e.target.value.trim();
clearTimeout(searchTimeout);
const target = e.target;
const query = target.value.trim();
if (searchTimeout) {
clearTimeout(searchTimeout);
}
if (query.length < SEARCH_MIN_CHARS) {
hideSearchResults();
return;
}
searchTimeout = setTimeout(() => {
performSearch(query);
}, SEARCH_DEBOUNCE_MS);
}
function handleSearchKeydown(e) {
const searchResults = document.getElementById('search-results');
if (!searchResults || searchResults.classList.contains('hidden')) {
return;
}
const items = searchResults.querySelectorAll('.search-result-item');
const currentIndex = parseInt(searchResults.dataset.selectedIndex || '-1');
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextIndex = Math.min(currentIndex + 1, items.length - 1);
selectSearchResult(items, nextIndex);
} else if (e.key === 'ArrowUp') {
}
else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevIndex = Math.max(currentIndex - 1, -1);
selectSearchResult(items, prevIndex);
} else if (e.key === 'Enter') {
}
else if (e.key === 'Enter') {
e.preventDefault();
if (currentIndex >= 0 && items[currentIndex]) {
items[currentIndex].querySelector('a').click();
const link = items[currentIndex].querySelector('a');
if (link)
link.click();
}
} else if (e.key === 'Escape') {
}
else if (e.key === 'Escape') {
hideSearchResults();
}
}
function selectSearchResult(items, index) {
items.forEach((item, i) => {
if (i === index) {
item.classList.add('bg-opacity-80');
} else {
}
else {
item.classList.remove('bg-opacity-80');
}
});
const searchResults = document.getElementById('search-results');
searchResults.dataset.selectedIndex = index.toString();
if (searchResults) {
searchResults.dataset.selectedIndex = index.toString();
}
}
function performSearch(query) {
const token = localStorage.getItem('token');
if (!token) {
console.warn('No authentication token found');
return;
}
showSearchLoading();
fetch(`/api/media-items/search?q=${encodeURIComponent(query)}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
.then(response => {
.then(response => {
if (response.status === 404) {
return { error: 'no results found', results: [] };
}
return response.json();
})
.then(data => {
.then((data) => {
hideSearchLoading();
if (data.error && data.error === 'no results found') {
if (data && 'error' in data && data.error === 'no results found') {
showNoResults(query);
} else if (Array.isArray(data) && data.length > 0) {
}
else if (Array.isArray(data) && data.length > 0) {
showSearchResults(data, query);
} else {
}
else if (Array.isArray(data)) {
showNoResults(query);
}
else {
showNoResults(query);
}
})
.catch(error => {
.catch(error => {
hideSearchLoading();
console.error('Search error:', error);
showSearchError();
});
}
function showSearchLoading() {
createSearchResultsContainer();
const searchResults = document.getElementById('search-results');
if (!searchResults)
return;
searchResults.innerHTML = `
<div class="p-4 text-center" style="color: var(--text-secondary)">
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2" style="border-color: var(--accent)"></div>
@@ -134,22 +134,19 @@ function showSearchLoading() {
`;
searchResults.classList.remove('hidden');
}
function hideSearchLoading() {
}
function showSearchResults(results, query) {
createSearchResultsContainer();
const searchResults = document.getElementById('search-results');
if (!searchResults)
return;
searchResults.dataset.selectedIndex = '-1';
const libraryIconMap = {
'ebooks': '📚',
'comics': '📖',
'manga': '🗾'
};
let html = `
<div class="p-3 border-b" style="border-color: var(--border)">
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">
@@ -158,19 +155,17 @@ function showSearchResults(results, query) {
</div>
<div class="max-h-96 overflow-y-auto">
`;
results.forEach((item, index) => {
const icon = libraryIconMap[item.library_type_name] || '📁';
const titleHtml = highlightMatch(item.title, query);
const authorHtml = item.author ? highlightMatch(item.author, query) : '';
html += `
<div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer"
<div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer"
style="border-color: var(--border); background-color: var(--bg-secondary)"
data-index="${index}">
<a href="/bookshelf"
<a href="/bookshelf"
class="block"
onclick="selectLibraryAndBook('${item.library_id}', '${item.id}')">
onclick="window.selectLibraryAndBook('${item.library_id}', '${item.id}')">
<div class="flex items-start space-x-3">
<div class="text-2xl">${icon}</div>
<div class="flex-1 min-w-0">
@@ -179,7 +174,7 @@ function showSearchResults(results, query) {
</h4>
${authorHtml ? `<p class="text-xs truncate" style="color: var(--text-secondary)">${authorHtml}</p>` : ''}
<p class="text-xs mt-1" style="color: var(--text-secondary)">
${item.library_name}
${escapeHtml(item.library_name)}
</p>
</div>
</div>
@@ -187,25 +182,23 @@ function showSearchResults(results, query) {
</div>
`;
});
html += `
</div>
<div class="p-2 border-t text-center" style="border-color: var(--border)">
<p class="text-xs" style="color: var(--text-secondary)">
Press <kbd class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary)">↑↓</kbd> to navigate,
Press <kbd class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary)">↑↓</kbd> to navigate,
<kbd class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary)">Enter</kbd> to select
</p>
</div>
`;
searchResults.innerHTML = html;
searchResults.classList.remove('hidden');
}
function showNoResults(query) {
createSearchResultsContainer();
const searchResults = document.getElementById('search-results');
if (!searchResults)
return;
searchResults.innerHTML = `
<div class="p-4 text-center">
<div class="text-4xl mb-2">🔍</div>
@@ -215,11 +208,11 @@ function showNoResults(query) {
`;
searchResults.classList.remove('hidden');
}
function showSearchError() {
createSearchResultsContainer();
const searchResults = document.getElementById('search-results');
if (!searchResults)
return;
searchResults.innerHTML = `
<div class="p-4 text-center">
<div class="text-4xl mb-2">⚠️</div>
@@ -229,14 +222,12 @@ function showSearchError() {
`;
searchResults.classList.remove('hidden');
}
function hideSearchResults() {
const searchResults = document.getElementById('search-results');
if (searchResults) {
searchResults.classList.add('hidden');
}
}
function createSearchResultsContainer() {
let searchResults = document.getElementById('search-results');
if (!searchResults) {
@@ -244,32 +235,31 @@ function createSearchResultsContainer() {
searchResults.id = 'search-results';
searchResults.className = 'hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border';
searchResults.style.cssText = 'background-color: var(--bg-secondary); border-color: var(--border)';
const searchInput = document.getElementById('header-search');
const searchContainer = searchInput.closest('.relative');
searchContainer.appendChild(searchResults);
if (searchInput) {
const searchContainer = searchInput.closest('.relative');
if (searchContainer) {
searchContainer.appendChild(searchResults);
}
}
}
}
function highlightMatch(text, query) {
if (!text) return '';
if (!text)
return '';
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return escapeHtml(text).replace(regex, '<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function selectLibraryAndBook(libraryId, bookId) {
localStorage.setItem('selectedLibrary', libraryId);
localStorage.setItem('selectedBook', bookId);
hideSearchResults();
}
document.addEventListener('DOMContentLoaded', initializeSearch);
window.selectLibraryAndBook = selectLibraryAndBook;
window.selectLibraryAndBook = selectLibraryAndBook;