218 lines
6.6 KiB
JavaScript
218 lines
6.6 KiB
JavaScript
/**
|
|
* 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 = '<div style="padding: 1rem;">🔍 Searching...</div>';
|
|
searchResults.style.display = 'block';
|
|
|
|
try {
|
|
const results = await this.searchPlots(query);
|
|
this.displaySearchResults(results, query);
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
searchResults.innerHTML = '<div style="padding: 1rem; color: red;">❌ Search failed</div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = '<div style="padding: 1rem;">📭 No plots found</div>';
|
|
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 += `
|
|
<div class="search-result-item" onclick="window.searchManager.openSearchResult('${result.href}')" title="${result.name}">
|
|
<div style="display: flex; gap: 0.7rem; align-items: center;">
|
|
<img src="${result.imgSrc}" style="width: 50px; height: 40px; object-fit: cover; border-radius: 4px;" />
|
|
<div style="flex: 1; min-width: 0;">
|
|
<div style="font-weight: 600; margin-bottom: 0.3rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
${highlightedName}
|
|
</div>
|
|
<div style="font-size: 0.8rem; color: var(--breadcrumb-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
📍 ${relativePath}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
searchResults.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Highlight search query in text
|
|
*/
|
|
highlightText(text, query) {
|
|
const regex = new RegExp(`(${query})`, 'gi');
|
|
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
}
|
|
}
|