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