1import { Controller } from '@hotwired/stimulus' 2 3// Dropdown controller â replaces dropdown.js (jQuery) with Stimulus. 4// 5// Usage: 6// <div data-controller="dropdown"> 7// <a href="#" data-action="click->dropdown#toggle" data-dropdown-target="trigger">Label â¾</a> 8// <ul data-dropdown-target="menu" class="hidden"> 9// <li data-type="status" data-id="unread"><a>Unread</a></li> 10// </ul> 11// </div> 12// 13// Values: 14// data-dropdown-urls-preset-value="true" â skip client-side URL building (hrefs already set) 15// 16export default class extends Controller { 17 static targets = ['trigger', 'menu'] 18 static values = { urlsPreset: { type: Boolean, default: false } } 19 20 connect () { 21 this._onClickOutside = this._onClickOutside.bind(this) 22 window.addEventListener('click', this._onClickOutside, true) 23 window.addEventListener('touchend', this._onClickOutside, true) 24 } 25 26 disconnect () { 27 window.removeEventListener('click', this._onClickOutside, true) 28 window.removeEventListener('touchend', this._onClickOutside, true) 29 } 30 31 toggle (event) { 32 event.preventDefault() 33 event.stopPropagation() 34 35 const wasOpen = !this.menuTarget.classList.contains('hidden') 36 37 // Close all other dropdowns first 38 this._closeAllDropdowns() 39 40 if (wasOpen) { 41 this.close() 42 } else { 43 this._open() 44 } 45 } 46 47 close () { 48 this.menuTarget.classList.add('hidden') 49 this.triggerTarget.classList.remove('active') 50 this._resetFilterState() 51 } 52 53 // -- Private ---------------------------------------------------------------- 54 55 _open () { 56 this.menuTarget.classList.remove('hidden') 57 this.triggerTarget.classList.add('active') 58 this._highlightSelected() 59 if (!this.urlsPresetValue) this._buildUrls() 60 this._scrollToSelected() 61 } 62 63 _closeAllDropdowns () { 64 this.application.controllers 65 .filter(c => c.identifier === 'dropdown' && c !== this) 66 .forEach(c => c.close()) 67 } 68 69 _onClickOutside (event) { 70 if (this.element.contains(event.target)) { 71 // Close the menu when clicking inside the menu (but not on the trigger) 72 if (this.menuTarget.contains(event.target) && !this.menuTarget.classList.contains('add-filter-menu')) { 73 this.close() 74 } 75 return 76 } 77 // Don't close if clicking inside another add-filter-menu 78 if (event.target.closest && event.target.closest('.add-filter-menu')) return 79 this.close() 80 } 81 82 // Build hrefs from data-type / data-id attributes on the current URL 83 _buildUrls () { 84 const pathname = window.location.pathname 85 const search = window.location.search.replace(/(current_id=\d*&?)/, '') 86 87 this.menuTarget.querySelectorAll('li[data-type]').forEach(li => { 88 const key = li.dataset.type 89 const value = li.dataset.id 90 const link = li.querySelector('a') 91 if (!link) return 92 93 if (key === 'source_locales') { 94 const match = pathname.match(/locales\/(.*\.\.)/) 95 if (match) link.href = pathname.replace(match[1], value + '..') + search 96 return 97 } 98 99 if (key === 'target_locales') { 100 const match = pathname.match(/(\.\..*)\/strings/) 101 if (match) link.href = pathname.replace(match[1], '..' + value) + search 102 return 103 } 104 105 const urlParams = new URLSearchParams(search) 106 if (value === '') urlParams.delete(key) 107 else urlParams.set(key, value) 108 link.href = urlParams.size === 0 ? pathname : `${pathname}?${urlParams}` 109 }) 110 } 111 112 // Mark the currently-active filter item with .selected 113 _highlightSelected () { 114 const filterId = this.menuTarget.id 115 if (!filterId) return 116 const filterName = filterId.replace(/_filter$/, '') 117 const value = this._currentValue(filterName) 118 119 this.menuTarget.querySelectorAll('li.selected').forEach(li => li.classList.remove('selected')) 120 const match = this.menuTarget.querySelector(`li[data-type="${filterName}"][data-id="${value}"]`) 121 if (match) match.classList.add('selected') 122 } 123 124 _currentValue (filter) { 125 const pathname = window.location.pathname 126 const urlParams = new URLSearchParams(window.location.search.replace(/#$/, '')) 127 128 switch (filter) { 129 case 'source_locales': { 130 const m = pathname.match(/locales\/(.*)\.\./); return m ? m[1] : '' 131 } 132 case 'target_locales': { 133 const m = pathname.match(/\.\.(.*)\/(strings|terms)/); return m ? m[1] : '' 134 } 135 case 'statistics': { 136 const m = pathname.match(/statistics\/(.*)$/); return m ? m[1] : '' 137 } 138 default:
139 return urlParams.get(filter) || '' 140 } 141 } 142 143 _scrollToSelected () { 144 const selected = this.menuTarget.querySelector('li.selected') 145 if (selected) selected.scrollIntoView({ block: 'nearest' }) 146 } 147 148 // Reset add-filter two-step navigation back to the filter-types panel 149 _resetFilterState () { 150 if (!this.menuTarget.classList.contains('add-filter-menu')) return 151 const types = this.menuTarget.querySelector('.filter-types') 152 const values = this.menuTarget.querySelector('.filter-values') 153 if (types) types.classList.remove('hidden') 154 if (values) values.classList.add('hidden') 155 this.menuTarget.querySelectorAll('.filter-value-panel').forEach(p => p.classList.remove('active')) 156 } 157};
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.