PageSourceSearch

https://albacore.ca/assets/controllers/shared/image_select_controller-b43571c4.js

js albacore.ca collected 2026-09-25 09:00:09 UTC 5,510 bytes, 193 lines download raw bytes

1// app/javascript/controllers/shared/image_select_controller.js
2
3import { Controller } from "@hotwired/stimulus"
4
5export default class extends Controller {
6  static targets = ["trigger", "dropdown", "input", "selectedImage", "displayText", "searchInput", "options", "createButton"]
7  static values = {
8    imageSize: { type: Number, default: 32 },
9    searchable: { type: Boolean, default: false },
10    creatableText: String
11  }
12
13  connect() {
14    // Initialize display if a value is pre-selected
15    this.initializeDisplay()
16
17    // Close dropdown on escape key
18    this.handleKeydown = this.handleKeydown.bind(this)
19    document.addEventListener('keydown', this.handleKeydown)
20
21    // Close dropdown when clicking outside
22    this.handleClickOutside = this.handleClickOutside.bind(this)
23    document.addEventListener('click', this.handleClickOutside)
24  }
25
26  disconnect() {
27    document.removeEventListener('keydown', this.handleKeydown)
28    document.removeEventListener('click', this.handleClickOutside)
29  }
30
31  initializeDisplay() {
32    const selectedId = this.inputTarget.value
33    if (selectedId) {
34      const selectedOption = this.dropdownTarget.querySelector(`[data-option-id='${selectedId}']`)
35      if (selectedOption) {
36        const optionLabel = selectedOption.dataset.optionLabel
37        const optionImage = selectedOption.dataset.optionImage
38
39        this.displayTextTarget.textContent = optionLabel
40        this.displayTextTarget.classList.remove('text-gray-500')
41        this.displayTextTarget.classList.add('text-gray-900')
42
43        this.updateImage(optionImage, optionLabel)
44      }
45    }
46  }
47
48  handleKeydown(e) {
49    if (e.key === 'Escape' && !this.dropdownTarget.classList.contains('hidden')) {
50      this.close()
51    }
52  }
53
54  handleClickOutside(e) {
55    if (!this.element.contains(e.target) && !this.dropdownTarget.classList.contains('hidden')) {
56      this.close()
57    }
58  }
59
60  /**
61   * Filter options based on search input
62   */
63  filterOptions(event) {
64    const query = event.target.value.toLowerCase()
65    const options = this.optionsTargets
66
67    let visibleCount = 0
68    options.forEach(option => {
69      const label = option.dataset.optionLabel.toLowerCase()
70      const matches = label.includes(query)
71      option.classList.toggle('hidden', !matches)
72      if (matches) visibleCount++
73    })
74
75    // Show/hide create button based on results
76    if (this.hasCreateButtonTarget && this.creatableTextValue) {
77      const hasResults = visibleCount > 0
78      this.createButtonTarget.classList.toggle('hidden', hasResults)
79
80      // Update create button text with search query
81      if (!hasResults && query.length > 0) {
82        const createLabel = this.createButtonTarget.querySelector('[data-create-label]')
83        if (createLabel) {
84          createLabel.textContent = `Create "${query}"`
85        }
86      }
87    }
88  }
89
90  /**
91   * Handle create button click
92   */
93  createOption(event) {
94    event.preventDefault()
95    event.stopPropagation()
96
97    const searchInput = this.hasSearchInputTarget ? this.searchInputTarget : null
98    if (!searchInput) return
99
100    const text = searchInput.value.trim()
101    if (!text) return
102
103    // Store the text to create in a data attribute
104    this.element.dataset.pendingClubName = text
105
106    // Update display to show the pending name
107    this.displayTextTarget.textContent = text
108    this.displayTextTarget.classList.remove('text-gray-500')
109    this.displayTextTarget.classList.add('text-gray-900')
110
111    // Clear input and close dropdown
112    searchInput.value = ''
113    this.close()
114  }
115
116  /**
117   * Toggle dropdown visibility
118   */
119  toggle(event) {
120    event.preventDefault()
121    event.stopPropagation()
122    this.dropdownTarget.classList.contains('hidden') ? this.open() : this.close()
123  }
124
125  /**
126   * Open dropdown
127   */
128  open() {
129    this.dropdownTarget.classList.remove('hidden')
130    // Focus search input if it exists
131    if (this.hasSearchInputTarget) {
132      setTimeout(() => this.searchInputTarget.focus(), 0)
133    }
134  }
135
136  /**
137   * Close dropdown
138   */
139  close() {
140    this.dropdownTarget.classList.add('hidden')
141  }
142
143  /**
144   * Select an option from the dropdown
145   */
146  select(event) {
147    event.preventDefault()
148    event.stopPropagation()
149
150    const button = event.currentTarget
151    const optionId = button.dataset.optionId
152    const optionLabel = button.dataset.optionLabel
153    const optionImage = button.dataset.optionImage
154
155    // Update hidden input value
156    this.inputTarget.value = optionId
157    this.inputTarget.dispatchEvent(new Event('change', { bubbles: true }))
158
159    // Update display text
160    this.displayTextTarget.textContent = optionLabel
161    if (optionId === "") {
162      this.displayTextTarget.classList.add('text-gray-500')
163      this.displayTextTarget.classList.remove('text-gray-900')
164    } else {
165      this.displayTextTarget.classList.remove('text-gray-500')
166      this.displayTextTarget.classList.add('text-gray-900')
167    }
168
169    // Update selected image
170    this.updateImage(optionImage, optionLabel)
171
172    // Close dropdown
173    this.close()
174  }
175
176  /**
177   * Update the selected image
178   */
179  updateImage(imageUrl, altText) {
180    const container = this.selectedImageTarget
181    container.innerHTML = ''
182
183    if (imageUrl) {
184      const img = document.createElement('img')
185      img.src = imageUrl
186      img.alt = altText
187      img.className = 'object-contain rounded'
188      img.style.width = `${this.imageSizeValue}px`
189      img.style.height = `${this.imageSizeValue}px`
190      container.appendChild(img)
191    }
192  }
193}

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.