1import { Controller } from "@hotwired/stimulus" 2 3// #200 (Amir 2026-07-06): instant same-page results for the support search â the client's OWN 4// tickets + documentation (harvested index now; ma-10002's site-content-index.json later â see 5// docs/specs/STATIC-SITE-CONTRACT.md). â/â + Enter opens the highlighted result; Esc closes. 6// No submit anywhere: the old Search-button navigation to wpml.org is retired. 7export default class extends Controller { 8 static targets = ["input", "list"] 9 static values = { url: String } 10 11 connect() { 12 this.timer = null 13 this.highlighted = -1 14 } 15 16 query() { 17 clearTimeout(this.timer) 18 const q = this.inputTarget.value.trim() 19 if (q.length < 3) return this.close() 20 this.timer = setTimeout(async () => { 21 try { 22 const res = await fetch(`${this.urlValue}?q=${encodeURIComponent(q)}`, { headers: { Accept: "application/json" } }) 23 const data = await res.json() 24 this.render(data.results || []) 25 } catch { this.close() } 26 }, 200) 27 } 28 29 render(results) { 30 this.highlighted = -1 31 if (!results.length) { 32 this.listTarget.innerHTML = `<li class="px-4 py-3 text-sm text-gray-500">No matches</li>` 33 this.listTarget.hidden = false 34 return 35 } 36 this.listTarget.innerHTML = results.map(r => ` 37 <li> 38 <a href="${r.url}" class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm text-gray-800 hover:bg-brand-soft" data-support-search-target="item"> 39 <span class="truncate">${this.escape(r.title)}</span> 40 <span class="shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-gray-500">${this.escape(r.type)}</span> 41 </a> 42 </li>`).join("") 43 this.listTarget.hidden = false 44 } 45 46 keydown(event) { 47 if (this.listTarget.hidden) return 48 const items = this.listTarget.querySelectorAll("a") 49 if (event.key === "ArrowDown" || event.key === "ArrowUp") { 50 event.preventDefault() 51 this.highlighted += event.key === "ArrowDown" ? 1 : -1 52 this.highlighted = Math.max(-1, Math.min(items.length - 1, this.highlighted)) 53 items.forEach((a, i) => a.classList.toggle("bg-brand-soft", i === this.highlighted)) 54 } else if (event.key === "Enter" && this.highlighted >= 0) { 55 event.preventDefault() 56 items[this.highlighted].click() 57 } else if (event.key === "Escape") { 58 this.close() 59 } 60 } 61 62 outside(event) { 63 if (!this.element.contains(event.target)) this.close() 64 } 65 66 close() { 67 this.listTarget.hidden = true 68 this.listTarget.innerHTML = "" 69 this.highlighted = -1 70 } 71 72 escape(s) { 73 const d = document.createElement("div") 74 d.textContent = s ?? "" 75 return d.innerHTML 76 } 77}
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.