PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/ecommerce/help_search_controller-6820f362.js

js littlebirdelectronics.com.au collected 2026-09-25 14:10:35 UTC 4,199 bytes, 155 lines download raw bytes

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.