https://littlebirdelectronics.com.au/assets/controllers/ecommerce/help_search_controller-6820f362.js
1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["input", "category", "article", "empty", "aiResults", "cards"] 5 static values = { suggestUrl: String } 6 7 connect() { 8 this.timeout = null 9 this.aiTimeout = null 10 11 // Restore search from URL on page load 12 const params = new URLSearchParams(window.location.search) 13 const q = params.get("q") 14 if (q) { 15 this.inputTarget.value = q 16 this.applyFilter(q) 17 } 18 19 this.inputTarget.focus() 20 } 21 22 disconnect() { 23 clearTimeout(this.timeout) 24 clearTimeout(this.aiTimeout) 25 } 26 27 filter() { 28 clearTimeout(this.timeout) 29 this.timeout = setTimeout(() => { 30 const query = this.inputTarget.value.trim() 31 this.updateURL(query) 32 this.applyFilter(query.toLowerCase()) 33 }, 150) 34 } 35 36 clear() { 37 this.inputTarget.value = "" 38 this.updateURL("") 39 this.showAll() 40 this.hideAiResults() 41 this.inputTarget.focus() 42 } 43 44 applyFilter(query) { 45 if (query === "") { 46 this.showAll() 47 this.hideAiResults() 48 return 49 } 50 51 const words = query.split(/\s+/).filter(Boolean) 52 let totalVisible = 0 53 54 this.categoryTargets.forEach((categoryEl) => { 55 let categoryVisible = 0 56 57 // Find article targets within this category 58 this.articleTargets.forEach((articleEl) => { 59 if (!categoryEl.contains(articleEl)) return 60 61 const searchText = articleEl.dataset.searchText || "" 62 const matches = words.every(word => searchText.includes(word)) 63 64 articleEl.style.display = matches ? "" : "none" 65 if (matches) categoryVisible++ 66 }) 67 68 categoryEl.style.display = categoryVisible > 0 ? "" : "none" 69 totalVisible += categoryVisible 70 }) 71 72 // Hide category cards when filtering 73 if (this.hasCardsTarget) { 74 this.cardsTarget.style.display = totalVisible === this.articleTargets.length ? "" : "none" 75 } 76 77 if (this.hasEmptyTarget) { 78 this.emptyTarget.style.display = totalVisible === 0 ? "" : "none" 79 } 80 81 // If 0 local results, ask AI for suggestions 82 if (totalVisible === 0 && this.hasSuggestUrlValue) { 83 this.fetchAiSuggestions(query) 84 } else { 85 this.hideAiResults() 86 } 87 } 88 89 async fetchAiSuggestions(query) { 90 // Debounce AI calls 91 clearTimeout(this.aiTimeout) 92 this.aiTimeout = setTimeout(async () => { 93 try { 94 const url = `${this.suggestUrlValue}?q=${encodeURIComponent(query)}` 95 const response = await fetch(url, { 96 headers: { "Accept": "application/json" } 97 }) 98 if (!response.ok) return 99 100 const data = await response.json() 101 if (data.slugs && data.slugs.length > 0) { 102 this.showAiResults(data.slugs) 103 } 104 } catch (e) { 105 // Silently fail â AI suggestions are a nice-to-have 106 } 107 }, 500) 108 } 109 110 showAiResults(slugs) { 111 if (!this.hasAiResultsTarget) return 112 113 // Show matching articles by highlighting them 114 const matchedArticles = this.articleTargets.filter(el => slugs.includes(el.dataset.slug)) 115 if (matchedArticles.length === 0) return 116 117 // Build the AI results list 118 const list = matchedArticles.map(el => { 119 const clone = el.cloneNode(true) 120 clone.style.display = "" 121 return clone.outerHTML 122 }).join("") 123 124 this.aiResultsTarget.querySelector("[data-ai-list]").innerHTML = list 125 this.aiResultsTarget.style.display = "" 126 127 // Hide the generic empty state since we have AI results 128 if (this.hasEmptyTarget) { 129 this.emptyTarget.style.display = "none" 130 } 131 } 132 133 hideAiResults() { 134 if (this.hasAiResultsTarget) { 135 this.aiResultsTarget.style.display = "none" 136 } 137 } 138 139 updateURL(query) { 140 const url = new URL(window.location) 141 if (query) { 142 url.searchParams.set("q", query) 143 } else { 144 url.searchParams.delete("q") 145 } 146 history.replaceState({}, "", url) 147 } 148 149 showAll() { 150 this.categoryTargets.forEach(el => el.style.display = "") 151 this.articleTargets.forEach(el => el.style.display = "") 152 if (this.hasEmptyTarget) this.emptyTarget.style.display = "none" 153 if (this.hasCardsTarget) this.cardsTarget.style.display = "" 154 } 155}
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.