PageSourceSearch

https://www.vkdb.jp/assets/controllers/unit_select_controller-5f9f7e39.js

js vkdb.jp collected 2026-09-25 04:24:00 UTC 4,019 bytes, 153 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Single-select autocomplete for unit lookup.
4// Sets a hidden unit_id field.
5export default class extends Controller {
6  static targets = ["input", "results", "unitId"]
7  static values = { url: String }
8
9  connect() {
10    this.timeout = null
11    this.activeIndex = -1
12    this.handleClickOutside = this.handleClickOutside.bind(this)
13    document.addEventListener('click', this.handleClickOutside)
14  }
15
16  disconnect() {
17    document.removeEventListener('click', this.handleClickOutside)
18  }
19
20  handleClickOutside(event) {
21    if (!this.element.contains(event.target)) {
22      this.hideResults()
23    }
24  }
25
26  search() {
27    clearTimeout(this.timeout)
28    const query = this.inputTarget.value.trim()
29
30    this.unitIdTarget.value = ''
31
32    if (query.length < 2) {
33      this.hideResults()
34      return
35    }
36
37    this.timeout = setTimeout(() => this.performSearch(query), 300)
38  }
39
40  onKeydown(event) {
41    const items = this.resultItems
42
43    switch (event.key) {
44      case 'ArrowDown':
45        event.preventDefault()
46        if (this.resultsTarget.classList.contains('hidden')) return
47        this.activeIndex = Math.min(this.activeIndex + 1, items.length - 1)
48        this.highlightItem()
49        break
50      case 'ArrowUp':
51        event.preventDefault()
52        if (this.resultsTarget.classList.contains('hidden')) return
53        this.activeIndex = Math.max(this.activeIndex - 1, 0)
54        this.highlightItem()
55        break
56      case 'Enter':
57        event.preventDefault()
58        if (this.activeIndex >= 0 && items[this.activeIndex]) {
59          items[this.activeIndex].click()
60        }
61        break
62      case 'Escape':
63        this.hideResults()
64        this.inputTarget.blur()
65        break
66    }
67  }
68
69  get resultItems() {
70    return Array.from(this.resultsTarget.querySelectorAll('[data-id]'))
71  }
72
73  highlightItem() {
74    const items = this.resultItems
75    items.forEach((item, index) => {
76      if (index === this.activeIndex) {
77        item.classList.add('bg-gray-100', 'dark:bg-gray-700')
78      } else {
79        item.classList.remove('bg-gray-100', 'dark:bg-gray-700')
80      }
81    })
82    if (items[this.activeIndex]) {
83      items[this.activeIndex].scrollIntoView({ block: 'nearest' })
84    }
85  }
86
87  async performSearch(query) {
88    try {
89      const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`, {
90        headers: {
91          'Accept': 'application/json',
92          'X-Requested-With': 'XMLHttpRequest'
93        }
94      })
95      if (!response.ok) throw new Error('Search failed')
96      const data = await response.json()
97      this.displayResults(data)
98    } catch (error) {
99      console.error('Unit select error:', error)
100    }
101  }
102
103  displayResults(items) {
104    this.activeIndex = -1
105
106    if (items.length === 0) {
107      this.hideResults()
108      return
109    }
110
111    this.resultsTarget.innerHTML = items.map(item => `
112      <div class="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
113           data-action="click->unit-select#selectItem"
114           data-id="${item.id}"
115           data-name="${this.escapeHtml(item.name)}">
116        <div class="font-medium text-gray-900 dark:text-gray-100">${this.escapeHtml(item.name)}</div>
117        ${item.key ? `<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">${this.escapeHtml(item.key)}</div>` : ''}
118      </div>
119    `).join('')
120
121    this.showResults()
122  }
123
124  selectItem(event) {
125    const id = event.currentTarget.dataset.id
126    const name = event.currentTarget.dataset.name
127
128    this.unitIdTarget.value = id
129    this.inputTarget.value = name
130
131    this.hideResults()
132  }
133
134  clear() {
135    this.unitIdTarget.value = ''
136    this.inputTarget.value = ''
137  }
138
139  showResults() {
140    this.resultsTarget.classList.remove('hidden')
141  }
142
143  hideResults() {
144    this.activeIndex = -1
145    this.resultsTarget.classList.add('hidden')
146  }
147
148  escapeHtml(text) {
149    const div = document.createElement('div')
150    div.textContent = text
151    return div.innerHTML
152  }
153}

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.