1/** 2 * SaddlebackDropdown 3 * 4 * Reusable wrapper around the site-wide select2 dropdown pattern 5 * (`.saddleback-dropdown form-selector` + jQuery select2). jQuery and select2 6 * are loaded globally in MasterLayout, so `$` is available at runtime. 7 * 8 * Usage (v-model friendly): 9 * <SaddlebackDropdown 10 * dropdownId="mySort" 11 * label="Sort By" 12 * :options="[{ id: 'newest', text: 'Newest First' }]" 13 * v-model="sortBy" 14 * /> 15 */ 16export default { 17 name: 'SaddlebackDropdown', 18 props: { 19 // Stable, unique DOM id for the underlying <select> (select2 needs one). 20 dropdownId: { 21 type: String, 22 required: true, 23 }, 24 // Site-wide select2 data shape: [{ id, text }]. 25 options: { 26 type: Array, 27 default: () => [], 28 }, 29 modelValue: { 30 type: [String, Number], 31 default: '', 32 }, 33 // Optional visible label rendered above the control. 34 label: { 35 type: String, 36 default: '', 37 }, 38 // Accessible name; falls back to `label`. 39 ariaLabel: { 40 type: String, 41 default: '', 42 }, 43 // Show the select2 search box (hidden by default to match site usage). 44 searchable: { 45 type: Boolean, 46 default: false, 47 }, 48 }, 49 emits: ['update:modelValue', 'change'], 50 watch: { 51 modelValue(newValue) { 52 const $el = $(`#${this.dropdownId}`); 53 // Guard against feedback loops: only push down when it actually differs. 54 if ($el.val() !== String(newValue)) { 55 $el.val(newValue).trigger('change.select2'); 56 } 57 }, 58 options: { 59 deep: true, 60 handler(newOptions) { 61 // Rebuild when options change (e.g. derived from async data). 62 this.destroyDropdown(); 63 this.initDropdown(newOptions); 64 }, 65 }, 66 }, 67 mounted() { 68 this.initDropdown(this.options); 69 }, 70 beforeUnmount() { 71 this.destroyDropdown(); 72 }, 73 methods: { 74 initDropdown(options) { 75 $(`#${this.dropdownId}`) 76 .select2({ 77 minimumResultsForSearch: this.searchable ? 0 : Infinity, 78 mobile: true, 79 width: '100%', 80 data: options, 81 }) 82 // Native `change` = user interaction; `change.select2` (used for 83 // programmatic updates below) does not re-fire this handler. 84 .on('change', (e) => { 85 const value = e.target.value; 86 this.$emit('update:modelValue', value); 87 this.$emit('change', value); 88 }) 89 .val(this.modelValue) 90 .trigger('change.select2'); 91 }, 92 destroyDropdown() { 93 const $el = $(`#${this.dropdownId}`); 94 if ($el.data('select2')) { 95 $el.off('change'); 96 $el.select2('destroy'); 97 $el.empty(); 98 } 99 }, 100 }, 101 template: ` 102 <div class="saddleback-dropdown form-selector"> 103 <label v-if="label" :for="dropdownId" class="dropdown-label">{{ label }}</label> 104 <select :id="dropdownId" :aria-label="ariaLabel || label"></select> 105 </div> 106 `, 107};
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.