-
-
Save mattymatty76/c996d3b77f298b2ec133be59992df9d4 to your computer and use it in GitHub Desktop.
/*! | |
* 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; | |
} | |
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); |
I'm glad it's working.
I updated that library, customizing it, because the original version didn't work: so I adapted it to my bootstrap 5.3 project and now I can use bootstrap-select without problems. Regarding the ajaxselect class, I inserted it in case ajax-bootstrap-select is used because it uses bootstrap-select: in this case is useful to force the refresh of the list which, if very long, was not drawn correctly: at least in my case.
About backwards-compatible I think it's not a problem because if you need to use bootstrap-select in old version (bootstrap 3 or bootstrap 4) you can use old bootstrap-select; otherwise in bootstrap 5 you don't have an alternative because the current official version doesn't work (bootstra-select release v1.14.0-beta3) and this is the reason why I have updated/alterated that library in a new custom version for my project, but without the ambition of replacing the original project or library: it was something I did for myself and I published it thinking it could be useful for others too.
Thank you @abeverley
Thanks for this @mattymatty76 - works great for me. Can I just ask what the reason is for checking
hasClass('ajaxselect')
? I found that it wasn't working for my ajax-select as I didn't have that class in my select element (I was initialising using JS). Once I added it in everything worked as expected, but this means it is not backwards-compatible and others may fall foul.
@abeverley if you have a form with some components including an ajax-bootstrap-select and a simple bootstrap-select, you can't discriminate between them if you don't put a class like ajaxselect at the component ajax-bootstrap-select: this because if you don't put a check and always apply this.setSize(true) to all selects, slows everything down tremendously especially if the simple bootstrap-selects are large: since simple bootstrap-select is loaded just one time when you open the form, you don't need to call setSize; whereas an ajax-bootstrap-select changes everytime you type a char, so you need to call setSize to force it to redraw correctly
@mattymatty76 I appreciate you. It is working for me.
Thanks to you it's a pleasure
Great Job, Thanks.
Thank you! It's a pleasure to hear that works and solves your problem
Hi,
@mattymatty76 , I tyr use this, but don´t work.
I'm use laravel and include by vite.
I don't know what's the problema.
Could you point me in the direction, perhaps?
Bootstrap 5.3; Laravel 10 wiht livewire 3.4.
Tks.
Hi @lindomar-pjs,
I'm very sorry but I'm not an expert in laravel: have you tried to find some guides like this:
https://wpdean.com/add-bootstrap-to-laravel
maybe it could be a solution, keep in mind that the library works in a context without frameworks, when you use it directly, but (I think) in your case you need to adapt it with the laravel ruls, but I repeat I'm not an expert and so I could say inaccuracies
can anyone point me to direction on how to use this with ajax source? using the same options i used on bootstrap-select.js shows error
`jQuery.Deferred exception: selectedOptions.map is not a function TypeError: selectedOptions.map is not a function
at Selectpicker.render (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:1990:33)
at Selectpicker.<anonymous> (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:1095:22)
at http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:1676:38
at Selectpicker.data (http://127.0.0.1:3000/roles:352:21)
at Selectpicker.fetchData (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:1669:26)
at Selectpicker.init (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:1094:18)
at new Selectpicker (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:985:14)
at HTMLSelectElement.<anonymous> (http://127.0.0.1:3000/js/bootstrap-select-v1.14.0-gamma1.js:3621:56)
at Function.each (http://127.0.0.1:3000/js/jquery-3.7.1.min.js:2:3129)
at ce.fn.init.each (http://127.0.0.1:3000/js/jquery-3.7.1.min.js:2:1594) undefined`
`jquery-3.7.1.min.js:2 Uncaught TypeError: selectedOptions.map is not a function
at Selectpicker.render (bootstrap-select-v1.14.0-gamma1.js:1990:33)
at Selectpicker.<anonymous> (bootstrap-select-v1.14.0-gamma1.js:1095:22)
at bootstrap-select-v1.14.0-gamma1.js:1676:38
at Selectpicker.data (roles:352:21)
at Selectpicker.fetchData (bootstrap-select-v1.14.0-gamma1.js:1669:26)
at Selectpicker.init (bootstrap-select-v1.14.0-gamma1.js:1094:18)
at new Selectpicker (bootstrap-select-v1.14.0-gamma1.js:985:14)
at HTMLSelectElement.<anonymous> (bootstrap-select-v1.14.0-gamma1.js:3621:56)
at Function.each (jquery-3.7.1.min.js:2:3129)
at ce.fn.init.each (jquery-3.7.1.min.js:2:1594)`
my init code is
`$('.selectpickerWithAjax').selectpicker({
source: {
data: function (callback) {
var array = [];
callback(array);
},
load: function (callback, searchTerm) {
$.ajax(this.$element.data('abs-ajax-url'), {
data: {q : searchTerm} })
.then((response) => callback(response))
},
search: function (callback, page, searchTerm) {
$.ajax(this.$element.data('abs-ajax-url'), { data: {q : searchTerm} })
.then((response) => callback(response))
}
}
})`
Hey @mattymatty76, thanks for doing this!
Do you remember why you changed chunkSize from 40 to Number.MAX_VALUE on line 1009?
Thanks for this @mattymatty76 - works great for me. Can I just ask what the reason is for checking
hasClass('ajaxselect')
? I found that it wasn't working for my ajax-select as I didn't have that class in my select element (I was initialising using JS). Once I added it in everything worked as expected, but this means it is not backwards-compatible and others may fall foul.