1 // Load product data once 2 let products = []; 3 4 fetch('/products.json') 5 .then(response => response.json()) 6 .then(data => { 7 products = data; 8 console.log('Products loaded:', products.length); 9 }) 10 .catch(err => { 11 console.error('Failed to load products.json', err); 12 }); 13 14 const searchInput = document.getElementById('search-input'); 15 const searchResults = document.getElementById('search-results'); 16 17 // Escape special regex characters 18 function escapeRegex(str) { 19 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 20 } 21 22 // Highlight matches in text 23 function highlightMatch(text, term) { 24 if (!term) return text; 25 const escapedTerm = escapeRegex(term); 26 const regex = new RegExp(`(${escapedTerm})`, 'gi'); 27 return text.replace(regex, '<span class="highlight">$1</span>'); 28 } 29 30 // Search logic 31 function performSearch(query) { 32 if (!query.trim() || products.length === 0) { 33 searchResults.style.display = 'none'; 34 return; 35 } 36 37 const term = query.toLowerCase(); 38 const results = products.filter(product => { 39 const searchable = 40 product.title.toLowerCase() + 41 ' ' + 42 product.description.toLowerCase() + 43 ' ' + 44 product.keywords.toLowerCase(); 45 return searchable.includes(term); 46 }).slice(0, 8); // Limit to top 8 results 47 48 // Render results 49 if (results.length > 0) { 50 searchResults.innerHTML = results.map(product => ` 51 <a href="${product.url}" class="search-result-item"> 52 <div class="search-result-title">${highlightMatch(product.title, query)}</div> 53 <div class="search-result-desc">${highlightMatch(product.description, query)}</div> 54 </a> 55 `).join(''); 56 searchResults.style.display = 'block'; 57 } else { 58 searchResults.innerHTML = '<div class="search-result-item">No products found</div>'; 59 searchResults.style.display = 'block'; 60 } 61 } 62 63 // Event listeners 64 searchInput.addEventListener('input', () => { 65 performSearch(searchInput.value); 66 }); 67 68 // Hide results when clicking outside 69 document.addEventListener('click', (e) => { 70 if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { 71 searchResults.style.display = 'none'; 72 } 73 }); 74 75 // Navigate with keyboard (optional enhancement) 76 searchInput.addEventListener('keydown', (e) => { 77 if (e.key === 'Escape') { 78 searchResults.style.display = 'none'; 79 } 80 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.