1window.fSelect = (() => { 2 3 var build = {}; 4 5 class fSelect { 6 7 constructor(selector, options) { 8 let that = this; 9 10 var defaults = { 11 placeholder: 'Select some options', 12 numDisplayed: 3, 13 overflowText: '{n} selected', 14 searchText: 'Search', 15 noResultsText: 'No results found', 16 showSearch: true, 17 optionFormatter: false 18 }; 19 20 that.settings = Object.assign({}, defaults, options); 21 build = {output: '', optgroup: 0, idx: 0}; 22 23 if ('string' === typeof selector) { 24 var nodes = Array.from(document.querySelectorAll(selector)); 25 } 26 else if (selector instanceof Node) { 27 var nodes = [selector]; 28 } 29 else if (Array.isArray(selector)) { 30 var nodes = selector; 31 } 32 else { 33 var nodes = []; 34 } 35 36 if ('undefined' === typeof window.fSelectInit) { 37 window.fSelectInit = { 38 searchCache: '', 39 lastChoice: null, 40 lastFocus: null, 41 activeEl: null 42 }; 43 that.bindEvents(); 44 } 45 46 nodes.forEach((input) => { 47 if (typeof input.fselect === 'object') { 48 input.fselect.destroy(); 49 } 50 51 that.settings.multiple = input.matches('[multiple]'); 52 input.fselect = that; 53 that.input = input; 54 that.create(); 55 }); 56 } 57 58 create() { 59 var that = this; 60 var options = that.buildOptions(); 61 var label = that.getDropdownLabel(); 62 var mode = (that.settings.multiple) ? 'multiple' : 'single'; 63 var searchClass = (that.settings.showSearch) ? '' : ' fs-hidden'; 64 var noResultsClass = (build.idx < 2) ? '' : ' fs-hidden'; 65 66 var html = ` 67 <div class="fs-wrap ${mode}" tabindex="0"> 68 <div class="fs-label-wrap"> 69 <div class="fs-label">${label}</div> 70 <span class="fs-arrow"></span> 71 </div> 72 <div class="fs-dropdown fs-hidden"> 73 <div class="fs-search${searchClass}"> 74 <input type="text" placeholder="${that.settings.searchText}" /> 75 </div> 76 <div class="fs-no-results${noResultsClass}">${that.settings.noResultsText}</div> 77 <div class="fs-options">${options}</div> 78 </div> 79 </div> 80 `; 81 82 var tpl = document.createElement('template'); 83 tpl.innerHTML = html;
84 var wrap = tpl.content.querySelector('.fs-wrap'); 85 that.input.parentNode.insertBefore(wrap, that.input.nextSibling); 86 that.input.classList.add('fs-hidden'); 87 88 // add a relationship link 89 that.input._rel = wrap; 90 wrap._rel = that.input; 91 } 92 93 destroy() { 94 this.input._rel.remove(); 95 this.input.classList.remove('fs-hidden'); 96 delete this.input._rel; 97 } 98 99 reload() { 100 this.destroy(); 101 this.create(); 102 } 103 104 open() { 105 var wrap = this.input._rel; 106 wrap.classList.add('fs-open'); 107 wrap.querySelector('.fs-dropdown').classList.remove('fs-hidden'); 108 109 // don't auto-focus for touch devices 110 if (! window.matchMedia("(pointer: coarse)").matches) { 111 wrap.querySelector('.fs-search input').focus(); 112 } 113 114 window.fSelectInit.lastChoice = this.getSelectedOptions('value'); 115 window.fSelectInit.activeEl = wrap; 116 117 this.trigger('fs:opened', wrap); 118 } 119 120 close() { 121 this.input._rel.classList.remove('fs-open');
122 this.input._rel.querySelector('.fs-dropdown').classList.add('fs-hidden'); 123 124 window.fSelectInit.searchCache = ''; 125 window.fSelectInit.lastChoice = null; 126 window.fSelectInit.lastFocus = null; 127 window.fSelectInit.activeEl = null; 128 129 this.trigger('fs:closed', this.input._rel); 130 } 131 132 buildOptions(parent) { 133 var that = this; 134 var parent = parent || that.input; 135 136 Array.from(parent.children).forEach((node) => { 137 if ('optgroup' === node.nodeName.toLowerCase()) { 138 var opt = `<div class="fs-optgroup-label" data-group="${build.optgroup}">${node.label}</div>`; 139 build.output += opt; 140 that.buildOptions(node); 141 build.optgroup++; 142 } 143 else { 144 var val = node.value; 145 146 // skip the first choice in multi-select mode 147 if (0 === build.idx && '' === val && that.settings.multiple) { 148 build.idx++; 149 return; 150 } 151 152 var classes = ['fs-option', 'g' + build.optgroup]; 153 154 // append existing classes 155 node.className.split(' ').forEach((className) => { 156 if ('' !== className) { 157 classes.push(className); 158 } 159 }); 160 161 if (node.matches('[disabled]')) classes.push('disabled'); 162 if (node.matches('[selected]')) classes.push('selected'); 163 classes = classes.join(' '); 164 165 if ('function' === typeof that.settings.optionFormatter) { 166 node.label = that.settings.optionFormatter(node.label, node); 167 } 168 169 var opt = `<div class="${classes}" data-value="${val}" data-idx="${build.idx}" tabindex="-1"><span class="fs-checkbox"><i></i></span><div class="fs-option-label">${node.label}</div></div>`; 170 171 build.output += opt; 172 build.idx++; 173 } 174 }); 175 176 return build.output; 177 } 178 179 getSelectedOptions(field, context) { 180 var context = context || this.input; 181 return Array.from(context.selectedOptions).map((opt) => { 182 return (field) ? opt[field] : opt; 183 }); 184 } 185 186 getAdjacentSibling(which) { 187 var that = this; 188 var which = which || 'next'; 189 var sibling = window.fSelectInit.lastFocus; 190 var selector = '.fs-option:not(.fs-hidden):not(.disabled)'; 191 192 if (sibling) { 193 sibling = sibling[which + 'ElementSibling']; 194 195 while (sibling) { 196 if (sibling.matches(selector)) break; 197 sibling = sibling[which + 'ElementSibling']; 198 } 199 200 return sibling; 201 } 202 else if ('next' == which) { 203 sibling = that.input._rel.querySelector(selector); 204 } 205 206 return sibling; 207 } 208 209 getDropdownLabel() { 210 var that = this; 211 var labelText = that.getSelectedOptions('text'); 212 213 if (labelText.length < 1) { 214 labelText = that.settings.placeholder; 215 } 216 else if (labelText.length > that.settings.numDisplayed) { 217 labelText = that.settings.overflowText.replace('{n}', labelText.length); 218 } 219 else { 220 labelText = labelText.join(', '); 221 } 222 223 return labelText; 224 } 225 226 debounce(func, wait) { 227 var timeout; 228 return (...args) => { 229 var boundFunc = func.bind(this, ...args); 230 clearTimeout(timeout); 231 timeout = setTimeout(boundFunc, wait); 232 } 233 } 234 235 trigger(eventName, ...args) { 236 document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]})); 237 } 238 239 on(eventName, elementSelector, handler) { 240 document.addEventListener(eventName, function(e) { 241 // loop parent nodes from the target to the delegation node 242 for (var target = e.target; target && target != this; target = target.parentNode) { 243 if (target.matches(elementSelector)) { 244 handler.call(target, e); 245 break; 246 } 247 } 248 }, false); 249 } 250 251 bindEvents() { 252 var that = this; 253 var optionSelector = '.fs-option:not(.fs-hidden):not(.disabled)'; 254 var unaccented = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 255 256 // debounce search for better performance 257 that.on('keyup', '.fs-search input', that.debounce(function(e) { 258 var wrap = e.target.closest('.fs-wrap'); 259 var options = wrap._rel.options; 260 261 var matchOperators = /[|\\{}()[\]^$+*?.]/g; 262 var keywords = e.target.value.replace(matchOperators, '\\$&'); 263 keywords = unaccented(keywords); 264 265 // if the searchCache already has a prefixed version of this search 266 // then don't un-hide the existing exclusions 267 if (0 !== keywords.indexOf(window.fSelectInit.searchCache)) { 268 wrap.querySelectorAll('.fs-option, .fs-optgroup-label').forEach((node) => node.classList.remove('fs-hidden')); 269 } 270 271 window.fSelectInit.searchCache = keywords; 272 273 for (var i = 0; i < options.length; i++) { 274 if ('' === options[i].value) continue; 275 276 var needle = new RegExp(keywords, 'gi'); 277 var haystack = unaccented(options[i].text); 278 279 if (null === haystack.match(needle)) {
280 wrap.querySelector('.fs-option[data-idx="' + i + '"]').classList.add('fs-hidden'); 281 } 282 } 283 284 // hide optgroups if no choices 285 wrap.querySelectorAll('.fs-optgroup-label').forEach((node) => { 286 var group = node.getAttribute('data-group'); 287 var container = node.closest('.fs-options'); 288 var count = container.querySelectorAll('.fs-option.g' + group + ':not(.fs-hidden)').length; 289 290 if (count < 1) { 291 node.classList.add('fs-hidden'); 292 } 293 }); 294 295 // toggle the noResultsText div 296 if (wrap.querySelectorAll('.fs-option:not(.fs-hidden').length) { 297 wrap.querySelector('.fs-no-results').classList.add('fs-hidden'); 298 } 299 else { 300 wrap.querySelector('.fs-no-results').classList.remove('fs-hidden'); 301 } 302 303 }, 100)); 304 305 that.on('click', optionSelector, function(e) { 306 var wrap = this.closest('.fs-wrap'); 307 var value = this.getAttribute('data-value'); 308 var input = wrap._rel; 309 var isMultiple = wrap.classList.contains('multiple'); 310 311 if (!isMultiple) { 312 input.value = value; 313 wrap.querySelectorAll('.fs-option.selected').forEach((node) => node.classList.remove('selected')); 314 } 315 else { 316 var idx = parseInt(this.getAttribute('data-idx')); 317 input.options[idx].selected = !this.classList.contains('selected'); 318 } 319 320 this.classList.toggle('selected'); 321 322 var label = input.fselect.getDropdownLabel();
323 wrap.querySelector('.fs-label').innerHTML = label; 324 325 // fire a change event 326 input.dispatchEvent(new Event('change', { bubbles: true })); 327 input.fselect.trigger('fs:changed', wrap); 328 329 if (!isMultiple) { 330 input.fselect.close(); 331 } 332 333 e.stopImmediatePropagation(); 334 }); 335 336 that.on('keydown', '*', function(e) { 337 var wrap = this.closest('.fs-wrap'); 338 339 if (!wrap) return; 340 341 if (-1 < [38, 40, 27].indexOf(e.which)) { 342 e.preventDefault(); 343 } 344 345 if (32 == e.which || 13 == e.which) { // space, enter 346 if (e.target.closest('.fs-search')) { 347 // preserve spaces for search 348 } 349 else if (e.target.matches(optionSelector)) { 350 e.target.click(); 351 e.preventDefault(); 352 } 353 else {
354 wrap.querySelector('.fs-label').click(); 355 e.preventDefault(); 356 } 357 } 358 else if (38 == e.which) { // up 359 var sibling = wrap._rel.fselect.getAdjacentSibling('previous'); 360 window.fSelectInit.lastFocus = sibling; // stop at the search box 361 362 if (sibling) { 363 sibling.focus(); 364 } 365 else { 366 wrap.querySelector('.fs-search input').focus(); 367 } 368 } 369 else if (40 == e.which) { // down 370 var sibling = wrap._rel.fselect.getAdjacentSibling('next'); 371 372 if (sibling) { 373 sibling.focus(); 374 window.fSelectInit.lastFocus = sibling; // stop at the bottom 375 } 376 } 377 else if (9 == e.which || 27 == e.which) { // tab, esc 378 wrap._rel.fselect.close(); 379 } 380 }); 381 382 that.on('click', '*', function(e) { 383 var wrap = this.closest('.fs-wrap'); 384 var lastActive = window.fSelectInit.activeEl; 385 386 if (wrap) { 387 var labelWrap = this.closest('.fs-label-wrap'); 388 389 if (labelWrap) { 390 if (lastActive) { 391 lastActive._rel.fselect.close(); 392 } 393 if (wrap !== lastActive) { 394 wrap._rel.fselect.open(); 395 } 396 } 397 } 398 else { 399 if (lastActive) { 400 lastActive._rel.fselect.close(); 401 } 402 } 403 }); 404 } 405 } 406 407 var $ = (selector, options) => new fSelect(selector, options); 408 409 return $; 410 411})(); 412 413if ('undefined' !== typeof fUtil) { 414 fUtil.fn.fSelect = function(opts) { 415 this.each(function() { // no arrow function to preserve "this" 416 fSelect(this, opts); 417 }); 418 return this; 419 }; 420}
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.