PageSourceSearch

https://www.georgetowncoc.com/assets/controllers/omni_search_controller-80f6f670.js

js georgetowncoc.com collected 2026-10-02 16:42:22 UTC 4,000 bytes, 152 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["input", "panel", "frame", "option"]
5
6  connect() {
7    this._open = false
8    this._debounceTimer = null
9    this.activeIndex = -1
10
11    this._onDocumentKeydown = (e) => {
12      const isK = (e.key || "").toLowerCase() === "k"
13      if (!isK) return
14      if (!(e.ctrlKey || e.metaKey)) return
15
16      e.preventDefault()
17      this.open()
18    }
19
20    this._onDocumentMousedown = (e) => {
21      if (!this._open) return
22      if (this.element.contains(e.target)) return
23      this.close()
24    }
25
26    document.addEventListener("keydown", this._onDocumentKeydown)
27    document.addEventListener("mousedown", this._onDocumentMousedown)
28  }
29
30  disconnect() {
31    document.removeEventListener("keydown", this._onDocumentKeydown)
32    document.removeEventListener("mousedown", this._onDocumentMousedown)
33    window.clearTimeout(this._debounceTimer)
34  }
35
36  open() {
37    this.panelTarget.classList.remove("hidden")
38    this._open = true
39    this.inputTarget.setAttribute("aria-expanded", "true")
40    this.inputTarget.focus()
41  }
42
43  close() {
44    this.panelTarget.classList.add("hidden")
45    this._open = false
46    this.inputTarget.setAttribute("aria-expanded", "false")
47    this.clearActiveOption()
48  }
49
50  keydown(e) {
51    switch (e.key) {
52      case "ArrowDown":
53        e.preventDefault()
54        this.open()
55        this.moveActiveOption(1)
56        break
57      case "ArrowUp":
58        e.preventDefault()
59        this.open()
60        this.moveActiveOption(-1)
61        break
62      case "Enter": {
63        const option = this.activeOption()
64        if (!option) return
65
66        e.preventDefault()
67        option.click()
68        break
69      }
70      case "Escape":
71        if (!this._open) return
72
73        e.preventDefault()
74        this.close()
75        break
76      case "Tab":
77        this.close()
78        break
79    }
80  }
81
82  input() {
83    this.open()
84    this.clearActiveOption()
85
86    const q = this.inputTarget.value.trim()
87    const url = new URL(this.frameTarget.dataset.src, window.location.origin)
88
89    window.clearTimeout(this._debounceTimer)
90    this._debounceTimer = window.setTimeout(() => {
91      url.searchParams.set("q", q)
92      this.frameTarget.src = url.toString()
93    }, 200)
94  }
95
96  resultsLoaded() {
97    // `input()` clears the previous result selection before requesting a new
98    // frame. Do not clear a selection the user made after the new options
99    // rendered but before Turbo emitted its frame-load event.
100    if (this.activeIndex >= this.optionTargets.length) this.clearActiveOption()
101  }
102
103  activateOption(event) {
104    const index = this.optionTargets.indexOf(event.currentTarget)
105    if (index >= 0) this.setActiveOption(index)
106  }
107
108  moveActiveOption(direction) {
109    const options = this.optionTargets
110    if (options.length === 0) return
111
112    let nextIndex
113    if (this.activeIndex < 0) {
114      nextIndex = direction > 0 ? 0 : options.length - 1
115    } else {
116      nextIndex = (this.activeIndex + direction) % options.length
117      if (nextIndex < 0) nextIndex += options.length
118    }
119
120    this.setActiveOption(nextIndex)
121  }
122
123  activeOption() {
124    return this.activeIndex >= 0 ? this.optionTargets[this.activeIndex] : null
125  }
126
127  setActiveOption(index) {
128    this.activeIndex = index
129
130    this.optionTargets.forEach((option, optionIndex) => {
131      const active = optionIndex === index
132      option.classList.toggle("bg-zinc-100", active)
133      option.setAttribute("aria-selected", active ? "true" : "false")
134    })
135
136    const option = this.activeOption()
137    if (!option) return
138
139    this.inputTarget.setAttribute("aria-activedescendant", option.id)
140    option.scrollIntoView({ block: "nearest" })
141  }
142
143  clearActiveOption() {
144    this.activeIndex = -1
145    this.inputTarget.removeAttribute("aria-activedescendant")
146
147    this.optionTargets.forEach((option) => {
148      option.classList.remove("bg-zinc-100")
149      option.setAttribute("aria-selected", "false")
150    })
151  }
152}

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.