PageSourceSearch

https://www.vkdb.jp/assets/controllers/tag_group_select_controller-82b13b0d.js

js vkdb.jp collected 2026-09-25 04:23:52 UTC 3,728 bytes, 129 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// IndexGroup 単位のタグをインクリメンタル検索して選択するコンボボックス。
4// 選択すると、現在のURLの他のパラメータ(status, q など)は保持したまま
5// tag_index_id だけを差し替えて一覧ページへ遷移する。
6export default class extends Controller {
7  static targets = ["input", "results"]
8  static values = {
9    tags: Array
10  }
11
12  connect() {
13    this.activeIndex = -1
14    this.handleClickOutside = this.handleClickOutside.bind(this)
15    document.addEventListener('click', this.handleClickOutside)
16  }
17
18  disconnect() {
19    document.removeEventListener('click', this.handleClickOutside)
20  }
21
22  handleClickOutside(event) {
23    if (!this.element.contains(event.target)) {
24      this.hideResults()
25    }
26  }
27
28  search() {
29    const query = this.inputTarget.value.trim().toLowerCase()
30    const filtered = query
31      ? this.tagsValue.filter(tag => tag.name.toLowerCase().includes(query))
32      : this.tagsValue
33
34    this.displayResults(filtered)
35  }
36
37  onKeydown(event) {
38    const items = this.resultItems
39
40    switch (event.key) {
41      case 'ArrowDown':
42        event.preventDefault()
43        if (this.resultsTarget.classList.contains('hidden')) {
44          this.search()
45          return
46        }
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  displayResults(items) {
88    this.activeIndex = -1
89
90    if (items.length === 0) {
91      this.resultsTarget.innerHTML = `<div class="px-4 py-2 text-sm text-gray-400 dark:text-gray-500">該当するタグがありません</div>`
92    } else {
93      this.resultsTarget.innerHTML = items.map(tag => `
94        <div class="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer text-sm text-gray-900 dark:text-gray-100"
95             role="option"
96             data-action="click->tag-group-select#selectItem"
97             data-id="${tag.id}">
98          ${this.escapeHtml(tag.name)}
99        </div>
100      `).join('')
101    }
102
103    this.showResults()
104  }
105
106  selectItem(event) {
107    const id = event.currentTarget.dataset.id
108    const url = new URL(window.location.href)
109    url.searchParams.set('tag_index_id', id)
110    window.location.href = url.toString()
111  }
112
113  showResults() {
114    this.resultsTarget.classList.remove('hidden')
115    this.inputTarget.setAttribute('aria-expanded', 'true')
116  }
117
118  hideResults() {
119    this.activeIndex = -1
120    this.resultsTarget.classList.add('hidden')
121    this.inputTarget.setAttribute('aria-expanded', 'false')
122  }
123
124  escapeHtml(text) {
125    const div = document.createElement('div')
126    div.textContent = text
127    return div.innerHTML
128  }
129}

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.