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