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.