/** * Search functionality */ export class SearchManager { constructor(debounceMs = 300) { this.searchTimeout = null; this.SEARCH_DEBOUNCE_MS = debounceMs; this.init(); } /** * Initialize search functionality with debouncing */ init() { const searchBox = document.getElementById('searchBox'); const searchResults = document.getElementById('searchResults'); if (!searchBox || !searchResults) return; searchBox.addEventListener('input', (e) => { clearTimeout(this.searchTimeout); const query = e.target.value.trim(); if (query.length === 0) { searchResults.style.display = 'none'; return; } this.searchTimeout = setTimeout(() => { this.performSearch(query); }, this.SEARCH_DEBOUNCE_MS); }); document.addEventListener('click', (e) => { if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) { searchResults.style.display = 'none'; } }); } /** * Perform search across plot names */ async performSearch(query) { const searchResults = document.getElementById('searchResults'); if (!searchResults) return; searchResults.innerHTML = '
🔍 Searching...
'; searchResults.style.display = 'block'; try { const results = await this.searchPlots(query); this.displaySearchResults(results, query); } catch (error) { console.error('Search error:', error); searchResults.innerHTML = '
❌ Search failed
'; } } /** * Search for plots matching the query */ async searchPlots(query) { const results = []; const visited = new Set(); const lowerQuery = query.toLowerCase(); await this.searchInPage(window.location.pathname, lowerQuery, results, visited); await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5); return results.slice(0, 20); } /** * Search for plots in a specific page */ async searchInPage(path, query, results, visited, maxResults = 50) { if (visited.has(path) || results.length >= maxResults) return; visited.add(path); try { const response = await fetch(path); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const items = doc.querySelectorAll('.grid-item'); items.forEach(item => { const nameElement = item.querySelector('.plot-name'); const imgElement = item.querySelector('img'); const linkElement = item.querySelector('a'); if (nameElement && imgElement && linkElement) { const name = nameElement.textContent.toLowerCase(); if (name.includes(query)) { results.push({ name: nameElement.textContent, path: path, href: linkElement.href, imgSrc: imgElement.src, relevance: this.calculateRelevance(name, query) }); } } }); } catch (error) { console.error('Error searching in', path, error); } } /** * Recursively search in subdirectories */ async searchRecursive(path, query, results, visited, depth, maxDepth) { if (depth >= maxDepth || results.length >= 50) return; try { const response = await fetch(path); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const subdirs = doc.querySelectorAll('h2 + ul li a'); for (const subdir of subdirs) { const href = subdir.getAttribute('href'); if (href) { const baseUrl = path.replace(/\/[^\/]*$/, '/'); const subPath = baseUrl + href; await this.searchInPage(subPath, query, results, visited); await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth); } } } catch (error) { console.error('Error in recursive search:', error); } } /** * Calculate search relevance score */ calculateRelevance(text, query) { const exactMatch = text === query; const startsWith = text.startsWith(query); const wordMatch = text.split(/\s+/).some(word => word.startsWith(query)); if (exactMatch) return 100; if (startsWith) return 80; if (wordMatch) return 60; return 40; } /** * Display search results with highlighting */ displaySearchResults(results, query) { const searchResults = document.getElementById('searchResults'); if (!searchResults) return; if (results.length === 0) { searchResults.innerHTML = '
📭 No plots found
'; return; } results.sort((a, b) => b.relevance - a.relevance); let html = ''; results.forEach(result => { const highlightedName = this.highlightText(result.name, query); const relativePath = this.getRelativePath(result.path); html += `
${highlightedName}
📍 ${relativePath}
`; }); searchResults.innerHTML = html; } /** * Highlight search query in text */ highlightText(text, query) { const regex = new RegExp(`(${query})`, 'gi'); return text.replace(regex, '$1'); } /** * Get relative path for display */ getRelativePath(fullPath) { const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html'); return parts.length > 0 ? parts.join(' / ') : 'Root'; } /** * Open search result and track it */ openSearchResult(href) { if (window.recentPlotsManager) { window.recentPlotsManager.addToRecentPlots(href); } window.open(href, '_blank'); document.getElementById('searchResults').style.display = 'none'; } }