From cecd7b68c1667609c9b02b597dbb3bd6624196be Mon Sep 17 00:00:00 2001 From: Franco Fichtner Date: Wed, 21 Nov 2018 08:03:06 +0100 Subject: [PATCH] ui: whitespace sweep says this file needs a lot of cleanups --- src/opnsense/www/js/bootstrap-select.js | 5494 +++++++++++------------ 1 file changed, 2747 insertions(+), 2747 deletions(-) diff --git a/src/opnsense/www/js/bootstrap-select.js b/src/opnsense/www/js/bootstrap-select.js index a8db5cf31..875268609 100644 --- a/src/opnsense/www/js/bootstrap-select.js +++ b/src/opnsense/www/js/bootstrap-select.js @@ -1,10 +1,10 @@ -/*! - * Bootstrap-select v1.13.3 (https://developer.snapappointments.com/bootstrap-select) - * - * Copyright 2012-2018 SnapAppointments, LLC - * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) - */ - +/*! + * Bootstrap-select v1.13.3 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2018 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + (function (root, factory) { if (root === undefined && window !== undefined) root = window; if (typeof define === 'function' && define.amd) { @@ -22,2746 +22,2746 @@ } }(this, function (jQuery) { -(function ($) { - 'use strict'; - - var testElement = document.createElement('_'); - - 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); - } - }; - } - - // shallow array comparison - function isEqual (array1, array2) { - return array1.length === array2.length && array1.every(function(element, index) { - return element === array2[index]; - }); - }; - - // - if (!String.prototype.startsWith) { - (function () { - 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` - var defineProperty = (function () { - // IE 8 only supports `Object.defineProperty` on DOM elements - try { - var object = {}; - var $defineProperty = Object.defineProperty; - var result = $defineProperty(object, object, object) && $defineProperty; - } catch (error) { - } - return result; - }()); - 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 (defineProperty) { - defineProperty(String.prototype, 'startsWith', { - 'value': startsWith, - 'configurable': true, - 'writable': true - }); - } else { - String.prototype.startsWith = startsWith; - } - }()); - } - - if (!Object.keys) { - Object.keys = function ( - o, // object - k, // key - r // result array - ){ - // initialize object and result - r=[]; - // iterate over object keys - for (k in o) - // fill result array with non-prototypical keys - r.hasOwnProperty.call(o, k) && r.push(k); - // return result - return r; - }; - } - - // much faster than $.val() - function getSelectValues(select) { - var result = []; - var options = select && select.options; - var opt; - - if (select.multiple) { - for (var i = 0, len = options.length; i < len; i++) { - opt = options[i]; - - if (opt.selected) { - result.push(opt.value || opt.text); - } - } - } else { - result = select.value; - } - - return result; - } - - // 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 changed_arguments = 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); - } else if (el.fireEvent) { // for IE8 - event = document.createEventObject(); - event.eventType = eventName; - el.fireEvent('on' + eventName, event); - } else { - // fall back to jQuery.trigger - this.trigger(eventName); - } - }; - // - - function stringSearch(li, searchString, method, normalize) { - var stringTypes = [ - 'content', - '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 === 'content') { - string = string.replace(/<[^>]+>/g, ''); - } - - if (normalize) string = normalizeToBase(string); - string = string.toUpperCase(); - - if (method === 'contains') { - searchSuccess = string.indexOf(searchString) >= 0; - } else { - searchSuccess = string.startsWith(searchString); - } - - if (searchSuccess) break; - } - } - - return searchSuccess; - } - - function toInteger(value) { - return parseInt(value, 10) || 0; - } - - /** - * Remove all diatrics from the given text. - * @access private - * @param {String} text - * @returns {String} - */ - function normalizeToBase(text) { - var rExps = [ - {re: /[\xC0-\xC6]/g, ch: "A"}, - {re: /[\xE0-\xE6]/g, ch: "a"}, - {re: /[\xC8-\xCB]/g, ch: "E"}, - {re: /[\xE8-\xEB]/g, ch: "e"}, - {re: /[\xCC-\xCF]/g, ch: "I"}, - {re: /[\xEC-\xEF]/g, ch: "i"}, - {re: /[\xD2-\xD6]/g, ch: "O"}, - {re: /[\xF2-\xF6]/g, ch: "o"}, - {re: /[\xD9-\xDC]/g, ch: "U"}, - {re: /[\xF9-\xFC]/g, ch: "u"}, - {re: /[\xC7-\xE7]/g, ch: "c"}, - {re: /[\xD1]/g, ch: "N"}, - {re: /[\xF1]/g, ch: "n"} - ]; - $.each(rExps, function () { - text = text ? text.replace(this.re, this.ch) : ''; - }); - return text; - } - - - // List of HTML entities for escaping. - var escapeMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`' - }; - - var unescapeMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", - '`': '`' - }; - - // 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); - var htmlUnescape = createEscaper(unescapeMap); - - /** - * ------------------------------------------------------------------------ - * 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 version = { - success: false, - major: '3' - }; - - try { - version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); - version.major = version.full[0]; - version.success = true; - } - catch(err) { - 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); - } - - 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' - } - - var Selector = { - MENU: '.' + classNames.MENU - } - - if (version.major === '4') { - classNames.DIVIDER = 'dropdown-divider'; - classNames.SHOW = 'show'; - classNames.BUTTONCLASS = 'btn-light'; - classNames.POPOVERHEADER = 'popover-header'; - } - - var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); - var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); - var REGEXP_ENTER_OR_SPACE = new RegExp(keyCodes.ENTER + '|' + keyCodes.SPACE); - - 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: { - // store originalIndex (key) and newIndex (value) in this.selectpicker.main.map.newIndex for fast accessibility - // allows us to do this.main.elements[this.selectpicker.main.map.newIndex[index]] to select an element based on the originalIndex - map: { - newIndex: {}, - originalIndex: {} - } - }, - current: { - map: {} - }, // current changes if a search is in progress - search: { - map: {} - }, - view: {}, - keydown: { - keyHistory: '', - resetKeyHistory: { - start: function () { - return setTimeout(function () { - that.selectpicker.keydown.keyHistory = ''; - }, 800); - } - } - } - }; - // If we have no title yet, try to pull it from the html title attribute (jQuery doesnt' pick it up as it's not a - // data-attribute) - if (this.options.title === null) { - this.options.title = this.$element.attr('title'); - } - - // 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.13.3'; - - Selectpicker.BootstrapVersion = version.major; - - // 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', - doneButton: false, - doneButtonText: 'Close', - multipleSeparator: ', ', - styleBase: 'btn', - style: classNames.BUTTONCLASS, - size: 'auto', - title: null, - 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: 'glyphicon', - tickIcon: 'glyphicon-ok', - showTick: false, - template: { - caret: '' - }, - maxOptions: false, - mobile: false, - selectOnTab: false, - dropdownAlignRight: false, - windowPadding: 0, - virtualScroll: 600, - display: false - }; - - if (version.major === '4') { - Selectpicker.DEFAULTS.style = 'btn-light'; - Selectpicker.DEFAULTS.iconBase = ''; - Selectpicker.DEFAULTS.tickIcon = 'bs-ok-default'; - } - - Selectpicker.prototype = { - - constructor: Selectpicker, - - init: function () { - var that = this, - id = this.$element.attr('id'); - - this.$element.addClass('bs-select-hidden'); - - this.multiple = this.$element.prop('multiple'); - this.autofocus = this.$element.prop('autofocus'); - this.$newElement = this.createDropdown(); - this.createLi(); - this.$element - .after(this.$newElement) - .prependTo(this.$newElement); - this.$button = this.$newElement.children('button'); - this.$menu = this.$newElement.children(Selector.MENU); - this.$menuInner = this.$menu.children('.inner'); - this.$searchbox = this.$menu.find('input'); - - this.$element.removeClass('bs-select-hidden'); - - if (this.options.dropdownAlignRight === true) this.$menu.addClass(classNames.MENURIGHT); - - if (typeof id !== 'undefined') { - this.$button.attr('data-id', id); - } - - this.checkDisabled(); - this.clickListener(); - if (this.options.liveSearch) this.liveSearchListener(); - this.render(); - this.setStyle(); - this.setWidth(); - if (this.options.container) { - this.selectPosition(); - } else { - this.$element.on('hide.bs.select', 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.$menuInner.attr('aria-expanded', false); - that.$element.trigger('hide.bs.select', e); - }, - 'hidden.bs.dropdown': function (e) { - that.$element.trigger('hidden.bs.select', e); - }, - 'show.bs.dropdown': function (e) { - that.$menuInner.attr('aria-expanded', true); - that.$element.trigger('show.bs.select', e); - }, - 'shown.bs.dropdown': function (e) { - that.$element.trigger('shown.bs.select', e); - } - }); - - if (that.$element[0].hasAttribute('required')) { - this.$element.on('invalid', function () { - that.$button.addClass('bs-invalid'); - - that.$element.on({ - 'shown.bs.select.invalid': function () { - that.$element - .val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened - .off('shown.bs.select.invalid'); - }, - 'rendered.bs.select': function () { - // if select is no longer invalid, remove the bs-invalid class - if (this.validity.valid) that.$button.removeClass('bs-invalid'); - that.$element.off('rendered.bs.select'); - } - }); - - that.$button.on('blur.bs.select', function () { - that.$element.focus().blur(); - that.$button.off('blur.bs.select'); - }); - }); - } - - setTimeout(function () { - that.$element.trigger('loaded.bs.select'); - }); - }, - - 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' : '', - autofocus = this.autofocus ? ' autofocus' : ''; - - // Elements - var drop, - header = '', - searchbox = '', - actionsbox = '', - donebutton = ''; - - if (this.options.header) { - header = - '
' + - '' + - this.options.header + - '
'; - } - - if (this.options.liveSearch) { - searchbox = - ''; - } - - if (this.multiple && this.options.actionsBox) { - actionsbox = - '
' + - '
' + - '' + - '' + - '
' + - '
'; - } - - if (this.multiple && this.options.doneButton) { - donebutton = - '
' + - '
' + - '' + - '
' + - '
'; - } - - drop = - ''; - - return $(drop); - }, - - setPositionData: function () { - this.selectpicker.view.canHighlight = []; - - 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); - - li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height; - } - }, - - isVirtual: function () { - return (this.options.virtualScroll !== false) && this.selectpicker.main.elements.length >= this.options.virtualScroll || this.options.virtualScroll === true; - }, - - createView: function (isSearching, scrollTop) { - scrollTop = scrollTop || 0; - - var that = this; - - this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main; - - var $lis; - var active = []; - var selected; - var prevActive; - var activeIndex; - var prevActiveIndex; - - this.setPositionData(); - - 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.elements.length, - chunks = [], - chunkSize, - chunkCount, - firstChunk, - lastChunk, - currentChunk = undefined, - prevPositions, - positionIsDifferent, - previousElements, - menuIsDifferent = true, - isVirtual = that.isVirtual(); - - that.selectpicker.view.scrollTop = scrollTop; - - if (isVirtual === true) { - // if an option that is encountered that is wider than the current menu width, update the menu width accordingly - if (that.sizeInfo.hasScrollBar && that.$menu[0].offsetWidth > that.sizeInfo.totalMenuWidth) { - that.sizeInfo.menuWidth = that.$menu[0].offsetWidth; - that.sizeInfo.totalMenuWidth = that.sizeInfo.menuWidth + that.sizeInfo.scrollBarWidth; - that.$menu.css('min-width', that.sizeInfo.menuWidth); - } - } - - chunkSize = Math.ceil(that.sizeInfo.menuInnerHeight / that.sizeInfo.liHeight * 1.5); // number of options in a chunk - chunkCount = Math.round(size / chunkSize) || 1; // number of chunks - - for (var i = 0; i < chunkCount; i++) { - var end_of_chunk = (i + 1) * chunkSize; - - if (i === chunkCount - 1) { - end_of_chunk = size; - } - - chunks[i] = [ - (i) * chunkSize + (!i ? 0 : 1), - end_of_chunk - ]; - - if (!size) break; - - if (currentChunk === undefined && scrollTop <= that.selectpicker.current.data[end_of_chunk - 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 = Math.max(0, chunks[firstChunk][0]) || 0; - that.selectpicker.view.position1 = Math.min(size, chunks[lastChunk][1]) || 0; - - positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1; - - if (that.activeIndex !== undefined) { - prevActive = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.prevActiveIndex]]; - active = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.activeIndex]]; - selected = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.selectedIndex]]; - - if (init) { - if (that.activeIndex !== that.selectedIndex) { - active.classList.remove('active'); - if (active.firstChild) active.firstChild.classList.remove('active'); - } - that.activeIndex = undefined; - } - - if (that.activeIndex && that.activeIndex !== that.selectedIndex && selected && selected.length) { - selected.classList.remove('active'); - if (selected.firstChild) selected.firstChild.classList.remove('active'); - } - } - - if (that.prevActiveIndex !== undefined && that.prevActiveIndex !== that.activeIndex && that.prevActiveIndex !== that.selectedIndex && prevActive && prevActive.length) { - prevActive.classList.remove('active'); - if (prevActive.firstChild) prevActive.firstChild.classList.remove('active'); - } - - if (init || positionIsDifferent) { - previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : []; - - 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 = isVirtual === true ? that.selectpicker.view.visibleElements : that.selectpicker.current.elements; - - // 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++) { - menuFragment.appendChild(elements[i]); - } - - 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'; - } - - menuInner.firstChild.appendChild(menuFragment); - } - } - - that.prevActiveIndex = that.activeIndex; - - if (!that.options.liveSearch) { - that.$menuInner.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]; - - if (that.selectpicker.view.currentActive) { - that.selectpicker.view.currentActive.classList.remove('active'); - if (that.selectpicker.view.currentActive.firstChild) that.selectpicker.view.currentActive.firstChild.classList.remove('active'); - } - - if (newActive) { - newActive.classList.add('active'); - if (newActive.firstChild) newActive.firstChild.classList.add('active'); - } - - that.activeIndex = that.selectpicker.current.map.originalIndex[index]; - } - } - - $(window).off('resize.createView').on('resize.createView', function () { - scroll(that.$menuInner[0].scrollTop); - }); - }, - - createLi: function () { - var that = this, - mainElements = [], - widestOption, - availableOptionsCount = 0, - widestOptionLength = 0, - mainData = [], - optID = 0, - headerIndex = 0, - liIndex = -1; // increment liIndex whenever a new
  • element is created to ensure newIndex is correct - - if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option'); - - var elementTemplates = { - span: document.createElement('span'), - subtext: document.createElement('small'), - a: document.createElement('a'), - li: document.createElement('li'), - whitespace: document.createTextNode("\u00A0") - }, - checkMark, - fragment = document.createDocumentFragment(); - - if (that.options.showTick || that.multiple) { - checkMark = elementTemplates.span.cloneNode(false); - checkMark.className = that.options.iconBase + ' ' + that.options.tickIcon + ' check-mark'; - elementTemplates.a.appendChild(checkMark); - } - - elementTemplates.a.setAttribute('role', 'option'); - - elementTemplates.subtext.className = 'text-muted'; - - elementTemplates.text = elementTemplates.span.cloneNode(false); - elementTemplates.text.className = 'text'; - - // Helper functions - /** - * @param content - * @param [classes] - * @param [optgroup] - * @returns {HTMLElement} - */ - var generateLI = 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' && null !== optgroup) li.classList.add('optgroup-' + optgroup); - - return li; - }; - - /** - * @param text - * @param [classes] - * @param [inline] - * @returns {string} - */ - var generateA = 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.className = classes; - if (version.major === '4') a.classList.add('dropdown-item'); - if (inline) a.setAttribute('style', inline); - - return a; - }; - - var generateText = function (options) { - var textElement = elementTemplates.text.cloneNode(false), - optionSubtextElement, - optionIconElement; - - if (options.optionContent) { - textElement.innerHTML = options.optionContent; - } else { - textElement.textContent = options.text; - - if (options.optionIcon) { - var whitespace = elementTemplates.whitespace.cloneNode(false); - - optionIconElement = elementTemplates.span.cloneNode(false); - optionIconElement.className = that.options.iconBase + ' ' + options.optionIcon; - - fragment.appendChild(optionIconElement); - fragment.appendChild(whitespace); - } - - if (options.optionSubtext) { - optionSubtextElement = elementTemplates.subtext.cloneNode(false); - optionSubtextElement.innerHTML = options.optionSubtext; - textElement.appendChild(optionSubtextElement); - } - } - - fragment.appendChild(textElement); - - return fragment; - }; - - var generateLabel = function (options) { - var labelTextElement = elementTemplates.text.cloneNode(false), - labelSubtextElement, - labelIconElement; - - labelTextElement.innerHTML = options.labelEscaped; - - if (options.labelIcon) { - var whitespace = elementTemplates.whitespace.cloneNode(false); - - labelIconElement = elementTemplates.span.cloneNode(false); - labelIconElement.className = that.options.iconBase + ' ' + options.labelIcon; - - fragment.appendChild(labelIconElement); - fragment.appendChild(whitespace); - } - - if (options.labelSubtext) { - labelSubtextElement = elementTemplates.subtext.cloneNode(false); - labelSubtextElement.textContent = options.labelSubtext; - labelTextElement.appendChild(labelSubtextElement); - } - - fragment.appendChild(labelTextElement); - - return fragment; - } - - if (this.options.title && !this.multiple) { - // this option doesn't create a new
  • element, but does add a new option, so liIndex is decreased - // since newIndex is recalculated on every refresh, liIndex needs to be decreased even if the titleOption is already appended - liIndex--; - - var element = this.$element[0], - isSelected = false, - titleNotAppended = !this.selectpicker.view.titleOption.parentNode; - - 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 - var $opt = $(element.options[element.selectedIndex]); - isSelected = $opt.attr('selected') === undefined && 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 (isSelected) element.selectedIndex = 0; - } - - var $selectOptions = this.$element.find('option'); - - $selectOptions.each(function (index) { - var $this = $(this); - - liIndex++; - - if ($this.hasClass('bs-title-option')) return; - - var thisData = $this.data(); - - // Get the class and text for the option - var optionClass = this.className || '', - inline = htmlEscape(this.style.cssText), - optionContent = thisData.content, - text = this.textContent, - tokens = thisData.tokens, - subtext = thisData.subtext, - icon = thisData.icon, - $parent = $this.parent(), - parent = $parent[0], - isOptgroup = parent.tagName === 'OPTGROUP', - isOptgroupDisabled = isOptgroup && parent.disabled, - isDisabled = this.disabled || isOptgroupDisabled, - prevHiddenIndex, - showDivider = this.previousElementSibling && this.previousElementSibling.tagName === 'OPTGROUP', - textElement; - - var parentData = $parent.data(); - - if (thisData.hidden === true || that.options.hideDisabled && (isDisabled && !isOptgroup || isOptgroupDisabled)) { - // set prevHiddenIndex - the index of the first hidden option in a group of hidden options - // used to determine whether or not a divider should be placed after an optgroup if there are - // hidden options between the optgroup and the first visible option - prevHiddenIndex = thisData.prevHiddenIndex; - $this.next().data('prevHiddenIndex', (prevHiddenIndex !== undefined ? prevHiddenIndex : index)); - - liIndex--; - - // if previous element is not an optgroup - if (!showDivider) { - if (prevHiddenIndex !== undefined) { - // select the element **before** the first hidden element in the group - var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling; - - if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) { - showDivider = true; - } - } - } - - if (showDivider && mainData[mainData.length - 1].type !== 'divider') { - liIndex++; - mainElements.push( - generateLI( - false, - classNames.DIVIDER, - optID + 'div' - ) - ); - mainData.push({ - type: 'divider', - optID: optID - }); - } - - return; - } - - if (isOptgroup && thisData.divider !== true) { - if (that.options.hideDisabled && isDisabled) { - if (parentData.allOptionsDisabled === undefined) { - var $options = $parent.children(); - $parent.data('allOptionsDisabled', $options.filter(':disabled').length === $options.length); - } - - if ($parent.data('allOptionsDisabled')) { - liIndex--; - return; - } - } - - var optGroupClass = ' ' + parent.className || ''; - - if (!this.previousElementSibling) { // Is it the first option of the optgroup? - optID += 1; - - // Get the opt group label - var label = parent.label, - labelEscaped = htmlEscape(label), - labelSubtext = parentData.subtext, - labelIcon = parentData.icon; - - if (index !== 0 && mainElements.length > 0) { // Is it NOT the first option of the select && are there elements in the dropdown? - liIndex++; - mainElements.push( - generateLI( - false, - classNames.DIVIDER, - optID + 'div' - ) - ); - mainData.push({ - type: 'divider', - optID: optID - }); - } - liIndex++; - - var labelElement = generateLabel({ - labelEscaped: labelEscaped, - labelSubtext: labelSubtext, - labelIcon: labelIcon - }); - - mainElements.push(generateLI(labelElement, 'dropdown-header' + optGroupClass, optID)); - mainData.push({ - content: labelEscaped, - subtext: labelSubtext, - type: 'optgroup-label', - optID: optID - }); - - headerIndex = liIndex - 1; - } - - if (that.options.hideDisabled && isDisabled || thisData.hidden === true) { - liIndex--; - return; - } - - textElement = generateText({ - text: text, - optionContent: optionContent, - optionSubtext: subtext, - optionIcon: icon - }); - - mainElements.push(generateLI(generateA(textElement, 'opt ' + optionClass + optGroupClass, inline), '', optID)); - mainData.push({ - content: optionContent || text, - subtext: subtext, - tokens: tokens, - type: 'option', - optID: optID, - headerIndex: headerIndex, - lastIndex: headerIndex + parent.childElementCount, - originalIndex: index, - data: thisData - }); - - availableOptionsCount++; - } else if (thisData.divider === true) { - mainElements.push(generateLI(false, classNames.DIVIDER)); - mainData.push({ - type: 'divider', - originalIndex: index, - data: thisData - }); - } else { - // if previous element is not an optgroup and hideDisabled is true - if (!showDivider && that.options.hideDisabled) { - prevHiddenIndex = thisData.prevHiddenIndex; - - if (prevHiddenIndex !== undefined) { - // select the element **before** the first hidden element in the group - var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling; - - if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) { - showDivider = true; - } - } - } - - if (showDivider && mainData[mainData.length - 1].type !== 'divider') { - liIndex++; - mainElements.push( - generateLI( - false, - classNames.DIVIDER, - optID + 'div' - ) - ); - mainData.push({ - type: 'divider', - optID: optID - }); - } - - textElement = generateText({ - text: text, - optionContent: optionContent, - optionSubtext: subtext, - optionIcon: icon - }); - - mainElements.push(generateLI(generateA(textElement, optionClass, inline))); - mainData.push({ - content: optionContent || text, - subtext: subtext, - tokens: tokens, - type: 'option', - originalIndex: index, - data: thisData - }); - - availableOptionsCount++; - } - - that.selectpicker.main.map.newIndex[index] = liIndex; - that.selectpicker.main.map.originalIndex[liIndex] = index; - - // get the most recent option info added to mainData - var _mainDataLast = mainData[mainData.length - 1]; - - _mainDataLast.disabled = isDisabled; - - var combinedLength = 0; - - // count the number of characters in the option - not perfect, but should work in most cases - if (_mainDataLast.content) combinedLength += _mainDataLast.content.length; - if (_mainDataLast.subtext) combinedLength += _mainDataLast.subtext.length; - // if there is an icon, ensure this option's width is checked - if (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 - widestOption = mainElements[mainElements.length - 1]; - } - }); - - this.selectpicker.main.elements = mainElements; - this.selectpicker.main.data = mainData; - - this.selectpicker.current = this.selectpicker.main; - - this.selectpicker.view.widestOption = widestOption; - this.selectpicker.view.availableOptionsCount = availableOptionsCount; // faster way to get # of available options without filter - }, - - findLis: function () { - return this.$menuInner.find('.inner > li'); - }, - - render: function () { - var that = this, - $selectOptions = this.$element.find('option'), - selectedItems = [], - selectedItemsInTitle = []; - - this.togglePlaceholder(); - - this.tabIndex(); - - for (var i = 0, len = this.selectpicker.main.elements.length; i < len; i++) { - var index = this.selectpicker.main.map.originalIndex[i], - option = $selectOptions[index]; - - if (option && option.selected) { - selectedItems.push(option); - - if (selectedItemsInTitle.length < 100 && that.options.selectedTextFormat !== 'count' || selectedItems.length === 1) { - if (that.options.hideDisabled && (option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled)) return; - - var thisData = this.selectpicker.main.data[i].data, - icon = thisData.icon && that.options.showIcon ? ' ' : '', - subtext, - titleItem; - - if (that.options.showSubtext && thisData.subtext && !that.multiple) { - subtext = ' ' + thisData.subtext + ''; - } else { - subtext = ''; - } - - if (option.title) { - titleItem = option.title; - } else if (thisData.content && that.options.showContent) { - titleItem = thisData.content.toString(); - } else { - titleItem = icon + option.innerHTML.trim() + subtext; - } - - selectedItemsInTitle.push(titleItem); - } - } - } - - //Fixes issue in IE10 occurring when no default option is selected and at least one option is disabled - //Convert all the values into a comma delimited string - var title = !this.multiple ? selectedItemsInTitle[0] : selectedItemsInTitle.join(this.options.multipleSeparator); - - // add ellipsis - if (selectedItems.length > 50) title += '...'; - - // If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected etc.. - if (this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1) { - var max = this.options.selectedTextFormat.split('>'); - - if ((max.length > 1 && selectedItems.length > max[1]) || (max.length === 1 && selectedItems.length >= 2)) { - var totalCount = this.selectpicker.view.availableOptionsCount, - tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedItems.length, totalCount) : this.options.countSelectedText; - - title = tr8nText.replace('{0}', selectedItems.length.toString()).replace('{1}', totalCount.toString()); - } - } - - if (this.options.title == undefined) { - // use .attr to ensure undefined is returned if title attribute is not set - this.options.title = this.$element.attr('title'); - } - - if (this.options.selectedTextFormat == 'static') { - title = this.options.title; - } - - //If we dont have a title, then use the default, or if nothing is set at all, use the not selected text - if (!title) { - title = typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneSelectedText; - } - - //strip all HTML tags and trim the result, then unescape any escaped tags - this.$button[0].title = htmlUnescape(title.replace(/<[^>]*>?/g, '').trim()); - this.$button.find('.filter-option-inner-inner')[0].innerHTML = title; - - this.$element.trigger('rendered.bs.select'); - }, - - /** - * @param [style] - * @param [status] - */ - setStyle: function (style, status) { - if (this.$element.attr('class')) { - this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); - } - - var buttonClass = style ? style : this.options.style; - - if (status == 'add') { - this.$button.addClass(buttonClass); - } else if (status == 'remove') { - this.$button.removeClass(buttonClass); - } else { - this.$button.removeClass(this.options.style); - this.$button.addClass(buttonClass); - } - }, - - liHeight: function (refresh) { - if (!refresh && (this.options.size === false || this.sizeInfo)) return; - - if (!this.sizeInfo) this.sizeInfo = {}; - - var newElement = document.createElement('div'), - menu = document.createElement('div'), - menuInner = document.createElement('div'), - menuInnerInner = document.createElement('ul'), - divider = document.createElement('li'), - dropdownHeader = document.createElement('li'), - li = document.createElement('li'), - a = document.createElement('a'), - text = document.createElement('span'), - header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null, - search = this.options.liveSearch ? document.createElement('div') : 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.find('option')[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 = this.sizeInfo.selectWidth + 'px'; - 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')); - 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 = a.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), - // fall back to jQuery if getComputedStyle is not supported - menuStyle = window.getComputedStyle ? window.getComputedStyle(menu) : false, - menuWidth = menu.offsetWidth, - $menu = menuStyle ? null : $(menu), - menuPadding = { - vert: toInteger(menuStyle ? menuStyle.paddingTop : $menu.css('paddingTop')) + - toInteger(menuStyle ? menuStyle.paddingBottom : $menu.css('paddingBottom')) + - toInteger(menuStyle ? menuStyle.borderTopWidth : $menu.css('borderTopWidth')) + - toInteger(menuStyle ? menuStyle.borderBottomWidth : $menu.css('borderBottomWidth')), - horiz: toInteger(menuStyle ? menuStyle.paddingLeft : $menu.css('paddingLeft')) + - toInteger(menuStyle ? menuStyle.paddingRight : $menu.css('paddingRight')) + - toInteger(menuStyle ? menuStyle.borderLeftWidth : $menu.css('borderLeftWidth')) + - toInteger(menuStyle ? menuStyle.borderRightWidth : $menu.css('borderRightWidth')) - }, - menuExtras = { - vert: menuPadding.vert + - toInteger(menuStyle ? menuStyle.marginTop : $menu.css('marginTop')) + - toInteger(menuStyle ? menuStyle.marginBottom : $menu.css('marginBottom')) + 2, - horiz: menuPadding.horiz + - toInteger(menuStyle ? menuStyle.marginLeft : $menu.css('marginLeft')) + - toInteger(menuStyle ? menuStyle.marginRight : $menu.css('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.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.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; - - 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.elements.length + menuPadding.vert; - this.$newElement.toggleClass(classNames.DROPUP, this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot); - } - - if (this.options.size === 'auto') { - _minHeight = this.selectpicker.current.elements.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 = ''; - } - - if (this.options.dropdownAlignRight === 'auto') { - this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.$menu[0].offsetWidth - selectWidth)); - } - - this.$menu.css({ - 'max-height': maxHeight + 'px', - 'overflow': 'hidden', - 'min-height': minHeight + 'px' - }); - - this.$menuInner.css({ - 'max-height': menuInnerHeight + 'px', - 'overflow-y': 'auto', - 'min-height': menuInnerMinHeight + 'px' - }); - - this.sizeInfo['menuInnerHeight'] = menuInnerHeight; - - 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; - - this.$menu.css('min-width', this.sizeInfo.totalMenuWidth); - } - - 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) return; - - var that = this, - $window = $(window), - selectedIndex, - offset = 0; - - this.setMenuSize(); - - if (this.options.size === 'auto') { - this.$searchbox.off('input.setMenuSize propertychange.setMenuSize').on('input.setMenuSize propertychange.setMenuSize', function() { - return that.setMenuSize(); - }); - $window.off('resize.setMenuSize scroll.setMenuSize').on('resize.setMenuSize scroll.setMenuSize', function() { - return that.setMenuSize(); - }); - } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { - this.$searchbox.off('input.setMenuSize propertychange.setMenuSize'); - $window.off('resize.setMenuSize scroll.setMenuSize'); - } - - if (refresh) { - offset = this.$menuInner[0].scrollTop; - } else if (!that.multiple) { - selectedIndex = that.selectpicker.main.map.newIndex[that.$element[0].selectedIndex]; - - if (typeof selectedIndex === 'number' && that.options.size !== false) { - offset = that.sizeInfo.liHeight * selectedIndex; - offset = offset - (that.sizeInfo.menuInnerHeight / 2) + (that.sizeInfo.liHeight / 2); - } - } - - that.createView(false, offset); - }, - - setWidth: function () { - var that = this; - - if (this.options.width === 'auto') { - requestAnimationFrame(function() { - that.$menu.css('min-width', '0'); - 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.removeClass('fit-width'); - } - }, - - selectPosition: function () { - this.$bsContainer = $('
    '); - - 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).on('resize scroll', function () { - getPlacement(that.$newElement); - }); - - this.$element.on('hide.bs.select', function () { - that.$menu.data('height', that.$menu.height()); - that.$bsContainer.detach(); - }); - }, - - setOptionStatus: function () { - var that = this, - $selectOptions = this.$element.find('option'); - - 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 index = that.selectpicker.current.map.originalIndex[i + that.selectpicker.view.position0], // faster than $(li).data('originalIndex') - option = $selectOptions[index]; - - if (option) { - var liIndex = this.selectpicker.main.map.newIndex[index], - li = this.selectpicker.main.elements[liIndex]; - - that.setDisabled( - index, - option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled, - liIndex, - li - ); - - that.setSelected( - index, - option.selected, - liIndex, - li - ); - } - } - } - }, - - /** - * @param {number} index - the index of the option that is being changed - * @param {boolean} selected - true if the option is being selected, false if being deselected - */ - setSelected: function (index, selected, liIndex, li) { - var activeIndexIsSet = this.activeIndex !== undefined, - thisIsActive = this.activeIndex === index, - prevActiveIndex, - prevActive, - a, - // if current option is already active - // OR - // if the current option is being selected, it's NOT multiple, and - // activeIndex 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 activeIndex) - keepActive = thisIsActive || selected && !this.multiple && !activeIndexIsSet; - - if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index]; - if (!li) li = this.selectpicker.main.elements[liIndex]; - - a = li.firstChild; - - if (selected) { - this.selectedIndex = index; - } - - li.classList.toggle('selected', selected); - li.classList.toggle('active', keepActive); - - if (keepActive) { - this.selectpicker.view.currentActive = li; - this.activeIndex = index; - } - - if (a) { - a.classList.toggle('selected', selected); - a.classList.toggle('active', keepActive); - a.setAttribute('aria-selected', selected); - } - - if (!keepActive) { - if (!activeIndexIsSet && selected && this.prevActiveIndex !== undefined) { - prevActiveIndex = this.selectpicker.main.map.newIndex[this.prevActiveIndex]; - prevActive = this.selectpicker.main.elements[prevActiveIndex]; - - prevActive.classList.remove('active'); - if (prevActive.firstChild) { - prevActive.firstChild.classList.remove('active'); - } - } - } - }, - - /** - * @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 (index, disabled, liIndex, li) { - var a; - - if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index]; - if (!li) li = this.selectpicker.main.elements[liIndex]; - - a = li.firstChild; - - li.classList.toggle(classNames.DISABLED, disabled); - - if (a) { - if (version.major === '4') a.classList.toggle(classNames.DISABLED, disabled); - - a.setAttribute('aria-disabled', disabled); - - if (disabled) { - a.setAttribute('tabindex', -1); - } else { - a.setAttribute('tabindex', 0); - } - } - }, - - isDisabled: function () { - return this.$element[0].disabled; - }, - - checkDisabled: function () { - var that = this; - - if (this.isDisabled()) { - this.$newElement.addClass(classNames.DISABLED); - this.$button.addClass(classNames.DISABLED).attr('tabindex', -1).attr('aria-disabled', true); - } else { - if (this.$button.hasClass(classNames.DISABLED)) { - this.$newElement.removeClass(classNames.DISABLED); - this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false); - } - - if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) { - this.$button.removeAttr('tabindex'); - } - } - - this.$button.click(function () { - return !that.isDisabled(); - }); - }, - - togglePlaceholder: function () { - // much faster than calling $.val() - var element = this.$element[0], - selectedIndex = element.selectedIndex, - nothingSelected = selectedIndex === -1; - - if (!nothingSelected && !element.options[selectedIndex].value) nothingSelected = true; - - this.$button.toggleClass('bs-placeholder', nothingSelected); - }, - - tabIndex: function () { - if (this.$element.data('tabindex') !== this.$element.attr('tabindex') && - (this.$element.attr('tabindex') !== -98 && this.$element.attr('tabindex') !== '-98')) { - this.$element.data('tabindex', this.$element.attr('tabindex')); - this.$button.attr('tabindex', this.$element.data('tabindex')); - } - - this.$element.attr('tabindex', -98); - }, - - 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 (version.major > 3 && !that.dropdown) { - that.dropdown = that.$button.data('bs.dropdown'); - that.dropdown._menu = that.$menu[0]; - } - }); - - this.$button.on('click.bs.dropdown.data-api', function () { - if (!that.$newElement.hasClass(classNames.SHOW)) { - that.setSize(); - } - }); - - function setFocus () { - if (that.options.liveSearch) { - that.$searchbox.focus(); - } else { - that.$menuInner.focus(); - } - } - - function checkPopperExists () { - if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state.isCreated) { - setFocus(); - } else { - requestAnimationFrame(checkPopperExists); - } - } - - this.$element.on('shown.bs.select', 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(); - } - }); - - this.$menuInner.on('click', 'li a', function (e, retainActive) { - var $this = $(this), - position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, - clickedIndex = that.selectpicker.current.map.originalIndex[$this.parent().index() + position0], - prevValue = getSelectValues(that.$element[0]), - prevIndex = that.$element.prop('selectedIndex'), - triggerChange = true; - - // Don't close on multi choice menu - if (that.multiple && that.options.maxOptions !== 1) { - e.stopPropagation(); - } - - e.preventDefault(); - - //Don't run if we have been disabled - if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) { - var $options = that.$element.find('option'), - $option = $options.eq(clickedIndex), - state = $option.prop('selected'), - $optgroup = $option.parent('optgroup'), - $optgroupOptions = $optgroup.find('option'), - maxOptions = that.options.maxOptions, - maxOptionsGrp = $optgroup.data('maxOptions') || false; - - if (clickedIndex === that.activeIndex) retainActive = true; - - if (!retainActive) { - that.prevActiveIndex = that.activeIndex; - that.activeIndex = undefined; - } - - if (!that.multiple) { // Deselect all others if not multi select box - $options.prop('selected', false); - $option.prop('selected', true); - that.setSelected(clickedIndex, true); - } else { // Toggle the one we have chosen if we are multi select. - $option.prop('selected', !state); - - that.setSelected(clickedIndex, !state); - $this.blur(); - - if (maxOptions !== false || maxOptionsGrp !== false) { - var maxReached = maxOptions < $options.filter(':selected').length, - maxReachedGrp = maxOptionsGrp < $optgroup.find('option:selected').length; - - if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) { - if (maxOptions && maxOptions == 1) { - $options.prop('selected', false); - $option.prop('selected', true); - - for (var i = 0; i < $options.length; i++) { - that.setSelected(i, false); - } - - that.setSelected(clickedIndex, true); - } else if (maxOptionsGrp && maxOptionsGrp == 1) { - $optgroup.find('option:selected').prop('selected', false); - $option.prop('selected', true); - - for (var i = 0; i < $optgroupOptions.length; i++) { - var option = $optgroupOptions[i]; - that.setSelected($options.index(option), false); - } - - that.setSelected(clickedIndex, 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 = $('
    '); - // 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]); - } - - $option.prop('selected', false); - - that.$menu.append($notify); - - if (maxOptions && maxReached) { - $notify.append($('
    ' + maxTxt + '
    ')); - triggerChange = false; - that.$element.trigger('maxReached.bs.select'); - } - - if (maxOptionsGrp && maxReachedGrp) { - $notify.append($('
    ' + maxTxtGrp + '
    ')); - triggerChange = false; - that.$element.trigger('maxReachedGrp.bs.select'); - } - - setTimeout(function () { - that.setSelected(clickedIndex, false); - }, 10); - - $notify.delay(750).fadeOut(300, function () { - $(this).remove(); - }); - } - } - } - } - - if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) { - that.$button.focus(); - } else if (that.options.liveSearch) { - that.$searchbox.focus(); - } - - // Trigger select 'change' - if (triggerChange) { - if ((prevValue != getSelectValues(that.$element[0]) && that.multiple) || (prevIndex != that.$element.prop('selectedIndex') && !that.multiple)) { - // $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed. - changed_arguments = [clickedIndex, $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.focus(); - } else { - that.$button.focus(); - } - } - }); - - this.$menuInner.on('click', '.divider, .dropdown-header', function (e) { - e.preventDefault(); - e.stopPropagation(); - if (that.options.liveSearch) { - that.$searchbox.focus(); - } else { - that.$button.focus(); - } - }); - - this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () { - that.$button.click(); - }); - - this.$searchbox.on('click', function (e) { - e.stopPropagation(); - }); - - this.$menu.on('click', '.actions-btn', function (e) { - if (that.options.liveSearch) { - that.$searchbox.focus(); - } else { - that.$button.focus(); - } - - e.preventDefault(); - e.stopPropagation(); - - if ($(this).hasClass('bs-select-all')) { - that.selectAll(); - } else { - that.deselectAll(); - } - }); - - this.$element.on({ - 'change': function () { - that.render(); - that.$element.trigger('changed.bs.select', changed_arguments); - changed_arguments = null; - }, - 'focus': function () { - if (!that.options.mobile) that.$button.focus(); - } - }); - }, - - liveSearchListener: function () { - var that = this, - no_results = document.createElement('li'); - - this.$button.on('click.bs.dropdown.data-api', function () { - if (!!that.$searchbox.val()) { - that.$searchbox.val(''); - } - }); - - 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.val(); - - that.selectpicker.search.map.newIndex = {}; - that.selectpicker.search.map.originalIndex = {}; - that.selectpicker.search.elements = []; - that.selectpicker.search.data = []; - - if (searchValue) { - var i, - searchMatch = [], - q = searchValue.toUpperCase(), - cache = {}, - cacheArr = [], - searchStyle = that._searchStyle(), - normalizeSearch = that.options.liveSearchNormalize; - - that._$lisSelected = that.$menuInner.find('.selected'); - - 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]); - - if (li.hasOwnProperty('originalIndex')) { - that.selectpicker.search.map.newIndex[li.originalIndex] = searchMatch.length - 1; - that.selectpicker.search.map.originalIndex[searchMatch.length - 1] = li.originalIndex; - } - } - } - - that.activeIndex = undefined; - that.noScroll = true; - that.$menuInner.scrollTop(0); - that.selectpicker.search.elements = searchMatch; - that.createView(true); - - if (!searchMatch.length) { - no_results.className = 'no-results'; - no_results.innerHTML = that.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); - that.$menuInner[0].firstChild.appendChild(no_results); - } - } else { - that.$menuInner.scrollTop(0); - that.createView(false); - } - }); - }, - - _searchStyle: function () { - return this.options.liveSearchStyle || 'contains'; - }, - - val: function (value) { - if (typeof value !== 'undefined') { - this.$element.val(value); - this.render(); - this.$element.trigger('changed.bs.select', changed_arguments); - changed_arguments = null; - - return this.$element; - } else { - return this.$element.val(); - } - }, - - changeAll: function (status) { - if (!this.multiple) return; - if (typeof status === 'undefined') status = true; - - var $selectOptions = this.$element.find('option'), - previousSelected = 0, - currentSelected = 0, - prevValue = getSelectValues(this.$element[0]); - - this.$element.addClass('bs-select-hidden'); - - for (var i = 0; i < this.selectpicker.current.elements.length; i++) { - var liData = this.selectpicker.current.data[i], - index = this.selectpicker.current.map.originalIndex[i], // faster than $(li).data('originalIndex') - option = $selectOptions[index]; - - if (option && !option.disabled && liData.type !== 'divider') { - if (option.selected) previousSelected++; - option.selected = status; - if (option.selected) currentSelected++; - } - } - - this.$element.removeClass('bs-select-hidden'); - - if (previousSelected === currentSelected) return; - - this.setOptionStatus(); - - this.togglePlaceholder(); - - changed_arguments = [null, null, prevValue]; - - this.$element - .triggerNative('change'); - }, - - selectAll: function () { - return this.changeAll(true); - }, - - deselectAll: function () { - return this.changeAll(false); - }, - - toggle: function (e) { - e = e || window.event; - - if (e) e.stopPropagation(); - - this.$button.trigger('click.bs.dropdown.data-api'); - }, - - 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; - - isActive = that.$newElement.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 (e.which === keyCodes.ESCAPE && isActive) { - e.preventDefault(); - that.$button.trigger('click.bs.dropdown.data-api').focus(); - } - - if (isArrowKey) { // if up or down - if (!$items.length) return; - - // $items.index/.filter is too slow with a large list and no virtual scroll - index = isVirtual === true ? $items.index($items.filter('.active')) : that.selectpicker.current.map.newIndex[that.activeIndex]; - - if (index === undefined) index = -1; - - if (index !== -1) { - liActive = that.selectpicker.current.elements[index + position0]; - liActive.classList.remove('active'); - if (liActive.firstChild) liActive.firstChild.classList.remove('active'); - } - - 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 = 0; - - 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]; - 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 === 0) { - that.$menuInner[0].scrollTop = 0; - - liActiveIndex = 0; - } else { - activeLi = that.selectpicker.current.data[liActiveIndex]; - offset = activeLi.position - that.sizeInfo.menuInnerHeight; - - updateScroll = offset > scrollTop; - } - } - - liActive = that.selectpicker.current.elements[liActiveIndex]; - - if (liActive) { - liActive.classList.add('active'); - if (liActive.firstChild) liActive.firstChild.classList.add('active'); - } - - that.activeIndex = that.selectpicker.current.map.originalIndex[liActiveIndex]; - - that.selectpicker.view.currentActive = liActive; - - if (updateScroll) that.$menuInner[0].scrollTop = offset; - - if (that.options.liveSearch) { - that.$searchbox.focus(); - } else { - $this.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]) { - li.index = i; - matches.push(li.originalIndex); - } - } - - 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.activeIndex); - - if (matchIndex === -1 || matchIndex === matches.length - 1) { - matchIndex = 0; - } else { - matchIndex++; - } - } - - searchMatch = that.selectpicker.current.map.newIndex[matches[matchIndex]]; - - activeLi = that.selectpicker.current.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.current.elements[searchMatch]; - liActive.classList.add('active'); - if (liActive.firstChild) liActive.firstChild.classList.add('active'); - that.activeIndex = matches[matchIndex]; - - liActive.firstChild.focus(); - - if (updateScroll) that.$menuInner[0].scrollTop = offset; - - $this.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.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); - } - } - } - }, - - mobile: function () { - this.$element.addClass('mobile-device'); - }, - - refresh: function () { - // update options if data attributes have been changed - var config = $.extend({}, this.options, this.$element.data()); - this.options = config; - - this.selectpicker.main.map.newIndex = {}; - this.selectpicker.main.map.originalIndex = {}; - this.createLi(); - this.checkDisabled(); - this.render(); - this.setStyle(); - this.setWidth(); - - this.setSize(true); - - this.$element.trigger('refreshed.bs.select'); - }, - - 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(); - } - - this.$element - .off('.bs.select') - .removeData('selectpicker') - .removeClass('bs-select-hidden selectpicker'); - } - }; - - // 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 = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); - } - // fall back to use BootstrapVersion - catch(err) { - version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.'); - } - - version.major = version.full[0]; - version.success = true; - - if (version.major === '4') { - classNames.DIVIDER = 'dropdown-divider'; - classNames.SHOW = 'show'; - classNames.BUTTONCLASS = 'btn-light'; - Selectpicker.DEFAULTS.style = classNames.BUTTONCLASS = 'btn-light'; - classNames.POPOVERHEADER = 'popover-header'; - } - } - - var value; - var chain = this.each(function () { - var $this = $(this); - if ($this.is('select')) { - var data = $this.data('selectpicker'), - options = typeof _option == 'object' && _option; - - if (!data) { - var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, $this.data(), options); - config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), $this.data().template, options.template); - $this.data('selectpicker', (data = new Selectpicker(this, config))); - } else if (options) { - for (var i in options) { - if (options.hasOwnProperty(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; - }; - - $(document) - .off('keydown.bs.dropdown.data-api') - .on('keydown.bs.select', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', Selectpicker.prototype.keydown) - .on('focusin.modal', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', function (e) { - e.stopPropagation(); - }); - - // SELECTPICKER DATA-API - // ===================== - $(window).on('load.bs.select.data-api', function () { - $('.selectpicker').each(function () { - var $selectpicker = $(this); - Plugin.call($selectpicker, $selectpicker.data()); - }) - }); -})(jQuery); +(function ($) { + 'use strict'; + + var testElement = document.createElement('_'); + + 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); + } + }; + } + + // shallow array comparison + function isEqual (array1, array2) { + return array1.length === array2.length && array1.every(function(element, index) { + return element === array2[index]; + }); + }; + + // + if (!String.prototype.startsWith) { + (function () { + 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` + var defineProperty = (function () { + // IE 8 only supports `Object.defineProperty` on DOM elements + try { + var object = {}; + var $defineProperty = Object.defineProperty; + var result = $defineProperty(object, object, object) && $defineProperty; + } catch (error) { + } + return result; + }()); + 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 (defineProperty) { + defineProperty(String.prototype, 'startsWith', { + 'value': startsWith, + 'configurable': true, + 'writable': true + }); + } else { + String.prototype.startsWith = startsWith; + } + }()); + } + + if (!Object.keys) { + Object.keys = function ( + o, // object + k, // key + r // result array + ){ + // initialize object and result + r=[]; + // iterate over object keys + for (k in o) + // fill result array with non-prototypical keys + r.hasOwnProperty.call(o, k) && r.push(k); + // return result + return r; + }; + } + + // much faster than $.val() + function getSelectValues(select) { + var result = []; + var options = select && select.options; + var opt; + + if (select.multiple) { + for (var i = 0, len = options.length; i < len; i++) { + opt = options[i]; + + if (opt.selected) { + result.push(opt.value || opt.text); + } + } + } else { + result = select.value; + } + + return result; + } + + // 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 changed_arguments = 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); + } else if (el.fireEvent) { // for IE8 + event = document.createEventObject(); + event.eventType = eventName; + el.fireEvent('on' + eventName, event); + } else { + // fall back to jQuery.trigger + this.trigger(eventName); + } + }; + // + + function stringSearch(li, searchString, method, normalize) { + var stringTypes = [ + 'content', + '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 === 'content') { + string = string.replace(/<[^>]+>/g, ''); + } + + if (normalize) string = normalizeToBase(string); + string = string.toUpperCase(); + + if (method === 'contains') { + searchSuccess = string.indexOf(searchString) >= 0; + } else { + searchSuccess = string.startsWith(searchString); + } + + if (searchSuccess) break; + } + } + + return searchSuccess; + } + + function toInteger(value) { + return parseInt(value, 10) || 0; + } + + /** + * Remove all diatrics from the given text. + * @access private + * @param {String} text + * @returns {String} + */ + function normalizeToBase(text) { + var rExps = [ + {re: /[\xC0-\xC6]/g, ch: "A"}, + {re: /[\xE0-\xE6]/g, ch: "a"}, + {re: /[\xC8-\xCB]/g, ch: "E"}, + {re: /[\xE8-\xEB]/g, ch: "e"}, + {re: /[\xCC-\xCF]/g, ch: "I"}, + {re: /[\xEC-\xEF]/g, ch: "i"}, + {re: /[\xD2-\xD6]/g, ch: "O"}, + {re: /[\xF2-\xF6]/g, ch: "o"}, + {re: /[\xD9-\xDC]/g, ch: "U"}, + {re: /[\xF9-\xFC]/g, ch: "u"}, + {re: /[\xC7-\xE7]/g, ch: "c"}, + {re: /[\xD1]/g, ch: "N"}, + {re: /[\xF1]/g, ch: "n"} + ]; + $.each(rExps, function () { + text = text ? text.replace(this.re, this.ch) : ''; + }); + return text; + } + + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + var unescapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '`': '`' + }; + + // 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); + var htmlUnescape = createEscaper(unescapeMap); + + /** + * ------------------------------------------------------------------------ + * 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 version = { + success: false, + major: '3' + }; + + try { + version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); + version.major = version.full[0]; + version.success = true; + } + catch(err) { + 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); + } + + 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' + } + + var Selector = { + MENU: '.' + classNames.MENU + } + + if (version.major === '4') { + classNames.DIVIDER = 'dropdown-divider'; + classNames.SHOW = 'show'; + classNames.BUTTONCLASS = 'btn-light'; + classNames.POPOVERHEADER = 'popover-header'; + } + + var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); + var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); + var REGEXP_ENTER_OR_SPACE = new RegExp(keyCodes.ENTER + '|' + keyCodes.SPACE); + + 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: { + // store originalIndex (key) and newIndex (value) in this.selectpicker.main.map.newIndex for fast accessibility + // allows us to do this.main.elements[this.selectpicker.main.map.newIndex[index]] to select an element based on the originalIndex + map: { + newIndex: {}, + originalIndex: {} + } + }, + current: { + map: {} + }, // current changes if a search is in progress + search: { + map: {} + }, + view: {}, + keydown: { + keyHistory: '', + resetKeyHistory: { + start: function () { + return setTimeout(function () { + that.selectpicker.keydown.keyHistory = ''; + }, 800); + } + } + } + }; + // If we have no title yet, try to pull it from the html title attribute (jQuery doesnt' pick it up as it's not a + // data-attribute) + if (this.options.title === null) { + this.options.title = this.$element.attr('title'); + } + + // 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.13.3'; + + Selectpicker.BootstrapVersion = version.major; + + // 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', + doneButton: false, + doneButtonText: 'Close', + multipleSeparator: ', ', + styleBase: 'btn', + style: classNames.BUTTONCLASS, + size: 'auto', + title: null, + 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: 'glyphicon', + tickIcon: 'glyphicon-ok', + showTick: false, + template: { + caret: '' + }, + maxOptions: false, + mobile: false, + selectOnTab: false, + dropdownAlignRight: false, + windowPadding: 0, + virtualScroll: 600, + display: false + }; + + if (version.major === '4') { + Selectpicker.DEFAULTS.style = 'btn-light'; + Selectpicker.DEFAULTS.iconBase = ''; + Selectpicker.DEFAULTS.tickIcon = 'bs-ok-default'; + } + + Selectpicker.prototype = { + + constructor: Selectpicker, + + init: function () { + var that = this, + id = this.$element.attr('id'); + + this.$element.addClass('bs-select-hidden'); + + this.multiple = this.$element.prop('multiple'); + this.autofocus = this.$element.prop('autofocus'); + this.$newElement = this.createDropdown(); + this.createLi(); + this.$element + .after(this.$newElement) + .prependTo(this.$newElement); + this.$button = this.$newElement.children('button'); + this.$menu = this.$newElement.children(Selector.MENU); + this.$menuInner = this.$menu.children('.inner'); + this.$searchbox = this.$menu.find('input'); + + this.$element.removeClass('bs-select-hidden'); + + if (this.options.dropdownAlignRight === true) this.$menu.addClass(classNames.MENURIGHT); + + if (typeof id !== 'undefined') { + this.$button.attr('data-id', id); + } + + this.checkDisabled(); + this.clickListener(); + if (this.options.liveSearch) this.liveSearchListener(); + this.render(); + this.setStyle(); + this.setWidth(); + if (this.options.container) { + this.selectPosition(); + } else { + this.$element.on('hide.bs.select', 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.$menuInner.attr('aria-expanded', false); + that.$element.trigger('hide.bs.select', e); + }, + 'hidden.bs.dropdown': function (e) { + that.$element.trigger('hidden.bs.select', e); + }, + 'show.bs.dropdown': function (e) { + that.$menuInner.attr('aria-expanded', true); + that.$element.trigger('show.bs.select', e); + }, + 'shown.bs.dropdown': function (e) { + that.$element.trigger('shown.bs.select', e); + } + }); + + if (that.$element[0].hasAttribute('required')) { + this.$element.on('invalid', function () { + that.$button.addClass('bs-invalid'); + + that.$element.on({ + 'shown.bs.select.invalid': function () { + that.$element + .val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened + .off('shown.bs.select.invalid'); + }, + 'rendered.bs.select': function () { + // if select is no longer invalid, remove the bs-invalid class + if (this.validity.valid) that.$button.removeClass('bs-invalid'); + that.$element.off('rendered.bs.select'); + } + }); + + that.$button.on('blur.bs.select', function () { + that.$element.focus().blur(); + that.$button.off('blur.bs.select'); + }); + }); + } + + setTimeout(function () { + that.$element.trigger('loaded.bs.select'); + }); + }, + + 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' : '', + autofocus = this.autofocus ? ' autofocus' : ''; + + // Elements + var drop, + header = '', + searchbox = '', + actionsbox = '', + donebutton = ''; + + if (this.options.header) { + header = + '
    ' + + '' + + this.options.header + + '
    '; + } + + if (this.options.liveSearch) { + searchbox = + ''; + } + + if (this.multiple && this.options.actionsBox) { + actionsbox = + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + } + + if (this.multiple && this.options.doneButton) { + donebutton = + '
    ' + + '
    ' + + '' + + '
    ' + + '
    '; + } + + drop = + ''; + + return $(drop); + }, + + setPositionData: function () { + this.selectpicker.view.canHighlight = []; + + 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); + + li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height; + } + }, + + isVirtual: function () { + return (this.options.virtualScroll !== false) && this.selectpicker.main.elements.length >= this.options.virtualScroll || this.options.virtualScroll === true; + }, + + createView: function (isSearching, scrollTop) { + scrollTop = scrollTop || 0; + + var that = this; + + this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main; + + var $lis; + var active = []; + var selected; + var prevActive; + var activeIndex; + var prevActiveIndex; + + this.setPositionData(); + + 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.elements.length, + chunks = [], + chunkSize, + chunkCount, + firstChunk, + lastChunk, + currentChunk = undefined, + prevPositions, + positionIsDifferent, + previousElements, + menuIsDifferent = true, + isVirtual = that.isVirtual(); + + that.selectpicker.view.scrollTop = scrollTop; + + if (isVirtual === true) { + // if an option that is encountered that is wider than the current menu width, update the menu width accordingly + if (that.sizeInfo.hasScrollBar && that.$menu[0].offsetWidth > that.sizeInfo.totalMenuWidth) { + that.sizeInfo.menuWidth = that.$menu[0].offsetWidth; + that.sizeInfo.totalMenuWidth = that.sizeInfo.menuWidth + that.sizeInfo.scrollBarWidth; + that.$menu.css('min-width', that.sizeInfo.menuWidth); + } + } + + chunkSize = Math.ceil(that.sizeInfo.menuInnerHeight / that.sizeInfo.liHeight * 1.5); // number of options in a chunk + chunkCount = Math.round(size / chunkSize) || 1; // number of chunks + + for (var i = 0; i < chunkCount; i++) { + var end_of_chunk = (i + 1) * chunkSize; + + if (i === chunkCount - 1) { + end_of_chunk = size; + } + + chunks[i] = [ + (i) * chunkSize + (!i ? 0 : 1), + end_of_chunk + ]; + + if (!size) break; + + if (currentChunk === undefined && scrollTop <= that.selectpicker.current.data[end_of_chunk - 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 = Math.max(0, chunks[firstChunk][0]) || 0; + that.selectpicker.view.position1 = Math.min(size, chunks[lastChunk][1]) || 0; + + positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1; + + if (that.activeIndex !== undefined) { + prevActive = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.prevActiveIndex]]; + active = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.activeIndex]]; + selected = that.selectpicker.current.elements[that.selectpicker.current.map.newIndex[that.selectedIndex]]; + + if (init) { + if (that.activeIndex !== that.selectedIndex) { + active.classList.remove('active'); + if (active.firstChild) active.firstChild.classList.remove('active'); + } + that.activeIndex = undefined; + } + + if (that.activeIndex && that.activeIndex !== that.selectedIndex && selected && selected.length) { + selected.classList.remove('active'); + if (selected.firstChild) selected.firstChild.classList.remove('active'); + } + } + + if (that.prevActiveIndex !== undefined && that.prevActiveIndex !== that.activeIndex && that.prevActiveIndex !== that.selectedIndex && prevActive && prevActive.length) { + prevActive.classList.remove('active'); + if (prevActive.firstChild) prevActive.firstChild.classList.remove('active'); + } + + if (init || positionIsDifferent) { + previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : []; + + 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 = isVirtual === true ? that.selectpicker.view.visibleElements : that.selectpicker.current.elements; + + // 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++) { + menuFragment.appendChild(elements[i]); + } + + 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'; + } + + menuInner.firstChild.appendChild(menuFragment); + } + } + + that.prevActiveIndex = that.activeIndex; + + if (!that.options.liveSearch) { + that.$menuInner.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]; + + if (that.selectpicker.view.currentActive) { + that.selectpicker.view.currentActive.classList.remove('active'); + if (that.selectpicker.view.currentActive.firstChild) that.selectpicker.view.currentActive.firstChild.classList.remove('active'); + } + + if (newActive) { + newActive.classList.add('active'); + if (newActive.firstChild) newActive.firstChild.classList.add('active'); + } + + that.activeIndex = that.selectpicker.current.map.originalIndex[index]; + } + } + + $(window).off('resize.createView').on('resize.createView', function () { + scroll(that.$menuInner[0].scrollTop); + }); + }, + + createLi: function () { + var that = this, + mainElements = [], + widestOption, + availableOptionsCount = 0, + widestOptionLength = 0, + mainData = [], + optID = 0, + headerIndex = 0, + liIndex = -1; // increment liIndex whenever a new
  • element is created to ensure newIndex is correct + + if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option'); + + var elementTemplates = { + span: document.createElement('span'), + subtext: document.createElement('small'), + a: document.createElement('a'), + li: document.createElement('li'), + whitespace: document.createTextNode("\u00A0") + }, + checkMark, + fragment = document.createDocumentFragment(); + + if (that.options.showTick || that.multiple) { + checkMark = elementTemplates.span.cloneNode(false); + checkMark.className = that.options.iconBase + ' ' + that.options.tickIcon + ' check-mark'; + elementTemplates.a.appendChild(checkMark); + } + + elementTemplates.a.setAttribute('role', 'option'); + + elementTemplates.subtext.className = 'text-muted'; + + elementTemplates.text = elementTemplates.span.cloneNode(false); + elementTemplates.text.className = 'text'; + + // Helper functions + /** + * @param content + * @param [classes] + * @param [optgroup] + * @returns {HTMLElement} + */ + var generateLI = 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' && null !== optgroup) li.classList.add('optgroup-' + optgroup); + + return li; + }; + + /** + * @param text + * @param [classes] + * @param [inline] + * @returns {string} + */ + var generateA = 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.className = classes; + if (version.major === '4') a.classList.add('dropdown-item'); + if (inline) a.setAttribute('style', inline); + + return a; + }; + + var generateText = function (options) { + var textElement = elementTemplates.text.cloneNode(false), + optionSubtextElement, + optionIconElement; + + if (options.optionContent) { + textElement.innerHTML = options.optionContent; + } else { + textElement.textContent = options.text; + + if (options.optionIcon) { + var whitespace = elementTemplates.whitespace.cloneNode(false); + + optionIconElement = elementTemplates.span.cloneNode(false); + optionIconElement.className = that.options.iconBase + ' ' + options.optionIcon; + + fragment.appendChild(optionIconElement); + fragment.appendChild(whitespace); + } + + if (options.optionSubtext) { + optionSubtextElement = elementTemplates.subtext.cloneNode(false); + optionSubtextElement.innerHTML = options.optionSubtext; + textElement.appendChild(optionSubtextElement); + } + } + + fragment.appendChild(textElement); + + return fragment; + }; + + var generateLabel = function (options) { + var labelTextElement = elementTemplates.text.cloneNode(false), + labelSubtextElement, + labelIconElement; + + labelTextElement.innerHTML = options.labelEscaped; + + if (options.labelIcon) { + var whitespace = elementTemplates.whitespace.cloneNode(false); + + labelIconElement = elementTemplates.span.cloneNode(false); + labelIconElement.className = that.options.iconBase + ' ' + options.labelIcon; + + fragment.appendChild(labelIconElement); + fragment.appendChild(whitespace); + } + + if (options.labelSubtext) { + labelSubtextElement = elementTemplates.subtext.cloneNode(false); + labelSubtextElement.textContent = options.labelSubtext; + labelTextElement.appendChild(labelSubtextElement); + } + + fragment.appendChild(labelTextElement); + + return fragment; + } + + if (this.options.title && !this.multiple) { + // this option doesn't create a new
  • element, but does add a new option, so liIndex is decreased + // since newIndex is recalculated on every refresh, liIndex needs to be decreased even if the titleOption is already appended + liIndex--; + + var element = this.$element[0], + isSelected = false, + titleNotAppended = !this.selectpicker.view.titleOption.parentNode; + + 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 + var $opt = $(element.options[element.selectedIndex]); + isSelected = $opt.attr('selected') === undefined && 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 (isSelected) element.selectedIndex = 0; + } + + var $selectOptions = this.$element.find('option'); + + $selectOptions.each(function (index) { + var $this = $(this); + + liIndex++; + + if ($this.hasClass('bs-title-option')) return; + + var thisData = $this.data(); + + // Get the class and text for the option + var optionClass = this.className || '', + inline = htmlEscape(this.style.cssText), + optionContent = thisData.content, + text = this.textContent, + tokens = thisData.tokens, + subtext = thisData.subtext, + icon = thisData.icon, + $parent = $this.parent(), + parent = $parent[0], + isOptgroup = parent.tagName === 'OPTGROUP', + isOptgroupDisabled = isOptgroup && parent.disabled, + isDisabled = this.disabled || isOptgroupDisabled, + prevHiddenIndex, + showDivider = this.previousElementSibling && this.previousElementSibling.tagName === 'OPTGROUP', + textElement; + + var parentData = $parent.data(); + + if (thisData.hidden === true || that.options.hideDisabled && (isDisabled && !isOptgroup || isOptgroupDisabled)) { + // set prevHiddenIndex - the index of the first hidden option in a group of hidden options + // used to determine whether or not a divider should be placed after an optgroup if there are + // hidden options between the optgroup and the first visible option + prevHiddenIndex = thisData.prevHiddenIndex; + $this.next().data('prevHiddenIndex', (prevHiddenIndex !== undefined ? prevHiddenIndex : index)); + + liIndex--; + + // if previous element is not an optgroup + if (!showDivider) { + if (prevHiddenIndex !== undefined) { + // select the element **before** the first hidden element in the group + var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling; + + if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) { + showDivider = true; + } + } + } + + if (showDivider && mainData[mainData.length - 1].type !== 'divider') { + liIndex++; + mainElements.push( + generateLI( + false, + classNames.DIVIDER, + optID + 'div' + ) + ); + mainData.push({ + type: 'divider', + optID: optID + }); + } + + return; + } + + if (isOptgroup && thisData.divider !== true) { + if (that.options.hideDisabled && isDisabled) { + if (parentData.allOptionsDisabled === undefined) { + var $options = $parent.children(); + $parent.data('allOptionsDisabled', $options.filter(':disabled').length === $options.length); + } + + if ($parent.data('allOptionsDisabled')) { + liIndex--; + return; + } + } + + var optGroupClass = ' ' + parent.className || ''; + + if (!this.previousElementSibling) { // Is it the first option of the optgroup? + optID += 1; + + // Get the opt group label + var label = parent.label, + labelEscaped = htmlEscape(label), + labelSubtext = parentData.subtext, + labelIcon = parentData.icon; + + if (index !== 0 && mainElements.length > 0) { // Is it NOT the first option of the select && are there elements in the dropdown? + liIndex++; + mainElements.push( + generateLI( + false, + classNames.DIVIDER, + optID + 'div' + ) + ); + mainData.push({ + type: 'divider', + optID: optID + }); + } + liIndex++; + + var labelElement = generateLabel({ + labelEscaped: labelEscaped, + labelSubtext: labelSubtext, + labelIcon: labelIcon + }); + + mainElements.push(generateLI(labelElement, 'dropdown-header' + optGroupClass, optID)); + mainData.push({ + content: labelEscaped, + subtext: labelSubtext, + type: 'optgroup-label', + optID: optID + }); + + headerIndex = liIndex - 1; + } + + if (that.options.hideDisabled && isDisabled || thisData.hidden === true) { + liIndex--; + return; + } + + textElement = generateText({ + text: text, + optionContent: optionContent, + optionSubtext: subtext, + optionIcon: icon + }); + + mainElements.push(generateLI(generateA(textElement, 'opt ' + optionClass + optGroupClass, inline), '', optID)); + mainData.push({ + content: optionContent || text, + subtext: subtext, + tokens: tokens, + type: 'option', + optID: optID, + headerIndex: headerIndex, + lastIndex: headerIndex + parent.childElementCount, + originalIndex: index, + data: thisData + }); + + availableOptionsCount++; + } else if (thisData.divider === true) { + mainElements.push(generateLI(false, classNames.DIVIDER)); + mainData.push({ + type: 'divider', + originalIndex: index, + data: thisData + }); + } else { + // if previous element is not an optgroup and hideDisabled is true + if (!showDivider && that.options.hideDisabled) { + prevHiddenIndex = thisData.prevHiddenIndex; + + if (prevHiddenIndex !== undefined) { + // select the element **before** the first hidden element in the group + var prevHidden = $selectOptions[prevHiddenIndex].previousElementSibling; + + if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) { + showDivider = true; + } + } + } + + if (showDivider && mainData[mainData.length - 1].type !== 'divider') { + liIndex++; + mainElements.push( + generateLI( + false, + classNames.DIVIDER, + optID + 'div' + ) + ); + mainData.push({ + type: 'divider', + optID: optID + }); + } + + textElement = generateText({ + text: text, + optionContent: optionContent, + optionSubtext: subtext, + optionIcon: icon + }); + + mainElements.push(generateLI(generateA(textElement, optionClass, inline))); + mainData.push({ + content: optionContent || text, + subtext: subtext, + tokens: tokens, + type: 'option', + originalIndex: index, + data: thisData + }); + + availableOptionsCount++; + } + + that.selectpicker.main.map.newIndex[index] = liIndex; + that.selectpicker.main.map.originalIndex[liIndex] = index; + + // get the most recent option info added to mainData + var _mainDataLast = mainData[mainData.length - 1]; + + _mainDataLast.disabled = isDisabled; + + var combinedLength = 0; + + // count the number of characters in the option - not perfect, but should work in most cases + if (_mainDataLast.content) combinedLength += _mainDataLast.content.length; + if (_mainDataLast.subtext) combinedLength += _mainDataLast.subtext.length; + // if there is an icon, ensure this option's width is checked + if (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 + widestOption = mainElements[mainElements.length - 1]; + } + }); + + this.selectpicker.main.elements = mainElements; + this.selectpicker.main.data = mainData; + + this.selectpicker.current = this.selectpicker.main; + + this.selectpicker.view.widestOption = widestOption; + this.selectpicker.view.availableOptionsCount = availableOptionsCount; // faster way to get # of available options without filter + }, + + findLis: function () { + return this.$menuInner.find('.inner > li'); + }, + + render: function () { + var that = this, + $selectOptions = this.$element.find('option'), + selectedItems = [], + selectedItemsInTitle = []; + + this.togglePlaceholder(); + + this.tabIndex(); + + for (var i = 0, len = this.selectpicker.main.elements.length; i < len; i++) { + var index = this.selectpicker.main.map.originalIndex[i], + option = $selectOptions[index]; + + if (option && option.selected) { + selectedItems.push(option); + + if (selectedItemsInTitle.length < 100 && that.options.selectedTextFormat !== 'count' || selectedItems.length === 1) { + if (that.options.hideDisabled && (option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled)) return; + + var thisData = this.selectpicker.main.data[i].data, + icon = thisData.icon && that.options.showIcon ? ' ' : '', + subtext, + titleItem; + + if (that.options.showSubtext && thisData.subtext && !that.multiple) { + subtext = ' ' + thisData.subtext + ''; + } else { + subtext = ''; + } + + if (option.title) { + titleItem = option.title; + } else if (thisData.content && that.options.showContent) { + titleItem = thisData.content.toString(); + } else { + titleItem = icon + option.innerHTML.trim() + subtext; + } + + selectedItemsInTitle.push(titleItem); + } + } + } + + //Fixes issue in IE10 occurring when no default option is selected and at least one option is disabled + //Convert all the values into a comma delimited string + var title = !this.multiple ? selectedItemsInTitle[0] : selectedItemsInTitle.join(this.options.multipleSeparator); + + // add ellipsis + if (selectedItems.length > 50) title += '...'; + + // If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected etc.. + if (this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1) { + var max = this.options.selectedTextFormat.split('>'); + + if ((max.length > 1 && selectedItems.length > max[1]) || (max.length === 1 && selectedItems.length >= 2)) { + var totalCount = this.selectpicker.view.availableOptionsCount, + tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedItems.length, totalCount) : this.options.countSelectedText; + + title = tr8nText.replace('{0}', selectedItems.length.toString()).replace('{1}', totalCount.toString()); + } + } + + if (this.options.title == undefined) { + // use .attr to ensure undefined is returned if title attribute is not set + this.options.title = this.$element.attr('title'); + } + + if (this.options.selectedTextFormat == 'static') { + title = this.options.title; + } + + //If we dont have a title, then use the default, or if nothing is set at all, use the not selected text + if (!title) { + title = typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneSelectedText; + } + + //strip all HTML tags and trim the result, then unescape any escaped tags + this.$button[0].title = htmlUnescape(title.replace(/<[^>]*>?/g, '').trim()); + this.$button.find('.filter-option-inner-inner')[0].innerHTML = title; + + this.$element.trigger('rendered.bs.select'); + }, + + /** + * @param [style] + * @param [status] + */ + setStyle: function (style, status) { + if (this.$element.attr('class')) { + this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); + } + + var buttonClass = style ? style : this.options.style; + + if (status == 'add') { + this.$button.addClass(buttonClass); + } else if (status == 'remove') { + this.$button.removeClass(buttonClass); + } else { + this.$button.removeClass(this.options.style); + this.$button.addClass(buttonClass); + } + }, + + liHeight: function (refresh) { + if (!refresh && (this.options.size === false || this.sizeInfo)) return; + + if (!this.sizeInfo) this.sizeInfo = {}; + + var newElement = document.createElement('div'), + menu = document.createElement('div'), + menuInner = document.createElement('div'), + menuInnerInner = document.createElement('ul'), + divider = document.createElement('li'), + dropdownHeader = document.createElement('li'), + li = document.createElement('li'), + a = document.createElement('a'), + text = document.createElement('span'), + header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null, + search = this.options.liveSearch ? document.createElement('div') : 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.find('option')[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 = this.sizeInfo.selectWidth + 'px'; + 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')); + 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 = a.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), + // fall back to jQuery if getComputedStyle is not supported + menuStyle = window.getComputedStyle ? window.getComputedStyle(menu) : false, + menuWidth = menu.offsetWidth, + $menu = menuStyle ? null : $(menu), + menuPadding = { + vert: toInteger(menuStyle ? menuStyle.paddingTop : $menu.css('paddingTop')) + + toInteger(menuStyle ? menuStyle.paddingBottom : $menu.css('paddingBottom')) + + toInteger(menuStyle ? menuStyle.borderTopWidth : $menu.css('borderTopWidth')) + + toInteger(menuStyle ? menuStyle.borderBottomWidth : $menu.css('borderBottomWidth')), + horiz: toInteger(menuStyle ? menuStyle.paddingLeft : $menu.css('paddingLeft')) + + toInteger(menuStyle ? menuStyle.paddingRight : $menu.css('paddingRight')) + + toInteger(menuStyle ? menuStyle.borderLeftWidth : $menu.css('borderLeftWidth')) + + toInteger(menuStyle ? menuStyle.borderRightWidth : $menu.css('borderRightWidth')) + }, + menuExtras = { + vert: menuPadding.vert + + toInteger(menuStyle ? menuStyle.marginTop : $menu.css('marginTop')) + + toInteger(menuStyle ? menuStyle.marginBottom : $menu.css('marginBottom')) + 2, + horiz: menuPadding.horiz + + toInteger(menuStyle ? menuStyle.marginLeft : $menu.css('marginLeft')) + + toInteger(menuStyle ? menuStyle.marginRight : $menu.css('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.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.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; + + 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.elements.length + menuPadding.vert; + this.$newElement.toggleClass(classNames.DROPUP, this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot); + } + + if (this.options.size === 'auto') { + _minHeight = this.selectpicker.current.elements.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 = ''; + } + + if (this.options.dropdownAlignRight === 'auto') { + this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.$menu[0].offsetWidth - selectWidth)); + } + + this.$menu.css({ + 'max-height': maxHeight + 'px', + 'overflow': 'hidden', + 'min-height': minHeight + 'px' + }); + + this.$menuInner.css({ + 'max-height': menuInnerHeight + 'px', + 'overflow-y': 'auto', + 'min-height': menuInnerMinHeight + 'px' + }); + + this.sizeInfo['menuInnerHeight'] = menuInnerHeight; + + 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; + + this.$menu.css('min-width', this.sizeInfo.totalMenuWidth); + } + + 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) return; + + var that = this, + $window = $(window), + selectedIndex, + offset = 0; + + this.setMenuSize(); + + if (this.options.size === 'auto') { + this.$searchbox.off('input.setMenuSize propertychange.setMenuSize').on('input.setMenuSize propertychange.setMenuSize', function() { + return that.setMenuSize(); + }); + $window.off('resize.setMenuSize scroll.setMenuSize').on('resize.setMenuSize scroll.setMenuSize', function() { + return that.setMenuSize(); + }); + } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { + this.$searchbox.off('input.setMenuSize propertychange.setMenuSize'); + $window.off('resize.setMenuSize scroll.setMenuSize'); + } + + if (refresh) { + offset = this.$menuInner[0].scrollTop; + } else if (!that.multiple) { + selectedIndex = that.selectpicker.main.map.newIndex[that.$element[0].selectedIndex]; + + if (typeof selectedIndex === 'number' && that.options.size !== false) { + offset = that.sizeInfo.liHeight * selectedIndex; + offset = offset - (that.sizeInfo.menuInnerHeight / 2) + (that.sizeInfo.liHeight / 2); + } + } + + that.createView(false, offset); + }, + + setWidth: function () { + var that = this; + + if (this.options.width === 'auto') { + requestAnimationFrame(function() { + that.$menu.css('min-width', '0'); + 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.removeClass('fit-width'); + } + }, + + selectPosition: function () { + this.$bsContainer = $('
    '); + + 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).on('resize scroll', function () { + getPlacement(that.$newElement); + }); + + this.$element.on('hide.bs.select', function () { + that.$menu.data('height', that.$menu.height()); + that.$bsContainer.detach(); + }); + }, + + setOptionStatus: function () { + var that = this, + $selectOptions = this.$element.find('option'); + + 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 index = that.selectpicker.current.map.originalIndex[i + that.selectpicker.view.position0], // faster than $(li).data('originalIndex') + option = $selectOptions[index]; + + if (option) { + var liIndex = this.selectpicker.main.map.newIndex[index], + li = this.selectpicker.main.elements[liIndex]; + + that.setDisabled( + index, + option.disabled || option.parentNode.tagName === 'OPTGROUP' && option.parentNode.disabled, + liIndex, + li + ); + + that.setSelected( + index, + option.selected, + liIndex, + li + ); + } + } + } + }, + + /** + * @param {number} index - the index of the option that is being changed + * @param {boolean} selected - true if the option is being selected, false if being deselected + */ + setSelected: function (index, selected, liIndex, li) { + var activeIndexIsSet = this.activeIndex !== undefined, + thisIsActive = this.activeIndex === index, + prevActiveIndex, + prevActive, + a, + // if current option is already active + // OR + // if the current option is being selected, it's NOT multiple, and + // activeIndex 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 activeIndex) + keepActive = thisIsActive || selected && !this.multiple && !activeIndexIsSet; + + if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index]; + if (!li) li = this.selectpicker.main.elements[liIndex]; + + a = li.firstChild; + + if (selected) { + this.selectedIndex = index; + } + + li.classList.toggle('selected', selected); + li.classList.toggle('active', keepActive); + + if (keepActive) { + this.selectpicker.view.currentActive = li; + this.activeIndex = index; + } + + if (a) { + a.classList.toggle('selected', selected); + a.classList.toggle('active', keepActive); + a.setAttribute('aria-selected', selected); + } + + if (!keepActive) { + if (!activeIndexIsSet && selected && this.prevActiveIndex !== undefined) { + prevActiveIndex = this.selectpicker.main.map.newIndex[this.prevActiveIndex]; + prevActive = this.selectpicker.main.elements[prevActiveIndex]; + + prevActive.classList.remove('active'); + if (prevActive.firstChild) { + prevActive.firstChild.classList.remove('active'); + } + } + } + }, + + /** + * @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 (index, disabled, liIndex, li) { + var a; + + if (!liIndex) liIndex = this.selectpicker.main.map.newIndex[index]; + if (!li) li = this.selectpicker.main.elements[liIndex]; + + a = li.firstChild; + + li.classList.toggle(classNames.DISABLED, disabled); + + if (a) { + if (version.major === '4') a.classList.toggle(classNames.DISABLED, disabled); + + a.setAttribute('aria-disabled', disabled); + + if (disabled) { + a.setAttribute('tabindex', -1); + } else { + a.setAttribute('tabindex', 0); + } + } + }, + + isDisabled: function () { + return this.$element[0].disabled; + }, + + checkDisabled: function () { + var that = this; + + if (this.isDisabled()) { + this.$newElement.addClass(classNames.DISABLED); + this.$button.addClass(classNames.DISABLED).attr('tabindex', -1).attr('aria-disabled', true); + } else { + if (this.$button.hasClass(classNames.DISABLED)) { + this.$newElement.removeClass(classNames.DISABLED); + this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false); + } + + if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) { + this.$button.removeAttr('tabindex'); + } + } + + this.$button.click(function () { + return !that.isDisabled(); + }); + }, + + togglePlaceholder: function () { + // much faster than calling $.val() + var element = this.$element[0], + selectedIndex = element.selectedIndex, + nothingSelected = selectedIndex === -1; + + if (!nothingSelected && !element.options[selectedIndex].value) nothingSelected = true; + + this.$button.toggleClass('bs-placeholder', nothingSelected); + }, + + tabIndex: function () { + if (this.$element.data('tabindex') !== this.$element.attr('tabindex') && + (this.$element.attr('tabindex') !== -98 && this.$element.attr('tabindex') !== '-98')) { + this.$element.data('tabindex', this.$element.attr('tabindex')); + this.$button.attr('tabindex', this.$element.data('tabindex')); + } + + this.$element.attr('tabindex', -98); + }, + + 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 (version.major > 3 && !that.dropdown) { + that.dropdown = that.$button.data('bs.dropdown'); + that.dropdown._menu = that.$menu[0]; + } + }); + + this.$button.on('click.bs.dropdown.data-api', function () { + if (!that.$newElement.hasClass(classNames.SHOW)) { + that.setSize(); + } + }); + + function setFocus () { + if (that.options.liveSearch) { + that.$searchbox.focus(); + } else { + that.$menuInner.focus(); + } + } + + function checkPopperExists () { + if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state.isCreated) { + setFocus(); + } else { + requestAnimationFrame(checkPopperExists); + } + } + + this.$element.on('shown.bs.select', 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(); + } + }); + + this.$menuInner.on('click', 'li a', function (e, retainActive) { + var $this = $(this), + position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, + clickedIndex = that.selectpicker.current.map.originalIndex[$this.parent().index() + position0], + prevValue = getSelectValues(that.$element[0]), + prevIndex = that.$element.prop('selectedIndex'), + triggerChange = true; + + // Don't close on multi choice menu + if (that.multiple && that.options.maxOptions !== 1) { + e.stopPropagation(); + } + + e.preventDefault(); + + //Don't run if we have been disabled + if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) { + var $options = that.$element.find('option'), + $option = $options.eq(clickedIndex), + state = $option.prop('selected'), + $optgroup = $option.parent('optgroup'), + $optgroupOptions = $optgroup.find('option'), + maxOptions = that.options.maxOptions, + maxOptionsGrp = $optgroup.data('maxOptions') || false; + + if (clickedIndex === that.activeIndex) retainActive = true; + + if (!retainActive) { + that.prevActiveIndex = that.activeIndex; + that.activeIndex = undefined; + } + + if (!that.multiple) { // Deselect all others if not multi select box + $options.prop('selected', false); + $option.prop('selected', true); + that.setSelected(clickedIndex, true); + } else { // Toggle the one we have chosen if we are multi select. + $option.prop('selected', !state); + + that.setSelected(clickedIndex, !state); + $this.blur(); + + if (maxOptions !== false || maxOptionsGrp !== false) { + var maxReached = maxOptions < $options.filter(':selected').length, + maxReachedGrp = maxOptionsGrp < $optgroup.find('option:selected').length; + + if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) { + if (maxOptions && maxOptions == 1) { + $options.prop('selected', false); + $option.prop('selected', true); + + for (var i = 0; i < $options.length; i++) { + that.setSelected(i, false); + } + + that.setSelected(clickedIndex, true); + } else if (maxOptionsGrp && maxOptionsGrp == 1) { + $optgroup.find('option:selected').prop('selected', false); + $option.prop('selected', true); + + for (var i = 0; i < $optgroupOptions.length; i++) { + var option = $optgroupOptions[i]; + that.setSelected($options.index(option), false); + } + + that.setSelected(clickedIndex, 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 = $('
    '); + // 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]); + } + + $option.prop('selected', false); + + that.$menu.append($notify); + + if (maxOptions && maxReached) { + $notify.append($('
    ' + maxTxt + '
    ')); + triggerChange = false; + that.$element.trigger('maxReached.bs.select'); + } + + if (maxOptionsGrp && maxReachedGrp) { + $notify.append($('
    ' + maxTxtGrp + '
    ')); + triggerChange = false; + that.$element.trigger('maxReachedGrp.bs.select'); + } + + setTimeout(function () { + that.setSelected(clickedIndex, false); + }, 10); + + $notify.delay(750).fadeOut(300, function () { + $(this).remove(); + }); + } + } + } + } + + if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) { + that.$button.focus(); + } else if (that.options.liveSearch) { + that.$searchbox.focus(); + } + + // Trigger select 'change' + if (triggerChange) { + if ((prevValue != getSelectValues(that.$element[0]) && that.multiple) || (prevIndex != that.$element.prop('selectedIndex') && !that.multiple)) { + // $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed. + changed_arguments = [clickedIndex, $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.focus(); + } else { + that.$button.focus(); + } + } + }); + + this.$menuInner.on('click', '.divider, .dropdown-header', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (that.options.liveSearch) { + that.$searchbox.focus(); + } else { + that.$button.focus(); + } + }); + + this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () { + that.$button.click(); + }); + + this.$searchbox.on('click', function (e) { + e.stopPropagation(); + }); + + this.$menu.on('click', '.actions-btn', function (e) { + if (that.options.liveSearch) { + that.$searchbox.focus(); + } else { + that.$button.focus(); + } + + e.preventDefault(); + e.stopPropagation(); + + if ($(this).hasClass('bs-select-all')) { + that.selectAll(); + } else { + that.deselectAll(); + } + }); + + this.$element.on({ + 'change': function () { + that.render(); + that.$element.trigger('changed.bs.select', changed_arguments); + changed_arguments = null; + }, + 'focus': function () { + if (!that.options.mobile) that.$button.focus(); + } + }); + }, + + liveSearchListener: function () { + var that = this, + no_results = document.createElement('li'); + + this.$button.on('click.bs.dropdown.data-api', function () { + if (!!that.$searchbox.val()) { + that.$searchbox.val(''); + } + }); + + 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.val(); + + that.selectpicker.search.map.newIndex = {}; + that.selectpicker.search.map.originalIndex = {}; + that.selectpicker.search.elements = []; + that.selectpicker.search.data = []; + + if (searchValue) { + var i, + searchMatch = [], + q = searchValue.toUpperCase(), + cache = {}, + cacheArr = [], + searchStyle = that._searchStyle(), + normalizeSearch = that.options.liveSearchNormalize; + + that._$lisSelected = that.$menuInner.find('.selected'); + + 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]); + + if (li.hasOwnProperty('originalIndex')) { + that.selectpicker.search.map.newIndex[li.originalIndex] = searchMatch.length - 1; + that.selectpicker.search.map.originalIndex[searchMatch.length - 1] = li.originalIndex; + } + } + } + + that.activeIndex = undefined; + that.noScroll = true; + that.$menuInner.scrollTop(0); + that.selectpicker.search.elements = searchMatch; + that.createView(true); + + if (!searchMatch.length) { + no_results.className = 'no-results'; + no_results.innerHTML = that.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); + that.$menuInner[0].firstChild.appendChild(no_results); + } + } else { + that.$menuInner.scrollTop(0); + that.createView(false); + } + }); + }, + + _searchStyle: function () { + return this.options.liveSearchStyle || 'contains'; + }, + + val: function (value) { + if (typeof value !== 'undefined') { + this.$element.val(value); + this.render(); + this.$element.trigger('changed.bs.select', changed_arguments); + changed_arguments = null; + + return this.$element; + } else { + return this.$element.val(); + } + }, + + changeAll: function (status) { + if (!this.multiple) return; + if (typeof status === 'undefined') status = true; + + var $selectOptions = this.$element.find('option'), + previousSelected = 0, + currentSelected = 0, + prevValue = getSelectValues(this.$element[0]); + + this.$element.addClass('bs-select-hidden'); + + for (var i = 0; i < this.selectpicker.current.elements.length; i++) { + var liData = this.selectpicker.current.data[i], + index = this.selectpicker.current.map.originalIndex[i], // faster than $(li).data('originalIndex') + option = $selectOptions[index]; + + if (option && !option.disabled && liData.type !== 'divider') { + if (option.selected) previousSelected++; + option.selected = status; + if (option.selected) currentSelected++; + } + } + + this.$element.removeClass('bs-select-hidden'); + + if (previousSelected === currentSelected) return; + + this.setOptionStatus(); + + this.togglePlaceholder(); + + changed_arguments = [null, null, prevValue]; + + this.$element + .triggerNative('change'); + }, + + selectAll: function () { + return this.changeAll(true); + }, + + deselectAll: function () { + return this.changeAll(false); + }, + + toggle: function (e) { + e = e || window.event; + + if (e) e.stopPropagation(); + + this.$button.trigger('click.bs.dropdown.data-api'); + }, + + 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; + + isActive = that.$newElement.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 (e.which === keyCodes.ESCAPE && isActive) { + e.preventDefault(); + that.$button.trigger('click.bs.dropdown.data-api').focus(); + } + + if (isArrowKey) { // if up or down + if (!$items.length) return; + + // $items.index/.filter is too slow with a large list and no virtual scroll + index = isVirtual === true ? $items.index($items.filter('.active')) : that.selectpicker.current.map.newIndex[that.activeIndex]; + + if (index === undefined) index = -1; + + if (index !== -1) { + liActive = that.selectpicker.current.elements[index + position0]; + liActive.classList.remove('active'); + if (liActive.firstChild) liActive.firstChild.classList.remove('active'); + } + + 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 = 0; + + 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]; + 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 === 0) { + that.$menuInner[0].scrollTop = 0; + + liActiveIndex = 0; + } else { + activeLi = that.selectpicker.current.data[liActiveIndex]; + offset = activeLi.position - that.sizeInfo.menuInnerHeight; + + updateScroll = offset > scrollTop; + } + } + + liActive = that.selectpicker.current.elements[liActiveIndex]; + + if (liActive) { + liActive.classList.add('active'); + if (liActive.firstChild) liActive.firstChild.classList.add('active'); + } + + that.activeIndex = that.selectpicker.current.map.originalIndex[liActiveIndex]; + + that.selectpicker.view.currentActive = liActive; + + if (updateScroll) that.$menuInner[0].scrollTop = offset; + + if (that.options.liveSearch) { + that.$searchbox.focus(); + } else { + $this.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]) { + li.index = i; + matches.push(li.originalIndex); + } + } + + 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.activeIndex); + + if (matchIndex === -1 || matchIndex === matches.length - 1) { + matchIndex = 0; + } else { + matchIndex++; + } + } + + searchMatch = that.selectpicker.current.map.newIndex[matches[matchIndex]]; + + activeLi = that.selectpicker.current.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.current.elements[searchMatch]; + liActive.classList.add('active'); + if (liActive.firstChild) liActive.firstChild.classList.add('active'); + that.activeIndex = matches[matchIndex]; + + liActive.firstChild.focus(); + + if (updateScroll) that.$menuInner[0].scrollTop = offset; + + $this.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.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); + } + } + } + }, + + mobile: function () { + this.$element.addClass('mobile-device'); + }, + + refresh: function () { + // update options if data attributes have been changed + var config = $.extend({}, this.options, this.$element.data()); + this.options = config; + + this.selectpicker.main.map.newIndex = {}; + this.selectpicker.main.map.originalIndex = {}; + this.createLi(); + this.checkDisabled(); + this.render(); + this.setStyle(); + this.setWidth(); + + this.setSize(true); + + this.$element.trigger('refreshed.bs.select'); + }, + + 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(); + } + + this.$element + .off('.bs.select') + .removeData('selectpicker') + .removeClass('bs-select-hidden selectpicker'); + } + }; + + // 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 = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); + } + // fall back to use BootstrapVersion + catch(err) { + version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.'); + } + + version.major = version.full[0]; + version.success = true; + + if (version.major === '4') { + classNames.DIVIDER = 'dropdown-divider'; + classNames.SHOW = 'show'; + classNames.BUTTONCLASS = 'btn-light'; + Selectpicker.DEFAULTS.style = classNames.BUTTONCLASS = 'btn-light'; + classNames.POPOVERHEADER = 'popover-header'; + } + } + + var value; + var chain = this.each(function () { + var $this = $(this); + if ($this.is('select')) { + var data = $this.data('selectpicker'), + options = typeof _option == 'object' && _option; + + if (!data) { + var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, $this.data(), options); + config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), $this.data().template, options.template); + $this.data('selectpicker', (data = new Selectpicker(this, config))); + } else if (options) { + for (var i in options) { + if (options.hasOwnProperty(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; + }; + + $(document) + .off('keydown.bs.dropdown.data-api') + .on('keydown.bs.select', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', Selectpicker.prototype.keydown) + .on('focusin.modal', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bs-searchbox input', function (e) { + e.stopPropagation(); + }); + + // SELECTPICKER DATA-API + // ===================== + $(window).on('load.bs.select.data-api', function () { + $('.selectpicker').each(function () { + var $selectpicker = $(this); + Plugin.call($selectpicker, $selectpicker.data()); + }) + }); +})(jQuery); }));