1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["input", "dropdown", "spinner", "clear"] 5 static values = { 6 minChars: Number, 7 url: String 8 } 9 10 connect() { 11 this.minCharsValue = this.minCharsValue || 2 12 this.urlValue = this.urlValue || "/searches/search" 13 this.debounceTimer = null 14 this.lastQuery = "" 15 16 // Handle clicks outside to close dropdown 17 this.outsideClickHandler = this.handleOutsideClick.bind(this) 18 document.addEventListener('click', this.outsideClickHandler) 19 } 20 21 disconnect() { 22 document.removeEventListener('click', this.outsideClickHandler) 23 } 24 25 search() { 26 const query = this.inputTarget.value.trim() 27 28 // Clear button visibility 29 this.toggleClearButton(query.length > 0) 30 31 // Don't search if too short or unchanged 32 if (query.length < this.minCharsValue) { 33 this.hideDropdown() 34 return 35 } 36 37 if (query === this.lastQuery) return 38 39 this.lastQuery = query 40 this.showSpinner() 41 42 clearTimeout(this.debounceTimer) 43 this.debounceTimer = setTimeout(() => { 44 this.fetchSearchResults(query) 45 }, 300) 46 } 47 48 async fetchSearchResults(query) { 49 try { 50 const url = `${this.urlValue}?query=${encodeURIComponent(query)}` 51 const response = await fetch(url, { 52 headers: { 53 'Accept': 'text/vnd.turbo-stream.html', 54 'X-Requested-With': 'XMLHttpRequest' 55 } 56 }) 57 58 if (response.ok) { 59 const html = await response.text() 60 this.dropdownTarget.innerHTML = html 61 this.showDropdown() 62 } 63 } catch (error) { 64 console.error("Search error:", error) 65 } finally { 66 this.hideSpinner() 67 } 68 } 69 70 clear(event) { 71 if (event) event.preventDefault() 72 this.inputTarget.value = "" 73 this.lastQuery = "" 74 this.hideDropdown() 75 this.toggleClearButton(false) 76 this.inputTarget.focus() 77 } 78 79 showDropdown() { 80 if (this.hasDropdownTarget) { 81 this.dropdownTarget.classList.remove("hidden") 82 } 83 } 84 85 hideDropdown() { 86 if (this.hasDropdownTarget) { 87 this.dropdownTarget.classList.add("hidden") 88 } 89 } 90 91 showSpinner() { 92 if (this.hasSpinnerTarget) { 93 this.spinnerTarget.classList.remove("hidden") 94 } 95 } 96 97 hideSpinner() { 98 if (this.hasSpinnerTarget) { 99 this.spinnerTarget.classList.add("hidden") 100 } 101 } 102 103 toggleClearButton(show) { 104 if (this.hasClearTarget) { 105 if (show) { 106 this.clearTarget.classList.remove("hidden") 107 } else { 108 this.clearTarget.classList.add("hidden") 109 } 110 } 111 } 112 113 handleOutsideClick(event) { 114 // Don't close if clicking inside component 115 if (this.element.contains(event.target)) return 116 117 this.hideDropdown() 118 } 119};
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.