vendor: 111,455 bytes, lines 1-3152
1(function ($) { 2 'use strict'; 3 4 var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']; 5 6 var uriAttrs = [ 7 'background', 8 'cite', 9 'href', 10 'itemtype', 11 'longdesc', 12 'poster', 13 'src', 14 'xlink:href' 15 ]; 16 17 var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; 18 19 var DefaultWhitelist = { 20 // Global attributes allowed on any supplied element below. 21 '*': ['class', 'dir', 'id', 'lang', 'role', 'tabindex', 'style', ARIA_ATTRIBUTE_PATTERN], 22 a: ['target', 'href', 'title', 'rel'], 23 area: [], 24 b: [], 25 br: [], 26 col: [], 27 code: [], 28 div: [], 29 em: [], 30 hr: [], 31 h1: [], 32 h2: [], 33 h3: [], 34 h4: [], 35 h5: [], 36 h6: [], 37 i: [], 38 img: ['src', 'alt', 'title', 'width', 'height'], 39 li: [], 40 ol: [], 41 p: [], 42 pre: [], 43 s: [], 44 small: [], 45 span: [], 46 sub: [], 47 sup: [], 48 strong: [], 49 u: [], 50 ul: [] 51 } 52 53 /** 54 * A pattern that recognizes a commonly useful subset of URLs that are safe. 55 * 56 * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts 57 */ 58 var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; 59 60 /** 61 * A pattern that matches safe data URLs. Only matches image, video and audio types. 62 * 63 * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts 64 */ 65 var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; 66 67 function allowedAttribute (attr, allowedAttributeList) { 68 var attrName = attr.nodeName.toLowerCase() 69 70 if ($.inArray(attrName, allowedAttributeList) !== -1) { 71 if ($.inArray(attrName, uriAttrs) !== -1) { 72 return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)) 73 } 74 75 return true 76 } 77 78 var regExp = $(allowedAttributeList).filter(function (index, value) { 79 return value instanceof RegExp 80 }) 81 82 // Check if a regular expression validates the attribute. 83 for (var i = 0, l = regExp.length; i < l; i++) { 84 if (attrName.match(regExp[i])) { 85 return true 86 } 87 } 88 89 return false 90 } 91 92 function sanitizeHtml (unsafeElements, whiteList, sanitizeFn) { 93 if (sanitizeFn && typeof sanitizeFn === 'function') { 94 return sanitizeFn(unsafeElements); 95 } 96 97 var whitelistKeys = Object.keys(whiteList); 98 99 for (var i = 0, len = unsafeElements.length; i < len; i++) { 100 var elements = unsafeElements[i].querySelectorAll('*'); 101 102 for (var j = 0, len2 = elements.length; j < len2; j++) { 103 var el = elements[j]; 104 var elName = el.nodeName.toLowerCase(); 105 106 if (whitelistKeys.indexOf(elName) === -1) { 107 el.parentNode.removeChild(el); 108 109 continue; 110 } 111 112 var attributeList = [].slice.call(el.attributes); 113 var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []); 114 115 for (var k = 0, len3 = attributeList.length; k < len3; k++) { 116 var attr = attributeList[k]; 117 118 if (!allowedAttribute(attr, whitelistedAttributes)) { 119 el.removeAttribute(attr.nodeName); 120 } 121 } 122 } 123 } 124 } 125 126 // Polyfill for browsers with no classList support 127 // Remove in v2 128 if (!('classList' in document.createElement('_'))) { 129 (function (view) { 130 if (!('Element' in view)) return; 131 132 var classListProp = 'classList', 133 protoProp = 'prototype', 134 elemCtrProto = view.Element[protoProp], 135 objCtr = Object, 136 classListGetter = function () { 137 var $elem = $(this); 138 139 return { 140 add: function (classes) { 141 classes = Array.prototype.slice.call(arguments).join(' '); 142 return $elem.addClass(classes); 143 }, 144 remove: function (classes) { 145 classes = Array.prototype.slice.call(arguments).join(' '); 146 return $elem.removeClass(classes); 147 }, 148 toggle: function (classes, force) { 149 return $elem.toggleClass(classes, force); 150 }, 151 contains: function (classes) { 152 return $elem.hasClass(classes); 153 } 154 } 155 }; 156 157 if (objCtr.defineProperty) { 158 var classListPropDesc = { 159 get: classListGetter, 160 enumerable: true, 161 configurable: true 162 }; 163 try { 164 objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 165 } catch (ex) { // IE 8 doesn't support enumerable:true 166 // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 167 // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected 168 if (ex.number === undefined || ex.number === -0x7FF5EC54) { 169 classListPropDesc.enumerable = false; 170 objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 171 } 172 } 173 } else if (objCtr[protoProp].__defineGetter__) { 174 elemCtrProto.__defineGetter__(classListProp, classListGetter); 175 } 176 }(window)); 177 } 178 179 var testElement = document.createElement('_'); 180 181 testElement.classList.add('c1', 'c2'); 182 183 if (!testElement.classList.contains('c2')) { 184 var _add = DOMTokenList.prototype.add, 185 _remove = DOMTokenList.prototype.remove; 186 187 DOMTokenList.prototype.add = function () { 188 Array.prototype.forEach.call(arguments, _add.bind(this)); 189 } 190 191 DOMTokenList.prototype.remove = function () { 192 Array.prototype.forEach.call(arguments, _remove.bind(this)); 193 } 194 } 195 196 testElement.classList.toggle('c3', false); 197 198 // Polyfill for IE 10 and Firefox <24, where classList.toggle does not 199 // support the second argument. 200 if (testElement.classList.contains('c3')) { 201 var _toggle = DOMTokenList.prototype.toggle; 202 203 DOMTokenList.prototype.toggle = function (token, force) { 204 if (1 in arguments && !this.contains(token) === !force) { 205 return force; 206 } else { 207 return _toggle.call(this, token); 208 } 209 }; 210 } 211 212 testElement = null; 213 214 // shallow array comparison 215 function isEqual (array1, array2) { 216 return array1.length === array2.length && array1.every(function (element, index) { 217 return element === array2[index]; 218 }); 219 }; 220 221 // <editor-fold desc="Shims"> 222 if (!String.prototype.startsWith) { 223 (function () { 224 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` 225 var defineProperty = (function () { 226 // IE 8 only supports `Object.defineProperty` on DOM elements 227 try { 228 var object = {}; 229 var $defineProperty = Object.defineProperty; 230 var result = $defineProperty(object, object, object) && $defineProperty; 231 } catch (error) { 232 } 233 return result; 234 }()); 235 var toString = {}.toString; 236 var startsWith = function (search) { 237 if (this == null) { 238 throw new TypeError(); 239 } 240 var string = String(this); 241 if (search && toString.call(search) == '[object RegExp]') { 242 throw new TypeError(); 243 } 244 var stringLength = string.length; 245 var searchString = String(search); 246 var searchLength = searchString.length; 247 var position = arguments.length > 1 ? arguments[1] : undefined; 248 // `ToInteger` 249 var pos = position ? Number(position) : 0; 250 if (pos != pos) { // better `isNaN` 251 pos = 0; 252 } 253 var start = Math.min(Math.max(pos, 0), stringLength); 254 // Avoid the `indexOf` call if no match is possible 255 if (searchLength + start > stringLength) { 256 return false; 257 } 258 var index = -1; 259 while (++index < searchLength) { 260 if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) { 261 return false; 262 } 263 } 264 return true; 265 }; 266 if (defineProperty) { 267 defineProperty(String.prototype, 'startsWith', { 268 'value': startsWith, 269 'configurable': true, 270 'writable': true 271 }); 272 } else { 273 String.prototype.startsWith = startsWith; 274 } 275 }()); 276 } 277 278 if (!Object.keys) { 279 Object.keys = function ( 280 o, // object 281 k, // key 282 r // result array 283 ) { 284 // initialize object and result 285 r = []; 286 // iterate over object keys 287 for (k in o) { 288 // fill result array with non-prototypical keys 289 r.hasOwnProperty.call(o, k) && r.push(k); 290 } 291 // return result 292 return r; 293 }; 294 } 295 296 if (HTMLSelectElement && !HTMLSelectElement.prototype.hasOwnProperty('selectedOptions')) { 297 Object.defineProperty(HTMLSelectElement.prototype, 'selectedOptions', { 298 get: function () { 299 return this.querySelectorAll(':checked'); 300 } 301 }); 302 } 303 304 function getSelectedOptions (select, ignoreDisabled) { 305 var selectedOptions = select.selectedOptions, 306 options = [], 307 opt; 308 309 if (ignoreDisabled) { 310 for (var i = 0, len = selectedOptions.length; i < len; i++) { 311 opt = selectedOptions[i]; 312 313 if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) { 314 options.push(opt); 315 } 316 } 317 318 return options; 319 } 320 321 return selectedOptions; 322 } 323 324 // much faster than $.val() 325 function getSelectValues (select, selectedOptions) { 326 var value = [], 327 options = selectedOptions || select.selectedOptions, 328 opt; 329 330 for (var i = 0, len = options.length; i < len; i++) { 331 opt = options[i]; 332 333 if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) { 334 value.push(opt.value); 335 } 336 } 337 338 if (!select.multiple) { 339 return !value.length ? null : value[0]; 340 } 341 342 return value; 343 } 344 345 // set data-selected on select element if the value has been programmatically selected 346 // prior to initialization of bootstrap-select 347 // * consider removing or replacing an alternative method * 348 var valHooks = { 349 useDefault: false, 350 _set: $.valHooks.select.set 351 }; 352 353 $.valHooks.select.set = function (elem, value) { 354 if (value && !valHooks.useDefault) $(elem).data('selected', true); 355 356 return valHooks._set.apply(this, arguments); 357 }; 358 359 var changedArguments = null; 360 361 var EventIsSupported = (function () { 362 try { 363 new Event('change'); 364 return true; 365 } catch (e) { 366 return false; 367 } 368 })(); 369 370 $.fn.triggerNative = function (eventName) { 371 var el = this[0], 372 event; 373 374 if (el.dispatchEvent) { // for modern browsers & IE9+ 375 if (EventIsSupported) { 376 // For modern browsers 377 event = new Event(eventName, { 378 bubbles: true 379 }); 380 } else { 381 // For IE since it doesn't support Event constructor 382 event = document.createEvent('Event'); 383 event.initEvent(eventName, true, false); 384 } 385 386 el.dispatchEvent(event); 387 } else if (el.fireEvent) { // for IE8 388 event = document.createEventObject(); 389 event.eventType = eventName; 390 el.fireEvent('on' + eventName, event); 391 } else { 392 // fall back to jQuery.trigger 393 this.trigger(eventName); 394 } 395 }; 396 // </editor-fold> 397 398 function stringSearch (li, searchString, method, normalize) { 399 var stringTypes = [ 400 'display', 401 'subtext', 402 'tokens' 403 ], 404 searchSuccess = false; 405 406 for (var i = 0; i < stringTypes.length; i++) { 407 var stringType = stringTypes[i], 408 string = li[stringType]; 409 410 if (string) { 411 string = string.toString(); 412 413 // Strip HTML tags. This isn't perfect, but it's much faster than any other method 414 if (stringType === 'display') { 415 string = string.replace(/<[^>]+>/g, ''); 416 } 417 418 if (normalize) string = normalizeToBase(string); 419 string = string.toUpperCase(); 420 421 if (method === 'contains') { 422 searchSuccess = string.indexOf(searchString) >= 0; 423 } else { 424 searchSuccess = string.startsWith(searchString); 425 } 426 427 if (searchSuccess) break; 428 } 429 } 430 431 return searchSuccess; 432 } 433 434 function toInteger (value) { 435 return parseInt(value, 10) || 0; 436 } 437 438 // Borrowed from Lodash (_.deburr) 439 /** Used to map Latin Unicode letters to basic Latin letters. */ 440 var deburredLetters = { 441 // Latin-1 Supplement block. 442 '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', 443 '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', 444 '\xc7': 'C', '\xe7': 'c', 445 '\xd0': 'D', '\xf0': 'd', 446 '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', 447 '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', 448 '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', 449 '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', 450 '\xd1': 'N', '\xf1': 'n', 451 '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', 452 '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', 453 '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', 454 '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', 455 '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', 456 '\xc6': 'Ae', '\xe6': 'ae', 457 '\xde': 'Th', '\xfe': 'th', 458 '\xdf': 'ss', 459 // Latin Extended-A block. 460 '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', 461 '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', 462 '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', 463 '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', 464 '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', 465 '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', 466 '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', 467 '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', 468 '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', 469 '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', 470 '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', 471 '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', 472 '\u0134': 'J', '\u0135': 'j', 473 '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', 474 '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', 475 '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', 476 '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', 477 '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', 478 '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', 479 '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', 480 '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', 481 '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', 482 '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', 483 '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', 484 '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', 485 '\u0163': 't', '\u0165': 't', '\u0167': 't', 486 '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', 487 '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', 488 '\u0174': 'W', '\u0175': 'w', 489 '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', 490 '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', 491 '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', 492 '\u0132': 'IJ', '\u0133': 'ij', 493 '\u0152': 'Oe', '\u0153': 'oe', 494 '\u0149': "'n", '\u017f': 's' 495 }; 496 497 /** Used to match Latin Unicode letters (excluding mathematical operators). */ 498 var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; 499 500 /** Used to compose unicode character classes. */ 501 var rsComboMarksRange = '\\u0300-\\u036f', 502 reComboHalfMarksRange = '\\ufe20-\\ufe2f', 503 rsComboSymbolsRange = '\\u20d0-\\u20ff', 504 rsComboMarksExtendedRange = '\\u1ab0-\\u1aff', 505 rsComboMarksSupplementRange = '\\u1dc0-\\u1dff', 506 rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange; 507 508 /** Used to compose unicode capture groups. */ 509 var rsCombo = '[' + rsComboRange + ']'; 510 511 /** 512 * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and 513 * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). 514 */ 515 var reComboMark = RegExp(rsCombo, 'g'); 516 517 function deburrLetter (key) { 518 return deburredLetters[key]; 519 }; 520 521 function normalizeToBase (string) { 522 string = string.toString(); 523 return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); 524 } 525 526 // List of HTML entities for escaping. 527 var escapeMap = { 528 '&': '&', 529 '<': '<', 530 '>': '>', 531 '"': '"', 532 "'": ''', 533 '`': '`' 534 }; 535 536 // Functions for escaping and unescaping strings to/from HTML interpolation. 537 var createEscaper = function (map) { 538 var escaper = function (match) { 539 return map[match]; 540 }; 541 // Regexes for identifying a key that needs to be escaped. 542 var source = '(?:' + Object.keys(map).join('|') + ')'; 543 var testRegexp = RegExp(source); 544 var replaceRegexp = RegExp(source, 'g'); 545 return function (string) { 546 string = string == null ? '' : '' + string; 547 return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 548 }; 549 }; 550 551 var htmlEscape = createEscaper(escapeMap); 552 553 /** 554 * ------------------------------------------------------------------------ 555 * Constants 556 * ------------------------------------------------------------------------ 557 */ 558 559 var keyCodeMap = { 560 32: ' ', 561 48: '0', 562 49: '1', 563 50: '2', 564 51: '3', 565 52: '4', 566 53: '5', 567 54: '6', 568 55: '7', 569 56: '8', 570 57: '9', 571 59: ';', 572 65: 'A', 573 66: 'B', 574 67: 'C', 575 68: 'D', 576 69: 'E', 577 70: 'F', 578 71: 'G', 579 72: 'H', 580 73: 'I', 581 74: 'J', 582 75: 'K', 583 76: 'L', 584 77: 'M', 585 78: 'N', 586 79: 'O', 587 80: 'P', 588 81: 'Q', 589 82: 'R', 590 83: 'S', 591 84: 'T', 592 85: 'U', 593 86: 'V', 594 87: 'W', 595 88: 'X', 596 89: 'Y', 597 90: 'Z', 598 96: '0', 599 97: '1', 600 98: '2', 601 99: '3', 602 100: '4', 603 101: '5', 604 102: '6', 605 103: '7', 606 104: '8', 607 105: '9' 608 }; 609 610 var keyCodes = { 611 ESCAPE: 27, // KeyboardEvent.which value for Escape (Esc) key 612 ENTER: 13, // KeyboardEvent.which value for Enter key 613 SPACE: 32, // KeyboardEvent.which value for space key 614 TAB: 9, // KeyboardEvent.which value for tab key 615 ARROW_UP: 38, // KeyboardEvent.which value for up arrow key 616 ARROW_DOWN: 40 // KeyboardEvent.which value for down arrow key 617 } 618 619 var version = { 620 success: false, 621 major: '3' 622 }; 623 624 try { 625 version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); 626 version.major = version.full[0]; 627 version.success = true; 628 } catch (err) { 629 // do nothing 630 } 631 632 var selectId = 0; 633 634 var EVENT_KEY = '.bs.select'; 635 636 var classNames = { 637 DISABLED: 'disabled', 638 DIVIDER: 'divider', 639 SHOW: 'open', 640 DROPUP: 'dropup', 641 MENU: 'dropdown-menu', 642 MENURIGHT: 'dropdown-menu-right', 643 MENULEFT: 'dropdown-menu-left', 644 // to-do: replace with more advanced template/customization options 645 BUTTONCLASS: 'btn-default', 646 POPOVERHEADER: 'popover-title', 647 ICONBASE: 'glyphicon', 648 TICKICON: 'glyphicon-ok' 649 } 650 651 var Selector = { 652 MENU: '.' + classNames.MENU 653 } 654 655 var elementTemplates = { 656 span: document.createElement('span'), 657 i: document.createElement('i'), 658 subtext: document.createElement('small'), 659 a: document.createElement('a'), 660 li: document.createElement('li'), 661 whitespace: document.createTextNode('\u00A0'), 662 fragment: document.createDocumentFragment() 663 } 664 665 elementTemplates.a.setAttribute('role', 'option'); 666 if (version.major === '4') elementTemplates.a.className = 'dropdown-item'; 667 668 elementTemplates.subtext.className = 'text-muted'; 669 670 elementTemplates.text = elementTemplates.span.cloneNode(false); 671 elementTemplates.text.className = 'text'; 672 673 elementTemplates.checkMark = elementTemplates.span.cloneNode(false); 674 675 var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); 676 var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); 677 678 var generateOption = { 679 li: function (content, classes, optgroup) { 680 var li = elementTemplates.li.cloneNode(false); 681 682 if (content) { 683 if (content.nodeType === 1 || content.nodeType === 11) { 684 li.appendChild(content); 685 } else { 686 li.innerHTML = content; 687 } 688 } 689 690 if (typeof classes !== 'undefined' && classes !== '') li.className = classes; 691 if (typeof optgroup !== 'undefined' && optgroup !== null) li.classList.add('optgroup-' + optgroup); 692 693 return li; 694 }, 695 696 a: function (text, classes, inline) { 697 var a = elementTemplates.a.cloneNode(true); 698 699 if (text) { 700 if (text.nodeType === 11) { 701 a.appendChild(text); 702 } else { 703 a.insertAdjacentHTML('beforeend', text); 704 } 705 } 706 707 if (typeof classes !== 'undefined' && classes !== '') a.classList.add.apply(a.classList, classes.split(' ')); 708 if (inline) a.setAttribute('style', inline); 709 710 return a; 711 }, 712 713 text: function (options, useFragment) { 714 var textElement = elementTemplates.text.cloneNode(false), 715 subtextElement, 716 iconElement; 717 718 if (options.content) { 719 textElement.innerHTML = options.content; 720 } else { 721 textElement.textContent = options.text; 722 723 if (options.icon) { 724 var whitespace = elementTemplates.whitespace.cloneNode(false); 725 726 // need to use <i> for icons in the button to prevent a breaking change 727 // note: switch to span in next major release 728 iconElement = (useFragment === true ? elementTemplates.i : elementTemplates.span).cloneNode(false); 729 iconElement.className = this.options.iconBase + ' ' + options.icon; 730 731 elementTemplates.fragment.appendChild(iconElement); 732 elementTemplates.fragment.appendChild(whitespace); 733 } 734 735 if (options.subtext) { 736 subtextElement = elementTemplates.subtext.cloneNode(false); 737 subtextElement.textContent = options.subtext; 738 textElement.appendChild(subtextElement); 739 } 740 } 741 742 if (useFragment === true) { 743 while (textElement.childNodes.length > 0) { 744 elementTemplates.fragment.appendChild(textElement.childNodes[0]); 745 } 746 } else { 747 elementTemplates.fragment.appendChild(textElement); 748 } 749 750 return elementTemplates.fragment; 751 }, 752 753 label: function (options) { 754 var textElement = elementTemplates.text.cloneNode(false), 755 subtextElement, 756 iconElement; 757 758 textElement.innerHTML = options.display; 759 760 if (options.icon) { 761 var whitespace = elementTemplates.whitespace.cloneNode(false); 762 763 iconElement = elementTemplates.span.cloneNode(false); 764 iconElement.className = this.options.iconBase + ' ' + options.icon; 765 766 elementTemplates.fragment.appendChild(iconElement); 767 elementTemplates.fragment.appendChild(whitespace); 768 } 769 770 if (options.subtext) { 771 subtextElement = elementTemplates.subtext.cloneNode(false); 772 subtextElement.textContent = options.subtext; 773 textElement.appendChild(subtextElement); 774 } 775 776 elementTemplates.fragment.appendChild(textElement); 777 778 return elementTemplates.fragment; 779 } 780 } 781 782 var Selectpicker = function (element, options) { 783 var that = this; 784 785 // bootstrap-select has been initialized - revert valHooks.select.set back to its original function 786 if (!valHooks.useDefault) { 787 $.valHooks.select.set = valHooks._set; 788 valHooks.useDefault = true; 789 } 790 791 this.$element = $(element); 792 this.$newElement = null; 793 this.$button = null; 794 this.$menu = null; 795 this.options = options; 796 this.selectpicker = { 797 main: {}, 798 search: {}, 799 current: {}, // current changes if a search is in progress 800 view: {}, 801 isSearching: false, 802 keydown: { 803 keyHistory: '', 804 resetKeyHistory: { 805 start: function () { 806 return setTimeout(function () { 807 that.selectpicker.keydown.keyHistory = ''; 808 }, 800); 809 } 810 } 811 } 812 }; 813 814 this.sizeInfo = {}; 815 816 // If we have no title yet, try to pull it from the html title attribute (jQuery doesnt' pick it up as it's not a 817 // data-attribute) 818 if (this.options.title === null) { 819 this.options.title = this.$element.attr('title'); 820 } 821 822 // Format window padding 823 var winPad = this.options.windowPadding; 824 if (typeof winPad === 'number') { 825 this.options.windowPadding = [winPad, winPad, winPad, winPad]; 826 } 827 828 // Expose public methods 829 this.val = Selectpicker.prototype.val; 830 this.render = Selectpicker.prototype.render; 831 this.refresh = Selectpicker.prototype.refresh; 832 this.setStyle = Selectpicker.prototype.setStyle; 833 this.selectAll = Selectpicker.prototype.selectAll; 834 this.deselectAll = Selectpicker.prototype.deselectAll; 835 this.destroy = Selectpicker.prototype.destroy; 836 this.remove = Selectpicker.prototype.remove; 837 this.show = Selectpicker.prototype.show; 838 this.hide = Selectpicker.prototype.hide; 839 840 this.init(); 841 }; 842 843 Selectpicker.VERSION = '1.13.14'; 844 845 // part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both. 846 Selectpicker.DEFAULTS = { 847 noneSelectedText: 'Nothing selected', 848 noneResultsText: 'No results matched {0}', 849 countSelectedText: function (numSelected, numTotal) { 850 return (numSelected == 1) ? '{0} item selected' : '{0} items selected'; 851 }, 852 maxOptionsText: function (numAll, numGroup) { 853 return [ 854 (numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)', 855 (numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)' 856 ]; 857 }, 858 selectAllText: 'Select All', 859 deselectAllText: 'Deselect All', 860 doneButton: false, 861 doneButtonText: 'Close', 862 multipleSeparator: ', ', 863 styleBase: 'btn', 864 style: classNames.BUTTONCLASS, 865 size: 'auto', 866 title: null, 867 selectedTextFormat: 'values', 868 width: false, 869 container: false, 870 hideDisabled: false, 871 showSubtext: false, 872 showIcon: true, 873 showContent: true, 874 dropupAuto: true, 875 header: false, 876 liveSearch: false, 877 liveSearchPlaceholder: null, 878 liveSearchNormalize: false, 879 liveSearchStyle: 'contains', 880 actionsBox: false, 881 iconBase: classNames.ICONBASE, 882 tickIcon: classNames.TICKICON, 883 showTick: false, 884 template: { 885 caret: '<span class="caret"></span>' 886 }, 887 maxOptions: false, 888 mobile: false, 889 selectOnTab: false, 890 dropdownAlignRight: false, 891 windowPadding: 0, 892 virtualScroll: 600, 893 display: false, 894 sanitize: true, 895 sanitizeFn: null, 896 whiteList: DefaultWhitelist 897 }; 898 899 Selectpicker.prototype = { 900 901 constructor: Selectpicker, 902 903 init: function () { 904 var that = this, 905 id = this.$element.attr('id'); 906 907 selectId++; 908 this.selectId = 'bs-select-' + selectId; 909 910 this.$element[0].classList.add('bs-select-hidden'); 911 912 this.multiple = this.$element.prop('multiple'); 913 this.autofocus = this.$element.prop('autofocus'); 914 915 if (this.$element[0].classList.contains('show-tick')) { 916 this.options.showTick = true; 917 } 918 919 this.$newElement = this.createDropdown(); 920 this.buildData(); 921 this.$element 922 .after(this.$newElement) 923 .prependTo(this.$newElement); 924 925 this.$button = this.$newElement.children('button'); 926 this.$menu = this.$newElement.children(Selector.MENU); 927 this.$menuInner = this.$menu.children('.inner'); 928 this.$searchbox = this.$menu.find('input'); 929 930 this.$element[0].classList.remove('bs-select-hidden'); 931 932 if (this.options.dropdownAlignRight === true) this.$menu[0].classList.add(classNames.MENURIGHT); 933 934 if (typeof id !== 'undefined') { 935 this.$button.attr('data-id', id); 936 } 937 938 this.checkDisabled(); 939 this.clickListener(); 940 941 if (this.options.liveSearch) { 942 this.liveSearchListener(); 943 this.focusedParent = this.$searchbox[0]; 944 } else { 945 this.focusedParent = this.$menuInner[0]; 946 } 947 948 this.setStyle(); 949 this.render(); 950 this.setWidth(); 951 if (this.options.container) { 952 this.selectPosition(); 953 } else { 954 this.$element.on('hide' + EVENT_KEY, function () { 955 if (that.isVirtual()) { 956 // empty menu on close 957 var menuInner = that.$menuInner[0], 958 emptyMenu = menuInner.firstChild.cloneNode(false); 959 960 // replace the existing UL with an empty one - this is faster than $.empty() or innerHTML = '' 961 menuInner.replaceChild(emptyMenu, menuInner.firstChild); 962 menuInner.scrollTop = 0; 963 } 964 }); 965 } 966 this.$menu.data('this', this); 967 this.$newElement.data('this', this); 968 if (this.options.mobile) this.mobile(); 969 970 this.$newElement.on({ 971 'hide.bs.dropdown': function (e) { 972 that.$element.trigger('hide' + EVENT_KEY, e); 973 }, 974 'hidden.bs.dropdown': function (e) { 975 that.$element.trigger('hidden' + EVENT_KEY, e); 976 }, 977 'show.bs.dropdown': function (e) { 978 that.$element.trigger('show' + EVENT_KEY, e); 979 }, 980 'shown.bs.dropdown': function (e) { 981 that.$element.trigger('shown' + EVENT_KEY, e); 982 } 983 }); 984 985 if (that.$element[0].hasAttribute('required')) { 986 this.$element.on('invalid' + EVENT_KEY, function () { 987 that.$button[0].classList.add('bs-invalid'); 988 989 that.$element 990 .on('shown' + EVENT_KEY + '.invalid', function () { 991 that.$element 992 .val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened 993 .off('shown' + EVENT_KEY + '.invalid'); 994 }) 995 .on('rendered' + EVENT_KEY, function () { 996 // if select is no longer invalid, remove the bs-invalid class 997 if (this.validity.valid) that.$button[0].classList.remove('bs-invalid'); 998 that.$element.off('rendered' + EVENT_KEY); 999 }); 1000 1001 that.$button.on('blur' + EVENT_KEY, function () { 1002 that.$element.trigger('focus').trigger('blur'); 1003 that.$button.off('blur' + EVENT_KEY); 1004 }); 1005 }); 1006 } 1007 1008 setTimeout(function () { 1009 that.buildList(); 1010 that.$element.trigger('loaded' + EVENT_KEY); 1011 }); 1012 }, 1013 1014 createDropdown: function () { 1015 // Options 1016 // If we are multiple or showTick option is set, then add the show-tick class 1017 var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '', 1018 multiselectable = this.multiple ? ' aria-multiselectable="true"' : '', 1019 inputGroup = '', 1020 autofocus = this.autofocus ? ' autofocus' : ''; 1021 1022 if (version.major < 4 && this.$element.parent().hasClass('input-group')) { 1023 inputGroup = ' input-group-btn'; 1024 } 1025 1026 // Elements 1027 var drop, 1028 header = '', 1029 searchbox = '', 1030 actionsbox = '', 1031 donebutton = ''; 1032 1033 if (this.options.header) { 1034 header = 1035 '<div class="' + classNames.POPOVERHEADER + '">' + 1036 '<button type="button" class="close" aria-hidden="true">×</button>' + 1037 this.options.header + 1038 '</div>'; 1039 } 1040 1041 if (this.options.liveSearch) { 1042 searchbox = 1043 '<div class="bs-searchbox">' + 1044 '<input type="search" class="form-control" autocomplete="off"' + 1045 ( 1046 this.options.liveSearchPlaceholder === null ? '' 1047 : 1048 ' placeholder="' + htmlEscape(this.options.liveSearchPlaceholder) + '"' 1049 ) + 1050 ' role="combobox" aria-label="Search" aria-controls="' + this.selectId + '" aria-autocomplete="list">' + 1051 '</div>'; 1052 } 1053 1054 if (this.multiple && this.options.actionsBox) { 1055 actionsbox = 1056 '<div class="bs-actionsbox">' + 1057 '<div class="btn-group btn-group-sm btn-block">' + 1058 '<button type="button" class="actions-btn bs-select-all btn ' + classNames.BUTTONCLASS + '">' + 1059 this.options.selectAllText + 1060 '</button>' + 1061 '<button type="button" class="actions-btn bs-deselect-all btn ' + classNames.BUTTONCLASS + '">' + 1062 this.options.deselectAllText + 1063 '</button>' + 1064 '</div>' + 1065 '</div>'; 1066 } 1067 1068 if (this.multiple && this.options.doneButton) { 1069 donebutton = 1070 '<div class="bs-donebutton">' + 1071 '<div class="btn-group btn-block">' + 1072 '<button type="button" class="btn btn-sm ' + classNames.BUTTONCLASS + '">' + 1073 this.options.doneButtonText + 1074 '</button>' + 1075 '</div>' + 1076 '</div>'; 1077 } 1078 1079 drop = 1080 '<div class="dropdown bootstrap-select' + showTick + inputGroup + '">' + 1081 '<button type="button" class="' + this.options.styleBase + ' dropdown-toggle" ' + (this.options.display === 'static' ? 'data-display="static"' : '') + 'data-toggle="dropdown"' + autofocus + ' role="combobox" aria-owns="' + this.selectId + '" aria-haspopup="listbox" aria-expanded="false">' + 1082 '<div class="filter-option">' + 1083 '<div class="filter-option-inner">' + 1084 '<div class="filter-option-inner-inner"></div>' + 1085 '</div> ' + 1086 '</div>' + 1087 ( 1088 version.major === '4' ? '' 1089 : 1090 '<span class="bs-caret">' + 1091 this.options.template.caret + 1092 '</span>' 1093 ) + 1094 '</button>' + 1095 '<div class="' + classNames.MENU + ' ' + (version.major === '4' ? '' : classNames.SHOW) + '">' + 1096 header + 1097 searchbox + 1098 actionsbox + 1099 '<div class="inner ' + classNames.SHOW + '" role="listbox" id="' + this.selectId + '" tabindex="-1" ' + multiselectable + '>' + 1100 '<ul class="' + classNames.MENU + ' inner ' + (version.major === '4' ? classNames.SHOW : '') + '" role="presentation">' + 1101 '</ul>' + 1102 '</div>' + 1103 donebutton + 1104 '</div>' + 1105 '</div>'; 1106 1107 return $(drop); 1108 }, 1109 1110 setPositionData: function () { 1111 this.selectpicker.view.canHighlight = []; 1112 this.selectpicker.view.size = 0; 1113 1114 for (var i = 0; i < this.selectpicker.current.data.length; i++) { 1115 var li = this.selectpicker.current.data[i], 1116 canHighlight = true; 1117 1118 if (li.type === 'divider') { 1119 canHighlight = false; 1120 li.height = this.sizeInfo.dividerHeight; 1121 } else if (li.type === 'optgroup-label') { 1122 canHighlight = false; 1123 li.height = this.sizeInfo.dropdownHeaderHeight; 1124 } else { 1125 li.height = this.sizeInfo.liHeight; 1126 } 1127 1128 if (li.disabled) canHighlight = false; 1129 1130 this.selectpicker.view.canHighlight.push(canHighlight); 1131 1132 if (canHighlight) { 1133 this.selectpicker.view.size++; 1134 li.posinset = this.selectpicker.view.size; 1135 } 1136 1137 li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height; 1138 } 1139 }, 1140 1141 isVirtual: function () { 1142 return (this.options.virtualScroll !== false) && (this.selectpicker.main.elements.length >= this.options.virtualScroll) || this.options.virtualScroll === true; 1143 }, 1144 1145 createView: function (isSearching, setSize, refresh) { 1146 var that = this, 1147 scrollTop = 0, 1148 active = [], 1149 selected, 1150 prevActive; 1151 1152 this.selectpicker.isSearching = isSearching; 1153 this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main; 1154 1155 this.setPositionData(); 1156 1157 if (setSize) { 1158 if (refresh) { 1159 scrollTop = this.$menuInner[0].scrollTop; 1160 } else if (!that.multiple) { 1161 var element = that.$element[0], 1162 selectedIndex = (element.options[element.selectedIndex] || {}).liIndex; 1163 1164 if (typeof selectedIndex === 'number' && that.options.size !== false) { 1165 var selectedData = that.selectpicker.main.data[selectedIndex], 1166 position = selectedData && selectedData.position; 1167 1168 if (position) { 1169 scrollTop = position - ((that.sizeInfo.menuInnerHeight + that.sizeInfo.liHeight) / 2); 1170 } 1171 } 1172 } 1173 } 1174 1175 scroll(scrollTop, true); 1176 1177 this.$menuInner.off('scroll.createView').on('scroll.createView', function (e, updateValue) { 1178 if (!that.noScroll) scroll(this.scrollTop, updateValue); 1179 that.noScroll = false; 1180 }); 1181 1182 function scroll (scrollTop, init) { 1183 var size = that.selectpicker.current.elements.length, 1184 chunks = [], 1185 chunkSize, 1186 chunkCount, 1187 firstChunk, 1188 lastChunk, 1189 currentChunk, 1190 prevPositions, 1191 positionIsDifferent, 1192 previousElements, 1193 menuIsDifferent = true, 1194 isVirtual = that.isVirtual(); 1195 1196 that.selectpicker.view.scrollTop = scrollTop; 1197 1198 chunkSize = Math.ceil(that.sizeInfo.menuInnerHeight / that.sizeInfo.liHeight * 1.5); // number of options in a chunk 1199 chunkCount = Math.round(size / chunkSize) || 1; // number of chunks 1200 1201 for (var i = 0; i < chunkCount; i++) { 1202 var endOfChunk = (i + 1) * chunkSize; 1203 1204 if (i === chunkCount - 1) { 1205 endOfChunk = size; 1206 } 1207 1208 chunks[i] = [ 1209 (i) * chunkSize + (!i ? 0 : 1), 1210 endOfChunk 1211 ]; 1212 1213 if (!size) break; 1214 1215 if (currentChunk === undefined && scrollTop - 1 <= that.selectpicker.current.data[endOfChunk - 1].position - that.sizeInfo.menuInnerHeight) { 1216 currentChunk = i; 1217 } 1218 } 1219 1220 if (currentChunk === undefined) currentChunk = 0; 1221 1222 prevPositions = [that.selectpicker.view.position0, that.selectpicker.view.position1]; 1223 1224 // always display previous, current, and next chunks 1225 firstChunk = Math.max(0, currentChunk - 1); 1226 lastChunk = Math.min(chunkCount - 1, currentChunk + 1); 1227 1228 that.selectpicker.view.position0 = isVirtual === false ? 0 : (Math.max(0, chunks[firstChunk][0]) || 0); 1229 that.selectpicker.view.position1 = isVirtual === false ? size : (Math.min(size, chunks[lastChunk][1]) || 0); 1230 1231 positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1; 1232 1233 if (that.activeIndex !== undefined) { 1234 prevActive = that.selectpicker.main.elements[that.prevActiveIndex]; 1235 active = that.selectpicker.main.elements[that.activeIndex]; 1236 selected = that.selectpicker.main.elements[that.selectedIndex]; 1237 1238 if (init) { 1239 if (that.activeIndex !== that.selectedIndex) { 1240 that.defocusItem(active); 1241 } 1242 that.activeIndex = undefined; 1243 } 1244 1245 if (that.activeIndex && that.activeIndex !== that.selectedIndex) { 1246 that.defocusItem(selected); 1247 } 1248 } 1249 1250 if (that.prevActiveIndex !== undefined && that.prevActiveIndex !== that.activeIndex && that.prevActiveIndex !== that.selectedIndex) { 1251 that.defocusItem(prevActive); 1252 } 1253 1254 if (init || positionIsDifferent) { 1255 previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : []; 1256 1257 if (isVirtual === false) { 1258 that.selectpicker.view.visibleElements = that.selectpicker.current.elements; 1259 } else { 1260 that.selectpicker.view.visibleElements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1); 1261 } 1262 1263 that.setOptionStatus(); 1264 1265 // if searching, check to make sure the list has actually been updated before updating DOM 1266 // this prevents unnecessary repaints 1267 if (isSearching || (isVirtual === false && init)) menuIsDifferent = !isEqual(previousElements, that.selectpicker.view.visibleElements); 1268 1269 // if virtual scroll is disabled and not searching, 1270 // menu should never need to be updated more than once 1271 if ((init || isVirtual === true) && menuIsDifferent) { 1272 var menuInner = that.$menuInner[0], 1273 menuFragment = document.createDocumentFragment(), 1274 emptyMenu = menuInner.firstChild.cloneNode(false), 1275 marginTop, 1276 marginBottom, 1277 elements = that.selectpicker.view.visibleElements, 1278 toSanitize = []; 1279 1280 // replace the existing UL with an empty one - this is faster than $.empty() 1281 menuInner.replaceChild(emptyMenu, menuInner.firstChild); 1282 1283 for (var i = 0, visibleElementsLen = elements.length; i < visibleElementsLen; i++) { 1284 var element = elements[i], 1285 elText, 1286 elementData; 1287 1288 if (that.options.sanitize) { 1289 elText = element.lastChild; 1290 1291 if (elText) { 1292 elementData = that.selectpicker.current.data[i + that.selectpicker.view.position0]; 1293 1294 if (elementData && elementData.content && !elementData.sanitized) { 1295 toSanitize.push(elText); 1296 elementData.sanitized = true; 1297 } 1298 } 1299 } 1300 1301 menuFragment.appendChild(element); 1302 } 1303 1304 if (that.options.sanitize && toSanitize.length) { 1305 sanitizeHtml(toSanitize, that.options.whiteList, that.options.sanitizeFn); 1306 } 1307 1308 if (isVirtual === true) { 1309 marginTop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position); 1310 marginBottom = (that.selectpicker.view.position1 > size - 1 ? 0 : that.selectpicker.current.data[size - 1].position - that.selectpicker.current.data[that.selectpicker.view.position1 - 1].position); 1311 1312 menuInner.firstChild.style.marginTop = marginTop + 'px'; 1313 menuInner.firstChild.style.marginBottom = marginBottom + 'px'; 1314 } else { 1315 menuInner.firstChild.style.marginTop = 0; 1316 menuInner.firstChild.style.marginBottom = 0; 1317 } 1318 1319 menuInner.firstChild.appendChild(menuFragment); 1320 1321 // if an option is encountered that is wider than the current menu width, update the menu width accordingly 1322 // switch to ResizeObserver with increased browser support 1323 if (isVirtual === true && that.sizeInfo.hasScrollBar) { 1324 var menuInnerInnerWidth = menuInner.firstChild.offsetWidth; 1325 1326 if (init && menuInnerInnerWidth < that.sizeInfo.menuInnerInnerWidth && that.sizeInfo.totalMenuWidth > that.sizeInfo.selectWidth) { 1327 menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; 1328 } else if (menuInnerInnerWidth > that.sizeInfo.menuInnerInnerWidth) { 1329 // set to 0 to get actual width of menu 1330 that.$menu[0].style.minWidth = 0; 1331 1332 var actualMenuWidth = menuInner.firstChild.offsetWidth; 1333 1334 if (actualMenuWidth > that.sizeInfo.menuInnerInnerWidth) { 1335 that.sizeInfo.menuInnerInnerWidth = actualMenuWidth; 1336 menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; 1337 } 1338 1339 // reset to default CSS styling 1340 that.$menu[0].style.minWidth = ''; 1341 } 1342 } 1343 } 1344 } 1345 1346 that.prevActiveIndex = that.activeIndex; 1347 1348 if (!that.options.liveSearch) { 1349 that.$menuInner.trigger('focus'); 1350 } else if (isSearching && init) { 1351 var index = 0, 1352 newActive; 1353 1354 if (!that.selectpicker.view.canHighlight[index]) { 1355 index = 1 + that.selectpicker.view.canHighlight.slice(1).indexOf(true); 1356 } 1357 1358 newActive = that.selectpicker.view.visibleElements[index]; 1359 1360 that.defocusItem(that.selectpicker.view.currentActive); 1361 1362 that.activeIndex = (that.selectpicker.current.data[index] || {}).index; 1363 1364 that.focusItem(newActive); 1365 } 1366 } 1367 1368 $(window) 1369 .off('resize' + EVENT_KEY + '.' + this.selectId + '.createView') 1370 .on('resize' + EVENT_KEY + '.' + this.selectId + '.createView', function () { 1371 var isActive = that.$newElement.hasClass(classNames.SHOW); 1372 1373 if (isActive) scroll(that.$menuInner[0].scrollTop); 1374 }); 1375 }, 1376 1377 focusItem: function (li, liData, noStyle) { 1378 if (li) { 1379 liData = liData || this.selectpicker.main.data[this.activeIndex]; 1380 var a = li.firstChild; 1381 1382 if (a) { 1383 a.setAttribute('aria-setsize', this.selectpicker.view.size); 1384 a.setAttribute('aria-posinset', liData.posinset); 1385 1386 if (noStyle !== true) { 1387 this.focusedParent.setAttribute('aria-activedescendant', a.id); 1388 li.classList.add('active'); 1389 a.classList.add('active'); 1390 } 1391 } 1392 } 1393 }, 1394 1395 defocusItem: function (li) { 1396 if (li) { 1397 li.classList.remove('active'); 1398 if (li.firstChild) li.firstChild.classList.remove('active'); 1399 } 1400 }, 1401 1402 setPlaceholder: function () { 1403 var updateIndex = false; 1404 1405 if (this.options.title && !this.multiple) { 1406 if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option'); 1407 1408 // this option doesn't create a new <li> element, but does add a new option at the start, 1409 // so startIndex should increase to prevent having to check every option for the bs-title-option class 1410 updateIndex = true; 1411 1412 var element = this.$element[0], 1413 isSelected = false, 1414 titleNotAppended = !this.selectpicker.view.titleOption.parentNode; 1415 1416 if (titleNotAppended) { 1417 // Use native JS to prepend option (faster) 1418 this.selectpicker.view.titleOption.className = 'bs-title-option'; 1419 this.selectpicker.view.titleOption.value = ''; 1420 1421 // Check if selected or data-selected attribute is already set on an option. If not, select the titleOption option. 1422 // the selected item may have been changed by user or programmatically before the bootstrap select plugin runs, 1423 // if so, the select will have the data-selected attribute 1424 var $opt = $(element.options[element.selectedIndex]); 1425 isSelected = $opt.attr('selected') === undefined && this.$element.data('selected') === undefined; 1426 } 1427 1428 if (titleNotAppended || this.selectpicker.view.titleOption.index !== 0) { 1429 element.insertBefore(this.selectpicker.view.titleOption, element.firstChild); 1430 } 1431 1432 // Set selected *after* appending to select, 1433 // otherwise the option doesn't get selected in IE 1434 // set using selectedIndex, as setting the selected attr to true here doesn't work in IE11 1435 if (isSelected) element.selectedIndex = 0; 1436 } 1437 1438 return updateIndex; 1439 }, 1440 1441 buildData: function () { 1442 var optionSelector = ':not([hidden]):not([data-hidden="true"])', 1443 mainData = [], 1444 optID = 0, 1445 startIndex = this.setPlaceholder() ? 1 : 0; // append the titleOption if necessary and skip the first option in the loop 1446 1447 if (this.options.hideDisabled) optionSelector += ':not(:disabled)'; 1448 1449 var selectOptions = this.$element[0].querySelectorAll('select > *' + optionSelector); 1450 1451 function addDivider (config) { 1452 var previousData = mainData[mainData.length - 1]; 1453 1454 // ensure optgroup doesn't create back-to-back dividers 1455 if ( 1456 previousData && 1457 previousData.type === 'divider' && 1458 (previousData.optID || config.optID) 1459 ) { 1460 return; 1461 } 1462 1463 config = config || {}; 1464 config.type = 'divider'; 1465 1466 mainData.push(config); 1467 } 1468 1469 function addOption (option, config) { 1470 config = config || {}; 1471 1472 config.divider = option.getAttribute('data-divider') === 'true'; 1473 1474 if (config.divider) { 1475 addDivider({ 1476 optID: config.optID 1477 }); 1478 } else { 1479 var liIndex = mainData.length, 1480 cssText = option.style.cssText, 1481 inlineStyle = cssText ? htmlEscape(cssText) : '', 1482 optionClass = (option.className || '') + (config.optgroupClass || ''); 1483 1484 if (config.optID) optionClass = 'opt ' + optionClass; 1485 1486 config.optionClass = optionClass.trim(); 1487 config.inlineStyle = inlineStyle; 1488 config.text = option.textContent; 1489 1490 config.content = option.getAttribute('data-content'); 1491 config.tokens = option.getAttribute('data-tokens'); 1492 config.subtext = option.getAttribute('data-subtext'); 1493 config.icon = option.getAttribute('data-icon'); 1494 1495 option.liIndex = liIndex; 1496 1497 config.display = config.content || config.text; 1498 config.type = 'option'; 1499 config.index = liIndex; 1500 config.option = option; 1501 config.selected = !!option.selected; 1502 config.disabled = config.disabled || !!option.disabled; 1503 1504 mainData.push(config); 1505 } 1506 } 1507 1508 function addOptgroup (index, selectOptions) { 1509 var optgroup = selectOptions[index], 1510 previous = selectOptions[index - 1], 1511 next = selectOptions[index + 1], 1512 options = optgroup.querySelectorAll('option' + optionSelector); 1513 1514 if (!options.length) return; 1515 1516 var config = { 1517 display: htmlEscape(optgroup.label), 1518 subtext: optgroup.getAttribute('data-subtext'), 1519 icon: optgroup.getAttribute('data-icon'), 1520 type: 'optgroup-label', 1521 optgroupClass: ' ' + (optgroup.className || '') 1522 }, 1523 headerIndex, 1524 lastIndex; 1525 1526 optID++; 1527 1528 if (previous) { 1529 addDivider({ optID: optID }); 1530 } 1531 1532 config.optID = optID; 1533 1534 mainData.push(config); 1535 1536 for (var j = 0, len = options.length; j < len; j++) { 1537 var option = options[j]; 1538 1539 if (j === 0) { 1540 headerIndex = mainData.length - 1; 1541 lastIndex = headerIndex + len; 1542 } 1543 1544 addOption(option, { 1545 headerIndex: headerIndex, 1546 lastIndex: lastIndex, 1547 optID: config.optID, 1548 optgroupClass: config.optgroupClass, 1549 disabled: optgroup.disabled 1550 }); 1551 } 1552 1553 if (next) { 1554 addDivider({ optID: optID }); 1555 } 1556 } 1557 1558 for (var len = selectOptions.length; startIndex < len; startIndex++) { 1559 var item = selectOptions[startIndex]; 1560 1561 if (item.tagName !== 'OPTGROUP') { 1562 addOption(item, {}); 1563 } else { 1564 addOptgroup(startIndex, selectOptions); 1565 } 1566 } 1567 1568 this.selectpicker.main.data = this.selectpicker.current.data = mainData; 1569 }, 1570 1571 buildList: function () { 1572 var that = this, 1573 selectData = this.selectpicker.main.data, 1574 mainElements = [], 1575 widestOptionLength = 0; 1576 1577 if ((that.options.showTick || that.multiple) && !elementTemplates.checkMark.parentNode) { 1578 elementTemplates.checkMark.className = this.options.iconBase + ' ' + that.options.tickIcon + ' check-mark'; 1579 elementTemplates.a.appendChild(elementTemplates.checkMark); 1580 } 1581 1582 function buildElement (item) { 1583 var liElement, 1584 combinedLength = 0; 1585 1586 switch (item.type) { 1587 case 'divider': 1588 liElement = generateOption.li( 1589 false, 1590 classNames.DIVIDER, 1591 (item.optID ? item.optID + 'div' : undefined) 1592 ); 1593 1594 break; 1595 1596 case 'option': 1597 liElement = generateOption.li( 1598 generateOption.a( 1599 generateOption.text.call(that, item), 1600 item.optionClass, 1601 item.inlineStyle 1602 ), 1603 '', 1604 item.optID 1605 ); 1606 1607 if (liElement.firstChild) { 1608 liElement.firstChild.id = that.selectId + '-' + item.index; 1609 } 1610 1611 break; 1612 1613 case 'optgroup-label': 1614 liElement = generateOption.li( 1615 generateOption.label.call(that, item), 1616 'dropdown-header' + item.optgroupClass, 1617 item.optID 1618 ); 1619 1620 break; 1621 } 1622 1623 mainElements.push(liElement); 1624 1625 // count the number of characters in the option - not perfect, but should work in most cases 1626 if (item.display) combinedLength += item.display.length; 1627 if (item.subtext) combinedLength += item.subtext.length; 1628 // if there is an icon, ensure this option's width is checked 1629 if (item.icon) combinedLength += 1; 1630 1631 if (combinedLength > widestOptionLength) { 1632 widestOptionLength = combinedLength; 1633 1634 // guess which option is the widest 1635 // use this when calculating menu width 1636 // not perfect, but it's fast, and the width will be updating accordingly when scrolling 1637 that.selectpicker.view.widestOption = mainElements[mainElements.length - 1]; 1638 } 1639 } 1640 1641 for (var len = selectData.length, i = 0; i < len; i++) { 1642 var item = selectData[i]; 1643 1644 buildElement(item); 1645 } 1646 1647 this.selectpicker.main.elements = this.selectpicker.current.elements = mainElements; 1648 }, 1649 1650 findLis: function () { 1651 return this.$menuInner.find('.inner > li'); 1652 }, 1653 1654 render: function () { 1655 var that = this, 1656 element = this.$element[0], 1657 // ensure titleOption is appended and selected (if necessary) before getting selectedOptions 1658 placeholderSelected = this.setPlaceholder() && element.selectedIndex === 0, 1659 selectedOptions = getSelectedOptions(element, this.options.hideDisabled), 1660 selectedCount = selectedOptions.length, 1661 button = this.$button[0], 1662 buttonInner = button.querySelector('.filter-option-inner-inner'), 1663 multipleSeparator = document.createTextNode(this.options.multipleSeparator), 1664 titleFragment = elementTemplates.fragment.cloneNode(false), 1665 showCount, 1666 countMax, 1667 hasContent = false; 1668 1669 button.classList.toggle('bs-placeholder', that.multiple ? !selectedCount : !getSelectValues(element, selectedOptions)); 1670 1671 this.tabIndex(); 1672 1673 if (this.options.selectedTextFormat === 'static') { 1674 titleFragment = generateOption.text.call(this, { text: this.options.title }, true); 1675 } else { 1676 showCount = this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1 && selectedCount > 1; 1677 1678 // determine if the number of selected options will be shown (showCount === true) 1679 if (showCount) { 1680 countMax = this.options.selectedTextFormat.split('>'); 1681 showCount = (countMax.length > 1 && selectedCount > countMax[1]) || (countMax.length === 1 && selectedCount >= 2); 1682 } 1683 1684 // only loop through all selected options if the count won't be shown 1685 if (showCount === false) { 1686 if (!placeholderSelected) { 1687 for (var selectedIndex = 0; selectedIndex < selectedCount; selectedIndex++) { 1688 if (selectedIndex < 50) { 1689 var option = selectedOptions[selectedIndex], 1690 thisData = this.selectpicker.main.data[option.liIndex], 1691 titleOptions = {}; 1692 1693 if (this.multiple && selectedIndex > 0) { 1694 titleFragment.appendChild(multipleSeparator.cloneNode(false)); 1695 } 1696 1697 if (option.title) { 1698 titleOptions.text = option.title; 1699 } else if (thisData) { 1700 if (thisData.content && that.options.showContent) { 1701 titleOptions.content = thisData.content.toString(); 1702 hasContent = true; 1703 } else { 1704 if (that.options.showIcon) { 1705 titleOptions.icon = thisData.icon; 1706 } 1707 if (that.options.showSubtext && !that.multiple && thisData.subtext) titleOptions.subtext = ' ' + thisData.subtext; 1708 titleOptions.text = option.textContent.trim(); 1709 } 1710 } 1711 1712 titleFragment.appendChild(generateOption.text.call(this, titleOptions, true)); 1713 } else { 1714 break; 1715 } 1716 } 1717 1718 // add ellipsis 1719 if (selectedCount > 49) { 1720 titleFragment.appendChild(document.createTextNode('...')); 1721 } 1722 } 1723 } else { 1724 var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([data-divider="true"])'; 1725 if (this.options.hideDisabled) optionSelector += ':not(:disabled)'; 1726 1727 // If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected, etc. 1728 var totalCount = this.$element[0].querySelectorAll('select > option' + optionSelector + ', optgroup' + optionSelector + ' option' + optionSelector).length, 1729 tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedCount, totalCount) : this.options.countSelectedText; 1730 1731 titleFragment = generateOption.text.call(this, { 1732 text: tr8nText.replace('{0}', selectedCount.toString()).replace('{1}', totalCount.toString()) 1733 }, true); 1734 } 1735 } 1736 1737 if (this.options.title == undefined) { 1738 // use .attr to ensure undefined is returned if title attribute is not set 1739 this.options.title = this.$element.attr('title'); 1740 } 1741 1742 // If the select doesn't have a title, then use the default, or if nothing is set at all, use noneSelectedText 1743 if (!titleFragment.childNodes.length) { 1744 titleFragment = generateOption.text.call(this, { 1745 text: typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneSelectedText 1746 }, true); 1747 } 1748 1749 // strip all HTML tags and trim the result, then unescape any escaped tags 1750 button.title = titleFragment.textContent.replace(/<[^>]*>?/g, '').trim(); 1751 1752 if (this.options.sanitize && hasContent) { 1753 sanitizeHtml([titleFragment], that.options.whiteList, that.options.sanitizeFn); 1754 } 1755 1756 buttonInner.innerHTML = ''; 1757 buttonInner.appendChild(titleFragment); 1758 1759 if (version.major < 4 && this.$newElement[0].classList.contains('bs3-has-addon')) { 1760 var filterExpand = button.querySelector('.filter-expand'), 1761 clone = buttonInner.cloneNode(true); 1762 1763 clone.className = 'filter-expand'; 1764 1765 if (filterExpand) { 1766 button.replaceChild(clone, filterExpand); 1767 } else { 1768 button.appendChild(clone); 1769 } 1770 } 1771 1772 this.$element.trigger('rendered' + EVENT_KEY); 1773 }, 1774 1775 /** 1776 * @param [style] 1777 * @param [status] 1778 */ 1779 setStyle: function (newStyle, status) { 1780 var button = this.$button[0], 1781 newElement = this.$newElement[0], 1782 style = this.options.style.trim(), 1783 buttonClass; 1784 1785 if (this.$element.attr('class')) { 1786 this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); 1787 } 1788 1789 if (version.major < 4) { 1790 newElement.classList.add('bs3'); 1791 1792 if (newElement.parentNode.classList.contains('input-group') && 1793 (newElement.previousElementSibling || newElement.nextElementSibling) && 1794 (newElement.previousElementSibling || newElement.nextElementSibling).classList.contains('input-group-addon') 1795 ) { 1796 newElement.classList.add('bs3-has-addon'); 1797 } 1798 } 1799 1800 if (newStyle) { 1801 buttonClass = newStyle.trim(); 1802 } else { 1803 buttonClass = style; 1804 } 1805 1806 if (status == 'add') { 1807 if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' ')); 1808 } else if (status == 'remove') { 1809 if (buttonClass) button.classList.remove.apply(button.classList, buttonClass.split(' ')); 1810 } else { 1811 if (style) button.classList.remove.apply(button.classList, style.split(' ')); 1812 if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' ')); 1813 } 1814 }, 1815 1816 liHeight: function (refresh) { 1817 if (!refresh && (this.options.size === false || Object.keys(this.sizeInfo).length)) return; 1818 1819 var newElement = document.createElement('div'), 1820 menu = document.createElement('div'), 1821 menuInner = document.createElement('div'), 1822 menuInnerInner = document.createElement('ul'), 1823 divider = document.createElement('li'), 1824 dropdownHeader = document.createElement('li'), 1825 li = document.createElement('li'), 1826 a = document.createElement('a'), 1827 text = document.createElement('span'), 1828 header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null, 1829 search = this.options.liveSearch ? document.createElement('div') : null, 1830 actions = this.options.actionsBox && this.multiple && this.$menu.find('.bs-actionsbox').length > 0 ? this.$menu.find('.bs-actionsbox')[0].cloneNode(true) : null, 1831 doneButton = this.options.doneButton && this.multiple && this.$menu.find('.bs-donebutton').length > 0 ? this.$menu.find('.bs-donebutton')[0].cloneNode(true) : null, 1832 firstOption = this.$element.find('option')[0]; 1833 1834 this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth; 1835 1836 text.className = 'text'; 1837 a.className = 'dropdown-item ' + (firstOption ? firstOption.className : ''); 1838 newElement.className = this.$menu[0].parentNode.className + ' ' + classNames.SHOW; 1839 newElement.style.width = 0; // ensure button width doesn't affect natural width of menu when calculating 1840 if (this.options.width === 'auto') menu.style.minWidth = 0; 1841 menu.className = classNames.MENU + ' ' + classNames.SHOW; 1842 menuInner.className = 'inner ' + classNames.SHOW; 1843 menuInnerInner.className = classNames.MENU + ' inner ' + (version.major === '4' ? classNames.SHOW : ''); 1844 divider.className = classNames.DIVIDER; 1845 dropdownHeader.className = 'dropdown-header'; 1846 1847 text.appendChild(document.createTextNode('\u200b')); 1848 a.appendChild(text); 1849 li.appendChild(a); 1850 dropdownHeader.appendChild(text.cloneNode(true)); 1851 1852 if (this.selectpicker.view.widestOption) { 1853 menuInnerInner.appendChild(this.selectpicker.view.widestOption.cloneNode(true)); 1854 } 1855 1856 menuInnerInner.appendChild(li); 1857 menuInnerInner.appendChild(divider); 1858 menuInnerInner.appendChild(dropdownHeader); 1859 if (header) menu.appendChild(header); 1860 if (search) { 1861 var input = document.createElement('input'); 1862 search.className = 'bs-searchbox'; 1863 input.className = 'form-control'; 1864 search.appendChild(input); 1865 menu.appendChild(search); 1866 } 1867 if (actions) menu.appendChild(actions); 1868 menuInner.appendChild(menuInnerInner); 1869 menu.appendChild(menuInner); 1870 if (doneButton) menu.appendChild(doneButton); 1871 newElement.appendChild(menu); 1872 1873 document.body.appendChild(newElement); 1874 1875 var liHeight = li.offsetHeight, 1876 dropdownHeaderHeight = dropdownHeader ? dropdownHeader.offsetHeight : 0, 1877 headerHeight = header ? header.offsetHeight : 0, 1878 searchHeight = search ? search.offsetHeight : 0, 1879 actionsHeight = actions ? actions.offsetHeight : 0, 1880 doneButtonHeight = doneButton ? doneButton.offsetHeight : 0, 1881 dividerHeight = $(divider).outerHeight(true), 1882 // fall back to jQuery if getComputedStyle is not supported 1883 menuStyle = window.getComputedStyle ? window.getComputedStyle(menu) : false, 1884 menuWidth = menu.offsetWidth, 1885 $menu = menuStyle ? null : $(menu), 1886 menuPadding = { 1887 vert: toInteger(menuStyle ? menuStyle.paddingTop : $menu.css('paddingTop')) + 1888 toInteger(menuStyle ? menuStyle.paddingBottom : $menu.css('paddingBottom')) + 1889 toInteger(menuStyle ? menuStyle.borderTopWidth : $menu.css('borderTopWidth')) + 1890 toInteger(menuStyle ? menuStyle.borderBottomWidth : $menu.css('borderBottomWidth')), 1891 horiz: toInteger(menuStyle ? menuStyle.paddingLeft : $menu.css('paddingLeft')) + 1892 toInteger(menuStyle ? menuStyle.paddingRight : $menu.css('paddingRight')) + 1893 toInteger(menuStyle ? menuStyle.borderLeftWidth : $menu.css('borderLeftWidth')) + 1894 toInteger(menuStyle ? menuStyle.borderRightWidth : $menu.css('borderRightWidth')) 1895 }, 1896 menuExtras = { 1897 vert: menuPadding.vert + 1898 toInteger(menuStyle ? menuStyle.marginTop : $menu.css('marginTop')) + 1899 toInteger(menuStyle ? menuStyle.marginBottom : $menu.css('marginBottom')) + 2, 1900 horiz: menuPadding.horiz + 1901 toInteger(menuStyle ? menuStyle.marginLeft : $menu.css('marginLeft')) + 1902 toInteger(menuStyle ? menuStyle.marginRight : $menu.css('marginRight')) + 2 1903 }, 1904 scrollBarWidth; 1905 1906 menuInner.style.overflowY = 'scroll'; 1907 1908 scrollBarWidth = menu.offsetWidth - menuWidth; 1909 1910 document.body.removeChild(newElement); 1911 1912 this.sizeInfo.liHeight = liHeight; 1913 this.sizeInfo.dropdownHeaderHeight = dropdownHeaderHeight; 1914 this.sizeInfo.headerHeight = headerHeight; 1915 this.sizeInfo.searchHeight = searchHeight; 1916 this.sizeInfo.actionsHeight = actionsHeight; 1917 this.sizeInfo.doneButtonHeight = doneButtonHeight; 1918 this.sizeInfo.dividerHeight = dividerHeight; 1919 this.sizeInfo.menuPadding = menuPadding; 1920 this.sizeInfo.menuExtras = menuExtras; 1921 this.sizeInfo.menuWidth = menuWidth; 1922 this.sizeInfo.menuInnerInnerWidth = menuWidth - menuPadding.horiz; 1923 this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth; 1924 this.sizeInfo.scrollBarWidth = scrollBarWidth; 1925 this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight; 1926 1927 this.setPositionData(); 1928 }, 1929 1930 getSelectPosition: function () { 1931 var that = this, 1932 $window = $(window), 1933 pos = that.$newElement.offset(), 1934 $container = $(that.options.container), 1935 containerPos; 1936 1937 if (that.options.container && $container.length && !$container.is('body')) { 1938 containerPos = $container.offset(); 1939 containerPos.top += parseInt($container.css('borderTopWidth')); 1940 containerPos.left += parseInt($container.css('borderLeftWidth')); 1941 } else { 1942 containerPos = { top: 0, left: 0 }; 1943 } 1944 1945 var winPad = that.options.windowPadding; 1946 1947 this.sizeInfo.selectOffsetTop = pos.top - containerPos.top - $window.scrollTop(); 1948 this.sizeInfo.selectOffsetBot = $window.height() - this.sizeInfo.selectOffsetTop - this.sizeInfo.selectHeight - containerPos.top - winPad[2]; 1949 this.sizeInfo.selectOffsetLeft = pos.left - containerPos.left - $window.scrollLeft(); 1950 this.sizeInfo.selectOffsetRight = $window.width() - this.sizeInfo.selectOffsetLeft - this.sizeInfo.selectWidth - containerPos.left - winPad[1]; 1951 this.sizeInfo.selectOffsetTop -= winPad[0]; 1952 this.sizeInfo.selectOffsetLeft -= winPad[3]; 1953 }, 1954 1955 setMenuSize: function (isAuto) { 1956 this.getSelectPosition(); 1957 1958 var selectWidth = this.sizeInfo.selectWidth, 1959 liHeight = this.sizeInfo.liHeight, 1960 headerHeight = this.sizeInfo.headerHeight, 1961 searchHeight = this.sizeInfo.searchHeight, 1962 actionsHeight = this.sizeInfo.actionsHeight, 1963 doneButtonHeight = this.sizeInfo.doneButtonHeight, 1964 divHeight = this.sizeInfo.dividerHeight, 1965 menuPadding = this.sizeInfo.menuPadding, 1966 menuInnerHeight, 1967 menuHeight, 1968 divLength = 0, 1969 minHeight, 1970 _minHeight, 1971 maxHeight, 1972 menuInnerMinHeight, 1973 estimate, 1974 isDropup; 1975 1976 if (this.options.dropupAuto) { 1977 // Get the estimated height of the menu without scrollbars. 1978 // This is useful for smaller menus, where there might be plenty of room 1979 // below the button without setting dropup, but we can't know 1980 // the exact height of the menu until createView is called later 1981 estimate = liHeight * this.selectpicker.current.elements.length + menuPadding.vert; 1982 1983 isDropup = this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot; 1984 1985 // ensure dropup doesn't change while searching (so menu doesn't bounce back and forth) 1986 if (this.selectpicker.isSearching === true) { 1987 isDropup = this.selectpicker.dropup; 1988 } 1989 1990 this.$newElement.toggleClass(classNames.DROPUP, isDropup); 1991 this.selectpicker.dropup = isDropup; 1992 } 1993 1994 if (this.options.size === 'auto') { 1995 _minHeight = this.selectpicker.current.elements.length > 3 ? this.sizeInfo.liHeight * 3 + this.sizeInfo.menuExtras.vert - 2 : 0; 1996 menuHeight = this.sizeInfo.selectOffsetBot - this.sizeInfo.menuExtras.vert; 1997 minHeight = _minHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight; 1998 menuInnerMinHeight = Math.max(_minHeight - menuPadding.vert, 0); 1999 2000 if (this.$newElement.hasClass(classNames.DROPUP)) { 2001 menuHeight = this.sizeInfo.selectOffsetTop - this.sizeInfo.menuExtras.vert; 2002 } 2003 2004 maxHeight = menuHeight; 2005 menuInnerHeight = menuHeight - headerHeight - searchHeight - actionsHeight - doneButtonHeight - menuPadding.vert; 2006 } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { 2007 for (var i = 0; i < this.options.size; i++) { 2008 if (this.selectpicker.current.data[i].type === 'divider') divLength++; 2009 } 2010 2011 menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding.vert; 2012 menuInnerHeight = menuHeight - menuPadding.vert; 2013 maxHeight = menuHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight; 2014 minHeight = menuInnerMinHeight = ''; 2015 } 2016 2017 this.$menu.css({ 2018 'max-height': maxHeight + 'px', 2019 'overflow': 'hidden', 2020 'min-height': minHeight + 'px' 2021 }); 2022 2023 this.$menuInner.css({ 2024 'max-height': menuInnerHeight + 'px', 2025 'overflow-y': 'auto', 2026 'min-height': menuInnerMinHeight + 'px' 2027 }); 2028 2029 // ensure menuInnerHeight is always a positive number to prevent issues calculating chunkSize in createView 2030 this.sizeInfo.menuInnerHeight = Math.max(menuInnerHeight, 1); 2031 2032 if (this.selectpicker.current.data.length && this.selectpicker.current.data[this.selectpicker.current.data.length - 1].position > this.sizeInfo.menuInnerHeight) { 2033 this.sizeInfo.hasScrollBar = true; 2034 this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth + this.sizeInfo.scrollBarWidth; 2035 } 2036 2037 if (this.options.dropdownAlignRight === 'auto') { 2038 this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.sizeInfo.totalMenuWidth - selectWidth)); 2039 } 2040 2041 if (this.dropdown && this.dropdown._popper) this.dropdown._popper.update(); 2042 }, 2043 2044 setSize: function (refresh) { 2045 this.liHeight(refresh); 2046 2047 if (this.options.header) this.$menu.css('padding-top', 0); 2048 2049 if (this.options.size !== false) { 2050 var that = this, 2051 $window = $(window); 2052 2053 this.setMenuSize(); 2054 2055 if (this.options.liveSearch) { 2056 this.$searchbox 2057 .off('input.setMenuSize propertychange.setMenuSize') 2058 .on('input.setMenuSize propertychange.setMenuSize', function () { 2059 return that.setMenuSize(); 2060 }); 2061 } 2062 2063 if (this.options.size === 'auto') { 2064 $window 2065 .off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize') 2066 .on('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize', function () { 2067 return that.setMenuSize(); 2068 }); 2069 } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { 2070 $window.off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize'); 2071 } 2072 } 2073 2074 this.createView(false, true, refresh); 2075 }, 2076 2077 setWidth: function () { 2078 var that = this; 2079 2080 if (this.options.width === 'auto') { 2081 requestAnimationFrame(function () { 2082 that.$menu.css('min-width', '0'); 2083 2084 that.$element.on('loaded' + EVENT_KEY, function () { 2085 that.liHeight(); 2086 that.setMenuSize(); 2087 2088 // Get correct width if element is hidden 2089 var $selectClone = that.$newElement.clone().appendTo('body'), 2090 btnWidth = $selectClone.css('width', 'auto').children('button').outerWidth(); 2091 2092 $selectClone.remove(); 2093 2094 // Set width to whatever's larger, button title or longest option 2095 that.sizeInfo.selectWidth = Math.max(that.sizeInfo.totalMenuWidth, btnWidth); 2096 that.$newElement.css('width', that.sizeInfo.selectWidth + 'px'); 2097 }); 2098 }); 2099 } else if (this.options.width === 'fit') { 2100 // Remove inline min-width so width can be changed from 'auto' 2101 this.$menu.css('min-width', ''); 2102 this.$newElement.css('width', '').addClass('fit-width'); 2103 } else if (this.options.width) { 2104 // Remove inline min-width so width can be changed from 'auto' 2105 this.$menu.css('min-width', ''); 2106 this.$newElement.css('width', this.options.width); 2107 } else { 2108 // Remove inline min-width/width so width can be changed 2109 this.$menu.css('min-width', ''); 2110 this.$newElement.css('width', ''); 2111 } 2112 // Remove fit-width class if width is changed programmatically 2113 if (this.$newElement.hasClass('fit-width') && this.options.width !== 'fit') { 2114 this.$newElement[0].classList.remove('fit-width'); 2115 } 2116 }, 2117 2118 selectPosition: function () { 2119 this.$bsContainer = $('<div class="bs-container" />'); 2120 2121 var that = this, 2122 $container = $(this.options.container), 2123 pos, 2124 containerPos, 2125 actualHeight, 2126 getPlacement = function ($element) { 2127 var containerPosition = {}, 2128 // fall back to dropdown's default display setting if display is not manually set 2129 display = that.options.display || ( 2130 // Bootstrap 3 doesn't have $.fn.dropdown.Constructor.Default 2131 $.fn.dropdown.Constructor.Default ? $.fn.dropdown.Constructor.Default.display 2132 : false 2133 ); 2134 2135 that.$bsContainer.addClass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleClass(classNames.DROPUP, $element.hasClass(classNames.DROPUP)); 2136 pos = $element.offset(); 2137 2138 if (!$container.is('body')) { 2139 containerPos = $container.offset(); 2140 containerPos.top += parseInt($container.css('borderTopWidth')) - $container.scrollTop(); 2141 containerPos.left += parseInt($container.css('borderLeftWidth')) - $container.scrollLeft(); 2142 } else { 2143 containerPos = { top: 0, left: 0 }; 2144 } 2145 2146 actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : $element[0].offsetHeight; 2147 2148 // Bootstrap 4+ uses Popper for menu positioning 2149 if (version.major < 4 || display === 'static') { 2150 containerPosition.top = pos.top - containerPos.top + actualHeight; 2151 containerPosition.left = pos.left - containerPos.left; 2152 } 2153 2154 containerPosition.width = $element[0].offsetWidth; 2155 2156 that.$bsContainer.css(containerPosition); 2157 }; 2158 2159 this.$button.on('click.bs.dropdown.data-api', function () { 2160 if (that.isDisabled()) { 2161 return; 2162 } 2163 2164 getPlacement(that.$newElement); 2165 2166 that.$bsContainer 2167 .appendTo(that.options.container) 2168 .toggleClass(classNames.SHOW, !that.$button.hasClass(classNames.SHOW)) 2169 .append(that.$menu); 2170 }); 2171 2172 $(window) 2173 .off('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId) 2174 .on('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId, function () { 2175 var isActive = that.$newElement.hasClass(classNames.SHOW); 2176 2177 if (isActive) getPlacement(that.$newElement); 2178 }); 2179 2180 this.$element.on('hide' + EVENT_KEY, function () { 2181 that.$menu.data('height', that.$menu.height()); 2182 that.$bsContainer.detach(); 2183 }); 2184 }, 2185 2186 setOptionStatus: function (selectedOnly) { 2187 var that = this; 2188 2189 that.noScroll = false; 2190 2191 if (that.selectpicker.view.visibleElements && that.selectpicker.view.visibleElements.length) { 2192 for (var i = 0; i < that.selectpicker.view.visibleElements.length; i++) { 2193 var liData = that.selectpicker.current.data[i + that.selectpicker.view.position0], 2194 option = liData.option; 2195 2196 if (option) { 2197 if (selectedOnly !== true) { 2198 that.setDisabled( 2199 liData.index, 2200 liData.disabled 2201 ); 2202 } 2203 2204 that.setSelected( 2205 liData.index, 2206 option.selected 2207 ); 2208 } 2209 } 2210 } 2211 }, 2212 2213 /** 2214 * @param {number} index - the index of the option that is being changed 2215 * @param {boolean} selected - true if the option is being selected, false if being deselected 2216 */ 2217 setSelected: function (index, selected) { 2218 var li = this.selectpicker.main.elements[index], 2219 liData = this.selectpicker.main.data[index], 2220 activeIndexIsSet = this.activeIndex !== undefined, 2221 thisIsActive = this.activeIndex === index, 2222 prevActive, 2223 a, 2224 // if current option is already active 2225 // OR 2226 // if the current option is being selected, it's NOT multiple, and 2227 // activeIndex is undefined: 2228 // - when the menu is first being opened, OR 2229 // - after a search has been performed, OR 2230 // - when retainActive is false when selecting a new option (i.e. index of the newly selected option is not the same as the current activeIndex) 2231 keepActive = thisIsActive || (selected && !this.multiple && !activeIndexIsSet); 2232 2233 liData.selected = selected; 2234 2235 a = li.firstChild; 2236 2237 if (selected) { 2238 this.selectedIndex = index; 2239 } 2240 2241 li.classList.toggle('selected', selected); 2242 2243 if (keepActive) { 2244 this.focusItem(li, liData); 2245 this.selectpicker.view.currentActive = li; 2246 this.activeIndex = index; 2247 } else { 2248 this.defocusItem(li); 2249 } 2250 2251 if (a) { 2252 a.classList.toggle('selected', selected); 2253 2254 if (selected) { 2255 a.setAttribute('aria-selected', true); 2256 } else { 2257 if (this.multiple) { 2258 a.setAttribute('aria-selected', false); 2259 } else { 2260 a.removeAttribute('aria-selected'); 2261 } 2262 } 2263 } 2264 2265 if (!keepActive && !activeIndexIsSet && selected && this.prevActiveIndex !== undefined) { 2266 prevActive = this.selectpicker.main.elements[this.prevActiveIndex]; 2267 2268 this.defocusItem(prevActive); 2269 } 2270 }, 2271 2272 /** 2273 * @param {number} index - the index of the option that is being disabled 2274 * @param {boolean} disabled - true if the option is being disabled, false if being enabled 2275 */ 2276 setDisabled: function (index, disabled) { 2277 var li = this.selectpicker.main.elements[index], 2278 a; 2279 2280 this.selectpicker.main.data[index].disabled = disabled; 2281 2282 a = li.firstChild; 2283 2284 li.classList.toggle(classNames.DISABLED, disabled); 2285 2286 if (a) { 2287 if (version.major === '4') a.classList.toggle(classNames.DISABLED, disabled); 2288 2289 if (disabled) { 2290 a.setAttribute('aria-disabled', disabled); 2291 a.setAttribute('tabindex', -1); 2292 } else { 2293 a.removeAttribute('aria-disabled'); 2294 a.setAttribute('tabindex', 0); 2295 } 2296 } 2297 }, 2298 2299 isDisabled: function () { 2300 return this.$element[0].disabled; 2301 }, 2302 2303 checkDisabled: function () { 2304 if (this.isDisabled()) { 2305 this.$newElement[0].classList.add(classNames.DISABLED); 2306 this.$button.addClass(classNames.DISABLED).attr('tabindex', -1).attr('aria-disabled', true); 2307 } else { 2308 if (this.$button[0].classList.contains(classNames.DISABLED)) { 2309 this.$newElement[0].classList.remove(classNames.DISABLED); 2310 this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false); 2311 } 2312 2313 if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) { 2314 this.$button.removeAttr('tabindex'); 2315 } 2316 } 2317 }, 2318 2319 tabIndex: function () { 2320 if (this.$element.data('tabindex') !== this.$element.attr('tabindex') && 2321 (this.$element.attr('tabindex') !== -98 && this.$element.attr('tabindex') !== '-98')) { 2322 this.$element.data('tabindex', this.$element.attr('tabindex')); 2323 this.$button.attr('tabindex', this.$element.data('tabindex')); 2324 } 2325 2326 this.$element.attr('tabindex', -98); 2327 }, 2328 2329 clickListener: function () { 2330 var that = this, 2331 $document = $(document); 2332 2333 $document.data('spaceSelect', false); 2334 2335 this.$button.on('keyup', function (e) { 2336 if (/(32)/.test(e.keyCode.toString(10)) && $document.data('spaceSelect')) { 2337 e.preventDefault(); 2338 $document.data('spaceSelect', false); 2339 } 2340 }); 2341 2342 this.$newElement.on('show.bs.dropdown', function () { 2343 if (version.major > 3 && !that.dropdown) { 2344 that.dropdown = that.$button.data('bs.dropdown'); 2345 that.dropdown._menu = that.$menu[0]; 2346 } 2347 }); 2348 2349 this.$button.on('click.bs.dropdown.data-api', function () { 2350 if (!that.$newElement.hasClass(classNames.SHOW)) { 2351 that.setSize(); 2352 } 2353 }); 2354 2355 function setFocus () { 2356 if (that.options.liveSearch) { 2357 that.$searchbox.trigger('focus'); 2358 } else { 2359 that.$menuInner.trigger('focus'); 2360 } 2361 } 2362 2363 function checkPopperExists () { 2364 if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state.isCreated) { 2365 setFocus(); 2366 } else { 2367 requestAnimationFrame(checkPopperExists); 2368 } 2369 } 2370 2371 this.$element.on('shown' + EVENT_KEY, function () { 2372 if (that.$menuInner[0].scrollTop !== that.selectpicker.view.scrollTop) { 2373 that.$menuInner[0].scrollTop = that.selectpicker.view.scrollTop; 2374 } 2375 2376 if (version.major > 3) { 2377 requestAnimationFrame(checkPopperExists); 2378 } else { 2379 setFocus(); 2380 } 2381 }); 2382 2383 // ensure posinset and setsize are correct before selecting an option via a click 2384 this.$menuInner.on('mouseenter', 'li a', function (e) { 2385 var hoverLi = this.parentElement, 2386 position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, 2387 index = Array.prototype.indexOf.call(hoverLi.parentElement.children, hoverLi), 2388 hoverData = that.selectpicker.current.data[index + position0]; 2389 2390 that.focusItem(hoverLi, hoverData, true); 2391 }); 2392 2393 this.$menuInner.on('click', 'li a', function (e, retainActive) { 2394 var $this = $(this), 2395 element = that.$element[0], 2396 position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, 2397 clickedData = that.selectpicker.current.data[$this.parent().index() + position0], 2398 clickedIndex = clickedData.index, 2399 prevValue = getSelectValues(element), 2400 prevIndex = element.selectedIndex, 2401 prevOption = element.options[prevIndex], 2402 triggerChange = true; 2403 2404 // Don't close on multi choice menu 2405 if (that.multiple && that.options.maxOptions !== 1) { 2406 e.stopPropagation(); 2407 } 2408 2409 e.preventDefault(); 2410 2411 // Don't run if the select is disabled 2412 if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) { 2413 var option = clickedData.option, 2414 $option = $(option), 2415 state = option.selected, 2416 $optgroup = $option.parent('optgroup'), 2417 $optgroupOptions = $optgroup.find('option'), 2418 maxOptions = that.options.maxOptions, 2419 maxOptionsGrp = $optgroup.data('maxOptions') || false; 2420 2421 if (clickedIndex === that.activeIndex) retainActive = true; 2422 2423 if (!retainActive) { 2424 that.prevActiveIndex = that.activeIndex; 2425 that.activeIndex = undefined; 2426 } 2427 2428 if (!that.multiple) { // Deselect all others if not multi select box 2429 if (prevOption) prevOption.selected = false; 2430 option.selected = true; 2431 that.setSelected(clickedIndex, true); 2432 } else { // Toggle the one we have chosen if we are multi select. 2433 option.selected = !state; 2434 2435 that.setSelected(clickedIndex, !state); 2436 $this.trigger('blur'); 2437 2438 if (maxOptions !== false || maxOptionsGrp !== false) { 2439 var maxReached = maxOptions < getSelectedOptions(element).length, 2440 maxReachedGrp = maxOptionsGrp < $optgroup.find('option:selected').length; 2441 2442 if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) { 2443 if (maxOptions && maxOptions == 1) { 2444 element.selectedIndex = -1; 2445 option.selected = true; 2446 that.setOptionStatus(true); 2447 } else if (maxOptionsGrp && maxOptionsGrp == 1) { 2448 for (var i = 0; i < $optgroupOptions.length; i++) { 2449 var _option = $optgroupOptions[i]; 2450 _option.selected = false; 2451 that.setSelected(_option.liIndex, false); 2452 } 2453 2454 option.selected = true; 2455 that.setSelected(clickedIndex, true); 2456 } else { 2457 var maxOptionsText = typeof that.options.maxOptionsText === 'string' ? [that.options.maxOptionsText, that.options.maxOptionsText] : that.options.maxOptionsText, 2458 maxOptionsArr = typeof maxOptionsText === 'function' ? maxOptionsText(maxOptions, maxOptionsGrp) : maxOptionsText, 2459 maxTxt = maxOptionsArr[0].replace('{n}', maxOptions), 2460 maxTxtGrp = maxOptionsArr[1].replace('{n}', maxOptionsGrp), 2461 $notify = $('<div class="notify"></div>'); 2462 // If {var} is set in array, replace it 2463 /** @deprecated */ 2464 if (maxOptionsArr[2]) { 2465 maxTxt = maxTxt.replace('{var}', maxOptionsArr[2][maxOptions > 1 ? 0 : 1]); 2466 maxTxtGrp = maxTxtGrp.replace('{var}', maxOptionsArr[2][maxOptionsGrp > 1 ? 0 : 1]); 2467 } 2468 2469 option.selected = false; 2470 2471 that.$menu.append($notify); 2472 2473 if (maxOptions && maxReached) { 2474 $notify.append($('<div>' + maxTxt + '</div>')); 2475 triggerChange = false; 2476 that.$element.trigger('maxReached' + EVENT_KEY); 2477 } 2478 2479 if (maxOptionsGrp && maxReachedGrp) { 2480 $notify.append($('<div>' + maxTxtGrp + '</div>')); 2481 triggerChange = false; 2482 that.$element.trigger('maxReachedGrp' + EVENT_KEY); 2483 } 2484 2485 setTimeout(function () { 2486 that.setSelected(clickedIndex, false); 2487 }, 10); 2488 2489 $notify[0].classList.add('fadeOut'); 2490 2491 setTimeout(function () { 2492 $notify.remove(); 2493 }, 1050); 2494 } 2495 } 2496 } 2497 } 2498 2499 if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) { 2500 that.$button.trigger('focus'); 2501 } else if (that.options.liveSearch) { 2502 that.$searchbox.trigger('focus'); 2503 } 2504 2505 // Trigger select 'change' 2506 if (triggerChange) { 2507 if (that.multiple || prevIndex !== element.selectedIndex) { 2508 // $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed. 2509 changedArguments = [option.index, $option.prop('selected'), prevValue]; 2510 that.$element 2511 .triggerNative('change'); 2512 } 2513 } 2514 } 2515 }); 2516 2517 this.$menu.on('click', 'li.' + classNames.DISABLED + ' a, .' + classNames.POPOVERHEADER + ', .' + classNames.POPOVERHEADER + ' :not(.close)', function (e) { 2518 if (e.currentTarget == this) { 2519 e.preventDefault(); 2520 e.stopPropagation(); 2521 if (that.options.liveSearch && !$(e.target).hasClass('close')) { 2522 that.$searchbox.trigger('focus'); 2523 } else { 2524 that.$button.trigger('focus'); 2525 } 2526 } 2527 }); 2528 2529 this.$menuInner.on('click', '.divider, .dropdown-header', function (e) { 2530 e.preventDefault(); 2531 e.stopPropagation(); 2532 if (that.options.liveSearch) { 2533 that.$searchbox.trigger('focus'); 2534 } else { 2535 that.$button.trigger('focus'); 2536 } 2537 }); 2538 2539 this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () { 2540 that.$button.trigger('click'); 2541 }); 2542 2543 this.$searchbox.on('click', function (e) { 2544 e.stopPropagation(); 2545 }); 2546 2547 this.$menu.on('click', '.actions-btn', function (e) { 2548 if (that.options.liveSearch) { 2549 that.$searchbox.trigger('focus'); 2550 } else { 2551 that.$button.trigger('focus'); 2552 } 2553 2554 e.preventDefault(); 2555 e.stopPropagation(); 2556 2557 if ($(this).hasClass('bs-select-all')) { 2558 that.selectAll(); 2559 } else { 2560 that.deselectAll(); 2561 } 2562 }); 2563 2564 this.$element 2565 .on('change' + EVENT_KEY, function () { 2566 that.render(); 2567 that.$element.trigger('changed' + EVENT_KEY, changedArguments); 2568 changedArguments = null; 2569 }) 2570 .on('focus' + EVENT_KEY, function () { 2571 if (!that.options.mobile) that.$button.trigger('focus'); 2572 }); 2573 }, 2574 2575 liveSearchListener: function () { 2576 var that = this, 2577 noResults = document.createElement('li'); 2578 2579 this.$button.on('click.bs.dropdown.data-api', function () { 2580 if (!!that.$searchbox.val()) { 2581 that.$searchbox.val(''); 2582 } 2583 }); 2584 2585 this.$searchbox.on('click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api', function (e) { 2586 e.stopPropagation(); 2587 }); 2588 2589 this.$searchbox.on('input propertychange', function () { 2590 var searchValue = that.$searchbox.val(); 2591 2592 that.selectpicker.search.elements = []; 2593 that.selectpicker.search.data = []; 2594 2595 if (searchValue) { 2596 var i, 2597 searchMatch = [], 2598 q = searchValue.toUpperCase(), 2599 cache = {}, 2600 cacheArr = [], 2601 searchStyle = that._searchStyle(), 2602 normalizeSearch = that.options.liveSearchNormalize; 2603 2604 if (normalizeSearch) q = normalizeToBase(q); 2605 2606 for (var i = 0; i < that.selectpicker.main.data.length; i++) { 2607 var li = that.selectpicker.main.data[i]; 2608 2609 if (!cache[i]) { 2610 cache[i] = stringSearch(li, q, searchStyle, normalizeSearch); 2611 } 2612 2613 if (cache[i] && li.headerIndex !== undefined && cacheArr.indexOf(li.headerIndex) === -1) { 2614 if (li.headerIndex > 0) { 2615 cache[li.headerIndex - 1] = true; 2616 cacheArr.push(li.headerIndex - 1); 2617 } 2618 2619 cache[li.headerIndex] = true; 2620 cacheArr.push(li.headerIndex); 2621 2622 cache[li.lastIndex + 1] = true; 2623 } 2624 2625 if (cache[i] && li.type !== 'optgroup-label') cacheArr.push(i); 2626 } 2627 2628 for (var i = 0, cacheLen = cacheArr.length; i < cacheLen; i++) { 2629 var index = cacheArr[i], 2630 prevIndex = cacheArr[i - 1], 2631 li = that.selectpicker.main.data[index], 2632 liPrev = that.selectpicker.main.data[prevIndex]; 2633 2634 if (li.type !== 'divider' || (li.type === 'divider' && liPrev && liPrev.type !== 'divider' && cacheLen - 1 !== i)) { 2635 that.selectpicker.search.data.push(li); 2636 searchMatch.push(that.selectpicker.main.elements[index]); 2637 } 2638 } 2639 2640 that.activeIndex = undefined; 2641 that.noScroll = true; 2642 that.$menuInner.scrollTop(0); 2643 that.selectpicker.search.elements = searchMatch; 2644 that.createView(true); 2645 2646 if (!searchMatch.length) { 2647 noResults.className = 'no-results'; 2648 noResults.innerHTML = that.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); 2649 that.$menuInner[0].firstChild.appendChild(noResults); 2650 } 2651 } else { 2652 that.$menuInner.scrollTop(0); 2653 that.createView(false); 2654 } 2655 }); 2656 }, 2657 2658 _searchStyle: function () { 2659 return this.options.liveSearchStyle || 'contains'; 2660 }, 2661 2662 val: function (value) { 2663 var element = this.$element[0]; 2664 2665 if (typeof value !== 'undefined') { 2666 var prevValue = getSelectValues(element); 2667 2668 changedArguments = [null, null, prevValue]; 2669 2670 this.$element 2671 .val(value) 2672 .trigger('changed' + EVENT_KEY, changedArguments); 2673 2674 if (this.$newElement.hasClass(classNames.SHOW)) { 2675 if (this.multiple) { 2676 this.setOptionStatus(true); 2677 } else { 2678 var liSelectedIndex = (element.options[element.selectedIndex] || {}).liIndex; 2679 2680 if (typeof liSelectedIndex === 'number') { 2681 this.setSelected(this.selectedIndex, false); 2682 this.setSelected(liSelectedIndex, true); 2683 } 2684 } 2685 } 2686 2687 this.render(); 2688 2689 changedArguments = null; 2690 2691 return this.$element; 2692 } else { 2693 return this.$element.val(); 2694 } 2695 }, 2696 2697 changeAll: function (status) { 2698 if (!this.multiple) return; 2699 if (typeof status === 'undefined') status = true; 2700 2701 var element = this.$element[0], 2702 previousSelected = 0, 2703 currentSelected = 0, 2704 prevValue = getSelectValues(element); 2705 2706 element.classList.add('bs-select-hidden'); 2707 2708 for (var i = 0, data = this.selectpicker.current.data, len = data.length; i < len; i++) { 2709 var liData = data[i], 2710 option = liData.option; 2711 2712 if (option && !liData.disabled && liData.type !== 'divider') { 2713 if (liData.selected) previousSelected++; 2714 option.selected = status; 2715 if (status === true) currentSelected++; 2716 } 2717 } 2718 2719 element.classList.remove('bs-select-hidden'); 2720 2721 if (previousSelected === currentSelected) return; 2722 2723 this.setOptionStatus(); 2724 2725 changedArguments = [null, null, prevValue]; 2726 2727 this.$element 2728 .triggerNative('change'); 2729 }, 2730 2731 selectAll: function () { 2732 return this.changeAll(true); 2733 }, 2734 2735 deselectAll: function () { 2736 return this.changeAll(false); 2737 }, 2738 2739 toggle: function (e) { 2740 e = e || window.event; 2741 2742 if (e) e.stopPropagation(); 2743 2744 this.$button.trigger('click.bs.dropdown.data-api'); 2745 }, 2746 2747 keydown: function (e) { 2748 var $this = $(this), 2749 isToggle = $this.hasClass('dropdown-toggle'), 2750 $parent = isToggle ? $this.closest('.dropdown') : $this.closest(Selector.MENU), 2751 that = $parent.data('this'), 2752 $items = that.findLis(), 2753 index, 2754 isActive, 2755 liActive, 2756 activeLi, 2757 offset, 2758 updateScroll = false, 2759 downOnTab = e.which === keyCodes.TAB && !isToggle && !that.options.selectOnTab, 2760 isArrowKey = REGEXP_ARROW.test(e.which) || downOnTab, 2761 scrollTop = that.$menuInner[0].scrollTop, 2762 isVirtual = that.isVirtual(), 2763 position0 = isVirtual === true ? that.selectpicker.view.position0 : 0; 2764 2765 // do nothing if a function key is pressed 2766 if (e.which >= 112 && e.which <= 123) return; 2767 2768 isActive = that.$newElement.hasClass(classNames.SHOW); 2769 2770 if ( 2771 !isActive && 2772 ( 2773 isArrowKey || 2774 (e.which >= 48 && e.which <= 57) || 2775 (e.which >= 96 && e.which <= 105) || 2776 (e.which >= 65 && e.which <= 90) 2777 ) 2778 ) { 2779 that.$button.trigger('click.bs.dropdown.data-api'); 2780 2781 if (that.options.liveSearch) { 2782 that.$searchbox.trigger('focus'); 2783 return; 2784 } 2785 } 2786 2787 if (e.which === keyCodes.ESCAPE && isActive) { 2788 e.preventDefault(); 2789 that.$button.trigger('click.bs.dropdown.data-api').trigger('focus'); 2790 } 2791 2792 if (isArrowKey) { // if up or down 2793 if (!$items.length) return; 2794 2795 liActive = that.selectpicker.main.elements[that.activeIndex]; 2796 index = liActive ? Array.prototype.indexOf.call(liActive.parentElement.children, liActive) : -1; 2797 2798 if (index !== -1) { 2799 that.defocusItem(liActive); 2800 } 2801 2802 if (e.which === keyCodes.ARROW_UP) { // up 2803 if (index !== -1) index--; 2804 if (index + position0 < 0) index += $items.length; 2805 2806 if (!that.selectpicker.view.canHighlight[index + position0]) { 2807 index = that.selectpicker.view.canHighlight.slice(0, index + position0).lastIndexOf(true) - position0; 2808 if (index === -1) index = $items.length - 1; 2809 } 2810 } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down 2811 index++; 2812 if (index + position0 >= that.selectpicker.view.canHighlight.length) index = 0; 2813 2814 if (!that.selectpicker.view.canHighlight[index + position0]) { 2815 index = index + 1 + that.selectpicker.view.canHighlight.slice(index + position0 + 1).indexOf(true); 2816 } 2817 } 2818 2819 e.preventDefault(); 2820 2821 var liActiveIndex = position0 + index; 2822 2823 if (e.which === keyCodes.ARROW_UP) { // up 2824 // scroll to bottom and highlight last option 2825 if (position0 === 0 && index === $items.length - 1) { 2826 that.$menuInner[0].scrollTop = that.$menuInner[0].scrollHeight; 2827 2828 liActiveIndex = that.selectpicker.current.elements.length - 1; 2829 } else { 2830 activeLi = that.selectpicker.current.data[liActiveIndex]; 2831 offset = activeLi.position - activeLi.height; 2832 2833 updateScroll = offset < scrollTop; 2834 } 2835 } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down 2836 // scroll to top and highlight first option 2837 if (index === 0) { 2838 that.$menuInner[0].scrollTop = 0; 2839 2840 liActiveIndex = 0; 2841 } else { 2842 activeLi = that.selectpicker.current.data[liActiveIndex]; 2843 offset = activeLi.position - that.sizeInfo.menuInnerHeight; 2844 2845 updateScroll = offset > scrollTop; 2846 } 2847 } 2848 2849 liActive = that.selectpicker.current.elements[liActiveIndex]; 2850 2851 that.activeIndex = that.selectpicker.current.data[liActiveIndex].index; 2852 2853 that.focusItem(liActive); 2854 2855 that.selectpicker.view.currentActive = liActive; 2856 2857 if (updateScroll) that.$menuInner[0].scrollTop = offset; 2858 2859 if (that.options.liveSearch) { 2860 that.$searchbox.trigger('focus'); 2861 } else { 2862 $this.trigger('focus'); 2863 } 2864 } else if ( 2865 (!$this.is('input') && !REGEXP_TAB_OR_ESCAPE.test(e.which)) || 2866 (e.which === keyCodes.SPACE && that.selectpicker.keydown.keyHistory) 2867 ) { 2868 var searchMatch, 2869 matches = [], 2870 keyHistory; 2871 2872 e.preventDefault(); 2873 2874 that.selectpicker.keydown.keyHistory += keyCodeMap[e.which]; 2875 2876 if (that.selectpicker.keydown.resetKeyHistory.cancel) clearTimeout(that.selectpicker.keydown.resetKeyHistory.cancel); 2877 that.selectpicker.keydown.resetKeyHistory.cancel = that.selectpicker.keydown.resetKeyHistory.start(); 2878 2879 keyHistory = that.selectpicker.keydown.keyHistory; 2880 2881 // if all letters are the same, set keyHistory to just the first character when searching 2882 if (/^(.)\1+$/.test(keyHistory)) { 2883 keyHistory = keyHistory.charAt(0); 2884 } 2885 2886 // find matches 2887 for (var i = 0; i < that.selectpicker.current.data.length; i++) { 2888 var li = that.selectpicker.current.data[i], 2889 hasMatch; 2890 2891 hasMatch = stringSearch(li, keyHistory, 'startsWith', true); 2892 2893 if (hasMatch && that.selectpicker.view.canHighlight[i]) { 2894 matches.push(li.index); 2895 } 2896 } 2897 2898 if (matches.length) { 2899 var matchIndex = 0; 2900 2901 $items.removeClass('active').find('a').removeClass('active'); 2902 2903 // either only one key has been pressed or they are all the same key 2904 if (keyHistory.length === 1) { 2905 matchIndex = matches.indexOf(that.activeIndex); 2906 2907 if (matchIndex === -1 || matchIndex === matches.length - 1) { 2908 matchIndex = 0; 2909 } else { 2910 matchIndex++; 2911 } 2912 } 2913 2914 searchMatch = matches[matchIndex]; 2915 2916 activeLi = that.selectpicker.main.data[searchMatch]; 2917 2918 if (scrollTop - activeLi.position > 0) { 2919 offset = activeLi.position - activeLi.height; 2920 updateScroll = true; 2921 } else { 2922 offset = activeLi.position - that.sizeInfo.menuInnerHeight; 2923 // if the option is already visible at the current scroll position, just keep it the same 2924 updateScroll = activeLi.position > scrollTop + that.sizeInfo.menuInnerHeight; 2925 } 2926 2927 liActive = that.selectpicker.main.elements[searchMatch]; 2928 2929 that.activeIndex = matches[matchIndex]; 2930 2931 that.focusItem(liActive); 2932 2933 if (liActive) liActive.firstChild.focus(); 2934 2935 if (updateScroll) that.$menuInner[0].scrollTop = offset; 2936 2937 $this.trigger('focus'); 2938 } 2939 } 2940 2941 // Select focused option if "Enter", "Spacebar" or "Tab" (when selectOnTab is true) are pressed inside the menu. 2942 if ( 2943 isActive && 2944 ( 2945 (e.which === keyCodes.SPACE && !that.selectpicker.keydown.keyHistory) || 2946 e.which === keyCodes.ENTER || 2947 (e.which === keyCodes.TAB && that.options.selectOnTab) 2948 ) 2949 ) { 2950 if (e.which !== keyCodes.SPACE) e.preventDefault(); 2951 2952 if (!that.options.liveSearch || e.which !== keyCodes.SPACE) { 2953 that.$menuInner.find('.active a').trigger('click', true); // retain active class 2954 $this.trigger('focus'); 2955 2956 if (!that.options.liveSearch) { 2957 // Prevent screen from scrolling if the user hits the spacebar 2958 e.preventDefault(); 2959 // Fixes spacebar selection of dropdown items in FF & IE 2960 $(document).data('spaceSelect', true); 2961 } 2962 } 2963 } 2964 }, 2965 2966 mobile: function () { 2967 this.$element[0].classList.add('mobile-device'); 2968 }, 2969 2970 refresh: function () { 2971 // update options if data attributes have been changed 2972 var config = $.extend({}, this.options, this.$element.data()); 2973 this.options = config; 2974 2975 this.checkDisabled(); 2976 this.setStyle(); 2977 this.render(); 2978 this.buildData(); 2979 this.buildList(); 2980 this.setWidth(); 2981 2982 this.setSize(true); 2983 2984 this.$element.trigger('refreshed' + EVENT_KEY); 2985 }, 2986 2987 hide: function () { 2988 this.$newElement.hide(); 2989 }, 2990 2991 show: function () { 2992 this.$newElement.show(); 2993 }, 2994 2995 remove: function () { 2996 this.$newElement.remove(); 2997 this.$element.remove(); 2998 }, 2999 3000 destroy: function () { 3001 this.$newElement.before(this.$element).remove(); 3002 3003 if (this.$bsContainer) { 3004 this.$bsContainer.remove(); 3005 } else { 3006 this.$menu.remove(); 3007 } 3008 3009 this.$element 3010 .off(EVENT_KEY) 3011 .removeData('selectpicker') 3012 .removeClass('bs-select-hidden selectpicker'); 3013 3014 $(window).off(EVENT_KEY + '.' + this.selectId); 3015 } 3016 }; 3017 3018 // SELECTPICKER PLUGIN DEFINITION 3019 // ============================== 3020 function Plugin (option) { 3021 // get the args of the outer function.. 3022 var args = arguments; 3023 // The arguments of the function are explicitly re-defined from the argument list, because the shift causes them 3024 // to get lost/corrupted in android 2.3 and IE9 #715 #775 3025 var _option = option; 3026 3027 [].shift.apply(args); 3028 3029 // if the version was not set successfully 3030 if (!version.success) { 3031 // try to retreive it again 3032 try { 3033 version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); 3034 } catch (err) { 3035 // fall back to use BootstrapVersion if set 3036 if (Selectpicker.BootstrapVersion) { 3037 version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.'); 3038 } else { 3039 version.full = [version.major, '0', '0']; 3040 3041 console.warn( 3042 'There was an issue retrieving Bootstrap\'s version. ' + 3043 'Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision. ' + 3044 'If loading Bootstrap asynchronously, the version may need to be manually specified via $.fn.selectpicker.Constructor.BootstrapVersion.', 3045 err 3046 ); 3047 } 3048 } 3049 3050 version.major = version.full[0]; 3051 version.success = true; 3052 } 3053 3054 if (version.major === '4') { 3055 // some defaults need to be changed if using Bootstrap 4 3056 // check to see if they have already been manually changed before forcing them to update 3057 var toUpdate = []; 3058 3059 if (Selectpicker.DEFAULTS.style === classNames.BUTTONCLASS) toUpdate.push({ name: 'style', className: 'BUTTONCLASS' }); 3060 if (Selectpicker.DEFAULTS.iconBase === classNames.ICONBASE) toUpdate.push({ name: 'iconBase', className: 'ICONBASE' }); 3061 if (Selectpicker.DEFAULTS.tickIcon === classNames.TICKICON) toUpdate.push({ name: 'tickIcon', className: 'TICKICON' }); 3062 3063 classNames.DIVIDER = 'dropdown-divider'; 3064 classNames.SHOW = 'show'; 3065 classNames.BUTTONCLASS = 'btn-light'; 3066 classNames.POPOVERHEADER = 'popover-header'; 3067 classNames.ICONBASE = ''; 3068 classNames.TICKICON = 'bs-ok-default'; 3069 3070 for (var i = 0; i < toUpdate.length; i++) { 3071 var option = toUpdate[i]; 3072 Selectpicker.DEFAULTS[option.name] = classNames[option.className]; 3073 } 3074 } 3075 3076 var value; 3077 var chain = this.each(function () { 3078 var $this = $(this); 3079 if ($this.is('select')) { 3080 var data = $this.data('selectpicker'), 3081 options = typeof _option == 'object' && _option; 3082 3083 if (!data) { 3084 var dataAttributes = $this.data(); 3085 3086 for (var dataAttr in dataAttributes) { 3087 if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) { 3088 delete dataAttributes[dataAttr]; 3089 } 3090 } 3091 3092 var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, dataAttributes, options); 3093 config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), dataAttributes.template, options.template); 3094 $this.data('selectpicker', (data = new Selectpicker(this, config))); 3095 } else if (options) { 3096 for (var i in options) { 3097 if (options.hasOwnProperty(i)) { 3098 data.options[i] = options[i]; 3099 } 3100 } 3101 } 3102 3103 if (typeof _option == 'string') { 3104 if (data[_option] instanceof Function) { 3105 value = data[_option].apply(data, args); 3106 } else { 3107 value = data.options[_option]; 3108 } 3109 } 3110 } 3111 }); 3112 3113 if (typeof value !== 'undefined') { 3114 // noinspection JSUnusedAssignment 3115 return value; 3116 } else { 3117 return chain; 3118 } 3119 } 3120 3121 var old = $.fn.selectpicker; 3122 $.fn.selectpicker = Plugin; 3123 $.fn.selectpicker.Constructor = Selectpicker; 3124 3125 // SELECTPICKER NO CONFLICT 3126 // ======================== 3127 $.fn.selectpicker.noConflict = function () { 3128 $.fn.selectpicker = old; 3129 return this; 3130 }; 3131 3132 // get Bootstrap's keydown event handler for either Bootstrap 4 or Bootstrap 3 3133 var bootstrapKeydown = $.fn.dropdown.Constructor._dataApiKeydownHandler || $.fn.dropdown.Constructor.prototype.keydown; 3134 3135 $(document) 3136 .off('keydown.bs.dropdown.data-api') 3137 .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > [data-toggle="dropdown"]', bootstrapKeydown) 3138 .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > .dropdown-menu', bootstrapKeydown) 3139 .on('keydown' + EVENT_KEY, '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', Selectpicker.prototype.keydown) 3140 .on('focusin.modal', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', function (e) { 3141 e.stopPropagation(); 3142 }); 3143 3144 // SELECTPICKER DATA-API 3145 // ===================== 3146 $(window).on('load' + EVENT_KEY + '.data-api', function () { 3147 $('.selectpicker').each(function () { 3148 var $selectpicker = $(this); 3149 Plugin.call($selectpicker, $selectpicker.data()); 3150 }) 3151 }); 3152})(jQuery);
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.