1import {Controller} from '@hotwired/stimulus' 2 3export default class extends Controller { 4 connect() { 5 this.observers = [] 6 this.setupSelectize() 7 this.fixSelectizeForAda() 8 } 9 10 disconnect() { 11 this.observers.forEach(o => o.disconnect()) 12 } 13 14 setupSelectize() { 15 const defaultDelimiter = '|'; 16 const maxLimit = js_vars.configuration.job_alert_field_selection_limit; 17 const defaultMaxOptions = 1500; // Selectize defaults to 1000 rendered options, too low for custom fields with large option lists 18 // TODO: get rid of the loops 19 for (const field of ['departments', 'categories', 'levels', 'employment_types', 'workplace_types', 'locations', ...js_vars.configuration.field_mapping_names]) { 20 for (const select of this.element.querySelectorAll(`select[data-job-alert-field="${field}"]`)) { 21 const fieldName = select.dataset.fieldName 22 const $select = $ctj(select) 23 $select.selectize({ 24 onInitialize: () => { 25 this.setAriaLabelsOnSelectizeInput(select, fieldName) 26 this.setAriaLabelsOnSelectizeDropdown(select) 27 }, 28 delimiter: defaultDelimiter, 29 maxItems: maxLimit, 30 maxOptions: defaultMaxOptions, 31 valueField: 'value', 32 labelField: 'label', 33 searchField: 'label', 34 closeAfterSelect: true, 35 render: { 36 item: (item, escape) => `<div class="item">${escape(item.label)}</div>`, 37 option: (item, escape) => `<div id="selectize-option-${select.dataset.jobAlertField}-${escape(item.value)}" role="option" class="option" aria-label="${escape(item.label)}" aria-selected="false">${escape(item.label)}</div>` 38 }, 39 onDropdownOpen: () => this.updateSelectizeInputAriaExpanded(select, true), 40 onDropdownClose: () => this.updateSelectizeInputAriaExpanded(select, false), 41 onItemAdd: () => {this.updateSelectizeItemCount(select); this.updateSelectizeDropdownsAriaLabel(select)}, 42 onItemRemove: () => {this.updateSelectizeItemCount(select); this.updateSelectizeDropdownsAriaLabel(select)} 43 }) 44 const control = select.nextElementSibling 45 const observer = new MutationObserver(() => { 46 const active = control.querySelector('.option.active') 47 const input = control.querySelector('.selectize-input input') 48 active ? input.setAttribute('aria-activedescendant', active.id) : input.removeAttribute('aria-activedescendant') 49 }) 50 observer.observe(control.querySelector('.selectize-dropdown-content'), {subtree: true, attributes: true, attributeFilter: ['class']}) 51 this.observers.push(observer) 52 } 53 } 54 } 55 56 fixSelectizeForAda() { 57 for (const label of this.element.querySelectorAll('label.ada-remove-selectize')) { 58 label.setAttribute('aria-hidden', 'true') 59 } 60 } 61 62 setAriaLabelsOnSelectizeInput(select, fieldName) { 63 const selectizeInputInput = select.nextElementSibling.querySelector('.selectize-input input') 64 selectizeInputInput.setAttribute('role', 'combobox') 65 selectizeInputInput.setAttribute('aria-haspopup', 'listbox') 66 selectizeInputInput.setAttribute('aria-controls', `selectize-dropdown-${select.dataset.jobAlertField}`) 67 selectizeInputInput.setAttribute('aria-expanded', 'false') 68 selectizeInputInput.setAttribute('aria-label', fieldName) 69 } 70 71 setAriaLabelsOnSelectizeDropdown(select) { 72 const dropdown = select.nextElementSibling.querySelector('.selectize-dropdown') 73 dropdown.setAttribute('id', `selectize-dropdown-${select.dataset.jobAlertField}`) 74 dropdown.setAttribute('role', 'listbox') 75 dropdown.setAttribute('aria-multiselectable', 'true') 76 dropdown.querySelector('.selectize-dropdown-content').setAttribute('role', 'presentation') 77 this.updateSelectizeDropdownsAriaLabel(select) 78 } 79 80 updateSelectizeDropdownsAriaLabel(select) { 81 const selectedLabels = select.selectize.getValue().map(v => select.selectize.options[v].label) 82 let label = select.dataset.listboxLabel 83 if (selectedLabels.length > 0) { 84 label += '. ' + select.dataset.selectedSuffix.replace('%{items}', selectedLabels.join(', ')) 85 } 86 select.nextElementSibling.querySelector('.selectize-dropdown').setAttribute('aria-label', label) 87 } 88
89 updateSelectizeInputAriaExpanded(select, expanded) { 90 const selectizeInputInput = select.nextElementSibling.querySelector('.selectize-input input') 91 selectizeInputInput.setAttribute('aria-expanded', expanded) 92 } 93 94 updateSelectizeItemCount(select) { 95 const selectedValues = select.selectize.getValue() 96 const itemsSelected = selectedValues.length 97 const totalDropdownOptions = Object.keys(select.selectize.options).length 98 const fieldName = select.dataset.fieldName 99 const dropdownItemsSelectedLabel = select.dataset.dropdownLabel.replace('%{items_selected}', itemsSelected).replace('%{total_dropdown_options}', totalDropdownOptions).replace('%{field_name}', fieldName) 100 const selectedLabels = selectedValues.map(v => select.selectize.options[v].label) 101 const countAnnouncer = this.element.querySelector(`span[data-count-for="${select.id}"]`) 102 setTimeout(() => { 103 countAnnouncer.textContent = [...selectedLabels, dropdownItemsSelectedLabel].join(', ') 104 }, 200) 105 } 106}
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.