${this.escapeHtml(item.title)}
${this.escapeHtml(item.contents)}
// Configuration const DEFAULT_CONFIG = { search: { minChars: 2, // Minimum characters before searching maxResults: 5, // Maximum number of results to show fields: { // Fields to search through title: true, // Allow searching in title description: true, // Allow searching in description section: true // Allow searching in section } } }; class FastSearch { constructor({ searchInput, resultsContainer, json, searchResultTemplate = null, noResultsText = null, }) { this.searchInput = searchInput; this.resultsContainer = resultsContainer; this.json = json; this.searchResultTemplate = searchResultTemplate; this.noResultsText = noResultsText; this.init(); } init() { this.loadSearchIndex(); // this.initShortcutListener(); this.searchInput.addEventListener('input', (event) => { if (!this.searchIndex) { this.resultsContainer.innerHTML = '
'; return; } this.performSearch(this.searchInput.value); }); } // Load the search index async loadSearchIndex() { try { const response = await fetch(this.json); if (!response.ok) throw new Error('Failed to load search index'); const data = await response.json(); this.searchIndex = data.map(item => ({ ...item, searchableTitle: item.title?.toLowerCase() || '', searchableDesc: item.desc?.toLowerCase() || '', searchableSection: item.section?.toLowerCase() || '' })); } catch (error) { console.error('Error loading search index:', error); this.resultsContainer.innerHTML = ' '; } } escapeHtml(unsafe) { if (!unsafe) return ''; return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Simple fuzzy match for single words simpleFuzzyMatch(text, term) { if (text.includes(term)) return true; if (term.length < 3) return false; let matches = 0; let lastMatchIndex = -1; for (let i = 0; i < term.length; i++) { const found = text.indexOf(term[i], lastMatchIndex + 1); if (found > -1) { matches++; lastMatchIndex = found; } } return matches === term.length; } // Check if keyboard event matches shortcut config matchesShortcut(event, shortcutConfig) { return event.key === shortcutConfig.key && event.metaKey === shortcutConfig.metaKey && event.altKey === shortcutConfig.altKey && event.ctrlKey === shortcutConfig.ctrlKey && event.shiftKey === shortcutConfig.shiftKey; } initShortcutListener() { // Keyboard shortcuts document.addEventListener('keydown', (event) => { // ESC to close search if (event.key === 'Escape' && this.searchInput.focus) { this.searchInput.blur(); this.searchInput.value = ''; this.resultsContainer.innerHTML = ''; } }); } performSearch(term) { term = term.toLowerCase().trim(); if (!term || !this.searchIndex) { this.resultsContainer.innerHTML = ''; let resultsAvailable = false; return; } // Split search into terms const searchTerms = term.split(/\s+/).filter(t => t.length > 0); // Search with scoring const results = this.searchIndex .map(item => { let score = 0; const matchesAllTerms = searchTerms.every(term => { let matched = false; // Title matches (weighted higher) if (DEFAULT_CONFIG.search.fields.title) { if (item.searchableTitle.startsWith(term)) { score += 3; // Highest score for prefix matches in title matched = true; } else if (this.simpleFuzzyMatch(item.searchableTitle, term)) { score += 2; // Good score for fuzzy matches in title matched = true; } } // Other field matches if (!matched) { if (DEFAULT_CONFIG.search.fields.description && item.searchableDesc.includes(term)) { score += 0.5; // Lower score for description matches matched = true; } if (DEFAULT_CONFIG.search.fields.section && item.searchableSection.includes(term)) { score += 0.5; // Lower score for section matches matched = true; } } return matched; }); return { item, score: matchesAllTerms ? score : 0 }; }) .filter(result => result.score > 0) .sort((a, b) => b.score - a.score) .slice(0, DEFAULT_CONFIG.search.maxResults) .map(result => result.item); let resultsAvailable = results.length > 0; if (!resultsAvailable) { this.resultsContainer.innerHTML = 'Oops! No results found.
'; return; } const searchItems = results.map( (item) => { let categories = ''; let tags = ''; if (item.categories) { categories = item.categories.join(', '); categories = `${this.escapeHtml(item.contents)}