Created
August 29, 2023 06:15
-
-
Save mattymatty76/c996d3b77f298b2ec133be59992df9d4 to your computer and use it in GitHub Desktop.
bootstrap-select for bootstrap 5.3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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 = { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
"'": ''', | |
'`': '`' | |
}; | |
// 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">×</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>×</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"> </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; |