1// File#: _1_custom-select 2// Usage: codyhouse.co/license 3(function() { 4 // NOTE: you need the js code only when using the --custom-dropdown variation of the Custom Select component. Default version does nor require JS. 5 6 var CustomSelect = function(element) { 7 this.element = element; 8 this.select = this.element.getElementsByTagName('select')[0]; 9 this.optGroups = this.select.getElementsByTagName('optgroup'); 10 this.options = this.select.getElementsByTagName('option'); 11 this.selectedOption = getSelectedOptionText(this); 12 this.selectId = this.select.getAttribute('id'); 13 this.trigger = false; 14 this.dropdown = false; 15 this.customOptions = false; 16 this.arrowIcon = this.element.getElementsByTagName('svg'); 17 this.label = document.querySelector('[for="'+this.selectId+'"]'); 18 19 this.optionIndex = 0; // used while building the custom dropdown 20 21 initCustomSelect(this); // init markup 22 initCustomSelectEvents(this); // init event listeners 23 }; 24 25 function initCustomSelect(select) { 26 // create the HTML for the custom dropdown element 27 select.element.insertAdjacentHTML('beforeend', initButtonSelect(select) + initListSelect(select)); 28 29 // save custom elements 30 select.dropdown = select.element.getElementsByClassName('js-select__dropdown')[0]; 31 select.trigger = select.element.getElementsByClassName('js-select__button')[0]; 32 select.customOptions = select.dropdown.getElementsByClassName('js-select__item'); 33 34 // hide default select 35 Util.addClass(select.select, 'is-hidden'); 36 if(select.arrowIcon.length > 0 ) select.arrowIcon[0].style.display = 'none'; 37 38 // place dropdown 39 placeDropdown(select); 40 }; 41 42 function initCustomSelectEvents(select) { 43 // option selection in dropdown 44 initSelection(select); 45 46 // click events 47 select.trigger.addEventListener('click', function(){ 48 toggleCustomSelect(select, false); 49 }); 50 if(select.label) { 51 // move focus to custom trigger when clicking on <select> label 52 select.label.addEventListener('click', function(){ 53 Util.moveFocus(select.trigger); 54 }); 55 } 56 // keyboard navigation 57 select.dropdown.addEventListener('keydown', function(event){ 58 if(event.keyCode && event.keyCode == 38 || event.key && event.key.toLowerCase() == 'arrowup') { 59 keyboardCustomSelect(select, 'prev', event); 60 } else if(event.keyCode && event.keyCode == 40 || event.key && event.key.toLowerCase() == 'arrowdown') { 61 keyboardCustomSelect(select, 'next', event); 62 } 63 }); 64 // native <select> element has been updated -> update custom select as well 65 select.element.addEventListener('select-updated', function(event){ 66 resetCustomSelect(select); 67 }); 68 }; 69 70 function toggleCustomSelect(select, bool) { 71 var ariaExpanded; 72 if(bool) { 73 ariaExpanded = bool; 74 } else { 75 ariaExpanded = select.trigger.getAttribute('aria-expanded') == 'true' ? 'false' : 'true'; 76 } 77 select.trigger.setAttribute('aria-expanded', ariaExpanded); 78 if(ariaExpanded == 'true') { 79 var selectedOption = getSelectedOption(select); 80 Util.moveFocus(selectedOption); // fallback if transition is not supported 81 select.dropdown.addEventListener('transitionend', function cb(){ 82 Util.moveFocus(selectedOption); 83 select.dropdown.removeEventListener('transitionend', cb); 84 }); 85 placeDropdown(select); // place dropdown based on available space 86 } 87 }; 88 89 function placeDropdown(select) { 90 // remove placement classes to reset position 91 Util.removeClass(select.dropdown, 'select__dropdown--right select__dropdown--up'); 92 var triggerBoundingRect = select.trigger.getBoundingClientRect(); 93 Util.toggleClass(select.dropdown, 'select__dropdown--right', (document.documentElement.clientWidth - 5 < triggerBoundingRect.left + select.dropdown.offsetWidth)); 94 // check if there's enough space up or down 95 var moveUp = (window.innerHeight - triggerBoundingRect.bottom - 5) < triggerBoundingRect.top; 96 Util.toggleClass(select.dropdown, 'select__dropdown--up', moveUp); 97 // check if we need to set a max width 98 var maxHeight = moveUp ? triggerBoundingRect.top - 20 : window.innerHeight - triggerBoundingRect.bottom - 20; 99 // set max-height based on available space 100 select.dropdown.setAttribute('style', 'max-height: '+maxHeight+'px; width: '+triggerBoundingRect.width+'px;'); 101 }; 102 103 function keyboardCustomSelect(select, direction, event) { // navigate custom dropdown with keyboard 104 event.preventDefault(); 105 var index = Util.getIndexInArray(select.customOptions, document.activeElement); 106 index = (direction == 'next') ? index + 1 : index - 1; 107 if(index < 0) index = select.customOptions.length - 1; 108 if(index >= select.customOptions.length) index = 0; 109 Util.moveFocus(select.customOptions[index]); 110 }; 111 112 function initSelection(select) { // option selection 113 select.dropdown.addEventListener('click', function(event){ 114 var option = event.target.closest('.js-select__item'); 115 if(!option) return; 116 selectOption(select, option); 117 }); 118 }; 119 120 function selectOption(select, option) { 121 if(option.hasAttribute('aria-selected') && option.getAttribute('aria-selected') == 'true') { 122 // selecting the same option 123 select.trigger.setAttribute('aria-expanded', 'false'); // hide dropdown 124 } else { 125 var selectedOption = select.dropdown.querySelector('[aria-selected="true"]'); 126 if(selectedOption) selectedOption.setAttribute('aria-selected', 'false'); 127 option.setAttribute('aria-selected', 'true'); 128 select.trigger.getElementsByClassName('js-select__label')[0].textContent = option.textContent; 129 select.trigger.setAttribute('aria-expanded', 'false'); 130 // new option has been selected -> update native <select> element _ arai-label of trigger <button> 131 updateNativeSelect(select, option.getAttribute('data-index')); 132 updateTriggerAria(select); 133 } 134 // move focus back to trigger 135 select.trigger.focus(); 136 }; 137 138 function updateNativeSelect(select, index) { 139 select.select.selectedIndex = index; 140 select.select.dispatchEvent(new CustomEvent('change', {bubbles: true})); // trigger change event 141 }; 142 143 function updateTriggerAria(select) { 144 select.trigger.setAttribute('aria-label', select.options[select.select.selectedIndex].innerHTML+', '+select.label.textContent); 145 }; 146 147 function getSelectedOptionText(select) {// used to initialize the label of the custom select button 148 var label = ''; 149 if('selectedIndex' in select.select) { 150 label = select.options[select.select.selectedIndex].text; 151 } else { 152 label = select.select.querySelector('option[selected]').text; 153 } 154 return label; 155 156 }; 157 158 function initButtonSelect(select) { // create the button element -> custom select trigger 159 // check if we need to add custom classes to the button trigger 160 var customClasses = select.element.getAttribute('data-trigger-class') ? ' '+select.element.getAttribute('data-trigger-class') : ''; 161 162 var label = select.options[select.select.selectedIndex].innerHTML+', '+select.label.textContent; 163 164 var button = '<button type="button" class="js-select__button select__button'+customClasses+'" aria-label="'+label
164+'" aria-expanded="false" aria-controls="'+select.selectId+'-dropdown"><span aria-hidden="true" class="js-select__label select__label">'+select.selectedOption+'</span>'; 165 if(select.arrowIcon.length > 0 && select.arrowIcon[0].outerHTML) { 166 var clone = select.arrowIcon[0].cloneNode(true); 167 Util.removeClass(clone, 'select__icon'); 168 button = button +clone.outerHTML; 169 } 170 171 return button+'</button>'; 172 173 }; 174 175 function initListSelect(select) { // create custom select dropdown 176 var list = '<div class="js-select__dropdown select__dropdown" aria-describedby="'+select.selectId+'-description" id="'+select.selectId+'-dropdown">'; 177 list = list + getSelectLabelSR(select); 178 if(select.optGroups.length > 0) { 179 for(var i = 0; i < select.optGroups.length; i++) { 180 var optGroupList = select.optGroups[i].getElementsByTagName('option'), 181 optGroupLabel = '<li><span class="select__item select__item--optgroup">'+select.optGroups[i].getAttribute('label')+'</span></li>'; 182 list = list + '<ul class="select__list" role="listbox">'+optGroupLabel+getOptionsList(select, optGroupList) + '</ul>'; 183 } 184 } else { 185 list = list + '<ul class="select__list" role="listbox">'+getOptionsList(select, select.options) + '</ul>'; 186 } 187 return list; 188 }; 189 190 function getSelectLabelSR(select) { 191 if(select.label) { 192 return '<p class="sr-only" id="'+select.selectId+'-description">'+select.label.textContent+'</p>' 193 } else { 194 return ''; 195 } 196 }; 197 198 function resetCustomSelect(select) { 199 // <select> element has been updated (using an external control) - update custom select 200 var selectedOption = select.dropdown.querySelector('[aria-selected="true"]'); 201 if(selectedOption) selectedOption.setAttribute('aria-selected', 'false'); 202 var option = select.dropdown.querySelector('.js-select__item[data-index="'+select.select.selectedIndex+'"]'); 203 option.setAttribute('aria-selected', 'true'); 204 select.trigger.getElementsByClassName('js-select__label')[0].textContent = option.textContent; 205 select.trigger.setAttribute('aria-expanded', 'false'); 206 updateTriggerAria(select); 207 }; 208 209 function getOptionsList(select, options) { 210 var list = ''; 211 for(var i = 0; i < options.length; i++) { 212 var selected = options[i].hasAttribute('selected') ? ' aria-selected="true"' : ' aria-selected="false"'; 213 list = list + '<li><button type="button" class="reset js-select__item select__item select__item--option" role="option" data-value="'+options[i].value+'" '+selected+' data-index="'+select.optionIndex+'">'+options[i].text+'</button></li>'; 214 select.optionIndex = select.optionIndex + 1; 215 }; 216 return list; 217 }; 218 219 function getSelectedOption(select) { 220 var option = select.dropdown.querySelector('[aria-selected="true"]'); 221 if(option) return option; 222 else return select.dropdown.getElementsByClassName('js-select__item')[0]; 223 }; 224 225 function moveFocusToSelectTrigger(select) { 226 if(!document.activeElement.closest('.js-select')) return 227 select.trigger.focus(); 228 }; 229 230 function checkCustomSelectClick(select, target) { // close select when clicking outside it 231 if( !select.element.contains(target) ) toggleCustomSelect(select, 'false'); 232 }; 233 234 //initialize the CustomSelect objects 235 var customSelect = document.getElementsByClassName('js-select'); 236 if( customSelect.length > 0 ) { 237 var selectArray = []; 238 for( var i = 0; i < customSelect.length; i++) { 239 (function(i){selectArray.push(new CustomSelect(customSelect[i]));})(i); 240 } 241 242 // listen for key events 243 window.addEventListener('keyup', function(event){ 244 if( event.keyCode && event.keyCode == 27 || event.key && event.key.toLowerCase() == 'escape' ) { 245 // close custom select on 'Esc' 246 selectArray.forEach(function(element){ 247 moveFocusToSelectTrigger(element); // if focus is within dropdown, move it to dropdown trigger 248 toggleCustomSelect(element, 'false'); // close dropdown 249 }); 250 } 251 }); 252 // close custom select when clicking outside it 253 window.addEventListener('click', function(event){ 254 selectArray.forEach(function(element){ 255 checkCustomSelectClick(element, event.target); 256 }); 257 }); 258 } 259}());
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.