Skip to content

Instantly share code, notes, and snippets.

@mattymatty76
Created August 29, 2023 06:15
Show Gist options
  • Save mattymatty76/c996d3b77f298b2ec133be59992df9d4 to your computer and use it in GitHub Desktop.
Save mattymatty76/c996d3b77f298b2ec133be59992df9d4 to your computer and use it in GitHub Desktop.
bootstrap-select for bootstrap 5.3
/*!
* Bootstrap-select v1.14.0-gamma1 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2023 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
(function ($) {
'use strict';
var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'];
var uriAttrs = [
'background',
'cite',
'href',
'itemtype',
'longdesc',
'poster',
'src',
'xlink:href'
];
var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i;
var DefaultWhitelist = {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role', 'tabindex', 'style', ARIA_ATTRIBUTE_PATTERN],
a: ['target', 'href', 'title', 'rel'],
area: [],
b: [],
br: [],
col: [],
code: [],
div: [],
em: [],
hr: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
i: [],
img: ['src', 'alt', 'title', 'width', 'height'],
li: [],
ol: [],
p: [],
pre: [],
s: [],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
u: [],
ul: []
};
/**
* A pattern that recognizes a commonly useful subset of URLs that are safe.
*
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
*/
var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
/**
* A pattern that matches safe data URLs. Only matches image, video and audio types.
*
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
*/
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;
var ParseableAttributes = ['title', 'placeholder']; // attributes to use as settings, can add others in the future
function allowedAttribute (attr, allowedAttributeList) {
var attrName = attr.nodeName.toLowerCase();
if ($.inArray(attrName, allowedAttributeList) !== -1) {
if ($.inArray(attrName, uriAttrs) !== -1) {
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN));
}
return true;
}
var regExp = $(allowedAttributeList).filter(function (index, value) {
return value instanceof RegExp;
});
// Check if a regular expression validates the attribute.
for (var i = 0, l = regExp.length; i < l; i++) {
if (attrName.match(regExp[i])) {
return true;
}
}
return false;
}
function sanitizeHtml (unsafeElements, whiteList, sanitizeFn) {
if (sanitizeFn && typeof sanitizeFn === 'function') {
return sanitizeFn(unsafeElements);
}
var whitelistKeys = Object.keys(whiteList);
for (var i = 0, len = unsafeElements.length; i < len; i++) {
var elements = unsafeElements[i].querySelectorAll('*');
for (var j = 0, len2 = elements.length; j < len2; j++) {
var el = elements[j];
var elName = el.nodeName.toLowerCase();
if (whitelistKeys.indexOf(elName) === -1) {
el.parentNode.removeChild(el);
continue;
}
var attributeList = [].slice.call(el.attributes);
var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []);
for (var k = 0, len3 = attributeList.length; k < len3; k++) {
var attr = attributeList[k];
if (!allowedAttribute(attr, whitelistedAttributes)) {
el.removeAttribute(attr.nodeName);
}
}
}
}
}
function getAttributesObject ($select) {
var attributesObject = {},
attrVal;
ParseableAttributes.forEach(function (item) {
attrVal = $select.attr(item);
if (attrVal) attributesObject[item] = attrVal;
});
// for backwards compatibility
// (using title as placeholder is deprecated - remove in v2.0.0)
if (!attributesObject.placeholder && attributesObject.title) {
attributesObject.placeholder = attributesObject.title;
}
return attributesObject;
}
// Polyfill for browsers with no classList support
// Remove in v2
if (!('classList' in document.createElement('_'))) {
(function (view) {
if (!('Element' in view)) return;
var classListProp = 'classList',
protoProp = 'prototype',
elemCtrProto = view.Element[protoProp],
objCtr = Object,
classListGetter = function () {
var $elem = $(this);
return {
add: function (classes) {
classes = Array.prototype.slice.call(arguments).join(' ');
return $elem.addClass(classes);
},
remove: function (classes) {
classes = Array.prototype.slice.call(arguments).join(' ');
return $elem.removeClass(classes);
},
toggle: function (classes, force) {
return $elem.toggleClass(classes, force);
},
contains: function (classes) {
return $elem.hasClass(classes);
}
};
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter,
enumerable: true,
configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
// adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36
// modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected
if (ex.number === undefined || ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(window));
}
var testElement = document.createElement('_');
testElement.classList.add('c1', 'c2');
if (!testElement.classList.contains('c2')) {
var _add = DOMTokenList.prototype.add,
_remove = DOMTokenList.prototype.remove;
DOMTokenList.prototype.add = function () {
Array.prototype.forEach.call(arguments, _add.bind(this));
};
DOMTokenList.prototype.remove = function () {
Array.prototype.forEach.call(arguments, _remove.bind(this));
};
}
testElement.classList.toggle('c3', false);
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not
// support the second argument.
if (testElement.classList.contains('c3')) {
var _toggle = DOMTokenList.prototype.toggle;
DOMTokenList.prototype.toggle = function (token, force) {
if (1 in arguments && !this.contains(token) === !force) {
return force;
} else {
return _toggle.call(this, token);
}
};
}
testElement = null;
// Polyfill for IE (remove in v2)
Object.values = typeof Object.values === 'function' ? Object.values : function (obj) {
return Object.keys(obj).map(function (key) {
return obj[key];
});
};
// shallow array comparison
function isEqual (array1, array2) {
return array1.length === array2.length && array1.every(function (element, index) {
return element === array2[index];
});
};
// <editor-fold desc="Shims">
if (!String.prototype.startsWith) {
(function () {
'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
var toString = {}.toString;
var startsWith = function (search) {
if (this == null) {
throw new TypeError();
}
var string = String(this);
if (search && toString.call(search) == '[object RegExp]') {
throw new TypeError();
}
var stringLength = string.length;
var searchString = String(search);
var searchLength = searchString.length;
var position = arguments.length > 1 ? arguments[1] : undefined;
// `ToInteger`
var pos = position ? Number(position) : 0;
if (pos != pos) { // better `isNaN`
pos = 0;
}
var start = Math.min(Math.max(pos, 0), stringLength);
// Avoid the `indexOf` call if no match is possible
if (searchLength + start > stringLength) {
return false;
}
var index = -1;
while (++index < searchLength) {
if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
return false;
}
}
return true;
};
if (Object.defineProperty) {
Object.defineProperty(String.prototype, 'startsWith', {
'value': startsWith,
'configurable': true,
'writable': true
});
} else {
String.prototype.startsWith = startsWith;
}
}());
}
function toKebabCase (str) {
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, function ($, ofs) {
return (ofs ? '-' : '') + $.toLowerCase();
});
}
/*function getSelectedOptions () {
var options = this.selectpicker.main.data;
if (this.options.source.data || this.options.source.search) {
options = Object.values(this.selectpicker.optionValuesDataMap);
}
var selectedOptions = options.filter(function (item) {
if (item.selected) {
if (this.options.hideDisabled && item.disabled) return false;
return true;
}
return false;
}, this);
// ensure only 1 option is selected if multiple are set in the data source
if (this.options.source.data && !this.multiple && selectedOptions.length > 1) {
for (var i = 0; i < selectedOptions.length - 1; i++) {
selectedOptions[i].selected = false;
}
selectedOptions = [ selectedOptions[selectedOptions.length - 1] ];
}
return selectedOptions;
}*/
function getSelectedOptions (select, ignoreDisabled) {
var selectedOptions = select.selectedOptions,
options = [],
opt;
if (ignoreDisabled) {
for (var i = 0, len = selectedOptions.length; i < len; i++) {
opt = selectedOptions[i];
if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) {
options.push(opt);
}
}
return options;
}
return selectedOptions;
}
// much faster than $.val()
/*function getSelectValues (selectedOptions) {
var value = [],
options = selectedOptions || getSelectedOptions.call(this),
opt;
for (var i = 0, len = options.length; i < len; i++) {
opt = options[i];
if (!opt.disabled) {
value.push(opt.value === undefined ? opt.text : opt.value);
}
}
if (!this.multiple) {
return !value.length ? null : value[0];
}
return value;
}*/
function getSelectValues (select, selectedOptions) {
var value = [],
options = selectedOptions || select.selectedOptions,
opt;
for (var i = 0, len = options.length; i < len; i++) {
opt = options[i];
if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) {
value.push(opt.value);
}
}
if (!select.multiple) {
return !value.length ? null : value[0];
}
return value;
}
// set data-selected on select element if the value has been programmatically selected
// prior to initialization of bootstrap-select
// * consider removing or replacing an alternative method *
var valHooks = {
useDefault: false,
_set: $.valHooks.select.set
};
$.valHooks.select.set = function (elem, value) {
if (value && !valHooks.useDefault) $(elem).data('selected', true);
return valHooks._set.apply(this, arguments);
};
var changedArguments = null;
var EventIsSupported = (function () {
try {
new Event('change');
return true;
} catch (e) {
return false;
}
})();
$.fn.triggerNative = function (eventName) {
var el = this[0],
event;
if (el.dispatchEvent) { // for modern browsers & IE9+
if (EventIsSupported) {
// For modern browsers
event = new Event(eventName, {
bubbles: true
});
} else {
// For IE since it doesn't support Event constructor
event = document.createEvent('Event');
event.initEvent(eventName, true, false);
}
el.dispatchEvent(event);
}
};
// </editor-fold>
function stringSearch (li, searchString, method, normalize) {
var stringTypes = [
'display',
'subtext',
'tokens'
],
searchSuccess = false;
for (var i = 0; i < stringTypes.length; i++) {
var stringType = stringTypes[i],
string = li[stringType];
if (string) {
string = string.toString();
// Strip HTML tags. This isn't perfect, but it's much faster than any other method
if (stringType === 'display') {
string = string.replace(/<[^>]+>/g, '');
}
if (normalize) string = normalizeToBase(string);
string = string.toUpperCase();
if (typeof method === 'function') {
searchSuccess = method(string, searchString);
} else if (method === 'contains') {
searchSuccess = string.indexOf(searchString) >= 0;
} else if (method === 'containsAll') {
var searchArray = searchString.split(' ');
var notAllMatched = false;
searchSuccess = false;
for (var searchSubString in searchArray) {
searchSuccess = string.indexOf(searchArray[searchSubString]) >= 0;
if (!searchSuccess) notAllMatched = true;
}
if (notAllMatched) searchSuccess = false;
} else {
searchSuccess = string.startsWith(searchString);
}
if (searchSuccess) break;
}
}
return searchSuccess;
}
function toInteger (value) {
return parseInt(value, 10) || 0;
}
// Borrowed from Lodash (_.deburr)
/** Used to map Latin Unicode letters to basic Latin letters. */
var deburredLetters = {
// Latin-1 Supplement block.
'\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A',
'\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a',
'\xc7': 'C', '\xe7': 'c',
'\xd0': 'D', '\xf0': 'd',
'\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E',
'\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e',
'\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I',
'\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i',
'\xd1': 'N', '\xf1': 'n',
'\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O',
'\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o',
'\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U',
'\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u',
'\xdd': 'Y', '\xfd': 'y', '\xff': 'y',
'\xc6': 'Ae', '\xe6': 'ae',
'\xde': 'Th', '\xfe': 'th',
'\xdf': 'ss',
// Latin Extended-A block.
'\u0100': 'A', '\u0102': 'A', '\u0104': 'A',
'\u0101': 'a', '\u0103': 'a', '\u0105': 'a',
'\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C',
'\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c',
'\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd',
'\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E',
'\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e',
'\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G',
'\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g',
'\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h',
'\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I',
'\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i',
'\u0134': 'J', '\u0135': 'j',
'\u0136': 'K', '\u0137': 'k', '\u0138': 'k',
'\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L',
'\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l',
'\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N',
'\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n',
'\u014c': 'O', '\u014e': 'O', '\u0150': 'O',
'\u014d': 'o', '\u014f': 'o', '\u0151': 'o',
'\u0154': 'R', '\u0156': 'R', '\u0158': 'R',
'\u0155': 'r', '\u0157': 'r', '\u0159': 'r',
'\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S',
'\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's',
'\u0162': 'T', '\u0164': 'T', '\u0166': 'T',
'\u0163': 't', '\u0165': 't', '\u0167': 't',
'\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U',
'\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u',
'\u0174': 'W', '\u0175': 'w',
'\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y',
'\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z',
'\u017a': 'z', '\u017c': 'z', '\u017e': 'z',
'\u0132': 'IJ', '\u0133': 'ij',
'\u0152': 'Oe', '\u0153': 'oe',
'\u0149': "'n", '\u017f': 's'
};
/** Used to match Latin Unicode letters (excluding mathematical operators). */
var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g;
/** Used to compose unicode character classes. */
var rsComboMarksRange = '\\u0300-\\u036f',
reComboHalfMarksRange = '\\ufe20-\\ufe2f',
rsComboSymbolsRange = '\\u20d0-\\u20ff',
rsComboMarksExtendedRange = '\\u1ab0-\\u1aff',
rsComboMarksSupplementRange = '\\u1dc0-\\u1dff',
rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange;
/** Used to compose unicode capture groups. */
var rsCombo = '[' + rsComboRange + ']';
/**
* Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and
* [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).
*/
var reComboMark = RegExp(rsCombo, 'g');
function deburrLetter (key) {
return deburredLetters[key];
};
function normalizeToBase (string) {
string = string.toString();
return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');
}
// List of HTML entities for escaping.
var escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;'
};
// Functions for escaping and unescaping strings to/from HTML interpolation.
var createEscaper = function (map) {
var escaper = function (match) {
return map[match];
};
// Regexes for identifying a key that needs to be escaped.
var source = '(?:' + Object.keys(map).join('|') + ')';
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
return function (string) {
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
};
};
var htmlEscape = createEscaper(escapeMap);
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
var keyCodeMap = {
32: ' ',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
59: ';',
65: 'A',
66: 'B',
67: 'C',
68: 'D',
69: 'E',
70: 'F',
71: 'G',
72: 'H',
73: 'I',
74: 'J',
75: 'K',
76: 'L',
77: 'M',
78: 'N',
79: 'O',
80: 'P',
81: 'Q',
82: 'R',
83: 'S',
84: 'T',
85: 'U',
86: 'V',
87: 'W',
88: 'X',
89: 'Y',
90: 'Z',
96: '0',
97: '1',
98: '2',
99: '3',
100: '4',
101: '5',
102: '6',
103: '7',
104: '8',
105: '9'
};
var keyCodes = {
ESCAPE: 27, // KeyboardEvent.which value for Escape (Esc) key
ENTER: 13, // KeyboardEvent.which value for Enter key
SPACE: 32, // KeyboardEvent.which value for space key
TAB: 9, // KeyboardEvent.which value for tab key
ARROW_UP: 38, // KeyboardEvent.which value for up arrow key
ARROW_DOWN: 40 // KeyboardEvent.which value for down arrow key
};
var Dropdown = window.Dropdown || window.bootstrap && window.bootstrap.Dropdown;
function getVersion () {
var version;
try {
version = $.fn.dropdown.Constructor.VERSION;
} catch (err) {
version = Dropdown.VERSION;
}
return version;
}
var version = {
success: false,
major: '3'
};
try {
version.full = (getVersion() || '').split(' ')[0].split('.');
version.major = version.full[0];
version.success = true;
} catch (err) {
// do nothing
}
var selectId = 0;
var EVENT_KEY = '.bs.select';
var classNames = {
DISABLED: 'disabled',
DIVIDER: 'divider',
SHOW: 'open',
DROPUP: 'dropup',
MENU: 'dropdown-menu',
MENURIGHT: 'dropdown-menu-right',
MENULEFT: 'dropdown-menu-left',
// to-do: replace with more advanced template/customization options
BUTTONCLASS: 'btn-default',
POPOVERHEADER: 'popover-title',
ICONBASE: 'glyphicon',
TICKICON: 'glyphicon-ok'
};
var Selector = {
MENU: '.' + classNames.MENU,
DATA_TOGGLE: 'data-toggle="dropdown"'
};
var elementTemplates = {
div: document.createElement('div'),
span: document.createElement('span'),
i: document.createElement('i'),
subtext: document.createElement('small'),
a: document.createElement('a'),
li: document.createElement('li'),
whitespace: document.createTextNode('\u00A0'),
fragment: document.createDocumentFragment(),
option: document.createElement('option')
};
elementTemplates.selectedOption = elementTemplates.option.cloneNode(false);
elementTemplates.selectedOption.setAttribute('selected', true);
elementTemplates.noResults = elementTemplates.li.cloneNode(false);
elementTemplates.noResults.className = 'no-results';
elementTemplates.a.setAttribute('role', 'option');
elementTemplates.a.className = 'dropdown-item';
elementTemplates.subtext.className = 'text-muted';
elementTemplates.text = elementTemplates.span.cloneNode(false);
elementTemplates.text.className = 'text';
elementTemplates.checkMark = elementTemplates.span.cloneNode(false);
var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN);
var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE);
var generateOption = {
li: function (content, classes, optgroup) {
var li = elementTemplates.li.cloneNode(false);
if (content) {
if (content.nodeType === 1 || content.nodeType === 11) {
li.appendChild(content);
} else {
li.innerHTML = content;
}
}
if (typeof classes !== 'undefined' && classes !== '') li.className = classes;
if (typeof optgroup !== 'undefined' && optgroup !== null) li.classList.add('optgroup-' + optgroup);
return li;
},
a: function (text, classes, inline) {
var a = elementTemplates.a.cloneNode(true);
if (text) {
if (text.nodeType === 11) {
a.appendChild(text);
} else {
a.insertAdjacentHTML('beforeend', text);
}
}
if (typeof classes !== 'undefined' && classes !== '') a.classList.add.apply(a.classList, classes.split(/\s+/));
if (inline) a.setAttribute('style', inline);
return a;
},
text: function (options, useFragment) {
var textElement = elementTemplates.text.cloneNode(false),
subtextElement,
iconElement;
if (options.content) {
textElement.innerHTML = options.content;
} else {
textElement.textContent = options.text;
if (options.icon) {
var whitespace = elementTemplates.whitespace.cloneNode(false);
// need to use <i> for icons in the button to prevent a breaking change
// note: switch to span in next major release
iconElement = (useFragment === true ? elementTemplates.i : elementTemplates.span).cloneNode(false);
iconElement.className = this.options.iconBase + ' ' + options.icon;
elementTemplates.fragment.appendChild(iconElement);
elementTemplates.fragment.appendChild(whitespace);
}
if (options.subtext) {
subtextElement = elementTemplates.subtext.cloneNode(false);
subtextElement.textContent = options.subtext;
textElement.appendChild(subtextElement);
}
}
if (useFragment === true) {
while (textElement.childNodes.length > 0) {
elementTemplates.fragment.appendChild(textElement.childNodes[0]);
}
} else {
elementTemplates.fragment.appendChild(textElement);
}
return elementTemplates.fragment;
},
label: function (options) {
var textElement = elementTemplates.text.cloneNode(false),
subtextElement,
iconElement;
textElement.innerHTML = options.display;
if (options.icon) {
var whitespace = elementTemplates.whitespace.cloneNode(false);
iconElement = elementTemplates.span.cloneNode(false);
iconElement.className = this.options.iconBase + ' ' + options.icon;
elementTemplates.fragment.appendChild(iconElement);
elementTemplates.fragment.appendChild(whitespace);
}
if (options.subtext) {
subtextElement = elementTemplates.subtext.cloneNode(false);
subtextElement.textContent = options.subtext;
textElement.appendChild(subtextElement);
}
elementTemplates.fragment.appendChild(textElement);
return elementTemplates.fragment;
}
};
var getOptionData = {
fromOption: function (option, type) {
var value;
switch (type) {
case 'divider':
value = option.getAttribute('data-divider') === 'true';
break;
case 'text':
value = option.textContent;
break;
case 'label':
value = option.label;
break;
case 'style':
value = option.style.cssText;
break;
case 'title':
value = option.title;
break;
default:
value = option.getAttribute('data-' + toKebabCase(type));
break;
}
return value;
},
fromDataSource: function (option, type) {
var value;
switch (type) {
case 'text':
case 'label':
value = option.text || option.value || '';
break;
default:
value = option[type];
break;
}
return value;
}
};
function showNoResults (searchMatch, searchValue) {
if (!searchMatch.length) {
elementTemplates.noResults.innerHTML = this.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"');
this.$menuInner[0].firstChild.appendChild(elementTemplates.noResults);
}
}
function filterHidden (item) {
return !(item.hidden || this.options.hideDisabled && item.disabled);
}
var Selectpicker = function (element, options) {
var that = this;
// bootstrap-select has been initialized - revert valHooks.select.set back to its original function
if (!valHooks.useDefault) {
$.valHooks.select.set = valHooks._set;
valHooks.useDefault = true;
}
this.$element = $(element);
this.$newElement = null;
this.$button = null;
this.$menu = null;
this.options = options;
this.selectpicker = {
main: {
data: [],
optionQueue: elementTemplates.fragment.cloneNode(false),
hasMore: false
},
search: {
data: [],
hasMore: false
},
current: {}, // current is either equal to main or search depending on if a search is in progress
view: {},
// map of option values and their respective data (only used in conjunction with options.source)
optionValuesDataMap: {},
isSearching: false,
keydown: {
keyHistory: '',
resetKeyHistory: {
start: function () {
return setTimeout(function () {
that.selectpicker.keydown.keyHistory = '';
}, 800);
}
}
}
};
this.sizeInfo = {};
// Format window padding
var winPad = this.options.windowPadding;
if (typeof winPad === 'number') {
this.options.windowPadding = [winPad, winPad, winPad, winPad];
}
// Expose public methods
this.val = Selectpicker.prototype.val;
this.render = Selectpicker.prototype.render;
this.refresh = Selectpicker.prototype.refresh;
this.setStyle = Selectpicker.prototype.setStyle;
this.selectAll = Selectpicker.prototype.selectAll;
this.deselectAll = Selectpicker.prototype.deselectAll;
this.destroy = Selectpicker.prototype.destroy;
this.remove = Selectpicker.prototype.remove;
this.show = Selectpicker.prototype.show;
this.hide = Selectpicker.prototype.hide;
this.init();
};
Selectpicker.VERSION = '1.14.0-beta3';
// part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both.
Selectpicker.DEFAULTS = {
noneSelectedText: 'Nothing selected',
noneResultsText: 'No results matched {0}',
countSelectedText: function (numSelected, numTotal) {
return (numSelected == 1) ? '{0} item selected' : '{0} items selected';
},
maxOptionsText: function (numAll, numGroup) {
return [
(numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)',
(numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)'
];
},
selectAllText: 'Select All',
deselectAllText: 'Deselect All',
source: {
pageSize: 40
},
//chunkSize: 40,
chunkSize: Number.MAX_VALUE,
doneButton: false,
doneButtonText: 'Close',
multipleSeparator: ' | ',
styleBase: 'btn',
style: classNames.BUTTONCLASS,
size: 'auto',
title: null,
placeholder: null,
titleTip: null,
allowClear: false,
selectedTextFormat: 'values',
width: false,
container: false,
hideDisabled: false,
showSubtext: false,
showIcon: true,
showContent: true,
dropupAuto: true,
header: false,
liveSearch: false,
liveSearchPlaceholder: null,
liveSearchNormalize: false,
liveSearchStyle: 'contains',
actionsBox: false,
iconBase: classNames.ICONBASE,
tickIcon: classNames.TICKICON,
showTick: false,
template: {
caret: '<span class="caret"></span>'
},
maxOptions: false,
mobile: false,
selectOnTab: true,
dropdownAlignRight: false,
windowPadding: 0,
virtualScroll: 600,
display: false,
sanitize: true,
sanitizeFn: null,
whiteList: DefaultWhitelist
};
Selectpicker.prototype = {
constructor: Selectpicker,
init: function () {
var that = this,
id = this.$element.attr('id'),
element = this.$element[0],
form = element.form;
selectId++;
this.selectId = 'bs-select-' + selectId;
element.classList.add('bs-select-hidden');
this.multiple = this.$element.prop('multiple');
this.autofocus = this.$element.prop('autofocus');
if (element.classList.contains('show-tick')) {
this.options.showTick = true;
}
this.$newElement = this.createDropdown();
this.$element
.after(this.$newElement)
.prependTo(this.$newElement);
// ensure select is associated with form element if it got unlinked after moving it inside newElement
if (form && element.form === null) {
if (!form.id) form.id = 'form-' + this.selectId;
element.setAttribute('form', form.id);
}
this.$button = this.$newElement.children('button');
if (this.options.allowClear) this.$clearButton = this.$button.children('.bs-select-clear-selected');
this.$menu = this.$newElement.children(Selector.MENU);
this.$menuInner = this.$menu.children('.inner');
this.$searchbox = this.$menu.find('input');
element.classList.remove('bs-select-hidden');
this.fetchData(function () {
that.render(true);
that.buildList();
requestAnimationFrame(function () {
that.$element.trigger('loaded' + EVENT_KEY);
});
});
if (this.options.dropdownAlignRight === true) this.$menu[0].classList.add(classNames.MENURIGHT);
if (typeof id !== 'undefined') {
this.$button.attr('data-id', id);
}
this.checkDisabled();
this.clickListener();
if (version.major > 4) this.dropdown = new Dropdown(this.$button[0]);
if (this.options.liveSearch) {
this.liveSearchListener();
this.focusedParent = this.$searchbox[0];
} else {
this.focusedParent = this.$menuInner[0];
}
this.setStyle();
this.setWidth();
if (this.options.container) {
this.selectPosition();
} else {
this.$element.on('hide' + EVENT_KEY, function () {
if (that.isVirtual()) {
// empty menu on close
var menuInner = that.$menuInner[0],
emptyMenu = menuInner.firstChild.cloneNode(false);
// replace the existing UL with an empty one - this is faster than $.empty() or innerHTML = ''
menuInner.replaceChild(emptyMenu, menuInner.firstChild);
menuInner.scrollTop = 0;
}
});
}
this.$menu.data('this', this);
this.$newElement.data('this', this);
if (this.options.mobile) this.mobile();
this.$newElement.on({
'hide.bs.dropdown': function (e) {
that.$element.trigger('hide' + EVENT_KEY, e);
},
'hidden.bs.dropdown': function (e) {
that.$element.trigger('hidden' + EVENT_KEY, e);
},
'show.bs.dropdown': function (e) {
that.$element.trigger('show' + EVENT_KEY, e);
},
'shown.bs.dropdown': function (e) {
that.$element.trigger('shown' + EVENT_KEY, e);
}
});
if (element.hasAttribute('required')) {
this.$element.on('invalid' + EVENT_KEY, function () {
that.$button[0].classList.add('bs-invalid');
that.$element
.on('shown' + EVENT_KEY + '.invalid', function () {
that.$element
.val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened
.off('shown' + EVENT_KEY + '.invalid');
})
.on('rendered' + EVENT_KEY, function () {
// if select is no longer invalid, remove the bs-invalid class
if (this.validity.valid) that.$button[0].classList.remove('bs-invalid');
that.$element.off('rendered' + EVENT_KEY);
});
that.$button.on('blur' + EVENT_KEY, function () {
that.$element.trigger('focus').trigger('blur');
that.$button.off('blur' + EVENT_KEY);
});
});
}
if (form) {
$(form).on('reset' + EVENT_KEY, function () {
requestAnimationFrame(function () {
that.render();
});
});
}
},
createDropdown: function () {
// Options
// If we are multiple or showTick option is set, then add the show-tick class
var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '',
multiselectable = this.multiple ? ' aria-multiselectable="true"' : '',
inputGroup = '',
autofocus = this.autofocus ? ' autofocus' : '';
if (version.major < 4 && this.$element.parent().hasClass('input-group')) {
inputGroup = ' input-group-btn';
}
// Elements
var drop,
header = '',
searchbox = '',
actionsbox = '',
donebutton = '',
clearButton = '';
if (this.options.header) {
header =
'<div class="' + classNames.POPOVERHEADER + '">' +
'<button type="button" class="close" aria-hidden="true">&times;</button>' +
this.options.header +
'</div>';
}
if (this.options.liveSearch) {
searchbox =
'<div class="bs-searchbox">' +
'<input type="search" class="form-control" autocomplete="off"' +
(
this.options.liveSearchPlaceholder === null ? ''
:
' placeholder="' + htmlEscape(this.options.liveSearchPlaceholder) + '"'
) +
' role="combobox" aria-label="Search" aria-controls="' + this.selectId + '" aria-autocomplete="list">' +
'</div>';
}
if (this.multiple && this.options.actionsBox) {
actionsbox =
'<div class="bs-actionsbox">' +
'<div class="btn-group btn-group-sm">' +
'<button type="button" class="actions-btn bs-select-all btn ' + classNames.BUTTONCLASS + '">' +
this.options.selectAllText +
'</button>' +
'<button type="button" class="actions-btn bs-deselect-all btn ' + classNames.BUTTONCLASS + '">' +
this.options.deselectAllText +
'</button>' +
'</div>' +
'</div>';
}
if (this.multiple && this.options.doneButton) {
donebutton =
'<div class="bs-donebutton">' +
'<div class="btn-group">' +
'<button type="button" class="btn btn-sm ' + classNames.BUTTONCLASS + '">' +
this.options.doneButtonText +
'</button>' +
'</div>' +
'</div>';
}
if (this.options.allowClear) {
clearButton = '<span class="close bs-select-clear-selected" title="' + this.options.deselectAllText + '"><span>&times;</span>';
}
drop =
'<div class="dropdown bootstrap-select' + showTick + inputGroup + '">' +
'<button type="button" tabindex="-1" class="' +
this.options.styleBase +
' dropdown-toggle" ' +
(this.options.display === 'static' ? 'data-display="static"' : '') +
Selector.DATA_TOGGLE +
autofocus +
' role="combobox" aria-owns="' +
this.selectId +
'" aria-haspopup="listbox" aria-expanded="false">' +
'<div class="filter-option">' +
'<div class="filter-option-inner">' +
'<div class="filter-option-inner-inner">&nbsp;</div>' +
'</div> ' +
'</div>' +
clearButton +
'</span>' +
(
version.major >= '4' ? ''
:
'<span class="bs-caret">' +
this.options.template.caret +
'</span>'
) +
'</button>' +
'<div class="' + classNames.MENU + ' ' + (version.major >= '4' ? '' : classNames.SHOW) + '">' +
header +
searchbox +
actionsbox +
'<div class="inner ' + classNames.SHOW + '" role="listbox" id="' + this.selectId + '" tabindex="-1" ' + multiselectable + '>' +
'<ul class="' + classNames.MENU + ' inner ' + (version.major >= '4' ? classNames.SHOW : '') + '" role="presentation">' +
'</ul>' +
'</div>' +
donebutton +
'</div>' +
'</div>';
return $(drop);
},
setPositionData: function () {
this.selectpicker.view.canHighlight = [];
this.selectpicker.view.size = 0;
this.selectpicker.view.firstHighlightIndex = false;
for (var i = 0; i < this.selectpicker.current.data.length; i++) {
var li = this.selectpicker.current.data[i],
canHighlight = true;
if (li.type === 'divider') {
canHighlight = false;
li.height = this.sizeInfo.dividerHeight;
} else if (li.type === 'optgroup-label') {
canHighlight = false;
li.height = this.sizeInfo.dropdownHeaderHeight;
} else {
li.height = this.sizeInfo.liHeight;
}
if (li.disabled) canHighlight = false;
this.selectpicker.view.canHighlight.push(canHighlight);
if (canHighlight) {
this.selectpicker.view.size++;
li.posinset = this.selectpicker.view.size;
if (this.selectpicker.view.firstHighlightIndex === false) this.selectpicker.view.firstHighlightIndex = i;
}
li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height;
}
},
isVirtual: function () {
return (this.options.virtualScroll !== false) && (this.selectpicker.main.data.length >= this.options.virtualScroll) || this.options.virtualScroll === true;
},
createView: function (isSearching, setSize, refresh) {
var that = this,
scrollTop = 0;
this.selectpicker.isSearching = isSearching;
this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main;
this.setPositionData();
if (setSize) {
if (refresh) {
scrollTop = this.$menuInner[0].scrollTop;
} else if (!that.multiple) {
var element = that.$element[0],
selectedIndex = (element.options[element.selectedIndex] || {}).liIndex;
if (typeof selectedIndex === 'number' && that.options.size !== false) {
var selectedData = that.selectpicker.main.data[selectedIndex],
position = selectedData && selectedData.position;
if (position) {
scrollTop = position - ((that.sizeInfo.menuInnerHeight + that.sizeInfo.liHeight) / 2);
}
}
}
}
scroll(scrollTop, true);
this.$menuInner.off('scroll.createView').on('scroll.createView', function (e, updateValue) {
if (!that.noScroll) scroll(this.scrollTop, updateValue);
that.noScroll = false;
});
function scroll (scrollTop, init) {
var size = that.selectpicker.current.data.length,
chunks = [],
chunkSize,
chunkCount,
firstChunk,
lastChunk,
currentChunk,
prevPositions,
positionIsDifferent,
previousElements,
menuIsDifferent = true,
isVirtual = that.isVirtual();
that.selectpicker.view.scrollTop = scrollTop;
chunkSize = that.options.chunkSize; // number of options in a chunk
chunkCount = Math.ceil(size / chunkSize) || 1; // number of chunks
for (var i = 0; i < chunkCount; i++) {
var endOfChunk = (i + 1) * chunkSize;
if (i === chunkCount - 1) {
endOfChunk = size;
}
chunks[i] = [
(i) * chunkSize + (!i ? 0 : 1),
endOfChunk
];
if (!size) break;
if (currentChunk === undefined && scrollTop - 1 <= that.selectpicker.current.data[endOfChunk - 1].position - that.sizeInfo.menuInnerHeight) {
currentChunk = i;
}
}
if (currentChunk === undefined) currentChunk = 0;
prevPositions = [that.selectpicker.view.position0, that.selectpicker.view.position1];
// always display previous, current, and next chunks
firstChunk = Math.max(0, currentChunk - 1);
lastChunk = Math.min(chunkCount - 1, currentChunk + 1);
that.selectpicker.view.position0 = isVirtual === false ? 0 : (Math.max(0, chunks[firstChunk][0]) || 0);
that.selectpicker.view.position1 = isVirtual === false ? size : (Math.min(size, chunks[lastChunk][1]) || 0);
positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1;
if (that.activeElement !== undefined) {
if (init) {
if (that.activeElement !== that.selectedElement) {
that.defocusItem(that.activeElement);
}
that.activeElement = undefined;
}
if (that.activeElement !== that.selectedElement) {
that.defocusItem(that.selectedElement);
}
}
if (that.prevActiveElement !== undefined && that.prevActiveElement !== that.activeElement && that.prevActiveElement !== that.selectedElement) {
that.defocusItem(that.prevActiveElement);
}
if (init || positionIsDifferent || that.selectpicker.current.hasMore) {
previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : [];
if (isVirtual === false) {
that.selectpicker.view.visibleElements = that.selectpicker.current.elements;
} else {
that.selectpicker.view.visibleElements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1);
}
that.setOptionStatus();
// if searching, check to make sure the list has actually been updated before updating DOM
// this prevents unnecessary repaints
if (isSearching || (isVirtual === false && init)) menuIsDifferent = !isEqual(previousElements, that.selectpicker.view.visibleElements);
// if virtual scroll is disabled and not searching,
// menu should never need to be updated more than once
if ((init || isVirtual === true) && menuIsDifferent) {
var menuInner = that.$menuInner[0],
menuFragment = document.createDocumentFragment(),
emptyMenu = menuInner.firstChild.cloneNode(false),
marginTop,
marginBottom,
elements = that.selectpicker.view.visibleElements,
toSanitize = [];
// replace the existing UL with an empty one - this is faster than $.empty()
menuInner.replaceChild(emptyMenu, menuInner.firstChild);
for (var i = 0, visibleElementsLen = elements.length; i < visibleElementsLen; i++) {
var element = elements[i],
elText,
elementData;
if (that.options.sanitize) {
elText = element.lastChild;
if (elText) {
elementData = that.selectpicker.current.data[i + that.selectpicker.view.position0];
if (elementData && elementData.content && !elementData.sanitized) {
toSanitize.push(elText);
elementData.sanitized = true;
}
}
}
menuFragment.appendChild(element);
}
if (that.options.sanitize && toSanitize.length) {
sanitizeHtml(toSanitize, that.options.whiteList, that.options.sanitizeFn);
}
if (isVirtual === true) {
marginTop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position);
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);
menuInner.firstChild.style.marginTop = marginTop + 'px';
menuInner.firstChild.style.marginBottom = marginBottom + 'px';
} else {
menuInner.firstChild.style.marginTop = 0;
menuInner.firstChild.style.marginBottom = 0;
}
menuInner.firstChild.appendChild(menuFragment);
// if an option is encountered that is wider than the current menu width, update the menu width accordingly
// switch to ResizeObserver with increased browser support
if (isVirtual === true && that.sizeInfo.hasScrollBar) {
var menuInnerInnerWidth = menuInner.firstChild.offsetWidth;
if (init && menuInnerInnerWidth < that.sizeInfo.menuInnerInnerWidth && that.sizeInfo.totalMenuWidth > that.sizeInfo.selectWidth) {
menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px';
} else if (menuInnerInnerWidth > that.sizeInfo.menuInnerInnerWidth) {
// set to 0 to get actual width of menu
that.$menu[0].style.minWidth = 0;
var actualMenuWidth = menuInner.firstChild.offsetWidth;
if (actualMenuWidth > that.sizeInfo.menuInnerInnerWidth) {
that.sizeInfo.menuInnerInnerWidth = actualMenuWidth;
menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px';
}
// reset to default CSS styling
that.$menu[0].style.minWidth = '';
}
}
}
if ((!isSearching && that.options.source.data || isSearching && that.options.source.search) && that.selectpicker.current.hasMore && currentChunk === chunkCount - 1) {
// Don't load the next chunk until scrolling has started
// This prevents unnecessary requests while the user is typing if pageSize is <= chunkSize
if (scrollTop > 0) {
// Chunks use 0-based indexing, but pages use 1-based. Add 1 to convert and add 1 again to get next page
var page = Math.floor((currentChunk * that.options.chunkSize) / that.options.source.pageSize) + 2;
that.fetchData(function () {
that.render();
that.buildList(size, isSearching);
that.setPositionData();
scroll(scrollTop);
}, isSearching ? 'search' : 'data', page, isSearching ? that.selectpicker.search.previousValue : undefined);
}
}
}
that.prevActiveElement = that.activeElement;
if (!that.options.liveSearch) {
that.$menuInner.trigger('focus');
} else if (isSearching && init) {
var index = 0,
newActive;
if (!that.selectpicker.view.canHighlight[index]) {
index = 1 + that.selectpicker.view.canHighlight.slice(1).indexOf(true);
}
newActive = that.selectpicker.view.visibleElements[index];
that.defocusItem(that.selectpicker.view.currentActive);
that.activeElement = (that.selectpicker.current.data[index] || {}).element;
that.focusItem(newActive);
}
}
$(window)
.off('resize' + EVENT_KEY + '.' + this.selectId + '.createView')
.on('resize' + EVENT_KEY + '.' + this.selectId + '.createView', function () {
var isActive = that.$newElement.hasClass(classNames.SHOW);
if (isActive) scroll(that.$menuInner[0].scrollTop);
});
},
focusItem: function (li, liData, noStyle) {
if (li) {
liData = liData || this.selectpicker.current.data[this.selectpicker.current.elements.indexOf(this.activeElement)];
var a = li.firstChild;
if (a) {
a.setAttribute('aria-setsize', this.selectpicker.view.size);
a.setAttribute('aria-posinset', liData.posinset);
if (noStyle !== true) {
this.focusedParent.setAttribute('aria-activedescendant', a.id);
li.classList.add('active');
a.classList.add('active');
}
}
}
},
defocusItem: function (li) {
if (li) {
li.classList.remove('active');
if (li.firstChild) li.firstChild.classList.remove('active');
}
},
setPlaceholder: function () {
var that = this,
updateIndex = false;
if ((this.options.placeholder || this.options.allowClear) && !this.multiple) {
if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option');
// this option doesn't create a new <li> element, but does add a new option at the start,
// so startIndex should increase to prevent having to check every option for the bs-title-option class
updateIndex = true;
var element = this.$element[0],
selectTitleOption = false,
titleNotAppended = !this.selectpicker.view.titleOption.parentNode,
selectedIndex = element.selectedIndex,
selectedOption = element.options[selectedIndex],
firstSelectable = element.querySelector('select > *:not(:disabled)'),
firstSelectableIndex = firstSelectable ? firstSelectable.index : 0,
navigation = window.performance && window.performance.getEntriesByType('navigation'),
// Safari doesn't support getEntriesByType('navigation') - fall back to performance.navigation
isNotBackForward = (navigation && navigation.length) ? navigation[0].type !== 'back_forward' : window.performance.navigation.type !== 2;
if (titleNotAppended) {
// Use native JS to prepend option (faster)
this.selectpicker.view.titleOption.className = 'bs-title-option';
this.selectpicker.view.titleOption.value = '';
// Check if selected or data-selected attribute is already set on an option. If not, select the titleOption option.
// the selected item may have been changed by user or programmatically before the bootstrap select plugin runs,
// if so, the select will have the data-selected attribute
selectTitleOption = !selectedOption || (selectedIndex === firstSelectableIndex && selectedOption.defaultSelected === false && this.$element.data('selected') === undefined);
}
if (titleNotAppended || this.selectpicker.view.titleOption.index !== 0) {
element.insertBefore(this.selectpicker.view.titleOption, element.firstChild);
}
// Set selected *after* appending to select,
// otherwise the option doesn't get selected in IE
// set using selectedIndex, as setting the selected attr to true here doesn't work in IE11
if (selectTitleOption && isNotBackForward) {
element.selectedIndex = 0;
} else if (document.readyState !== 'complete') {
// if navigation type is back_forward, there's a chance the select will have its value set by BFCache
// wait for that value to be set, then run render again
window.addEventListener('pageshow', function () {
if (that.selectpicker.view.displayedValue !== element.value) that.render();
});
}
}
return updateIndex;
},
fetchData: function (callback, type, page, searchValue) {
page = page || 1;
type = type || 'data';
var that = this,
data = this.options.source[type],
builtData;
if (data) {
this.options.virtualScroll = true;
if (typeof data === 'function') {
data.call(
this,
function (data, more, totalItems) {
var current = that.selectpicker[type === 'search' ? 'search' : 'main'];
current.hasMore = more;
current.totalItems = totalItems;
builtData = that.buildData(data, type);
callback.call(that, builtData);
that.$element.trigger('fetched' + EVENT_KEY);
},
page,
searchValue
);
} else if (Array.isArray(data)) {
builtData = that.buildData(data, type);
callback.call(that, builtData);
}
} else {
builtData = this.buildData(false, type);
callback.call(that, builtData);
}
},
buildData: function (data, type) {
var that = this;
var dataGetter = data === false ? getOptionData.fromOption : getOptionData.fromDataSource;
var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([style*="display: none"])',
mainData = [],
startLen = this.selectpicker.main.data ? this.selectpicker.main.data.length : 0,
optID = 0,
startIndex = this.setPlaceholder() && !data ? 1 : 0; // append the titleOption if necessary and skip the first option in the loop
if (type === 'search') {
startLen = this.selectpicker.search.data.length;
}
if (this.options.hideDisabled) optionSelector += ':not(:disabled)';
var selectOptions = data ? data.filter(filterHidden, this) : this.$element[0].querySelectorAll('select > *' + optionSelector);
function addDivider (config) {
var previousData = mainData[mainData.length - 1];
// ensure optgroup doesn't create back-to-back dividers
if (
previousData &&
previousData.type === 'divider' &&
(previousData.optID || config.optID)
) {
return;
}
config = config || {};
config.type = 'divider';
mainData.push(config);
}
function addOption (item, config) {
config = config || {};
config.divider = dataGetter(item, 'divider');
if (config.divider === true) {
addDivider({
optID: config.optID
});
} else {
var liIndex = mainData.length + startLen,
cssText = dataGetter(item, 'style'),
inlineStyle = cssText ? htmlEscape(cssText) : '',
optionClass = (item.className || '') + (config.optgroupClass || '');
if (config.optID) optionClass = 'opt ' + optionClass;
config.optionClass = optionClass.trim();
config.inlineStyle = inlineStyle;
config.text = dataGetter(item, 'text');
config.title = dataGetter(item, 'title');
config.content = dataGetter(item, 'content');
config.tokens = dataGetter(item, 'tokens');
config.subtext = dataGetter(item, 'subtext');
config.icon = dataGetter(item, 'icon');
config.display = config.content || config.text;
config.value = item.value === undefined ? item.text : item.value;
config.type = 'option';
config.index = liIndex;
config.option = !item.option ? item : item.option; // reference option element if it exists
config.option.liIndex = liIndex;
config.selected = !!item.selected;
config.disabled = config.disabled || !!item.disabled;
if (data !== false) {
if (that.selectpicker.optionValuesDataMap[config.value]) {
config = $.extend(that.selectpicker.optionValuesDataMap[config.value], config);
} else {
that.selectpicker.optionValuesDataMap[config.value] = config;
}
}
mainData.push(config);
}
}
function addOptgroup (index, selectOptions) {
var optgroup = selectOptions[index],
// skip placeholder option
previous = index - 1 < startIndex ? false : selectOptions[index - 1],
next = selectOptions[index + 1],
options = data ? optgroup.children.filter(filterHidden, this) : optgroup.querySelectorAll('option' + optionSelector);
if (!options.length) return;
var config = {
display: htmlEscape(dataGetter(item, 'label')),
subtext: dataGetter(optgroup, 'subtext'),
icon: dataGetter(optgroup, 'icon'),
type: 'optgroup-label',
optgroupClass: ' ' + (optgroup.className || ''),
optgroup: optgroup
},
headerIndex,
lastIndex;
optID++;
if (previous) {
addDivider({ optID: optID });
}
config.optID = optID;
mainData.push(config);
for (var j = 0, len = options.length; j < len; j++) {
var option = options[j];
if (j === 0) {
headerIndex = mainData.length - 1;
lastIndex = headerIndex + len;
}
addOption(option, {
headerIndex: headerIndex,
lastIndex: lastIndex,
optID: config.optID,
optgroupClass: config.optgroupClass,
disabled: optgroup.disabled
});
}
if (next) {
addDivider({ optID: optID });
}
}
for (var len = selectOptions.length, i = startIndex; i < len; i++) {
var item = selectOptions[i],
children = item.children;
if (children && children.length) {
addOptgroup.call(this, i, selectOptions);
} else {
addOption.call(this, item, {});
}
}
switch (type) {
case 'data': {
if (!this.selectpicker.main.data) {
this.selectpicker.main.data = [];
}
Array.prototype.push.apply(this.selectpicker.main.data, mainData);
this.selectpicker.current.data = this.selectpicker.main.data;
break;
}
case 'search': {
Array.prototype.push.apply(this.selectpicker.search.data, mainData);
break;
}
}
return mainData;
},
buildList: function (size, searching) {
var that = this,
selectData = searching ? this.selectpicker.search.data : this.selectpicker.main.data,
mainElements = [],
widestOptionLength = 0;
if ((that.options.showTick || that.multiple) && !elementTemplates.checkMark.parentNode) {
elementTemplates.checkMark.className = this.options.iconBase + ' ' + that.options.tickIcon + ' check-mark';
elementTemplates.a.appendChild(elementTemplates.checkMark);
}
function buildElement (mainElements, item) {
var liElement,
combinedLength = 0;
switch (item.type) {
case 'divider':
liElement = generateOption.li(
false,
classNames.DIVIDER,
(item.optID ? item.optID + 'div' : undefined)
);
break;
case 'option':
liElement = generateOption.li(
generateOption.a(
generateOption.text.call(that, item),
item.optionClass,
item.inlineStyle
),
'',
item.optID
);
if (liElement.firstChild) {
liElement.firstChild.id = that.selectId + '-' + item.index;
}
break;
case 'optgroup-label':
liElement = generateOption.li(
generateOption.label.call(that, item),
'dropdown-header' + item.optgroupClass,
item.optID
);
break;
}
if (!item.element) {
item.element = liElement;
} else {
item.element.innerHTML = liElement.innerHTML;
}
mainElements.push(item.element);
// count the number of characters in the option - not perfect, but should work in most cases
if (item.display) combinedLength += item.display.length;
if (item.subtext) combinedLength += item.subtext.length;
// if there is an icon, ensure this option's width is checked
if (item.icon) combinedLength += 1;
if (combinedLength > widestOptionLength) {
widestOptionLength = combinedLength;
// guess which option is the widest
// use this when calculating menu width
// not perfect, but it's fast, and the width will be updating accordingly when scrolling
that.selectpicker.view.widestOption = mainElements[mainElements.length - 1];
}
}
var startIndex = size || 0;
for (var len = selectData.length, i = startIndex; i < len; i++) {
var item = selectData[i];
buildElement(mainElements, item);
}
if (size) {
if (searching) {
Array.prototype.push.apply(this.selectpicker.search.elements, mainElements);
} else {
Array.prototype.push.apply(this.selectpicker.main.elements, mainElements);
this.selectpicker.current.elements = this.selectpicker.main.elements;
}
} else {
if (searching) {
this.selectpicker.search.elements = mainElements;
} else {
this.selectpicker.main.elements = this.selectpicker.current.elements = mainElements;
}
}
},
findLis: function () {
return this.$menuInner.find('.inner > li');
},
render: function (init) {
var that = this,
element = this.$element[0],
// ensure titleOption is appended and selected (if necessary) before getting selectedOptions
placeholderSelected = this.setPlaceholder() && element.selectedIndex === 0,
//selectedOptions = getSelectedOptions.call(this),
selectedOptions = getSelectedOptions(element, this.options.hideDisabled),
selectedCount = selectedOptions.length,
//selectedValues = getSelectValues.call(this, selectedOptions),
selectedValues = getSelectValues(element, selectedOptions),
button = this.$button[0],
buttonInner = button.querySelector('.filter-option-inner-inner'),
multipleSeparator = document.createTextNode(this.options.multipleSeparator),
titleFragment = elementTemplates.fragment.cloneNode(false),
titleTipFragment = elementTemplates.fragment.cloneNode(false),
showCount,
countMax,
hasContent = false;
function createSelected (item) {
if (item.selected) {
that.createOption(item, true);
} else if (item.children && item.children.length) {
item.children.map(createSelected);
}
}
// create selected option elements to ensure select value is correct
if (this.options.source.data && init) {
selectedOptions.map(createSelected);
element.appendChild(this.selectpicker.main.optionQueue);
if (placeholderSelected) placeholderSelected = element.selectedIndex === 0;
}
button.classList.toggle('bs-placeholder', that.multiple ? !selectedCount : !selectedValues && selectedValues !== 0);
if (!that.multiple && selectedOptions.length === 1) {
that.selectpicker.view.displayedValue = selectedValues;
}
if (this.options.selectedTextFormat === 'static') {
titleFragment = generateOption.text.call(this, { text: this.options.placeholder }, true);
} else {
showCount = this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1 && selectedCount > 0;
// determine if the number of selected options will be shown (showCount === true)
if (showCount) {
countMax = this.options.selectedTextFormat.split('>');
showCount = (countMax.length > 1 && selectedCount > countMax[1]) || (countMax.length === 1 && selectedCount >= 2);
}
// only loop through all selected options if the count won't be shown
if (showCount === false) {
if (!placeholderSelected) {
for (var selectedIndex = 0; selectedIndex < selectedCount; selectedIndex++) {
if (selectedIndex < 50) {
var option = selectedOptions[selectedIndex],
thisData = this.selectpicker.main.data[option.liIndex],
titleOptions = {};
if (option) {
if (this.multiple && selectedIndex > 0) {
titleFragment.appendChild(multipleSeparator.cloneNode(false));
}
if (option.title) {
titleOptions.text = option.title;
}else if(thisData){
if (thisData.content && that.options.showContent) {
titleOptions.content = thisData.content.toString();
hasContent = true;
} else {
if (that.options.showIcon) {
//titleOptions.icon = option.icon;
titleOptions.icon = thisData.icon;
}
if (that.options.showSubtext && !that.multiple && option.subtext) titleOptions.subtext = ' ' + option.subtext;
titleOptions.text = option.text.trim();
}
}
titleFragment.appendChild(generateOption.text.call(this, titleOptions, true));
}
} else {
break;
}
}
// add ellipsis
if (selectedCount > 49) {
titleFragment.appendChild(document.createTextNode('...'));
}
}
} else {
var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([data-divider="true"]):not([style*="display: none"])';
if (this.options.hideDisabled) optionSelector += ':not(:disabled)';
// If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected, etc.
var totalCount = this.$element[0].querySelectorAll('select > option' + optionSelector + ', optgroup' + optionSelector + ' option' + optionSelector).length,
tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedCount, totalCount) : this.options.countSelectedText;
titleFragment = generateOption.text.call(this, {
text: tr8nText.replace('{0}', selectedCount.toString()).replace('{1}', totalCount.toString())
}, true);
}
}
// If the select doesn't have a title, then use the default, or if nothing is set at all, use noneSelectedText
if (!titleFragment.childNodes.length) {
titleFragment = generateOption.text.call(this, {
text: this.options.placeholder ? this.options.placeholder : this.options.noneSelectedText
}, true);
}
// if the select has a title, apply it to the button, and if not, apply titleFragment text
// strip all HTML tags and trim the result, then unescape any escaped tags
button.title = titleFragment.textContent.replace(/<[^>]*>?/g, '').trim();
if (this.options.sanitize && hasContent) {
sanitizeHtml([titleFragment], that.options.whiteList, that.options.sanitizeFn);
}
buttonInner.innerHTML = '';
buttonInner.appendChild(titleFragment);
if (this.options.titleTip && !button.querySelector('.title-tip')) {
var titleTip = document.createElement('div');
titleTip.setAttribute('class', 'title-tip');
titleTipFragment = generateOption.text.call(this, {
text: this.options.titleTip ? this.options.titleTip : ''
}, true);
titleTip.appendChild(titleTipFragment);
button.querySelector('.filter-option').prepend(titleTip);
}
if (version.major < 4 && this.$newElement[0].classList.contains('bs3-has-addon')) {
var filterExpand = button.querySelector('.filter-expand'),
clone = buttonInner.cloneNode(true);
clone.className = 'filter-expand';
if (filterExpand) {
button.replaceChild(clone, filterExpand);
} else {
button.appendChild(clone);
}
}
this.$element.trigger('rendered' + EVENT_KEY);
},
/**
* @param [style]
* @param [status]
*/
setStyle: function (newStyle, status) {
var button = this.$button[0],
newElement = this.$newElement[0],
style = this.options.style.trim(),
buttonClass;
if (this.$element.attr('class')) {
this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, ''));
}
if (version.major < 4) {
newElement.classList.add('bs3');
if (newElement.parentNode.classList && newElement.parentNode.classList.contains('input-group') &&
(newElement.previousElementSibling || newElement.nextElementSibling) &&
(newElement.previousElementSibling || newElement.nextElementSibling).classList.contains('input-group-addon')
) {
newElement.classList.add('bs3-has-addon');
}
}
if (newStyle) {
buttonClass = newStyle.trim();
} else {
buttonClass = style;
}
if (status == 'add') {
if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' '));
} else if (status == 'remove') {
if (buttonClass) button.classList.remove.apply(button.classList, buttonClass.split(' '));
} else {
if (style) button.classList.remove.apply(button.classList, style.split(' '));
if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' '));
}
},
liHeight: function (refresh) {
if (!refresh && (this.options.size === false || Object.keys(this.sizeInfo).length)) return;
var newElement = elementTemplates.div.cloneNode(false),
menu = elementTemplates.div.cloneNode(false),
menuInner = elementTemplates.div.cloneNode(false),
menuInnerInner = document.createElement('ul'),
divider = elementTemplates.li.cloneNode(false),
dropdownHeader = elementTemplates.li.cloneNode(false),
li,
a = elementTemplates.a.cloneNode(false),
text = elementTemplates.span.cloneNode(false),
header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null,
search = this.options.liveSearch ? elementTemplates.div.cloneNode(false) : null,
actions = this.options.actionsBox && this.multiple && this.$menu.find('.bs-actionsbox').length > 0 ? this.$menu.find('.bs-actionsbox')[0].cloneNode(true) : null,
doneButton = this.options.doneButton && this.multiple && this.$menu.find('.bs-donebutton').length > 0 ? this.$menu.find('.bs-donebutton')[0].cloneNode(true) : null,
firstOption = this.$element[0].options[0];
this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth;
text.className = 'text';
a.className = 'dropdown-item ' + (firstOption ? firstOption.className : '');
newElement.className = this.$menu[0].parentNode.className + ' ' + classNames.SHOW;
newElement.style.width = 0; // ensure button width doesn't affect natural width of menu when calculating
if (this.options.width === 'auto') menu.style.minWidth = 0;
menu.className = classNames.MENU + ' ' + classNames.SHOW;
menuInner.className = 'inner ' + classNames.SHOW;
menuInnerInner.className = classNames.MENU + ' inner ' + (version.major >= '4' ? classNames.SHOW : '');
divider.className = classNames.DIVIDER;
dropdownHeader.className = 'dropdown-header';
text.appendChild(document.createTextNode('\u200b'));
if (this.selectpicker.current.data.length) {
for (var i = 0; i < this.selectpicker.current.data.length; i++) {
var data = this.selectpicker.current.data[i];
if (data.type === 'option' && $(data.element.firstChild).css('display') !== 'none') {
li = data.element;
break;
}
}
} else {
li = elementTemplates.li.cloneNode(false);
a.appendChild(text);
li.appendChild(a);
}
dropdownHeader.appendChild(text.cloneNode(true));
if (this.selectpicker.view.widestOption) {
menuInnerInner.appendChild(this.selectpicker.view.widestOption.cloneNode(true));
}
menuInnerInner.appendChild(li);
menuInnerInner.appendChild(divider);
menuInnerInner.appendChild(dropdownHeader);
if (header) menu.appendChild(header);
if (search) {
var input = document.createElement('input');
search.className = 'bs-searchbox';
input.className = 'form-control';
search.appendChild(input);
menu.appendChild(search);
}
if (actions) menu.appendChild(actions);
menuInner.appendChild(menuInnerInner);
menu.appendChild(menuInner);
if (doneButton) menu.appendChild(doneButton);
newElement.appendChild(menu);
document.body.appendChild(newElement);
var liHeight = li.offsetHeight,
dropdownHeaderHeight = dropdownHeader ? dropdownHeader.offsetHeight : 0,
headerHeight = header ? header.offsetHeight : 0,
searchHeight = search ? search.offsetHeight : 0,
actionsHeight = actions ? actions.offsetHeight : 0,
doneButtonHeight = doneButton ? doneButton.offsetHeight : 0,
dividerHeight = $(divider).outerHeight(true),
menuStyle = window.getComputedStyle(menu),
menuWidth = menu.offsetWidth,
menuPadding = {
vert: toInteger(menuStyle.paddingTop) +
toInteger(menuStyle.paddingBottom) +
toInteger(menuStyle.borderTopWidth) +
toInteger(menuStyle.borderBottomWidth),
horiz: toInteger(menuStyle.paddingLeft) +
toInteger(menuStyle.paddingRight) +
toInteger(menuStyle.borderLeftWidth) +
toInteger(menuStyle.borderRightWidth)
},
menuExtras = {
vert: menuPadding.vert +
toInteger(menuStyle.marginTop) +
toInteger(menuStyle.marginBottom) + 2,
horiz: menuPadding.horiz +
toInteger(menuStyle.marginLeft) +
toInteger(menuStyle.marginRight) + 2
},
scrollBarWidth;
menuInner.style.overflowY = 'scroll';
scrollBarWidth = menu.offsetWidth - menuWidth;
document.body.removeChild(newElement);
this.sizeInfo.liHeight = liHeight;
this.sizeInfo.dropdownHeaderHeight = dropdownHeaderHeight;
this.sizeInfo.headerHeight = headerHeight;
this.sizeInfo.searchHeight = searchHeight;
this.sizeInfo.actionsHeight = actionsHeight;
this.sizeInfo.doneButtonHeight = doneButtonHeight;
this.sizeInfo.dividerHeight = dividerHeight;
this.sizeInfo.menuPadding = menuPadding;
this.sizeInfo.menuExtras = menuExtras;
this.sizeInfo.menuWidth = menuWidth;
this.sizeInfo.menuInnerInnerWidth = menuWidth - menuPadding.horiz;
this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth;
this.sizeInfo.scrollBarWidth = scrollBarWidth;
this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight;
this.setPositionData();
},
getSelectPosition: function () {
var that = this,
$window = $(window),
pos = that.$newElement.offset(),
$container = $(that.options.container),
containerPos;
if (that.options.container && $container.length && !$container.is('body')) {
containerPos = $container.offset();
containerPos.top += parseInt($container.css('borderTopWidth'));
containerPos.left += parseInt($container.css('borderLeftWidth'));
} else {
containerPos = { top: 0, left: 0 };
}
var winPad = that.options.windowPadding;
this.sizeInfo.selectOffsetTop = pos.top - containerPos.top - $window.scrollTop();
this.sizeInfo.selectOffsetBot = $window.height() - this.sizeInfo.selectOffsetTop - this.sizeInfo.selectHeight - containerPos.top - winPad[2];
this.sizeInfo.selectOffsetLeft = pos.left - containerPos.left - $window.scrollLeft();
this.sizeInfo.selectOffsetRight = $window.width() - this.sizeInfo.selectOffsetLeft - this.sizeInfo.selectWidth - containerPos.left - winPad[1];
this.sizeInfo.selectOffsetTop -= winPad[0];
this.sizeInfo.selectOffsetLeft -= winPad[3];
},
setMenuSize: function (isAuto) {
this.getSelectPosition();
var selectWidth = this.sizeInfo.selectWidth,
liHeight = this.sizeInfo.liHeight,
headerHeight = this.sizeInfo.headerHeight,
searchHeight = this.sizeInfo.searchHeight,
actionsHeight = this.sizeInfo.actionsHeight,
doneButtonHeight = this.sizeInfo.doneButtonHeight,
divHeight = this.sizeInfo.dividerHeight,
menuPadding = this.sizeInfo.menuPadding,
menuInnerHeight,
menuHeight,
divLength = 0,
minHeight,
_minHeight,
maxHeight,
menuInnerMinHeight,
estimate,
isDropup;
if (this.options.dropupAuto) {
// Get the estimated height of the menu without scrollbars.
// This is useful for smaller menus, where there might be plenty of room
// below the button without setting dropup, but we can't know
// the exact height of the menu until createView is called later
estimate = liHeight * this.selectpicker.current.data.length + menuPadding.vert;
isDropup = this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot;
// ensure dropup doesn't change while searching (so menu doesn't bounce back and forth)
if (this.selectpicker.isSearching === true) {
isDropup = this.selectpicker.dropup;
}
this.$newElement.toggleClass(classNames.DROPUP, isDropup);
this.selectpicker.dropup = isDropup;
}
if (this.options.size === 'auto') {
_minHeight = this.selectpicker.current.data.length > 3 ? this.sizeInfo.liHeight * 3 + this.sizeInfo.menuExtras.vert - 2 : 0;
menuHeight = this.sizeInfo.selectOffsetBot - this.sizeInfo.menuExtras.vert;
minHeight = _minHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight;
menuInnerMinHeight = Math.max(_minHeight - menuPadding.vert, 0);
if (this.$newElement.hasClass(classNames.DROPUP)) {
menuHeight = this.sizeInfo.selectOffsetTop - this.sizeInfo.menuExtras.vert;
}
maxHeight = menuHeight;
menuInnerHeight = menuHeight - headerHeight - searchHeight - actionsHeight - doneButtonHeight - menuPadding.vert;
} else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) {
for (var i = 0; i < this.options.size; i++) {
if (this.selectpicker.current.data[i].type === 'divider') divLength++;
}
menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding.vert;
menuInnerHeight = menuHeight - menuPadding.vert;
maxHeight = menuHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight;
minHeight = menuInnerMinHeight = '';
}
this.$menu.css({
'max-height': maxHeight + 'px',
'overflow': 'hidden',
'min-height': minHeight + 'px'
});
this.$menuInner.css({
'max-height': menuInnerHeight + 'px',
'overflow': 'hidden auto',
'min-height': menuInnerMinHeight + 'px'
});
// ensure menuInnerHeight is always a positive number to prevent issues calculating chunkSize in createView
this.sizeInfo.menuInnerHeight = Math.max(menuInnerHeight, 1);
if (this.selectpicker.current.data.length && this.selectpicker.current.data[this.selectpicker.current.data.length - 1].position > this.sizeInfo.menuInnerHeight) {
this.sizeInfo.hasScrollBar = true;
this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth + this.sizeInfo.scrollBarWidth;
}
if (this.options.dropdownAlignRight === 'auto') {
this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.sizeInfo.totalMenuWidth - selectWidth));
}
if (this.dropdown && this.dropdown._popper) this.dropdown._popper.update();
},
setSize: function (refresh) {
this.liHeight(refresh);
if (this.options.header) this.$menu.css('padding-top', 0);
if (this.options.size !== false) {
var that = this,
$window = $(window);
this.setMenuSize();
if (this.options.liveSearch) {
this.$searchbox
.off('input.setMenuSize propertychange.setMenuSize')
.on('input.setMenuSize propertychange.setMenuSize', function () {
return that.setMenuSize();
});
}
if (this.options.size === 'auto') {
$window
.off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize')
.on('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize', function () {
return that.setMenuSize();
});
} else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) {
$window.off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize');
}
}
this.createView(false, true, refresh);
},
setWidth: function () {
var that = this;
if (this.options.width === 'auto') {
requestAnimationFrame(function () {
that.$menu.css('min-width', '0');
that.$element.on('loaded' + EVENT_KEY, function () {
that.liHeight();
that.setMenuSize();
// Get correct width if element is hidden
var $selectClone = that.$newElement.clone().appendTo('body'),
btnWidth = $selectClone.css('width', 'auto').children('button').outerWidth();
$selectClone.remove();
// Set width to whatever's larger, button title or longest option
that.sizeInfo.selectWidth = Math.max(that.sizeInfo.totalMenuWidth, btnWidth);
that.$newElement.css('width', that.sizeInfo.selectWidth + 'px');
});
});
} else if (this.options.width === 'fit') {
// Remove inline min-width so width can be changed from 'auto'
this.$menu.css('min-width', '');
this.$newElement.css('width', '').addClass('fit-width');
} else if (this.options.width) {
// Remove inline min-width so width can be changed from 'auto'
this.$menu.css('min-width', '');
this.$newElement.css('width', this.options.width);
} else {
// Remove inline min-width/width so width can be changed
this.$menu.css('min-width', '');
this.$newElement.css('width', '');
}
// Remove fit-width class if width is changed programmatically
if (this.$newElement.hasClass('fit-width') && this.options.width !== 'fit') {
this.$newElement[0].classList.remove('fit-width');
}
},
selectPosition: function () {
this.$bsContainer = $('<div class="bs-container" />');
var that = this,
$container = $(this.options.container),
pos,
containerPos,
actualHeight,
getPlacement = function ($element) {
var containerPosition = {},
// fall back to dropdown's default display setting if display is not manually set
display = that.options.display || (
// Bootstrap 3 doesn't have $.fn.dropdown.Constructor.Default
$.fn.dropdown.Constructor.Default ? $.fn.dropdown.Constructor.Default.display
: false
);
that.$bsContainer.addClass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleClass(classNames.DROPUP, $element.hasClass(classNames.DROPUP));
pos = $element.offset();
if (!$container.is('body')) {
containerPos = $container.offset();
containerPos.top += parseInt($container.css('borderTopWidth')) - $container.scrollTop();
containerPos.left += parseInt($container.css('borderLeftWidth')) - $container.scrollLeft();
} else {
containerPos = { top: 0, left: 0 };
}
actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : $element[0].offsetHeight;
// Bootstrap 4+ uses Popper for menu positioning
if (version.major < 4 || display === 'static') {
containerPosition.top = pos.top - containerPos.top + actualHeight;
containerPosition.left = pos.left - containerPos.left;
}
containerPosition.width = $element[0].offsetWidth;
that.$bsContainer.css(containerPosition);
};
this.$button.on('click.bs.dropdown.data-api', function () {
if (that.isDisabled()) {
return;
}
getPlacement(that.$newElement);
that.$bsContainer
.appendTo(that.options.container)
.toggleClass(classNames.SHOW, !that.$button.hasClass(classNames.SHOW))
.append(that.$menu);
});
$(window)
.off('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId)
.on('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId, function () {
var isActive = that.$newElement.hasClass(classNames.SHOW);
if (isActive) getPlacement(that.$newElement);
});
this.$element.on('hide' + EVENT_KEY, function () {
that.$menu.data('height', that.$menu.height());
that.$bsContainer.detach();
});
},
createOption: function (data, init) {
var optionData = !data.option ? data : data.option;
if (optionData && optionData.nodeType !== 1) {
var option = (init ? elementTemplates.selectedOption : elementTemplates.option).cloneNode(true);
if (optionData.value !== undefined) option.value = optionData.value;
option.textContent = optionData.text;
option.selected = true;
if (optionData.liIndex !== undefined) {
option.liIndex = optionData.liIndex;
} else if (!init) {
option.liIndex = data.index;
}
data.option = option;
this.selectpicker.main.optionQueue.appendChild(option);
}
},
setOptionStatus: function (selectedOnly) {
var that = this;
that.noScroll = false;
if (that.selectpicker.view.visibleElements && that.selectpicker.view.visibleElements.length) {
for (var i = 0; i < that.selectpicker.view.visibleElements.length; i++) {
var liData = that.selectpicker.current.data[i + that.selectpicker.view.position0],
option = liData.option;
if (option) {
if (selectedOnly !== true) {
that.setDisabled(liData);
}
that.setSelected(liData);
}
}
// append optionQueue (documentFragment with option elements for select options)
if (this.options.source.data) this.$element[0].appendChild(this.selectpicker.main.optionQueue);
}
},
/**
* @param {Object} liData - the option object that is being changed
* @param {boolean} selected - true if the option is being selected, false if being deselected
*/
setSelected: function (liData, selected) {
selected = selected === undefined ? liData.selected : selected;
var li = liData.element,
activeElementIsSet = this.activeElement !== undefined,
thisIsActive = this.activeElement === li,
prevActive,
a,
// if current option is already active
// OR
// if the current option is being selected, it's NOT multiple, and
// activeElement is undefined:
// - when the menu is first being opened, OR
// - after a search has been performed, OR
// - 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)
keepActive = thisIsActive || (selected && !this.multiple && !activeElementIsSet);
if (!li) return;
if (selected !== undefined) {
liData.selected = selected;
if (liData.option) liData.option.selected = selected;
}
if (selected && this.options.source.data) {
this.createOption(liData, false);
}
a = li.firstChild;
if (selected) {
this.selectedElement = li;
}
li.classList.toggle('selected', selected);
if (keepActive) {
this.focusItem(li, liData);
this.selectpicker.view.currentActive = li;
this.activeElement = li;
} else {
this.defocusItem(li);
}
if (a) {
a.classList.toggle('selected', selected);
if (selected) {
a.setAttribute('aria-selected', true);
} else {
if (this.multiple) {
a.setAttribute('aria-selected', false);
} else {
a.removeAttribute('aria-selected');
}
}
}
if (!keepActive && !activeElementIsSet && selected && this.prevActiveElement !== undefined) {
prevActive = this.prevActiveElement;
this.defocusItem(prevActive);
}
},
/**
* @param {number} index - the index of the option that is being disabled
* @param {boolean} disabled - true if the option is being disabled, false if being enabled
*/
setDisabled: function (liData) {
var disabled = liData.disabled,
li = liData.element,
a;
if (!li) return;
a = li.firstChild;
li.classList.toggle(classNames.DISABLED, disabled);
if (a) {
if (version.major >= '4') a.classList.toggle(classNames.DISABLED, disabled);
if (disabled) {
a.setAttribute('aria-disabled', disabled);
a.setAttribute('tabindex', -1);
} else {
a.removeAttribute('aria-disabled');
a.setAttribute('tabindex', 0);
}
}
},
isDisabled: function () {
return this.$element[0].disabled;
},
checkDisabled: function () {
if (this.isDisabled()) {
this.$newElement[0].classList.add(classNames.DISABLED);
this.$button.addClass(classNames.DISABLED).attr('aria-disabled', true);
} else {
if (this.$button[0].classList.contains(classNames.DISABLED)) {
this.$newElement[0].classList.remove(classNames.DISABLED);
this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false);
}
}
},
clickListener: function () {
var that = this,
$document = $(document);
$document.data('spaceSelect', false);
this.$button.on('keyup', function (e) {
if (/(32)/.test(e.keyCode.toString(10)) && $document.data('spaceSelect')) {
e.preventDefault();
$document.data('spaceSelect', false);
}
});
this.$newElement.on('show.bs.dropdown', function () {
if (!that.dropdown && version.major >= '4') {
that.dropdown = that.$button.data('bs.dropdown');
that.dropdown._menu = that.$menu[0];
}
});
function clearSelection (e) {
if (that.multiple) {
that.deselectAll();
} else {
var element = that.$element[0],
prevValue = element.value,
prevIndex = element.selectedIndex,
prevOption = element.options[prevIndex],
prevData = prevOption ? that.selectpicker.main.data[prevOption.liIndex] : false;
if (prevData) {
that.setSelected(prevData, false);
}
element.selectedIndex = 0;
changedArguments = [prevIndex, false, prevValue];
that.$element.triggerNative('change');
}
// remove selected styling if menu is open
if (that.$newElement.hasClass(classNames.SHOW)) {
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
}
that.createView(false);
}
}
this.$button.on('click.bs.dropdown.data-api', function (e) {
if (that.options.allowClear) {
var target = e.target,
clearButton = that.$clearButton[0];
// IE doesn't support event listeners on child elements of buttons
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
target = document.elementFromPoint(e.clientX, e.clientY);
}
if (target === clearButton || target.parentElement === clearButton) {
e.stopImmediatePropagation();
clearSelection(e);
}
}
if (!that.$newElement.hasClass(classNames.SHOW)) {
that.setSize();
}
});
function setFocus () {
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
} else {
that.$menuInner.trigger('focus');
}
}
function checkPopperExists () {
if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state) {
setFocus();
} else {
requestAnimationFrame(checkPopperExists);
}
}
this.$element.on('shown' + EVENT_KEY, function () {
if (that.$menuInner[0].scrollTop !== that.selectpicker.view.scrollTop) {
that.$menuInner[0].scrollTop = that.selectpicker.view.scrollTop;
}
if (version.major > 3) {
requestAnimationFrame(checkPopperExists);
} else {
setFocus();
}
});
// ensure posinset and setsize are correct before selecting an option via a click
this.$menuInner.on('mouseenter', 'li a', function (e) {
var hoverLi = this.parentElement,
position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0,
index = Array.prototype.indexOf.call(hoverLi.parentElement.children, hoverLi),
hoverData = that.selectpicker.current.data[index + position0];
that.focusItem(hoverLi, hoverData, true);
});
this.$menuInner.on('click', 'li a', function (e, retainActive) {
var $this = $(this),
element = that.$element[0],
position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0,
clickedData = that.selectpicker.current.data[$this.parent().index() + position0],
clickedElement = clickedData.element,
prevValue = getSelectValues(element),
prevIndex = element.selectedIndex,
prevOption = element.options[prevIndex],
prevData = prevOption ? that.selectpicker.main.data[prevOption.liIndex] : false,
triggerChange = true;
// Don't close on multi choice menu
if (that.multiple && that.options.maxOptions !== 1) {
e.stopPropagation();
}
e.preventDefault();
// Don't run if the select is disabled
if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) {
var option = clickedData.option,
$option = $(option),
state = option.selected,
optgroupData = that.selectpicker.current.data.find(function (datum) {
return datum.optID === clickedData.optID && datum.type === 'optgroup-label';
}),
optgroup = optgroupData ? optgroupData.optgroup : undefined,
dataGetter = optgroup instanceof Element ? getOptionData.fromOption : getOptionData.fromDataSource,
optgroupOptions = optgroup && optgroup.children,
maxOptions = parseInt(that.options.maxOptions),
maxOptionsGrp = optgroup && parseInt(dataGetter(optgroup, 'maxOptions')) || false;
if (clickedElement === that.activeElement) retainActive = true;
if (!retainActive) {
that.prevActiveElement = that.activeElement;
that.activeElement = undefined;
}
if (!that.multiple || maxOptions === 1) { // Deselect previous option if not multi select
//if (prevData) that.setSelected(prevData, false);
//that.setSelected(clickedData, true);
if (prevData) that.setSelected(prevData, !!state);
that.setSelected(clickedData, !state);
} else { // Toggle the clicked option if multi select.
that.setSelected(clickedData, !state);
that.focusedParent.focus();
if (maxOptions !== false || maxOptionsGrp !== false) {
var maxReached = maxOptions < getSelectedOptions(element).length,
selectedGroupOptions = 0;
if (optgroup && optgroup.children) {
for (var i = 0; i < optgroup.children.length; i++) {
if (optgroup.children[i].selected) selectedGroupOptions++;
}
}
var maxReachedGrp = maxOptionsGrp < selectedGroupOptions;
if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) {
if (maxOptions && maxOptions === 1) {
element.selectedIndex = -1;
that.setOptionStatus(true);
} else if (maxOptionsGrp && maxOptionsGrp === 1) {
for (var i = 0; i < optgroupOptions.length; i++) {
var _option = optgroupOptions[i];
that.setSelected(that.selectpicker.current.data[_option.liIndex], false);
}
that.setSelected(clickedData, true);
} else {
var maxOptionsText = typeof that.options.maxOptionsText === 'string' ? [that.options.maxOptionsText, that.options.maxOptionsText] : that.options.maxOptionsText,
maxOptionsArr = typeof maxOptionsText === 'function' ? maxOptionsText(maxOptions, maxOptionsGrp) : maxOptionsText,
maxTxt = maxOptionsArr[0].replace('{n}', maxOptions),
maxTxtGrp = maxOptionsArr[1].replace('{n}', maxOptionsGrp),
$notify = $('<div class="notify"></div>');
// If {var} is set in array, replace it
/** @deprecated */
if (maxOptionsArr[2]) {
maxTxt = maxTxt.replace('{var}', maxOptionsArr[2][maxOptions > 1 ? 0 : 1]);
maxTxtGrp = maxTxtGrp.replace('{var}', maxOptionsArr[2][maxOptionsGrp > 1 ? 0 : 1]);
}
that.$menu.append($notify);
if (maxOptions && maxReached) {
$notify.append($('<div>' + maxTxt + '</div>'));
triggerChange = false;
that.$element.trigger('maxReached' + EVENT_KEY);
}
if (maxOptionsGrp && maxReachedGrp) {
$notify.append($('<div>' + maxTxtGrp + '</div>'));
triggerChange = false;
that.$element.trigger('maxReachedGrp' + EVENT_KEY);
}
setTimeout(function () {
that.setSelected(clickedData, false);
}, 10);
$notify[0].classList.add('fadeOut');
setTimeout(function () {
$notify.remove();
}, 1050);
}
}
}
}
if (that.options.source.data) that.$element[0].appendChild(that.selectpicker.main.optionQueue);
if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) {
that.$button.trigger('focus');
} else if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
}
// Trigger select 'change'
if (triggerChange) {
if (that.multiple || prevIndex !== element.selectedIndex) {
// $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed.
changedArguments = [option.index, $option.prop('selected'), prevValue];
that.$element
.triggerNative('change');
}
}
}
});
this.$menu.on('click', 'li.' + classNames.DISABLED + ' a, .' + classNames.POPOVERHEADER + ', .' + classNames.POPOVERHEADER + ' :not(.close)', function (e) {
if (e.currentTarget == this) {
e.preventDefault();
e.stopPropagation();
if (that.options.liveSearch && !$(e.target).hasClass('close')) {
that.$searchbox.trigger('focus');
} else {
that.$button.trigger('focus');
}
}
});
this.$menuInner.on('click', '.divider, .dropdown-header', function (e) {
e.preventDefault();
e.stopPropagation();
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
} else {
that.$button.trigger('focus');
}
});
this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () {
that.$button.trigger('click');
});
this.$searchbox.on('click', function (e) {
e.stopPropagation();
});
this.$menu.on('click', '.actions-btn', function (e) {
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
} else {
that.$button.trigger('focus');
}
e.preventDefault();
e.stopPropagation();
if ($(this).hasClass('bs-select-all')) {
that.selectAll();
} else {
that.deselectAll();
}
});
this.$button
.on('focus' + EVENT_KEY, function (e) {
var tabindex = that.$element[0].getAttribute('tabindex');
// only change when button is actually focused
if (tabindex !== undefined && e.originalEvent && e.originalEvent.isTrusted) {
// apply select element's tabindex to ensure correct order is followed when tabbing to the next element
this.setAttribute('tabindex', tabindex);
// set element's tabindex to -1 to allow for reverse tabbing
that.$element[0].setAttribute('tabindex', -1);
that.selectpicker.view.tabindex = tabindex;
}
})
.on('blur' + EVENT_KEY, function (e) {
// revert everything to original tabindex
if (that.selectpicker.view.tabindex !== undefined && e.originalEvent && e.originalEvent.isTrusted) {
that.$element[0].setAttribute('tabindex', that.selectpicker.view.tabindex);
this.setAttribute('tabindex', -1);
that.selectpicker.view.tabindex = undefined;
}
});
this.$element
.on('change' + EVENT_KEY, function () {
that.render();
that.$element.trigger('changed' + EVENT_KEY, changedArguments);
changedArguments = null;
})
.on('focus' + EVENT_KEY, function () {
if (!that.options.mobile) that.$button[0].focus();
});
},
liveSearchListener: function () {
var that = this;
this.$button.on('click.bs.dropdown.data-api', function () {
if (!!that.$searchbox.val()) {
that.$searchbox.val('');
that.selectpicker.search.previousValue = undefined;
}
});
this.$searchbox.on('click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api', function (e) {
e.stopPropagation();
});
this.$searchbox.on('input propertychange', function () {
var searchValue = that.$searchbox[0].value;
that.selectpicker.search.elements = [];
that.selectpicker.search.data = [];
if (searchValue) {
that.selectpicker.search.previousValue = searchValue;
if (that.options.source.search) {
that.fetchData(function (builtData) {
that.render();
that.buildList(undefined, true);
that.noScroll = true;
that.$menuInner.scrollTop(0);
that.createView(true);
showNoResults.call(that, builtData, searchValue);
}, 'search', 0, searchValue);
} else {
var i,
searchMatch = [],
q = searchValue.toUpperCase(),
cache = {},
cacheArr = [],
searchStyle = that._searchStyle(),
normalizeSearch = that.options.liveSearchNormalize;
if (normalizeSearch) q = normalizeToBase(q);
for (var i = 0; i < that.selectpicker.main.data.length; i++) {
var li = that.selectpicker.main.data[i];
if (!cache[i]) {
cache[i] = stringSearch(li, q, searchStyle, normalizeSearch);
}
if (cache[i] && li.headerIndex !== undefined && cacheArr.indexOf(li.headerIndex) === -1) {
if (li.headerIndex > 0) {
cache[li.headerIndex - 1] = true;
cacheArr.push(li.headerIndex - 1);
}
cache[li.headerIndex] = true;
cacheArr.push(li.headerIndex);
cache[li.lastIndex + 1] = true;
}
if (cache[i] && li.type !== 'optgroup-label') cacheArr.push(i);
}
for (var i = 0, cacheLen = cacheArr.length; i < cacheLen; i++) {
var index = cacheArr[i],
prevIndex = cacheArr[i - 1],
li = that.selectpicker.main.data[index],
liPrev = that.selectpicker.main.data[prevIndex];
if (li.type !== 'divider' || (li.type === 'divider' && liPrev && liPrev.type !== 'divider' && cacheLen - 1 !== i)) {
that.selectpicker.search.data.push(li);
searchMatch.push(that.selectpicker.main.elements[index]);
}
}
that.activeElement = undefined;
that.noScroll = true;
that.$menuInner.scrollTop(0);
that.selectpicker.search.elements = searchMatch;
that.createView(true);
showNoResults.call(that, searchMatch, searchValue);
}
} else if (that.selectpicker.search.previousValue) { // for IE11 (#2402)
that.$menuInner.scrollTop(0);
that.createView(false);
}
});
},
_searchStyle: function () {
return this.options.liveSearchStyle || 'contains';
},
val: function (value) {
var element = this.$element[0];
if (typeof value !== 'undefined') {
var selectedOptions = getSelectedOptions(element).length,
prevValue = getSelectValues(element, selectedOptions);
changedArguments = [null, null, prevValue];
if (!Array.isArray(value)) value = [ value ];
//value.map(String);
value = value.map(String);
for (var i = 0; i < selectedOptions.length; i++) {
var item = selectedOptions[i];
if (item && value.indexOf(String(item.value)) === -1) {
this.setSelected(item, false);
}
}
// only update selected value if it matches an existing option
this.selectpicker.main.data.filter(function (item) {
if (value.indexOf(String(item.value)) !== -1) {
this.setSelected(item, true);
return true;
}
return false;
}, this);
if (this.options.source.data) element.appendChild(this.selectpicker.main.optionQueue);
this.$element.trigger('changed' + EVENT_KEY, changedArguments);
if (this.$newElement.hasClass(classNames.SHOW)) {
if (this.multiple) {
this.setOptionStatus(true);
} else {
var liSelectedIndex = (element.options[element.selectedIndex] || {}).liIndex;
if (typeof liSelectedIndex === 'number') {
this.setSelected(this.selectpicker.current.data[liSelectedIndex], true);
}
}
}
//this.render();
this.refresh();
changedArguments = null;
return this.$element;
} else {
return this.$element.val();
}
},
changeAll: function (status) {
if (!this.multiple) return;
if (typeof status === 'undefined') status = true;
var element = this.$element[0],
previousSelected = 0,
currentSelected = 0,
prevValue = getSelectValues(element);
element.classList.add('bs-select-hidden');
for (var i = 0, data = this.selectpicker.current.data, len = data.length; i < len; i++) {
var liData = data[i],
option = liData.option;
if (option && !liData.disabled && liData.type !== 'divider') {
if (liData.selected) previousSelected++;
option.selected = status;
liData.selected = status;
if (status === true) currentSelected++;
}
}
element.classList.remove('bs-select-hidden');
if (previousSelected === currentSelected) return;
this.setOptionStatus();
changedArguments = [null, null, prevValue];
this.$element
.triggerNative('change');
},
selectAll: function () {
return this.changeAll(true);
},
deselectAll: function () {
return this.changeAll(false);
},
toggle: function (e, state) {
var isActive,
triggerClick = state === undefined;
e = e || window.event;
if (e) e.stopPropagation();
if (triggerClick === false) {
isActive = this.$newElement[0].classList.contains(classNames.SHOW);
triggerClick = state === true && isActive === false || state === false && isActive === true;
}
if (triggerClick) this.$button.trigger('click.bs.dropdown.data-api');
},
open: function (e) {
this.toggle(e, true);
},
close: function (e) {
this.toggle(e, false);
},
keydown: function (e) {
var $this = $(this),
isToggle = $this.hasClass('dropdown-toggle'),
$parent = isToggle ? $this.closest('.dropdown') : $this.closest(Selector.MENU),
that = $parent.data('this'),
$items = that.findLis(),
index,
isActive,
liActive,
activeLi,
offset,
updateScroll = false,
downOnTab = e.which === keyCodes.TAB && !isToggle && !that.options.selectOnTab,
isArrowKey = REGEXP_ARROW.test(e.which) || downOnTab,
scrollTop = that.$menuInner[0].scrollTop,
isVirtual = that.isVirtual(),
position0 = isVirtual === true ? that.selectpicker.view.position0 : 0;
// do nothing if a function key is pressed
if (e.which >= 112 && e.which <= 123) return;
isActive = that.$menu.hasClass(classNames.SHOW);
if (
!isActive &&
(
isArrowKey ||
(e.which >= 48 && e.which <= 57) ||
(e.which >= 96 && e.which <= 105) ||
(e.which >= 65 && e.which <= 90)
)
) {
that.$button.trigger('click.bs.dropdown.data-api');
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
return;
}
}
if (e.which === keyCodes.ESCAPE && isActive) {
e.preventDefault();
that.$button.trigger('click.bs.dropdown.data-api').trigger('focus');
}
if (isArrowKey) { // if up or down
if (!$items.length) return;
liActive = that.activeElement;
index = liActive ? Array.prototype.indexOf.call(liActive.parentElement.children, liActive) : -1;
if (index !== -1) {
that.defocusItem(liActive);
}
if (e.which === keyCodes.ARROW_UP) { // up
if (index !== -1) index--;
if (index + position0 < 0) index += $items.length;
if (!that.selectpicker.view.canHighlight[index + position0]) {
index = that.selectpicker.view.canHighlight.slice(0, index + position0).lastIndexOf(true) - position0;
if (index === -1) index = $items.length - 1;
}
} else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down
index++;
if (index + position0 >= that.selectpicker.view.canHighlight.length) index = that.selectpicker.view.firstHighlightIndex;
if (!that.selectpicker.view.canHighlight[index + position0]) {
index = index + 1 + that.selectpicker.view.canHighlight.slice(index + position0 + 1).indexOf(true);
}
}
e.preventDefault();
var liActiveIndex = position0 + index;
if (e.which === keyCodes.ARROW_UP) { // up
// scroll to bottom and highlight last option
if (position0 === 0 && index === $items.length - 1) {
that.$menuInner[0].scrollTop = that.$menuInner[0].scrollHeight;
liActiveIndex = that.selectpicker.current.elements.length - 1;
} else {
activeLi = that.selectpicker.current.data[liActiveIndex];
// could be undefined if no results exist
if (activeLi) {
offset = activeLi.position - activeLi.height;
updateScroll = offset < scrollTop;
}
}
} else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down
// scroll to top and highlight first option
if (index === that.selectpicker.view.firstHighlightIndex) {
that.$menuInner[0].scrollTop = 0;
liActiveIndex = that.selectpicker.view.firstHighlightIndex;
} else {
activeLi = that.selectpicker.current.data[liActiveIndex];
// could be undefined if no results exist
if (activeLi) {
offset = activeLi.position - that.sizeInfo.menuInnerHeight;
updateScroll = offset > scrollTop;
}
}
}
liActive = that.selectpicker.current.elements[liActiveIndex];
that.activeElement = (that.selectpicker.current.data[liActiveIndex] || {}).element;
that.focusItem(liActive);
that.selectpicker.view.currentActive = liActive;
if (updateScroll) that.$menuInner[0].scrollTop = offset;
if (that.options.liveSearch) {
that.$searchbox.trigger('focus');
} else {
$this.trigger('focus');
}
} else if (
(!$this.is('input') && !REGEXP_TAB_OR_ESCAPE.test(e.which)) ||
(e.which === keyCodes.SPACE && that.selectpicker.keydown.keyHistory)
) {
var searchMatch,
matches = [],
keyHistory;
e.preventDefault();
that.selectpicker.keydown.keyHistory += keyCodeMap[e.which];
if (that.selectpicker.keydown.resetKeyHistory.cancel) clearTimeout(that.selectpicker.keydown.resetKeyHistory.cancel);
that.selectpicker.keydown.resetKeyHistory.cancel = that.selectpicker.keydown.resetKeyHistory.start();
keyHistory = that.selectpicker.keydown.keyHistory;
// if all letters are the same, set keyHistory to just the first character when searching
if (/^(.)\1+$/.test(keyHistory)) {
keyHistory = keyHistory.charAt(0);
}
// find matches
for (var i = 0; i < that.selectpicker.current.data.length; i++) {
var li = that.selectpicker.current.data[i],
hasMatch;
hasMatch = stringSearch(li, keyHistory, 'startsWith', true);
if (hasMatch && that.selectpicker.view.canHighlight[i]) {
matches.push(li.element);
}
}
if (matches.length) {
var matchIndex = 0;
$items.removeClass('active').find('a').removeClass('active');
// either only one key has been pressed or they are all the same key
if (keyHistory.length === 1) {
matchIndex = matches.indexOf(that.activeElement);
if (matchIndex === -1 || matchIndex === matches.length - 1) {
matchIndex = 0;
} else {
matchIndex++;
}
}
searchMatch = matches[matchIndex];
activeLi = that.selectpicker.main.data[searchMatch];
if (scrollTop - activeLi.position > 0) {
offset = activeLi.position - activeLi.height;
updateScroll = true;
} else {
offset = activeLi.position - that.sizeInfo.menuInnerHeight;
// if the option is already visible at the current scroll position, just keep it the same
updateScroll = activeLi.position > scrollTop + that.sizeInfo.menuInnerHeight;
}
liActive = that.selectpicker.main.elements[searchMatch];
that.activeElement = liActive;
that.focusItem(liActive);
if (liActive) liActive.firstChild.focus();
if (updateScroll) that.$menuInner[0].scrollTop = offset;
$this.trigger('focus');
}
}
// Select focused option if "Enter", "Spacebar" or "Tab" (when selectOnTab is true) are pressed inside the menu.
if (
isActive &&
(
(e.which === keyCodes.SPACE && !that.selectpicker.keydown.keyHistory) ||
e.which === keyCodes.ENTER ||
(e.which === keyCodes.TAB && that.options.selectOnTab)
)
) {
if (e.which !== keyCodes.SPACE) e.preventDefault();
if (!that.options.liveSearch || e.which !== keyCodes.SPACE) {
that.$menuInner.find('.active a').trigger('click', true); // retain active class
$this.trigger('focus');
if (!that.options.liveSearch) {
// Prevent screen from scrolling if the user hits the spacebar
e.preventDefault();
// Fixes spacebar selection of dropdown items in FF & IE
$(document).data('spaceSelect', true);
}
}
if (e.which === keyCodes.ENTER) {
// hide dropdown menu
that.dropdown.hide();
}
}
},
mobile: function () {
// ensure mobile is set to true if mobile function is called after init
this.options.mobile = true;
this.$element[0].classList.add('mobile-device');
},
refresh: function () {
var that = this;
// update options if data attributes have been changed
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
this.options = config;
this.selectpicker.main.data = [];
if (this.options.source.data) {
this.render();
this.buildList();
} else {
this.fetchData(function () {
that.render();
that.buildList();
});
}
this.checkDisabled();
this.setStyle();
this.setWidth();
//this.setSize(true);
if(this.$element.hasClass('ajaxselect')) this.setSize(true);
this.$element.trigger('refreshed' + EVENT_KEY);
},
hide: function () {
this.$newElement.hide();
},
show: function () {
this.$newElement.show();
},
remove: function () {
this.$newElement.remove();
this.$element.remove();
},
destroy: function () {
this.$newElement.before(this.$element).remove();
if (this.$bsContainer) {
this.$bsContainer.remove();
} else {
this.$menu.remove();
}
if (this.selectpicker.view.titleOption && this.selectpicker.view.titleOption.parentNode) {
this.selectpicker.view.titleOption.parentNode.removeChild(this.selectpicker.view.titleOption);
}
this.$element
.off(EVENT_KEY)
.removeData('selectpicker')
.removeClass('bs-select-hidden selectpicker mobile-device');
$(window).off(EVENT_KEY + '.' + this.selectId);
}
};
// SELECTPICKER PLUGIN DEFINITION
// ==============================
function Plugin (option) {
// get the args of the outer function..
var args = arguments;
// The arguments of the function are explicitly re-defined from the argument list, because the shift causes them
// to get lost/corrupted in android 2.3 and IE9 #715 #775
var _option = option;
[].shift.apply(args);
// if the version was not set successfully
if (!version.success) {
// try to retreive it again
try {
version.full = (getVersion() || '').split(' ')[0].split('.');
} catch (err) {
// fall back to use BootstrapVersion if set
if (Selectpicker.BootstrapVersion) {
version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.');
} else {
version.full = [version.major, '0', '0'];
console.warn(
'There was an issue retrieving Bootstrap\'s version. ' +
'Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision. ' +
'If loading Bootstrap asynchronously, the version may need to be manually specified via $.fn.selectpicker.Constructor.BootstrapVersion.',
err
);
}
}
version.major = version.full[0];
version.success = true;
}
if (version.major >= '4') {
// some defaults need to be changed if using Bootstrap 4
// check to see if they have already been manually changed before forcing them to update
var toUpdate = [];
if (Selectpicker.DEFAULTS.style === classNames.BUTTONCLASS) toUpdate.push({ name: 'style', className: 'BUTTONCLASS' });
if (Selectpicker.DEFAULTS.iconBase === classNames.ICONBASE) toUpdate.push({ name: 'iconBase', className: 'ICONBASE' });
if (Selectpicker.DEFAULTS.tickIcon === classNames.TICKICON) toUpdate.push({ name: 'tickIcon', className: 'TICKICON' });
classNames.DIVIDER = 'dropdown-divider';
classNames.SHOW = 'show';
classNames.BUTTONCLASS = 'btn-light';
classNames.POPOVERHEADER = 'popover-header';
classNames.ICONBASE = '';
classNames.TICKICON = 'bs-ok-default';
for (var i = 0; i < toUpdate.length; i++) {
var option = toUpdate[i];
Selectpicker.DEFAULTS[option.name] = classNames[option.className];
}
}
if (version.major > '4') {
Selector.DATA_TOGGLE = 'data-bs-toggle="dropdown"';
}
var value;
var chain = this.each(function () {
var $this = $(this);
if ($this.is('select')) {
var data = $this.data('selectpicker'),
options = typeof _option == 'object' && _option;
// for backwards compatibility
// (using title as placeholder is deprecated - remove in v2.0.0)
if (options.title) options.placeholder = options.title;
if (!data) {
var dataAttributes = $this.data();
for (var dataAttr in dataAttributes) {
if (Object.prototype.hasOwnProperty.call(dataAttributes, dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
delete dataAttributes[dataAttr];
}
}
var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, getAttributesObject($this), dataAttributes, options); // this is correct order on initial render
config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), dataAttributes.template, options.template);
config.source = $.extend({}, Selectpicker.DEFAULTS.source, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.source : {}), options.source);
$this.data('selectpicker', (data = new Selectpicker(this, config)));
} else if (options) {
for (var i in options) {
if (Object.prototype.hasOwnProperty.call(options, i)) {
data.options[i] = options[i];
}
}
}
if (typeof _option == 'string') {
if (data[_option] instanceof Function) {
value = data[_option].apply(data, args);
} else {
value = data.options[_option];
}
}
}
});
if (typeof value !== 'undefined') {
// noinspection JSUnusedAssignment
return value;
} else {
return chain;
}
}
var old = $.fn.selectpicker;
$.fn.selectpicker = Plugin;
$.fn.selectpicker.Constructor = Selectpicker;
// SELECTPICKER NO CONFLICT
// ========================
$.fn.selectpicker.noConflict = function () {
$.fn.selectpicker = old;
return this;
};
// get Bootstrap's keydown event handler for either Bootstrap 4 or Bootstrap 3
function keydownHandler () {
if (version.major < 5) {
if ($.fn.dropdown) {
// wait to define until function is called in case Bootstrap isn't loaded yet
var bootstrapKeydown = $.fn.dropdown.Constructor._dataApiKeydownHandler || $.fn.dropdown.Constructor.prototype.keydown;
return bootstrapKeydown.apply(this, arguments);
}
} else {
return Dropdown.dataApiKeydownHandler;
}
}
$(document)
.off('keydown.bs.dropdown.data-api')
.on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > [' + Selector.DATA_TOGGLE + ']', keydownHandler)
.on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > .dropdown-menu', keydownHandler)
.on('keydown' + EVENT_KEY, '.bootstrap-select [' + Selector.DATA_TOGGLE + '], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', Selectpicker.prototype.keydown)
.on('focusin.modal', '.bootstrap-select [' + Selector.DATA_TOGGLE + '], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', function (e) {
e.stopPropagation();
});
// SELECTPICKER DATA-API
// =====================
document.addEventListener('DOMContentLoaded', function () {
$('.selectpicker').each(function () {
var $selectpicker = $(this);
Plugin.call($selectpicker, $selectpicker.data());
});
});
})(jQuery);
@bluebird75
Copy link

Hi @mattymatty76 . Thanks for this version, it works great.

Why create a fork where other people can propose improvements ?

Obviously, bootstrap-select is still relevant today

@mattymatty76
Copy link
Author

Hi @bluebird75,
and thanks to you: it's a pleasure to know that it is useful.
About fork, one thing is to make some changes that I needed and make them available to everyone, another thing is to take over an entire project like that...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment