Created
August 29, 2023 06:15
-
-
Save mattymatty76/c996d3b77f298b2ec133be59992df9d4 to your computer and use it in GitHub Desktop.
bootstrap-select for bootstrap 5.3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* Bootstrap-select v1.14.0-gamma1 (https://developer.snapappointments.com/bootstrap-select) | |
* | |
* Copyright 2012-2023 SnapAppointments, LLC | |
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) | |
*/ | |
(function ($) { | |
'use strict'; | |
var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']; | |
var uriAttrs = [ | |
'background', | |
'cite', | |
'href', | |
'itemtype', | |
'longdesc', | |
'poster', | |
'src', | |
'xlink:href' | |
]; | |
var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; | |
var DefaultWhitelist = { | |
// Global attributes allowed on any supplied element below. | |
'*': ['class', 'dir', 'id', 'lang', 'role', 'tabindex', 'style', ARIA_ATTRIBUTE_PATTERN], | |
a: ['target', 'href', 'title', 'rel'], | |
area: [], | |
b: [], | |
br: [], | |
col: [], | |
code: [], | |
div: [], | |
em: [], | |
hr: [], | |
h1: [], | |
h2: [], | |
h3: [], | |
h4: [], | |
h5: [], | |
h6: [], | |
i: [], | |
img: ['src', 'alt', 'title', 'width', 'height'], | |
li: [], | |
ol: [], | |
p: [], | |
pre: [], | |
s: [], | |
small: [], | |
span: [], | |
sub: [], | |
sup: [], | |
strong: [], | |
u: [], | |
ul: [] | |
}; | |
/** | |
* A pattern that recognizes a commonly useful subset of URLs that are safe. | |
* | |
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts | |
*/ | |
var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; | |
/** | |
* A pattern that matches safe data URLs. Only matches image, video and audio types. | |
* | |
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts | |
*/ | |
var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; | |
var ParseableAttributes = ['title', 'placeholder']; // attributes to use as settings, can add others in the future | |
function allowedAttribute (attr, allowedAttributeList) { | |
var attrName = attr.nodeName.toLowerCase(); | |
if ($.inArray(attrName, allowedAttributeList) !== -1) { | |
if ($.inArray(attrName, uriAttrs) !== -1) { | |
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)); | |
} | |
return true; | |
} | |
var regExp = $(allowedAttributeList).filter(function (index, value) { | |
return value instanceof RegExp; | |
}); | |
// Check if a regular expression validates the attribute. | |
for (var i = 0, l = regExp.length; i < l; i++) { | |
if (attrName.match(regExp[i])) { | |
return true; | |
} | |
} | |
return false; | |
} | |
function sanitizeHtml (unsafeElements, whiteList, sanitizeFn) { | |
if (sanitizeFn && typeof sanitizeFn === 'function') { | |
return sanitizeFn(unsafeElements); | |
} | |
var whitelistKeys = Object.keys(whiteList); | |
for (var i = 0, len = unsafeElements.length; i < len; i++) { | |
var elements = unsafeElements[i].querySelectorAll('*'); | |
for (var j = 0, len2 = elements.length; j < len2; j++) { | |
var el = elements[j]; | |
var elName = el.nodeName.toLowerCase(); | |
if (whitelistKeys.indexOf(elName) === -1) { | |
el.parentNode.removeChild(el); | |
continue; | |
} | |
var attributeList = [].slice.call(el.attributes); | |
var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []); | |
for (var k = 0, len3 = attributeList.length; k < len3; k++) { | |
var attr = attributeList[k]; | |
if (!allowedAttribute(attr, whitelistedAttributes)) { | |
el.removeAttribute(attr.nodeName); | |
} | |
} | |
} | |
} | |
} | |
function getAttributesObject ($select) { | |
var attributesObject = {}, | |
attrVal; | |
ParseableAttributes.forEach(function (item) { | |
attrVal = $select.attr(item); | |
if (attrVal) attributesObject[item] = attrVal; | |
}); | |
// for backwards compatibility | |
// (using title as placeholder is deprecated - remove in v2.0.0) | |
if (!attributesObject.placeholder && attributesObject.title) { | |
attributesObject.placeholder = attributesObject.title; | |
} | |
return attributesObject; | |
} | |
// Polyfill for browsers with no classList support | |
// Remove in v2 | |
if (!('classList' in document.createElement('_'))) { | |
(function (view) { | |
if (!('Element' in view)) return; | |
var classListProp = 'classList', | |
protoProp = 'prototype', | |
elemCtrProto = view.Element[protoProp], | |
objCtr = Object, | |
classListGetter = function () { | |
var $elem = $(this); | |
return { | |
add: function (classes) { | |
classes = Array.prototype.slice.call(arguments).join(' '); | |
return $elem.addClass(classes); | |
}, | |
remove: function (classes) { | |
classes = Array.prototype.slice.call(arguments).join(' '); | |
return $elem.removeClass(classes); | |
}, | |
toggle: function (classes, force) { | |
return $elem.toggleClass(classes, force); | |
}, | |
contains: function (classes) { | |
return $elem.hasClass(classes); | |
} | |
}; | |
}; | |
if (objCtr.defineProperty) { | |
var classListPropDesc = { | |
get: classListGetter, | |
enumerable: true, | |
configurable: true | |
}; | |
try { | |
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); | |
} catch (ex) { // IE 8 doesn't support enumerable:true | |
// adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 | |
// modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected | |
if (ex.number === undefined || ex.number === -0x7FF5EC54) { | |
classListPropDesc.enumerable = false; | |
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); | |
} | |
} | |
} else if (objCtr[protoProp].__defineGetter__) { | |
elemCtrProto.__defineGetter__(classListProp, classListGetter); | |
} | |
}(window)); | |
} | |
var testElement = document.createElement('_'); | |
testElement.classList.add('c1', 'c2'); | |
if (!testElement.classList.contains('c2')) { | |
var _add = DOMTokenList.prototype.add, | |
_remove = DOMTokenList.prototype.remove; | |
DOMTokenList.prototype.add = function () { | |
Array.prototype.forEach.call(arguments, _add.bind(this)); | |
}; | |
DOMTokenList.prototype.remove = function () { | |
Array.prototype.forEach.call(arguments, _remove.bind(this)); | |
}; | |
} | |
testElement.classList.toggle('c3', false); | |
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not | |
// support the second argument. | |
if (testElement.classList.contains('c3')) { | |
var _toggle = DOMTokenList.prototype.toggle; | |
DOMTokenList.prototype.toggle = function (token, force) { | |
if (1 in arguments && !this.contains(token) === !force) { | |
return force; | |
} else { | |
return _toggle.call(this, token); | |
} | |
}; | |
} | |
testElement = null; | |
// Polyfill for IE (remove in v2) | |
Object.values = typeof Object.values === 'function' ? Object.values : function (obj) { | |
return Object.keys(obj).map(function (key) { | |
return obj[key]; | |
}); | |
}; | |
// shallow array comparison | |
function isEqual (array1, array2) { | |
return array1.length === array2.length && array1.every(function (element, index) { | |
return element === array2[index]; | |
}); | |
}; | |
// <editor-fold desc="Shims"> | |
if (!String.prototype.startsWith) { | |
(function () { | |
'use strict'; // needed to support `apply`/`call` with `undefined`/`null` | |
var toString = {}.toString; | |
var startsWith = function (search) { | |
if (this == null) { | |
throw new TypeError(); | |
} | |
var string = String(this); | |
if (search && toString.call(search) == '[object RegExp]') { | |
throw new TypeError(); | |
} | |
var stringLength = string.length; | |
var searchString = String(search); | |
var searchLength = searchString.length; | |
var position = arguments.length > 1 ? arguments[1] : undefined; | |
// `ToInteger` | |
var pos = position ? Number(position) : 0; | |
if (pos != pos) { // better `isNaN` | |
pos = 0; | |
} | |
var start = Math.min(Math.max(pos, 0), stringLength); | |
// Avoid the `indexOf` call if no match is possible | |
if (searchLength + start > stringLength) { | |
return false; | |
} | |
var index = -1; | |
while (++index < searchLength) { | |
if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) { | |
return false; | |
} | |
} | |
return true; | |
}; | |
if (Object.defineProperty) { | |
Object.defineProperty(String.prototype, 'startsWith', { | |
'value': startsWith, | |
'configurable': true, | |
'writable': true | |
}); | |
} else { | |
String.prototype.startsWith = startsWith; | |
} | |
}()); | |
} | |
function toKebabCase (str) { | |
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, function ($, ofs) { | |
return (ofs ? '-' : '') + $.toLowerCase(); | |
}); | |
} | |
/*function getSelectedOptions () { | |
var options = this.selectpicker.main.data; | |
if (this.options.source.data || this.options.source.search) { | |
options = Object.values(this.selectpicker.optionValuesDataMap); | |
} | |
var selectedOptions = options.filter(function (item) { | |
if (item.selected) { | |
if (this.options.hideDisabled && item.disabled) return false; | |
return true; | |
} | |
return false; | |
}, this); | |
// ensure only 1 option is selected if multiple are set in the data source | |
if (this.options.source.data && !this.multiple && selectedOptions.length > 1) { | |
for (var i = 0; i < selectedOptions.length - 1; i++) { | |
selectedOptions[i].selected = false; | |
} | |
selectedOptions = [ selectedOptions[selectedOptions.length - 1] ]; | |
} | |
return selectedOptions; | |
}*/ | |
function getSelectedOptions (select, ignoreDisabled) { | |
var selectedOptions = select.selectedOptions, | |
options = [], | |
opt; | |
if (ignoreDisabled) { | |
for (var i = 0, len = selectedOptions.length; i < len; i++) { | |
opt = selectedOptions[i]; | |
if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) { | |
options.push(opt); | |
} | |
} | |
return options; | |
} | |
return selectedOptions; | |
} | |
// much faster than $.val() | |
/*function getSelectValues (selectedOptions) { | |
var value = [], | |
options = selectedOptions || getSelectedOptions.call(this), | |
opt; | |
for (var i = 0, len = options.length; i < len; i++) { | |
opt = options[i]; | |
if (!opt.disabled) { | |
value.push(opt.value === undefined ? opt.text : opt.value); | |
} | |
} | |
if (!this.multiple) { | |
return !value.length ? null : value[0]; | |
} | |
return value; | |
}*/ | |
function getSelectValues (select, selectedOptions) { | |
var value = [], | |
options = selectedOptions || select.selectedOptions, | |
opt; | |
for (var i = 0, len = options.length; i < len; i++) { | |
opt = options[i]; | |
if (!(opt.disabled || opt.parentNode.tagName === 'OPTGROUP' && opt.parentNode.disabled)) { | |
value.push(opt.value); | |
} | |
} | |
if (!select.multiple) { | |
return !value.length ? null : value[0]; | |
} | |
return value; | |
} | |
// set data-selected on select element if the value has been programmatically selected | |
// prior to initialization of bootstrap-select | |
// * consider removing or replacing an alternative method * | |
var valHooks = { | |
useDefault: false, | |
_set: $.valHooks.select.set | |
}; | |
$.valHooks.select.set = function (elem, value) { | |
if (value && !valHooks.useDefault) $(elem).data('selected', true); | |
return valHooks._set.apply(this, arguments); | |
}; | |
var changedArguments = null; | |
var EventIsSupported = (function () { | |
try { | |
new Event('change'); | |
return true; | |
} catch (e) { | |
return false; | |
} | |
})(); | |
$.fn.triggerNative = function (eventName) { | |
var el = this[0], | |
event; | |
if (el.dispatchEvent) { // for modern browsers & IE9+ | |
if (EventIsSupported) { | |
// For modern browsers | |
event = new Event(eventName, { | |
bubbles: true | |
}); | |
} else { | |
// For IE since it doesn't support Event constructor | |
event = document.createEvent('Event'); | |
event.initEvent(eventName, true, false); | |
} | |
el.dispatchEvent(event); | |
} | |
}; | |
// </editor-fold> | |
function stringSearch (li, searchString, method, normalize) { | |
var stringTypes = [ | |
'display', | |
'subtext', | |
'tokens' | |
], | |
searchSuccess = false; | |
for (var i = 0; i < stringTypes.length; i++) { | |
var stringType = stringTypes[i], | |
string = li[stringType]; | |
if (string) { | |
string = string.toString(); | |
// Strip HTML tags. This isn't perfect, but it's much faster than any other method | |
if (stringType === 'display') { | |
string = string.replace(/<[^>]+>/g, ''); | |
} | |
if (normalize) string = normalizeToBase(string); | |
string = string.toUpperCase(); | |
if (typeof method === 'function') { | |
searchSuccess = method(string, searchString); | |
} else if (method === 'contains') { | |
searchSuccess = string.indexOf(searchString) >= 0; | |
} else if (method === 'containsAll') { | |
var searchArray = searchString.split(' '); | |
var notAllMatched = false; | |
searchSuccess = false; | |
for (var searchSubString in searchArray) { | |
searchSuccess = string.indexOf(searchArray[searchSubString]) >= 0; | |
if (!searchSuccess) notAllMatched = true; | |
} | |
if (notAllMatched) searchSuccess = false; | |
} else { | |
searchSuccess = string.startsWith(searchString); | |
} | |
if (searchSuccess) break; | |
} | |
} | |
return searchSuccess; | |
} | |
function toInteger (value) { | |
return parseInt(value, 10) || 0; | |
} | |
// Borrowed from Lodash (_.deburr) | |
/** Used to map Latin Unicode letters to basic Latin letters. */ | |
var deburredLetters = { | |
// Latin-1 Supplement block. | |
'\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', | |
'\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', | |
'\xc7': 'C', '\xe7': 'c', | |
'\xd0': 'D', '\xf0': 'd', | |
'\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', | |
'\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', | |
'\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', | |
'\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', | |
'\xd1': 'N', '\xf1': 'n', | |
'\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', | |
'\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', | |
'\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', | |
'\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', | |
'\xdd': 'Y', '\xfd': 'y', '\xff': 'y', | |
'\xc6': 'Ae', '\xe6': 'ae', | |
'\xde': 'Th', '\xfe': 'th', | |
'\xdf': 'ss', | |
// Latin Extended-A block. | |
'\u0100': 'A', '\u0102': 'A', '\u0104': 'A', | |
'\u0101': 'a', '\u0103': 'a', '\u0105': 'a', | |
'\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', | |
'\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', | |
'\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', | |
'\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', | |
'\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', | |
'\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', | |
'\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', | |
'\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', | |
'\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', | |
'\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', | |
'\u0134': 'J', '\u0135': 'j', | |
'\u0136': 'K', '\u0137': 'k', '\u0138': 'k', | |
'\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', | |
'\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', | |
'\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', | |
'\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', | |
'\u014c': 'O', '\u014e': 'O', '\u0150': 'O', | |
'\u014d': 'o', '\u014f': 'o', '\u0151': 'o', | |
'\u0154': 'R', '\u0156': 'R', '\u0158': 'R', | |
'\u0155': 'r', '\u0157': 'r', '\u0159': 'r', | |
'\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', | |
'\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', | |
'\u0162': 'T', '\u0164': 'T', '\u0166': 'T', | |
'\u0163': 't', '\u0165': 't', '\u0167': 't', | |
'\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', | |
'\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', | |
'\u0174': 'W', '\u0175': 'w', | |
'\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', | |
'\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', | |
'\u017a': 'z', '\u017c': 'z', '\u017e': 'z', | |
'\u0132': 'IJ', '\u0133': 'ij', | |
'\u0152': 'Oe', '\u0153': 'oe', | |
'\u0149': "'n", '\u017f': 's' | |
}; | |
/** Used to match Latin Unicode letters (excluding mathematical operators). */ | |
var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; | |
/** Used to compose unicode character classes. */ | |
var rsComboMarksRange = '\\u0300-\\u036f', | |
reComboHalfMarksRange = '\\ufe20-\\ufe2f', | |
rsComboSymbolsRange = '\\u20d0-\\u20ff', | |
rsComboMarksExtendedRange = '\\u1ab0-\\u1aff', | |
rsComboMarksSupplementRange = '\\u1dc0-\\u1dff', | |
rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange; | |
/** Used to compose unicode capture groups. */ | |
var rsCombo = '[' + rsComboRange + ']'; | |
/** | |
* Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and | |
* [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). | |
*/ | |
var reComboMark = RegExp(rsCombo, 'g'); | |
function deburrLetter (key) { | |
return deburredLetters[key]; | |
}; | |
function normalizeToBase (string) { | |
string = string.toString(); | |
return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); | |
} | |
// List of HTML entities for escaping. | |
var escapeMap = { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
"'": ''', | |
'`': '`' | |
}; | |
// Functions for escaping and unescaping strings to/from HTML interpolation. | |
var createEscaper = function (map) { | |
var escaper = function (match) { | |
return map[match]; | |
}; | |
// Regexes for identifying a key that needs to be escaped. | |
var source = '(?:' + Object.keys(map).join('|') + ')'; | |
var testRegexp = RegExp(source); | |
var replaceRegexp = RegExp(source, 'g'); | |
return function (string) { | |
string = string == null ? '' : '' + string; | |
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; | |
}; | |
}; | |
var htmlEscape = createEscaper(escapeMap); | |
/** | |
* ------------------------------------------------------------------------ | |
* Constants | |
* ------------------------------------------------------------------------ | |
*/ | |
var keyCodeMap = { | |
32: ' ', | |
48: '0', | |
49: '1', | |
50: '2', | |
51: '3', | |
52: '4', | |
53: '5', | |
54: '6', | |
55: '7', | |
56: '8', | |
57: '9', | |
59: ';', | |
65: 'A', | |
66: 'B', | |
67: 'C', | |
68: 'D', | |
69: 'E', | |
70: 'F', | |
71: 'G', | |
72: 'H', | |
73: 'I', | |
74: 'J', | |
75: 'K', | |
76: 'L', | |
77: 'M', | |
78: 'N', | |
79: 'O', | |
80: 'P', | |
81: 'Q', | |
82: 'R', | |
83: 'S', | |
84: 'T', | |
85: 'U', | |
86: 'V', | |
87: 'W', | |
88: 'X', | |
89: 'Y', | |
90: 'Z', | |
96: '0', | |
97: '1', | |
98: '2', | |
99: '3', | |
100: '4', | |
101: '5', | |
102: '6', | |
103: '7', | |
104: '8', | |
105: '9' | |
}; | |
var keyCodes = { | |
ESCAPE: 27, // KeyboardEvent.which value for Escape (Esc) key | |
ENTER: 13, // KeyboardEvent.which value for Enter key | |
SPACE: 32, // KeyboardEvent.which value for space key | |
TAB: 9, // KeyboardEvent.which value for tab key | |
ARROW_UP: 38, // KeyboardEvent.which value for up arrow key | |
ARROW_DOWN: 40 // KeyboardEvent.which value for down arrow key | |
}; | |
var Dropdown = window.Dropdown || window.bootstrap && window.bootstrap.Dropdown; | |
function getVersion () { | |
var version; | |
try { | |
version = $.fn.dropdown.Constructor.VERSION; | |
} catch (err) { | |
version = Dropdown.VERSION; | |
} | |
return version; | |
} | |
var version = { | |
success: false, | |
major: '3' | |
}; | |
try { | |
version.full = (getVersion() || '').split(' ')[0].split('.'); | |
version.major = version.full[0]; | |
version.success = true; | |
} catch (err) { | |
// do nothing | |
} | |
var selectId = 0; | |
var EVENT_KEY = '.bs.select'; | |
var classNames = { | |
DISABLED: 'disabled', | |
DIVIDER: 'divider', | |
SHOW: 'open', | |
DROPUP: 'dropup', | |
MENU: 'dropdown-menu', | |
MENURIGHT: 'dropdown-menu-right', | |
MENULEFT: 'dropdown-menu-left', | |
// to-do: replace with more advanced template/customization options | |
BUTTONCLASS: 'btn-default', | |
POPOVERHEADER: 'popover-title', | |
ICONBASE: 'glyphicon', | |
TICKICON: 'glyphicon-ok' | |
}; | |
var Selector = { | |
MENU: '.' + classNames.MENU, | |
DATA_TOGGLE: 'data-toggle="dropdown"' | |
}; | |
var elementTemplates = { | |
div: document.createElement('div'), | |
span: document.createElement('span'), | |
i: document.createElement('i'), | |
subtext: document.createElement('small'), | |
a: document.createElement('a'), | |
li: document.createElement('li'), | |
whitespace: document.createTextNode('\u00A0'), | |
fragment: document.createDocumentFragment(), | |
option: document.createElement('option') | |
}; | |
elementTemplates.selectedOption = elementTemplates.option.cloneNode(false); | |
elementTemplates.selectedOption.setAttribute('selected', true); | |
elementTemplates.noResults = elementTemplates.li.cloneNode(false); | |
elementTemplates.noResults.className = 'no-results'; | |
elementTemplates.a.setAttribute('role', 'option'); | |
elementTemplates.a.className = 'dropdown-item'; | |
elementTemplates.subtext.className = 'text-muted'; | |
elementTemplates.text = elementTemplates.span.cloneNode(false); | |
elementTemplates.text.className = 'text'; | |
elementTemplates.checkMark = elementTemplates.span.cloneNode(false); | |
var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); | |
var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); | |
var generateOption = { | |
li: function (content, classes, optgroup) { | |
var li = elementTemplates.li.cloneNode(false); | |
if (content) { | |
if (content.nodeType === 1 || content.nodeType === 11) { | |
li.appendChild(content); | |
} else { | |
li.innerHTML = content; | |
} | |
} | |
if (typeof classes !== 'undefined' && classes !== '') li.className = classes; | |
if (typeof optgroup !== 'undefined' && optgroup !== null) li.classList.add('optgroup-' + optgroup); | |
return li; | |
}, | |
a: function (text, classes, inline) { | |
var a = elementTemplates.a.cloneNode(true); | |
if (text) { | |
if (text.nodeType === 11) { | |
a.appendChild(text); | |
} else { | |
a.insertAdjacentHTML('beforeend', text); | |
} | |
} | |
if (typeof classes !== 'undefined' && classes !== '') a.classList.add.apply(a.classList, classes.split(/\s+/)); | |
if (inline) a.setAttribute('style', inline); | |
return a; | |
}, | |
text: function (options, useFragment) { | |
var textElement = elementTemplates.text.cloneNode(false), | |
subtextElement, | |
iconElement; | |
if (options.content) { | |
textElement.innerHTML = options.content; | |
} else { | |
textElement.textContent = options.text; | |
if (options.icon) { | |
var whitespace = elementTemplates.whitespace.cloneNode(false); | |
// need to use <i> for icons in the button to prevent a breaking change | |
// note: switch to span in next major release | |
iconElement = (useFragment === true ? elementTemplates.i : elementTemplates.span).cloneNode(false); | |
iconElement.className = this.options.iconBase + ' ' + options.icon; | |
elementTemplates.fragment.appendChild(iconElement); | |
elementTemplates.fragment.appendChild(whitespace); | |
} | |
if (options.subtext) { | |
subtextElement = elementTemplates.subtext.cloneNode(false); | |
subtextElement.textContent = options.subtext; | |
textElement.appendChild(subtextElement); | |
} | |
} | |
if (useFragment === true) { | |
while (textElement.childNodes.length > 0) { | |
elementTemplates.fragment.appendChild(textElement.childNodes[0]); | |
} | |
} else { | |
elementTemplates.fragment.appendChild(textElement); | |
} | |
return elementTemplates.fragment; | |
}, | |
label: function (options) { | |
var textElement = elementTemplates.text.cloneNode(false), | |
subtextElement, | |
iconElement; | |
textElement.innerHTML = options.display; | |
if (options.icon) { | |
var whitespace = elementTemplates.whitespace.cloneNode(false); | |
iconElement = elementTemplates.span.cloneNode(false); | |
iconElement.className = this.options.iconBase + ' ' + options.icon; | |
elementTemplates.fragment.appendChild(iconElement); | |
elementTemplates.fragment.appendChild(whitespace); | |
} | |
if (options.subtext) { | |
subtextElement = elementTemplates.subtext.cloneNode(false); | |
subtextElement.textContent = options.subtext; | |
textElement.appendChild(subtextElement); | |
} | |
elementTemplates.fragment.appendChild(textElement); | |
return elementTemplates.fragment; | |
} | |
}; | |
var getOptionData = { | |
fromOption: function (option, type) { | |
var value; | |
switch (type) { | |
case 'divider': | |
value = option.getAttribute('data-divider') === 'true'; | |
break; | |
case 'text': | |
value = option.textContent; | |
break; | |
case 'label': | |
value = option.label; | |
break; | |
case 'style': | |
value = option.style.cssText; | |
break; | |
case 'title': | |
value = option.title; | |
break; | |
default: | |
value = option.getAttribute('data-' + toKebabCase(type)); | |
break; | |
} | |
return value; | |
}, | |
fromDataSource: function (option, type) { | |
var value; | |
switch (type) { | |
case 'text': | |
case 'label': | |
value = option.text || option.value || ''; | |
break; | |
default: | |
value = option[type]; | |
break; | |
} | |
return value; | |
} | |
}; | |
function showNoResults (searchMatch, searchValue) { | |
if (!searchMatch.length) { | |
elementTemplates.noResults.innerHTML = this.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); | |
this.$menuInner[0].firstChild.appendChild(elementTemplates.noResults); | |
} | |
} | |
function filterHidden (item) { | |
return !(item.hidden || this.options.hideDisabled && item.disabled); | |
} | |
var Selectpicker = function (element, options) { | |
var that = this; | |
// bootstrap-select has been initialized - revert valHooks.select.set back to its original function | |
if (!valHooks.useDefault) { | |
$.valHooks.select.set = valHooks._set; | |
valHooks.useDefault = true; | |
} | |
this.$element = $(element); | |
this.$newElement = null; | |
this.$button = null; | |
this.$menu = null; | |
this.options = options; | |
this.selectpicker = { | |
main: { | |
data: [], | |
optionQueue: elementTemplates.fragment.cloneNode(false), | |
hasMore: false | |
}, | |
search: { | |
data: [], | |
hasMore: false | |
}, | |
current: {}, // current is either equal to main or search depending on if a search is in progress | |
view: {}, | |
// map of option values and their respective data (only used in conjunction with options.source) | |
optionValuesDataMap: {}, | |
isSearching: false, | |
keydown: { | |
keyHistory: '', | |
resetKeyHistory: { | |
start: function () { | |
return setTimeout(function () { | |
that.selectpicker.keydown.keyHistory = ''; | |
}, 800); | |
} | |
} | |
} | |
}; | |
this.sizeInfo = {}; | |
// Format window padding | |
var winPad = this.options.windowPadding; | |
if (typeof winPad === 'number') { | |
this.options.windowPadding = [winPad, winPad, winPad, winPad]; | |
} | |
// Expose public methods | |
this.val = Selectpicker.prototype.val; | |
this.render = Selectpicker.prototype.render; | |
this.refresh = Selectpicker.prototype.refresh; | |
this.setStyle = Selectpicker.prototype.setStyle; | |
this.selectAll = Selectpicker.prototype.selectAll; | |
this.deselectAll = Selectpicker.prototype.deselectAll; | |
this.destroy = Selectpicker.prototype.destroy; | |
this.remove = Selectpicker.prototype.remove; | |
this.show = Selectpicker.prototype.show; | |
this.hide = Selectpicker.prototype.hide; | |
this.init(); | |
}; | |
Selectpicker.VERSION = '1.14.0-beta3'; | |
// part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both. | |
Selectpicker.DEFAULTS = { | |
noneSelectedText: 'Nothing selected', | |
noneResultsText: 'No results matched {0}', | |
countSelectedText: function (numSelected, numTotal) { | |
return (numSelected == 1) ? '{0} item selected' : '{0} items selected'; | |
}, | |
maxOptionsText: function (numAll, numGroup) { | |
return [ | |
(numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)', | |
(numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)' | |
]; | |
}, | |
selectAllText: 'Select All', | |
deselectAllText: 'Deselect All', | |
source: { | |
pageSize: 40 | |
}, | |
//chunkSize: 40, | |
chunkSize: Number.MAX_VALUE, | |
doneButton: false, | |
doneButtonText: 'Close', | |
multipleSeparator: ' | ', | |
styleBase: 'btn', | |
style: classNames.BUTTONCLASS, | |
size: 'auto', | |
title: null, | |
placeholder: null, | |
titleTip: null, | |
allowClear: false, | |
selectedTextFormat: 'values', | |
width: false, | |
container: false, | |
hideDisabled: false, | |
showSubtext: false, | |
showIcon: true, | |
showContent: true, | |
dropupAuto: true, | |
header: false, | |
liveSearch: false, | |
liveSearchPlaceholder: null, | |
liveSearchNormalize: false, | |
liveSearchStyle: 'contains', | |
actionsBox: false, | |
iconBase: classNames.ICONBASE, | |
tickIcon: classNames.TICKICON, | |
showTick: false, | |
template: { | |
caret: '<span class="caret"></span>' | |
}, | |
maxOptions: false, | |
mobile: false, | |
selectOnTab: true, | |
dropdownAlignRight: false, | |
windowPadding: 0, | |
virtualScroll: 600, | |
display: false, | |
sanitize: true, | |
sanitizeFn: null, | |
whiteList: DefaultWhitelist | |
}; | |
Selectpicker.prototype = { | |
constructor: Selectpicker, | |
init: function () { | |
var that = this, | |
id = this.$element.attr('id'), | |
element = this.$element[0], | |
form = element.form; | |
selectId++; | |
this.selectId = 'bs-select-' + selectId; | |
element.classList.add('bs-select-hidden'); | |
this.multiple = this.$element.prop('multiple'); | |
this.autofocus = this.$element.prop('autofocus'); | |
if (element.classList.contains('show-tick')) { | |
this.options.showTick = true; | |
} | |
this.$newElement = this.createDropdown(); | |
this.$element | |
.after(this.$newElement) | |
.prependTo(this.$newElement); | |
// ensure select is associated with form element if it got unlinked after moving it inside newElement | |
if (form && element.form === null) { | |
if (!form.id) form.id = 'form-' + this.selectId; | |
element.setAttribute('form', form.id); | |
} | |
this.$button = this.$newElement.children('button'); | |
if (this.options.allowClear) this.$clearButton = this.$button.children('.bs-select-clear-selected'); | |
this.$menu = this.$newElement.children(Selector.MENU); | |
this.$menuInner = this.$menu.children('.inner'); | |
this.$searchbox = this.$menu.find('input'); | |
element.classList.remove('bs-select-hidden'); | |
this.fetchData(function () { | |
that.render(true); | |
that.buildList(); | |
requestAnimationFrame(function () { | |
that.$element.trigger('loaded' + EVENT_KEY); | |
}); | |
}); | |
if (this.options.dropdownAlignRight === true) this.$menu[0].classList.add(classNames.MENURIGHT); | |
if (typeof id !== 'undefined') { | |
this.$button.attr('data-id', id); | |
} | |
this.checkDisabled(); | |
this.clickListener(); | |
if (version.major > 4) this.dropdown = new Dropdown(this.$button[0]); | |
if (this.options.liveSearch) { | |
this.liveSearchListener(); | |
this.focusedParent = this.$searchbox[0]; | |
} else { | |
this.focusedParent = this.$menuInner[0]; | |
} | |
this.setStyle(); | |
this.setWidth(); | |
if (this.options.container) { | |
this.selectPosition(); | |
} else { | |
this.$element.on('hide' + EVENT_KEY, function () { | |
if (that.isVirtual()) { | |
// empty menu on close | |
var menuInner = that.$menuInner[0], | |
emptyMenu = menuInner.firstChild.cloneNode(false); | |
// replace the existing UL with an empty one - this is faster than $.empty() or innerHTML = '' | |
menuInner.replaceChild(emptyMenu, menuInner.firstChild); | |
menuInner.scrollTop = 0; | |
} | |
}); | |
} | |
this.$menu.data('this', this); | |
this.$newElement.data('this', this); | |
if (this.options.mobile) this.mobile(); | |
this.$newElement.on({ | |
'hide.bs.dropdown': function (e) { | |
that.$element.trigger('hide' + EVENT_KEY, e); | |
}, | |
'hidden.bs.dropdown': function (e) { | |
that.$element.trigger('hidden' + EVENT_KEY, e); | |
}, | |
'show.bs.dropdown': function (e) { | |
that.$element.trigger('show' + EVENT_KEY, e); | |
}, | |
'shown.bs.dropdown': function (e) { | |
that.$element.trigger('shown' + EVENT_KEY, e); | |
} | |
}); | |
if (element.hasAttribute('required')) { | |
this.$element.on('invalid' + EVENT_KEY, function () { | |
that.$button[0].classList.add('bs-invalid'); | |
that.$element | |
.on('shown' + EVENT_KEY + '.invalid', function () { | |
that.$element | |
.val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened | |
.off('shown' + EVENT_KEY + '.invalid'); | |
}) | |
.on('rendered' + EVENT_KEY, function () { | |
// if select is no longer invalid, remove the bs-invalid class | |
if (this.validity.valid) that.$button[0].classList.remove('bs-invalid'); | |
that.$element.off('rendered' + EVENT_KEY); | |
}); | |
that.$button.on('blur' + EVENT_KEY, function () { | |
that.$element.trigger('focus').trigger('blur'); | |
that.$button.off('blur' + EVENT_KEY); | |
}); | |
}); | |
} | |
if (form) { | |
$(form).on('reset' + EVENT_KEY, function () { | |
requestAnimationFrame(function () { | |
that.render(); | |
}); | |
}); | |
} | |
}, | |
createDropdown: function () { | |
// Options | |
// If we are multiple or showTick option is set, then add the show-tick class | |
var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '', | |
multiselectable = this.multiple ? ' aria-multiselectable="true"' : '', | |
inputGroup = '', | |
autofocus = this.autofocus ? ' autofocus' : ''; | |
if (version.major < 4 && this.$element.parent().hasClass('input-group')) { | |
inputGroup = ' input-group-btn'; | |
} | |
// Elements | |
var drop, | |
header = '', | |
searchbox = '', | |
actionsbox = '', | |
donebutton = '', | |
clearButton = ''; | |
if (this.options.header) { | |
header = | |
'<div class="' + classNames.POPOVERHEADER + '">' + | |
'<button type="button" class="close" aria-hidden="true">×</button>' + | |
this.options.header + | |
'</div>'; | |
} | |
if (this.options.liveSearch) { | |
searchbox = | |
'<div class="bs-searchbox">' + | |
'<input type="search" class="form-control" autocomplete="off"' + | |
( | |
this.options.liveSearchPlaceholder === null ? '' | |
: | |
' placeholder="' + htmlEscape(this.options.liveSearchPlaceholder) + '"' | |
) + | |
' role="combobox" aria-label="Search" aria-controls="' + this.selectId + '" aria-autocomplete="list">' + | |
'</div>'; | |
} | |
if (this.multiple && this.options.actionsBox) { | |
actionsbox = | |
'<div class="bs-actionsbox">' + | |
'<div class="btn-group btn-group-sm">' + | |
'<button type="button" class="actions-btn bs-select-all btn ' + classNames.BUTTONCLASS + '">' + | |
this.options.selectAllText + | |
'</button>' + | |
'<button type="button" class="actions-btn bs-deselect-all btn ' + classNames.BUTTONCLASS + '">' + | |
this.options.deselectAllText + | |
'</button>' + | |
'</div>' + | |
'</div>'; | |
} | |
if (this.multiple && this.options.doneButton) { | |
donebutton = | |
'<div class="bs-donebutton">' + | |
'<div class="btn-group">' + | |
'<button type="button" class="btn btn-sm ' + classNames.BUTTONCLASS + '">' + | |
this.options.doneButtonText + | |
'</button>' + | |
'</div>' + | |
'</div>'; | |
} | |
if (this.options.allowClear) { | |
clearButton = '<span class="close bs-select-clear-selected" title="' + this.options.deselectAllText + '"><span>×</span>'; | |
} | |
drop = | |
'<div class="dropdown bootstrap-select' + showTick + inputGroup + '">' + | |
'<button type="button" tabindex="-1" class="' + | |
this.options.styleBase + | |
' dropdown-toggle" ' + | |
(this.options.display === 'static' ? 'data-display="static"' : '') + | |
Selector.DATA_TOGGLE + | |
autofocus + | |
' role="combobox" aria-owns="' + | |
this.selectId + | |
'" aria-haspopup="listbox" aria-expanded="false">' + | |
'<div class="filter-option">' + | |
'<div class="filter-option-inner">' + | |
'<div class="filter-option-inner-inner"> </div>' + | |
'</div> ' + | |
'</div>' + | |
clearButton + | |
'</span>' + | |
( | |
version.major >= '4' ? '' | |
: | |
'<span class="bs-caret">' + | |
this.options.template.caret + | |
'</span>' | |
) + | |
'</button>' + | |
'<div class="' + classNames.MENU + ' ' + (version.major >= '4' ? '' : classNames.SHOW) + '">' + | |
header + | |
searchbox + | |
actionsbox + | |
'<div class="inner ' + classNames.SHOW + '" role="listbox" id="' + this.selectId + '" tabindex="-1" ' + multiselectable + '>' + | |
'<ul class="' + classNames.MENU + ' inner ' + (version.major >= '4' ? classNames.SHOW : '') + '" role="presentation">' + | |
'</ul>' + | |
'</div>' + | |
donebutton + | |
'</div>' + | |
'</div>'; | |
return $(drop); | |
}, | |
setPositionData: function () { | |
this.selectpicker.view.canHighlight = []; | |
this.selectpicker.view.size = 0; | |
this.selectpicker.view.firstHighlightIndex = false; | |
for (var i = 0; i < this.selectpicker.current.data.length; i++) { | |
var li = this.selectpicker.current.data[i], | |
canHighlight = true; | |
if (li.type === 'divider') { | |
canHighlight = false; | |
li.height = this.sizeInfo.dividerHeight; | |
} else if (li.type === 'optgroup-label') { | |
canHighlight = false; | |
li.height = this.sizeInfo.dropdownHeaderHeight; | |
} else { | |
li.height = this.sizeInfo.liHeight; | |
} | |
if (li.disabled) canHighlight = false; | |
this.selectpicker.view.canHighlight.push(canHighlight); | |
if (canHighlight) { | |
this.selectpicker.view.size++; | |
li.posinset = this.selectpicker.view.size; | |
if (this.selectpicker.view.firstHighlightIndex === false) this.selectpicker.view.firstHighlightIndex = i; | |
} | |
li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height; | |
} | |
}, | |
isVirtual: function () { | |
return (this.options.virtualScroll !== false) && (this.selectpicker.main.data.length >= this.options.virtualScroll) || this.options.virtualScroll === true; | |
}, | |
createView: function (isSearching, setSize, refresh) { | |
var that = this, | |
scrollTop = 0; | |
this.selectpicker.isSearching = isSearching; | |
this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main; | |
this.setPositionData(); | |
if (setSize) { | |
if (refresh) { | |
scrollTop = this.$menuInner[0].scrollTop; | |
} else if (!that.multiple) { | |
var element = that.$element[0], | |
selectedIndex = (element.options[element.selectedIndex] || {}).liIndex; | |
if (typeof selectedIndex === 'number' && that.options.size !== false) { | |
var selectedData = that.selectpicker.main.data[selectedIndex], | |
position = selectedData && selectedData.position; | |
if (position) { | |
scrollTop = position - ((that.sizeInfo.menuInnerHeight + that.sizeInfo.liHeight) / 2); | |
} | |
} | |
} | |
} | |
scroll(scrollTop, true); | |
this.$menuInner.off('scroll.createView').on('scroll.createView', function (e, updateValue) { | |
if (!that.noScroll) scroll(this.scrollTop, updateValue); | |
that.noScroll = false; | |
}); | |
function scroll (scrollTop, init) { | |
var size = that.selectpicker.current.data.length, | |
chunks = [], | |
chunkSize, | |
chunkCount, | |
firstChunk, | |
lastChunk, | |
currentChunk, | |
prevPositions, | |
positionIsDifferent, | |
previousElements, | |
menuIsDifferent = true, | |
isVirtual = that.isVirtual(); | |
that.selectpicker.view.scrollTop = scrollTop; | |
chunkSize = that.options.chunkSize; // number of options in a chunk | |
chunkCount = Math.ceil(size / chunkSize) || 1; // number of chunks | |
for (var i = 0; i < chunkCount; i++) { | |
var endOfChunk = (i + 1) * chunkSize; | |
if (i === chunkCount - 1) { | |
endOfChunk = size; | |
} | |
chunks[i] = [ | |
(i) * chunkSize + (!i ? 0 : 1), | |
endOfChunk | |
]; | |
if (!size) break; | |
if (currentChunk === undefined && scrollTop - 1 <= that.selectpicker.current.data[endOfChunk - 1].position - that.sizeInfo.menuInnerHeight) { | |
currentChunk = i; | |
} | |
} | |
if (currentChunk === undefined) currentChunk = 0; | |
prevPositions = [that.selectpicker.view.position0, that.selectpicker.view.position1]; | |
// always display previous, current, and next chunks | |
firstChunk = Math.max(0, currentChunk - 1); | |
lastChunk = Math.min(chunkCount - 1, currentChunk + 1); | |
that.selectpicker.view.position0 = isVirtual === false ? 0 : (Math.max(0, chunks[firstChunk][0]) || 0); | |
that.selectpicker.view.position1 = isVirtual === false ? size : (Math.min(size, chunks[lastChunk][1]) || 0); | |
positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1; | |
if (that.activeElement !== undefined) { | |
if (init) { | |
if (that.activeElement !== that.selectedElement) { | |
that.defocusItem(that.activeElement); | |
} | |
that.activeElement = undefined; | |
} | |
if (that.activeElement !== that.selectedElement) { | |
that.defocusItem(that.selectedElement); | |
} | |
} | |
if (that.prevActiveElement !== undefined && that.prevActiveElement !== that.activeElement && that.prevActiveElement !== that.selectedElement) { | |
that.defocusItem(that.prevActiveElement); | |
} | |
if (init || positionIsDifferent || that.selectpicker.current.hasMore) { | |
previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : []; | |
if (isVirtual === false) { | |
that.selectpicker.view.visibleElements = that.selectpicker.current.elements; | |
} else { | |
that.selectpicker.view.visibleElements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1); | |
} | |
that.setOptionStatus(); | |
// if searching, check to make sure the list has actually been updated before updating DOM | |
// this prevents unnecessary repaints | |
if (isSearching || (isVirtual === false && init)) menuIsDifferent = !isEqual(previousElements, that.selectpicker.view.visibleElements); | |
// if virtual scroll is disabled and not searching, | |
// menu should never need to be updated more than once | |
if ((init || isVirtual === true) && menuIsDifferent) { | |
var menuInner = that.$menuInner[0], | |
menuFragment = document.createDocumentFragment(), | |
emptyMenu = menuInner.firstChild.cloneNode(false), | |
marginTop, | |
marginBottom, | |
elements = that.selectpicker.view.visibleElements, | |
toSanitize = []; | |
// replace the existing UL with an empty one - this is faster than $.empty() | |
menuInner.replaceChild(emptyMenu, menuInner.firstChild); | |
for (var i = 0, visibleElementsLen = elements.length; i < visibleElementsLen; i++) { | |
var element = elements[i], | |
elText, | |
elementData; | |
if (that.options.sanitize) { | |
elText = element.lastChild; | |
if (elText) { | |
elementData = that.selectpicker.current.data[i + that.selectpicker.view.position0]; | |
if (elementData && elementData.content && !elementData.sanitized) { | |
toSanitize.push(elText); | |
elementData.sanitized = true; | |
} | |
} | |
} | |
menuFragment.appendChild(element); | |
} | |
if (that.options.sanitize && toSanitize.length) { | |
sanitizeHtml(toSanitize, that.options.whiteList, that.options.sanitizeFn); | |
} | |
if (isVirtual === true) { | |
marginTop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position); | |
marginBottom = (that.selectpicker.view.position1 > size - 1 ? 0 : that.selectpicker.current.data[size - 1].position - that.selectpicker.current.data[that.selectpicker.view.position1 - 1].position); | |
menuInner.firstChild.style.marginTop = marginTop + 'px'; | |
menuInner.firstChild.style.marginBottom = marginBottom + 'px'; | |
} else { | |
menuInner.firstChild.style.marginTop = 0; | |
menuInner.firstChild.style.marginBottom = 0; | |
} | |
menuInner.firstChild.appendChild(menuFragment); | |
// if an option is encountered that is wider than the current menu width, update the menu width accordingly | |
// switch to ResizeObserver with increased browser support | |
if (isVirtual === true && that.sizeInfo.hasScrollBar) { | |
var menuInnerInnerWidth = menuInner.firstChild.offsetWidth; | |
if (init && menuInnerInnerWidth < that.sizeInfo.menuInnerInnerWidth && that.sizeInfo.totalMenuWidth > that.sizeInfo.selectWidth) { | |
menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; | |
} else if (menuInnerInnerWidth > that.sizeInfo.menuInnerInnerWidth) { | |
// set to 0 to get actual width of menu | |
that.$menu[0].style.minWidth = 0; | |
var actualMenuWidth = menuInner.firstChild.offsetWidth; | |
if (actualMenuWidth > that.sizeInfo.menuInnerInnerWidth) { | |
that.sizeInfo.menuInnerInnerWidth = actualMenuWidth; | |
menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; | |
} | |
// reset to default CSS styling | |
that.$menu[0].style.minWidth = ''; | |
} | |
} | |
} | |
if ((!isSearching && that.options.source.data || isSearching && that.options.source.search) && that.selectpicker.current.hasMore && currentChunk === chunkCount - 1) { | |
// Don't load the next chunk until scrolling has started | |
// This prevents unnecessary requests while the user is typing if pageSize is <= chunkSize | |
if (scrollTop > 0) { | |
// Chunks use 0-based indexing, but pages use 1-based. Add 1 to convert and add 1 again to get next page | |
var page = Math.floor((currentChunk * that.options.chunkSize) / that.options.source.pageSize) + 2; | |
that.fetchData(function () { | |
that.render(); | |
that.buildList(size, isSearching); | |
that.setPositionData(); | |
scroll(scrollTop); | |
}, isSearching ? 'search' : 'data', page, isSearching ? that.selectpicker.search.previousValue : undefined); | |
} | |
} | |
} | |
that.prevActiveElement = that.activeElement; | |
if (!that.options.liveSearch) { | |
that.$menuInner.trigger('focus'); | |
} else if (isSearching && init) { | |
var index = 0, | |
newActive; | |
if (!that.selectpicker.view.canHighlight[index]) { | |
index = 1 + that.selectpicker.view.canHighlight.slice(1).indexOf(true); | |
} | |
newActive = that.selectpicker.view.visibleElements[index]; | |
that.defocusItem(that.selectpicker.view.currentActive); | |
that.activeElement = (that.selectpicker.current.data[index] || {}).element; | |
that.focusItem(newActive); | |
} | |
} | |
$(window) | |
.off('resize' + EVENT_KEY + '.' + this.selectId + '.createView') | |
.on('resize' + EVENT_KEY + '.' + this.selectId + '.createView', function () { | |
var isActive = that.$newElement.hasClass(classNames.SHOW); | |
if (isActive) scroll(that.$menuInner[0].scrollTop); | |
}); | |
}, | |
focusItem: function (li, liData, noStyle) { | |
if (li) { | |
liData = liData || this.selectpicker.current.data[this.selectpicker.current.elements.indexOf(this.activeElement)]; | |
var a = li.firstChild; | |
if (a) { | |
a.setAttribute('aria-setsize', this.selectpicker.view.size); | |
a.setAttribute('aria-posinset', liData.posinset); | |
if (noStyle !== true) { | |
this.focusedParent.setAttribute('aria-activedescendant', a.id); | |
li.classList.add('active'); | |
a.classList.add('active'); | |
} | |
} | |
} | |
}, | |
defocusItem: function (li) { | |
if (li) { | |
li.classList.remove('active'); | |
if (li.firstChild) li.firstChild.classList.remove('active'); | |
} | |
}, | |
setPlaceholder: function () { | |
var that = this, | |
updateIndex = false; | |
if ((this.options.placeholder || this.options.allowClear) && !this.multiple) { | |
if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option'); | |
// this option doesn't create a new <li> element, but does add a new option at the start, | |
// so startIndex should increase to prevent having to check every option for the bs-title-option class | |
updateIndex = true; | |
var element = this.$element[0], | |
selectTitleOption = false, | |
titleNotAppended = !this.selectpicker.view.titleOption.parentNode, | |
selectedIndex = element.selectedIndex, | |
selectedOption = element.options[selectedIndex], | |
firstSelectable = element.querySelector('select > *:not(:disabled)'), | |
firstSelectableIndex = firstSelectable ? firstSelectable.index : 0, | |
navigation = window.performance && window.performance.getEntriesByType('navigation'), | |
// Safari doesn't support getEntriesByType('navigation') - fall back to performance.navigation | |
isNotBackForward = (navigation && navigation.length) ? navigation[0].type !== 'back_forward' : window.performance.navigation.type !== 2; | |
if (titleNotAppended) { | |
// Use native JS to prepend option (faster) | |
this.selectpicker.view.titleOption.className = 'bs-title-option'; | |
this.selectpicker.view.titleOption.value = ''; | |
// Check if selected or data-selected attribute is already set on an option. If not, select the titleOption option. | |
// the selected item may have been changed by user or programmatically before the bootstrap select plugin runs, | |
// if so, the select will have the data-selected attribute | |
selectTitleOption = !selectedOption || (selectedIndex === firstSelectableIndex && selectedOption.defaultSelected === false && this.$element.data('selected') === undefined); | |
} | |
if (titleNotAppended || this.selectpicker.view.titleOption.index !== 0) { | |
element.insertBefore(this.selectpicker.view.titleOption, element.firstChild); | |
} | |
// Set selected *after* appending to select, | |
// otherwise the option doesn't get selected in IE | |
// set using selectedIndex, as setting the selected attr to true here doesn't work in IE11 | |
if (selectTitleOption && isNotBackForward) { | |
element.selectedIndex = 0; | |
} else if (document.readyState !== 'complete') { | |
// if navigation type is back_forward, there's a chance the select will have its value set by BFCache | |
// wait for that value to be set, then run render again | |
window.addEventListener('pageshow', function () { | |
if (that.selectpicker.view.displayedValue !== element.value) that.render(); | |
}); | |
} | |
} | |
return updateIndex; | |
}, | |
fetchData: function (callback, type, page, searchValue) { | |
page = page || 1; | |
type = type || 'data'; | |
var that = this, | |
data = this.options.source[type], | |
builtData; | |
if (data) { | |
this.options.virtualScroll = true; | |
if (typeof data === 'function') { | |
data.call( | |
this, | |
function (data, more, totalItems) { | |
var current = that.selectpicker[type === 'search' ? 'search' : 'main']; | |
current.hasMore = more; | |
current.totalItems = totalItems; | |
builtData = that.buildData(data, type); | |
callback.call(that, builtData); | |
that.$element.trigger('fetched' + EVENT_KEY); | |
}, | |
page, | |
searchValue | |
); | |
} else if (Array.isArray(data)) { | |
builtData = that.buildData(data, type); | |
callback.call(that, builtData); | |
} | |
} else { | |
builtData = this.buildData(false, type); | |
callback.call(that, builtData); | |
} | |
}, | |
buildData: function (data, type) { | |
var that = this; | |
var dataGetter = data === false ? getOptionData.fromOption : getOptionData.fromDataSource; | |
var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([style*="display: none"])', | |
mainData = [], | |
startLen = this.selectpicker.main.data ? this.selectpicker.main.data.length : 0, | |
optID = 0, | |
startIndex = this.setPlaceholder() && !data ? 1 : 0; // append the titleOption if necessary and skip the first option in the loop | |
if (type === 'search') { | |
startLen = this.selectpicker.search.data.length; | |
} | |
if (this.options.hideDisabled) optionSelector += ':not(:disabled)'; | |
var selectOptions = data ? data.filter(filterHidden, this) : this.$element[0].querySelectorAll('select > *' + optionSelector); | |
function addDivider (config) { | |
var previousData = mainData[mainData.length - 1]; | |
// ensure optgroup doesn't create back-to-back dividers | |
if ( | |
previousData && | |
previousData.type === 'divider' && | |
(previousData.optID || config.optID) | |
) { | |
return; | |
} | |
config = config || {}; | |
config.type = 'divider'; | |
mainData.push(config); | |
} | |
function addOption (item, config) { | |
config = config || {}; | |
config.divider = dataGetter(item, 'divider'); | |
if (config.divider === true) { | |
addDivider({ | |
optID: config.optID | |
}); | |
} else { | |
var liIndex = mainData.length + startLen, | |
cssText = dataGetter(item, 'style'), | |
inlineStyle = cssText ? htmlEscape(cssText) : '', | |
optionClass = (item.className || '') + (config.optgroupClass || ''); | |
if (config.optID) optionClass = 'opt ' + optionClass; | |
config.optionClass = optionClass.trim(); | |
config.inlineStyle = inlineStyle; | |
config.text = dataGetter(item, 'text'); | |
config.title = dataGetter(item, 'title'); | |
config.content = dataGetter(item, 'content'); | |
config.tokens = dataGetter(item, 'tokens'); | |
config.subtext = dataGetter(item, 'subtext'); | |
config.icon = dataGetter(item, 'icon'); | |
config.display = config.content || config.text; | |
config.value = item.value === undefined ? item.text : item.value; | |
config.type = 'option'; | |
config.index = liIndex; | |
config.option = !item.option ? item : item.option; // reference option element if it exists | |
config.option.liIndex = liIndex; | |
config.selected = !!item.selected; | |
config.disabled = config.disabled || !!item.disabled; | |
if (data !== false) { | |
if (that.selectpicker.optionValuesDataMap[config.value]) { | |
config = $.extend(that.selectpicker.optionValuesDataMap[config.value], config); | |
} else { | |
that.selectpicker.optionValuesDataMap[config.value] = config; | |
} | |
} | |
mainData.push(config); | |
} | |
} | |
function addOptgroup (index, selectOptions) { | |
var optgroup = selectOptions[index], | |
// skip placeholder option | |
previous = index - 1 < startIndex ? false : selectOptions[index - 1], | |
next = selectOptions[index + 1], | |
options = data ? optgroup.children.filter(filterHidden, this) : optgroup.querySelectorAll('option' + optionSelector); | |
if (!options.length) return; | |
var config = { | |
display: htmlEscape(dataGetter(item, 'label')), | |
subtext: dataGetter(optgroup, 'subtext'), | |
icon: dataGetter(optgroup, 'icon'), | |
type: 'optgroup-label', | |
optgroupClass: ' ' + (optgroup.className || ''), | |
optgroup: optgroup | |
}, | |
headerIndex, | |
lastIndex; | |
optID++; | |
if (previous) { | |
addDivider({ optID: optID }); | |
} | |
config.optID = optID; | |
mainData.push(config); | |
for (var j = 0, len = options.length; j < len; j++) { | |
var option = options[j]; | |
if (j === 0) { | |
headerIndex = mainData.length - 1; | |
lastIndex = headerIndex + len; | |
} | |
addOption(option, { | |
headerIndex: headerIndex, | |
lastIndex: lastIndex, | |
optID: config.optID, | |
optgroupClass: config.optgroupClass, | |
disabled: optgroup.disabled | |
}); | |
} | |
if (next) { | |
addDivider({ optID: optID }); | |
} | |
} | |
for (var len = selectOptions.length, i = startIndex; i < len; i++) { | |
var item = selectOptions[i], | |
children = item.children; | |
if (children && children.length) { | |
addOptgroup.call(this, i, selectOptions); | |
} else { | |
addOption.call(this, item, {}); | |
} | |
} | |
switch (type) { | |
case 'data': { | |
if (!this.selectpicker.main.data) { | |
this.selectpicker.main.data = []; | |
} | |
Array.prototype.push.apply(this.selectpicker.main.data, mainData); | |
this.selectpicker.current.data = this.selectpicker.main.data; | |
break; | |
} | |
case 'search': { | |
Array.prototype.push.apply(this.selectpicker.search.data, mainData); | |
break; | |
} | |
} | |
return mainData; | |
}, | |
buildList: function (size, searching) { | |
var that = this, | |
selectData = searching ? this.selectpicker.search.data : this.selectpicker.main.data, | |
mainElements = [], | |
widestOptionLength = 0; | |
if ((that.options.showTick || that.multiple) && !elementTemplates.checkMark.parentNode) { | |
elementTemplates.checkMark.className = this.options.iconBase + ' ' + that.options.tickIcon + ' check-mark'; | |
elementTemplates.a.appendChild(elementTemplates.checkMark); | |
} | |
function buildElement (mainElements, item) { | |
var liElement, | |
combinedLength = 0; | |
switch (item.type) { | |
case 'divider': | |
liElement = generateOption.li( | |
false, | |
classNames.DIVIDER, | |
(item.optID ? item.optID + 'div' : undefined) | |
); | |
break; | |
case 'option': | |
liElement = generateOption.li( | |
generateOption.a( | |
generateOption.text.call(that, item), | |
item.optionClass, | |
item.inlineStyle | |
), | |
'', | |
item.optID | |
); | |
if (liElement.firstChild) { | |
liElement.firstChild.id = that.selectId + '-' + item.index; | |
} | |
break; | |
case 'optgroup-label': | |
liElement = generateOption.li( | |
generateOption.label.call(that, item), | |
'dropdown-header' + item.optgroupClass, | |
item.optID | |
); | |
break; | |
} | |
if (!item.element) { | |
item.element = liElement; | |
} else { | |
item.element.innerHTML = liElement.innerHTML; | |
} | |
mainElements.push(item.element); | |
// count the number of characters in the option - not perfect, but should work in most cases | |
if (item.display) combinedLength += item.display.length; | |
if (item.subtext) combinedLength += item.subtext.length; | |
// if there is an icon, ensure this option's width is checked | |
if (item.icon) combinedLength += 1; | |
if (combinedLength > widestOptionLength) { | |
widestOptionLength = combinedLength; | |
// guess which option is the widest | |
// use this when calculating menu width | |
// not perfect, but it's fast, and the width will be updating accordingly when scrolling | |
that.selectpicker.view.widestOption = mainElements[mainElements.length - 1]; | |
} | |
} | |
var startIndex = size || 0; | |
for (var len = selectData.length, i = startIndex; i < len; i++) { | |
var item = selectData[i]; | |
buildElement(mainElements, item); | |
} | |
if (size) { | |
if (searching) { | |
Array.prototype.push.apply(this.selectpicker.search.elements, mainElements); | |
} else { | |
Array.prototype.push.apply(this.selectpicker.main.elements, mainElements); | |
this.selectpicker.current.elements = this.selectpicker.main.elements; | |
} | |
} else { | |
if (searching) { | |
this.selectpicker.search.elements = mainElements; | |
} else { | |
this.selectpicker.main.elements = this.selectpicker.current.elements = mainElements; | |
} | |
} | |
}, | |
findLis: function () { | |
return this.$menuInner.find('.inner > li'); | |
}, | |
render: function (init) { | |
var that = this, | |
element = this.$element[0], | |
// ensure titleOption is appended and selected (if necessary) before getting selectedOptions | |
placeholderSelected = this.setPlaceholder() && element.selectedIndex === 0, | |
//selectedOptions = getSelectedOptions.call(this), | |
selectedOptions = getSelectedOptions(element, this.options.hideDisabled), | |
selectedCount = selectedOptions.length, | |
//selectedValues = getSelectValues.call(this, selectedOptions), | |
selectedValues = getSelectValues(element, selectedOptions), | |
button = this.$button[0], | |
buttonInner = button.querySelector('.filter-option-inner-inner'), | |
multipleSeparator = document.createTextNode(this.options.multipleSeparator), | |
titleFragment = elementTemplates.fragment.cloneNode(false), | |
titleTipFragment = elementTemplates.fragment.cloneNode(false), | |
showCount, | |
countMax, | |
hasContent = false; | |
function createSelected (item) { | |
if (item.selected) { | |
that.createOption(item, true); | |
} else if (item.children && item.children.length) { | |
item.children.map(createSelected); | |
} | |
} | |
// create selected option elements to ensure select value is correct | |
if (this.options.source.data && init) { | |
selectedOptions.map(createSelected); | |
element.appendChild(this.selectpicker.main.optionQueue); | |
if (placeholderSelected) placeholderSelected = element.selectedIndex === 0; | |
} | |
button.classList.toggle('bs-placeholder', that.multiple ? !selectedCount : !selectedValues && selectedValues !== 0); | |
if (!that.multiple && selectedOptions.length === 1) { | |
that.selectpicker.view.displayedValue = selectedValues; | |
} | |
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); |
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?
Hello :) in Ajax select there were some problems related to the resultset
and it's rendering... If I'm not wrong
Il gio 7 nov 2024, 18:34 tbmoomau ***@***.***> ha scritto:
… ***@***.**** commented on this gist.
------------------------------
Hey @mattymatty76 <https://github.com/mattymatty76>, thanks for doing
this!
Do you remember why you changed chunkSize from 40 to Number.MAX_VALUE on
line 1009?
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/mattymatty76/c996d3b77f298b2ec133be59992df9d4#gistcomment-5271095>
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ANMMHTFBD7UO4WWFOZGOLZTZ7OQBLBFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTENBTHA2TINBQU52HE2LHM5SXFJTDOJSWC5DF>
.
You are receiving this email because you were mentioned.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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