1/* global Turbo */ 2import { Controller } from '@hotwired/stimulus' 3import { post, destroy } from '@rails/request.js' 4 5export default class extends Controller { 6 static targets = ['dropdown', 'searchInput', 'collectionsList'] 7 static values = { 8 productId: Number, 9 addUrl: String, 10 removeUrl: String, 11 isOpen: { type: Boolean, default: false } 12 } 13 14 connect () { 15 this.boundHandleClickOutside = this.handleClickOutside.bind(this) 16 this.boundHandleEscape = this.handleEscape.bind(this) 17 18 // Restore open state if it was open before re-render 19 if (this.isOpenValue) { 20 this.open() 21 } 22 } 23 24 disconnect () { 25 document.removeEventListener('click', this.boundHandleClickOutside) 26 document.removeEventListener('keydown', this.boundHandleEscape) 27 } 28 29 toggle (event) { 30 event.preventDefault() 31 event.stopPropagation() 32 33 if (this.dropdownTarget.classList.contains('hidden')) { 34 this.open() 35 } else { 36 this.close() 37 } 38 } 39 40 open () { 41 if (this.hasDropdownTarget) { 42 this.dropdownTarget.classList.remove('hidden') 43 } 44 this.searchInputTarget.value = '' 45 this.filterCollections('') 46 this.searchInputTarget.focus() 47 this.isOpenValue = true 48 49 // Add click outside listener after a small delay to avoid immediate closure 50 setTimeout(() => { 51 document.addEventListener('click', this.boundHandleClickOutside) 52 document.addEventListener('keydown', this.boundHandleEscape) 53 }, 10) 54 } 55 56 close () { 57 if (this.hasDropdownTarget) { 58 this.dropdownTarget.classList.add('hidden') 59 } 60 this.isOpenValue = false 61 document.removeEventListener('click', this.boundHandleClickOutside) 62 document.removeEventListener('keydown', this.boundHandleEscape) 63 } 64 65 handleClickOutside (event) { 66 if (!this.element.contains(event.target)) { 67 this.close() 68 } 69 } 70 71 handleEscape (event) { 72 if (event.key === 'Escape') { 73 this.close() 74 } 75 } 76 77 search (event) { 78 const query = event.target.value.toLowerCase() 79 this.filterCollections(query) 80 } 81 82 filterCollections (query) { 83 const items = this.collectionsListTarget.querySelectorAll('[data-collection-item]') 84 85 items.forEach(item => { 86 const name = item.dataset.collectionName.toLowerCase() 87 if (name.includes(query)) { 88 item.classList.remove('hidden') 89 } else { 90 item.classList.add('hidden') 91 } 92 }) 93 } 94 95 async selectCollection (event) { 96 event.preventDefault() 97 event.stopPropagation() 98 99 const collectionId = event.currentTarget.dataset.collectionId 100 const isAdded = event.currentTarget.dataset.isAdded === 'true' 101 const button = event.currentTarget 102 103 // Disable button to prevent double clicks 104 button.disabled = true 105 106 try { 107 const url = isAdded ? this.removeUrlValue : this.addUrlValue 108 const requestMethod = isAdded ? destroy : post 109 110 const response = await requestMethod(url, { 111 headers: { 112 Accept: 'text/vnd.turbo-stream.html' 113 }, 114 body: JSON.stringify({ 115 collection_id: collectionId 116 }) 117 }) 118 119 if (response.ok) { 120 const html = await response.text 121 Turbo.renderStreamMessage(html) 122 123 // Dispatch event 124 const eventName = isAdded ? 'removed' : 'added' 125 this.dispatch(eventName, { detail: { collectionId } }) 126 } else { 127 console.error('Failed to update collection') 128 button.disabled = false 129 } 130 } catch (error) { 131 console.error('Error updating collection:', error) 132 button.disabled = false 133 } 134 } 135};
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.