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 // restore searchCache if there is still an input in search 115 window.fSelectInit.searchCache = wrap.querySelector('.f
115s-search input') 116 117 window.fSelectInit.lastChoice = this.getSelectedOptions('value'); 118 window.fSelectInit.activeEl = wrap; 119 120 this.trigger('fs:opened', wrap); 121 } 122 123 close() { 124 this.input._rel.classList.remove('fs-open'); 125 this.input._rel.querySelector('.fs-dropdown').classList.add('fs-hidden'); 126 127 window.fSelectInit.searchCache = ''; 128 window.fSelectInit.lastChoice = null; 129 window.fSelectInit.lastFocus = null; 130 window.fSelectInit.activeEl = null; 131 132 this.trigger('fs:closed', this.input._rel); 133 } 134 135 buildOptions(parent) { 136 var that = this; 137 var parent = parent || that.input; 138 139 Array.from(parent.children).forEach((node) => { 140 if ('optgroup' === node.nodeName.toLowerCase()) { 141 var opt = `<div class="fs-optgroup-label" data-group="${build.optgroup}">${node.label}</div>`; 142 build.output += opt; 143 that.buildOptions(node); 144 build.optgroup++; 145 } 146 else { 147 var val = node.value; 148 149 // skip the first choice in multi-select mode 150 if (0 === build.idx && '' === val && that.settings.multiple) { 151 build.idx++; 152 return; 153 } 154 155 var classes = ['fs-option', 'g' + build.optgroup]; 156 157 // append existing classes 158 node.className.split(' ').forEach((className) => { 159 if ('' !== className) { 160 classes.push(className); 161 } 162 }); 163 164 if (node.matches('[disabled]')) classes.push('disabled'); 165 if (node.matches('[selected]')) classes.push('selected'); 166 classes = classes.join(' '); 167 168 if ('function' === typeof that.settings.optionFormatter) { 169 node.label = that.settings.optionFormatter(node.label, node); 170 } 171 172 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>`; 173 174 build.output += opt; 175 build.idx++; 176 } 177 }); 178 179 return build.output; 180 } 181 182 getSelectedOptions(field, context) { 183 var context = context || this.input; 184 return Array.from(context.selectedOptions).map((opt) => { 185 return (field) ? opt[field] : opt; 186 }); 187 } 188 189 getAdjacentSibling(which) { 190 var that = this; 191 var which = which || 'next'; 192 var sibling = window.fSelectInit.lastFocus; 193 var selector = '.fs-option:not(.fs-hidden):not(.disabled)'; 194 195 if (sibling) { 196 sibling = sibling[which + 'ElementSibling']; 197 198 while (sibling) { 199 if (sibling.matches(selector)) break; 200 sibling = sibling[which + 'ElementSibling']; 201 } 202 203 return sibling; 204 } 205 else if ('next' == which) { 206 sibling = that.input._rel.querySelector(selector); 207 } 208 209 return sibling; 210 } 211 212 getDropdownLabel() { 213 var that = this; 214 var labelText = that.getSelectedOptions('text'); 215 216 if (labelText.length < 1) { 217 labelText = that.settings.placeholder; 218 } 219 else if (labelText.length > that.settings.numDisplayed) { 220 labelText = that.settings.overflowText.replace('{n}', labelText.length); 221 } 222 else { 223 labelText = labelText.join(', '); 224 } 225 226 return labelText; 227 } 228 229 debounce(func, wait) { 230 var timeout; 231 return (...args) => { 232 var boundFunc = func.bind(this, ...args); 233 clearTimeout(timeout); 234 timeout = setTimeout(boundFunc, wait); 235 } 236 } 237 238 trigger(eventName, ...args) { 239 document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]})); 240 } 241 242 on(eventName, elementSelector, handler) { 243 document.addEventListener(eventName, function(e) { 244 // loop parent nodes from the target to the delegation node 245 for (var target = e.target; target && target != this; target = target.parentNode) { 246 if (target.matches(elementSelector)) { 247 handler.call(target, e); 248 break; 249 } 250 } 251 }, false); 252 } 253 254 bindEvents() { 255 var that = this; 256 var optionSelector = '.fs-option:not(.fs-hidden):not(.disabled)'; 257 var unaccented = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 258 259 // debounce search for better performance 260 that.on('keyup', '.fs-search input', that.debounce(function(e) { 261 var wrap = e.target.closest('.fs-wrap'); 262 var options = wrap._rel.options; 263 264 var matchOperators = /[|\\{}()[\]^$+*?.]/g; 265 var keywords = e.target.value.replace(matchOperators, '\\$&'); 266 keywords = unaccented(keywords); 267 268 // if the searchCache already has a prefixed version of this search 269 // then don't un-hide the existing exclusions 270 if (0 !== keywords.indexOf(window.fSelectInit.searchCache)) { 271 wrap.querySelectorAll('.fs-option, .fs-optgroup-label').forEach((node) => node.classList.remove('fs-hidden')); 272 } 273 274 window.fSelectInit.searchCache = keywords; 275 276 for (var i = 0; i < options.length; i++) { 277 if ('' === options[i].value) continue; 278 279 var needle = new RegExp(keywords, 'gi'); 280 var haystack = unaccented(options[i].text); 281 282 if (null === haystack.match(needle)) {
283 wrap.querySelector('.fs-option[data-idx="' + i + '"]').classList.add('fs-hidden'); 284 } 285 } 286 287 // hide optgroups if no choices 288 wrap.querySelectorAll('.fs-optgroup-label').forEach((node) => { 289 var group = node.getAttribute('data-group'); 290 var container = node.closest('.fs-options'); 291 var count = container.querySelectorAll('.fs-option.g' + group + ':not(.fs-hidden)').length; 292 293 if (count < 1) { 294 node.classList.add('fs-hidden'); 295 } 296 }); 297 298 // toggle the noResultsText div 299 if (wrap.querySelectorAll('.fs-option:not(.fs-hidden)').length) { 300 wrap.querySelector('.fs-no-results').classList.add('fs-hidden'); 301 } 302 else { 303 wrap.querySelector('.fs-no-results').classList.remove('fs-hidden'); 304 } 305 306 }, 100)); 307 308 that.on('click', optionSelector, function(e) { 309 var wrap = this.closest('.fs-wrap'); 310 var value = this.getAttribute('data-value'); 311 var input = wrap._rel; 312 var isMultiple = wrap.classList.contains('multiple'); 313 314 if (!isMultiple) { 315 input.value = value; 316 wrap.querySelectorAll('.fs-option.selected').forEach((node) => node.classList.remove('selected')); 317 } 318 else { 319 var idx = parseInt(this.getAttribute('data-idx')); 320 input.options[idx].selected = !this.classList.contains('selected'); 321 } 322 323 this.classList.toggle('selected'); 324 325 var label = input.fselect.getDropdownLabel();
326 wrap.querySelector('.fs-label').innerHTML = label; 327 328 // fire a change event 329 input.dispatchEvent(new Event('change', { bubbles: true })); 330 input.fselect.trigger('fs:changed', wrap); 331 332 if (!isMultiple) { 333 input.fselect.close(); 334 } 335 336 e.stopImmediatePropagation(); 337 }); 338 339 that.on('keydown', '*', function(e) { 340 var wrap = this.closest('.fs-wrap'); 341 342 if (!wrap) return; 343 344 if (-1 < [38, 40, 27].indexOf(e.which)) { 345 e.preventDefault(); 346 } 347 348 if (32 == e.which || 13 == e.which) { // space, enter 349 if (e.target.closest('.fs-search')) { 350 // preserve spaces for search 351 } 352 else if (e.target.matches(optionSelector)) { 353 e.target.click(); 354 e.preventDefault(); 355 } 356 else {
357 wrap.querySelector('.fs-label').click(); 358 e.preventDefault(); 359 } 360 } 361 else if (38 == e.which) { // up 362 var sibling = wrap._rel.fselect.getAdjacentSibling('previous'); 363 window.fSelectInit.lastFocus = sibling; // stop at the search box 364 365 if (sibling) { 366 sibling.focus(); 367 } 368 else { 369 wrap.querySelector('.fs-search input').focus(); 370 } 371 } 372 else if (40 == e.which) { // down 373 var sibling = wrap._rel.fselect.getAdjacentSibling('next'); 374 375 if (sibling) { 376 sibling.focus(); 377 window.fSelectInit.lastFocus = sibling; // stop at the bottom 378 } 379 } 380 else if (9 == e.which || 27 == e.which) { // tab, esc 381 wrap._rel.fselect.close(); 382 } 383 }); 384 385 that.on('click', '*', function(e) { 386 var wrap = this.closest('.fs-wrap'); 387 var lastActive = window.fSelectInit.activeEl; 388 389 if (wrap) { 390 var labelWrap = this.closest('.fs-label-wrap'); 391 392 if (labelWrap) { 393 if (lastActive) { 394 lastActive._rel.fselect.close(); 395 } 396 if (wrap !== lastActive) { 397 wrap._rel.fselect.open(); 398 } 399 } 400 } 401 else { 402 if (lastActive) { 403 lastActive._rel.fselect.close(); 404 } 405 } 406 }); 407 } 408 } 409 410 var $ = (selector, options) => new fSelect(selector, options); 411 412 return $; 413 414})(); 415 416if ('undefined' !== typeof fUtil) { 417 fUtil.fn.fSelect = function(opts) { 418 this.each(function() { // no arrow function to preserve "this" 419 fSelect(this, opts); 420 }); 421 return this; 422 }; 423}
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.