(function ($) { 'use strict'; var disallowed_attributes = ['sanitize', 'whitelist', 'sanitizefn']; var uriattrs = [ 'background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href' ]; var aria_attribute_pattern = /^aria-[\w-]*$/i; var defaultwhitelist = { // global attributes allowed on any supplied element below. '*': ['class', 'dir', 'id', 'lang', 'role', 'tabindex', 'style', aria_attribute_pattern], a: ['target', 'href', 'title', 'rel'], area: [], b: [], br: [], col: [], code: [], div: [], em: [], hr: [], h1: [], h2: [], h3: [], h4: [], h5: [], h6: [], i: [], img: ['src', 'alt', 'title', 'width', 'height'], li: [], ol: [], p: [], pre: [], s: [], small: [], span: [], sub: [], sup: [], strong: [], u: [], ul: [] } /** * a pattern that recognizes a commonly useful subset of urls that are safe. * * shoutout to angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts */ var safe_url_pattern = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; /** * a pattern that matches safe data urls. only matches image, video and audio types. * * shoutout to angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts */ var data_url_pattern = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; function allowedattribute (attr, allowedattributelist) { var attrname = attr.nodename.tolowercase() if ($.inarray(attrname, allowedattributelist) !== -1) { if ($.inarray(attrname, uriattrs) !== -1) { return boolean(attr.nodevalue.match(safe_url_pattern) || attr.nodevalue.match(data_url_pattern)) } return true } var regexp = $(allowedattributelist).filter(function (index, value) { return value instanceof regexp }) // check if a regular expression validates the attribute. for (var i = 0, l = regexp.length; i < l; i++) { if (attrname.match(regexp[i])) { return true } } return false } function sanitizehtml (unsafeelements, whitelist, sanitizefn) { if (sanitizefn && typeof sanitizefn === 'function') { return sanitizefn(unsafeelements); } var whitelistkeys = object.keys(whitelist); for (var i = 0, len = unsafeelements.length; i < len; i++) { var elements = unsafeelements[i].queryselectorall('*'); for (var j = 0, len2 = elements.length; j < len2; j++) { var el = elements[j]; var elname = el.nodename.tolowercase(); if (whitelistkeys.indexof(elname) === -1) { el.parentnode.removechild(el); continue; } var attributelist = [].slice.call(el.attributes); var whitelistedattributes = [].concat(whitelist['*'] || [], whitelist[elname] || []); for (var k = 0, len3 = attributelist.length; k < len3; k++) { var attr = attributelist[k]; if (!allowedattribute(attr, whitelistedattributes)) { el.removeattribute(attr.nodename); } } } } } // polyfill for browsers with no classlist support // remove in v2 if (!('classlist' in document.createelement('_'))) { (function (view) { if (!('element' in view)) return; var classlistprop = 'classlist', protoprop = 'prototype', elemctrproto = view.element[protoprop], objctr = object, classlistgetter = function () { var $elem = $(this); return { add: function (classes) { classes = array.prototype.slice.call(arguments).join(' '); return $elem.addclass(classes); }, remove: function (classes) { classes = array.prototype.slice.call(arguments).join(' '); return $elem.removeclass(classes); }, toggle: function (classes, force) { return $elem.toggleclass(classes, force); }, contains: function (classes) { return $elem.hasclass(classes); } } }; if (objctr.defineproperty) { var classlistpropdesc = { get: classlistgetter, enumerable: true, configurable: true }; try { objctr.defineproperty(elemctrproto, classlistprop, classlistpropdesc); } catch (ex) { // ie 8 doesn't support enumerable:true // adding undefined to fight this issue https://github.com/eligrey/classlist.js/issues/36 // modernie ie8-msw7 machine has ie8 8.0.6001.18702 and is affected if (ex.number === undefined || ex.number === -0x7ff5ec54) { classlistpropdesc.enumerable = false; objctr.defineproperty(elemctrproto, classlistprop, classlistpropdesc); } } } else if (objctr[protoprop].__definegetter__) { elemctrproto.__definegetter__(classlistprop, classlistgetter); } }(window)); } var testelement = document.createelement('_'); testelement.classlist.add('c1', 'c2'); if (!testelement.classlist.contains('c2')) { var _add = domtokenlist.prototype.add, _remove = domtokenlist.prototype.remove; domtokenlist.prototype.add = function () { array.prototype.foreach.call(arguments, _add.bind(this)); } domtokenlist.prototype.remove = function () { array.prototype.foreach.call(arguments, _remove.bind(this)); } } testelement.classlist.toggle('c3', false); // polyfill for ie 10 and firefox <24, where classlist.toggle does not // support the second argument. if (testelement.classlist.contains('c3')) { var _toggle = domtokenlist.prototype.toggle; domtokenlist.prototype.toggle = function (token, force) { if (1 in arguments && !this.contains(token) === !force) { return force; } else { return _toggle.call(this, token); } }; } testelement = null; // 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; }; } if (htmlselectelement && !htmlselectelement.prototype.hasownproperty('selectedoptions')) { object.defineproperty(htmlselectelement.prototype, 'selectedoptions', { get: function () { return this.queryselectorall(':checked'); } }); } function getselectedoptions (select, ignoredisabled) { var selectedoptions = select.selectedoptions, options = [], opt; if (ignoredisabled) { for (var i = 0, len = selectedoptions.length; i < len; i++) { opt = selectedoptions[i]; if (!(opt.disabled || opt.parentnode.tagname === 'optgroup' && opt.parentnode.disabled)) { options.push(opt); } } return options; } return selectedoptions; } // much faster than $.val() function getselectvalues (select, selectedoptions) { var value = [], options = selectedoptions || select.selectedoptions, opt; for (var i = 0, len = options.length; i < len; i++) { opt = options[i]; if (!(opt.disabled || opt.parentnode.tagname === 'optgroup' && opt.parentnode.disabled)) { value.push(opt.value); } } if (!select.multiple) { return !value.length ? null : value[0]; } return value; } // set data-selected on select element if the value has been programmatically selected // prior to initialization of bootstrap-select // * consider removing or replacing an alternative method * var valhooks = { usedefault: false, _set: $.valhooks.select.set }; $.valhooks.select.set = function (elem, value) { if (value && !valhooks.usedefault) $(elem).data('selected', true); return valhooks._set.apply(this, arguments); }; var changedarguments = null; var eventissupported = (function () { try { new event('change'); return true; } catch (e) { return false; } })(); $.fn.triggernative = function (eventname) { var el = this[0], event; if (el.dispatchevent) { // for modern browsers & ie9+ if (eventissupported) { // for modern browsers event = new event(eventname, { bubbles: true }); } else { // for ie since it doesn't support event constructor event = document.createevent('event'); event.initevent(eventname, true, false); } el.dispatchevent(event); } 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 = [ 'display', 'subtext', 'tokens' ], searchsuccess = false; for (var i = 0; i < stringtypes.length; i++) { var stringtype = stringtypes[i], string = li[stringtype]; if (string) { string = string.tostring(); // strip html tags. this isn't perfect, but it's much faster than any other method if (stringtype === 'display') { string = string.replace(/<[^>]+>/g, ''); } if (normalize) string = normalizetobase(string); string = string.touppercase(); if (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; } // borrowed from lodash (_.deburr) /** used to map latin unicode letters to basic latin letters. */ var deburredletters = { // latin-1 supplement block. '\xc0': 'a', '\xc1': 'a', '\xc2': 'a', '\xc3': 'a', '\xc4': 'a', '\xc5': 'a', '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', '\xc7': 'c', '\xe7': 'c', '\xd0': 'd', '\xf0': 'd', '\xc8': 'e', '\xc9': 'e', '\xca': 'e', '\xcb': 'e', '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', '\xcc': 'i', '\xcd': 'i', '\xce': 'i', '\xcf': 'i', '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', '\xd1': 'n', '\xf1': 'n', '\xd2': 'o', '\xd3': 'o', '\xd4': 'o', '\xd5': 'o', '\xd6': 'o', '\xd8': 'o', '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', '\xd9': 'u', '\xda': 'u', '\xdb': 'u', '\xdc': 'u', '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', '\xdd': 'y', '\xfd': 'y', '\xff': 'y', '\xc6': 'ae', '\xe6': 'ae', '\xde': 'th', '\xfe': 'th', '\xdf': 'ss', // latin extended-a block. '\u0100': 'a', '\u0102': 'a', '\u0104': 'a', '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', '\u0106': 'c', '\u0108': 'c', '\u010a': 'c', '\u010c': 'c', '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', '\u010e': 'd', '\u0110': 'd', '\u010f': 'd', '\u0111': 'd', '\u0112': 'e', '\u0114': 'e', '\u0116': 'e', '\u0118': 'e', '\u011a': 'e', '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', '\u011c': 'g', '\u011e': 'g', '\u0120': 'g', '\u0122': 'g', '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', '\u0124': 'h', '\u0126': 'h', '\u0125': 'h', '\u0127': 'h', '\u0128': 'i', '\u012a': 'i', '\u012c': 'i', '\u012e': 'i', '\u0130': 'i', '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', '\u0134': 'j', '\u0135': 'j', '\u0136': 'k', '\u0137': 'k', '\u0138': 'k', '\u0139': 'l', '\u013b': 'l', '\u013d': 'l', '\u013f': 'l', '\u0141': 'l', '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', '\u0143': 'n', '\u0145': 'n', '\u0147': 'n', '\u014a': 'n', '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', '\u014c': 'o', '\u014e': 'o', '\u0150': 'o', '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', '\u0154': 'r', '\u0156': 'r', '\u0158': 'r', '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', '\u015a': 's', '\u015c': 's', '\u015e': 's', '\u0160': 's', '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', '\u0162': 't', '\u0164': 't', '\u0166': 't', '\u0163': 't', '\u0165': 't', '\u0167': 't', '\u0168': 'u', '\u016a': 'u', '\u016c': 'u', '\u016e': 'u', '\u0170': 'u', '\u0172': 'u', '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', '\u0174': 'w', '\u0175': 'w', '\u0176': 'y', '\u0177': 'y', '\u0178': 'y', '\u0179': 'z', '\u017b': 'z', '\u017d': 'z', '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', '\u0132': 'ij', '\u0133': 'ij', '\u0152': 'oe', '\u0153': 'oe', '\u0149': "'n", '\u017f': 's' }; /** used to match latin unicode letters (excluding mathematical operators). */ var relatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; /** used to compose unicode character classes. */ var rscombomarksrange = '\\u0300-\\u036f', recombohalfmarksrange = '\\ufe20-\\ufe2f', rscombosymbolsrange = '\\u20d0-\\u20ff', rscombomarksextendedrange = '\\u1ab0-\\u1aff', rscombomarkssupplementrange = '\\u1dc0-\\u1dff', rscomborange = rscombomarksrange + recombohalfmarksrange + rscombosymbolsrange + rscombomarksextendedrange + rscombomarkssupplementrange; /** used to compose unicode capture groups. */ var rscombo = '[' + rscomborange + ']'; /** * used to match [combining diacritical marks](https://en.wikipedia.org/wiki/combining_diacritical_marks) and * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/combining_diacritical_marks_for_symbols). */ var recombomark = regexp(rscombo, 'g'); function deburrletter (key) { return deburredletters[key]; }; function normalizetobase (string) { string = string.tostring(); return string && string.replace(relatin, deburrletter).replace(recombomark, ''); } // list of html entities for escaping. var escapemap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }; // functions for escaping and unescaping strings to/from html interpolation. var createescaper = function (map) { var escaper = function (match) { return map[match]; }; // regexes for identifying a key that needs to be escaped. var source = '(?:' + object.keys(map).join('|') + ')'; var testregexp = regexp(source); var replaceregexp = regexp(source, 'g'); return function (string) { string = string == null ? '' : '' + string; return testregexp.test(string) ? string.replace(replaceregexp, escaper) : string; }; }; var htmlescape = createescaper(escapemap); /** * ------------------------------------------------------------------------ * constants * ------------------------------------------------------------------------ */ var keycodemap = { 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z', 96: '0', 97: '1', 98: '2', 99: '3', 100: '4', 101: '5', 102: '6', 103: '7', 104: '8', 105: '9' }; var keycodes = { escape: 27, // keyboardevent.which value for escape (esc) key enter: 13, // keyboardevent.which value for enter key space: 32, // keyboardevent.which value for space key tab: 9, // keyboardevent.which value for tab key arrow_up: 38, // keyboardevent.which value for up arrow key arrow_down: 40 // keyboardevent.which value for down arrow key } var 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) { // do nothing } var selectid = 0; var event_key = '.bs.select'; var classnames = { disabled: 'disabled', divider: 'divider', show: 'open', dropup: 'dropup', menu: 'dropdown-menu', menuright: 'dropdown-menu-right', menuleft: 'dropdown-menu-left', // to-do: replace with more advanced template/customization options buttonclass: 'btn-default', popoverheader: 'popover-title', iconbase: 'glyphicon', tickicon: 'glyphicon-ok' } var selector = { menu: '.' + classnames.menu } var elementtemplates = { span: document.createelement('span'), i: document.createelement('i'), subtext: document.createelement('small'), a: document.createelement('a'), li: document.createelement('li'), whitespace: document.createtextnode('\u00a0'), fragment: document.createdocumentfragment() } elementtemplates.a.setattribute('role', 'option'); if (version.major === '4') elementtemplates.a.classname = 'dropdown-item'; elementtemplates.subtext.classname = 'text-muted'; elementtemplates.text = elementtemplates.span.clonenode(false); elementtemplates.text.classname = 'text'; elementtemplates.checkmark = elementtemplates.span.clonenode(false); var regexp_arrow = new regexp(keycodes.arrow_up + '|' + keycodes.arrow_down); var regexp_tab_or_escape = new regexp('^' + keycodes.tab + '$|' + keycodes.escape); var generateoption = { li: function (content, classes, optgroup) { var li = elementtemplates.li.clonenode(false); if (content) { if (content.nodetype === 1 || content.nodetype === 11) { li.appendchild(content); } else { li.innerhtml = content; } } if (typeof classes !== 'undefined' && classes !== '') li.classname = classes; if (typeof optgroup !== 'undefined' && optgroup !== null) li.classlist.add('optgroup-' + optgroup); return li; }, a: function (text, classes, inline) { var a = elementtemplates.a.clonenode(true); if (text) { if (text.nodetype === 11) { a.appendchild(text); } else { a.insertadjacenthtml('beforeend', text); } } if (typeof classes !== 'undefined' && classes !== '') a.classlist.add.apply(a.classlist, classes.split(' ')); if (inline) a.setattribute('style', inline); return a; }, text: function (options, usefragment) { var textelement = elementtemplates.text.clonenode(false), subtextelement, iconelement; if (options.content) { textelement.innerhtml = options.content; } else { textelement.textcontent = options.text; if (options.icon) { var whitespace = elementtemplates.whitespace.clonenode(false); // need to use for icons in the button to prevent a breaking change // note: switch to span in next major release iconelement = (usefragment === true ? elementtemplates.i : elementtemplates.span).clonenode(false); iconelement.classname = this.options.iconbase + ' ' + options.icon; elementtemplates.fragment.appendchild(iconelement); elementtemplates.fragment.appendchild(whitespace); } if (options.subtext) { subtextelement = elementtemplates.subtext.clonenode(false); subtextelement.textcontent = options.subtext; textelement.appendchild(subtextelement); } } if (usefragment === true) { while (textelement.childnodes.length > 0) { elementtemplates.fragment.appendchild(textelement.childnodes[0]); } } else { elementtemplates.fragment.appendchild(textelement); } return elementtemplates.fragment; }, label: function (options) { var textelement = elementtemplates.text.clonenode(false), subtextelement, iconelement; textelement.innerhtml = options.display; if (options.icon) { var whitespace = elementtemplates.whitespace.clonenode(false); iconelement = elementtemplates.span.clonenode(false); iconelement.classname = this.options.iconbase + ' ' + options.icon; elementtemplates.fragment.appendchild(iconelement); elementtemplates.fragment.appendchild(whitespace); } if (options.subtext) { subtextelement = elementtemplates.subtext.clonenode(false); subtextelement.textcontent = options.subtext; textelement.appendchild(subtextelement); } elementtemplates.fragment.appendchild(textelement); return elementtemplates.fragment; } } var 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: {}, search: {}, current: {}, // current changes if a search is in progress view: {}, issearching: false, keydown: { keyhistory: '', resetkeyhistory: { start: function () { return settimeout(function () { that.selectpicker.keydown.keyhistory = ''; }, 800); } } } }; this.sizeinfo = {}; // 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.14'; // 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: classnames.iconbase, tickicon: classnames.tickicon, showtick: false, template: { caret: '' }, maxoptions: false, mobile: false, selectontab: false, dropdownalignright: false, windowpadding: 0, virtualscroll: 600, display: false, sanitize: true, sanitizefn: null, whitelist: defaultwhitelist }; selectpicker.prototype = { constructor: selectpicker, init: function () { var that = this, id = this.$element.attr('id'); selectid++; this.selectid = 'bs-select-' + selectid; this.$element[0].classlist.add('bs-select-hidden'); this.multiple = this.$element.prop('multiple'); this.autofocus = this.$element.prop('autofocus'); if (this.$element[0].classlist.contains('show-tick')) { this.options.showtick = true; } this.$newelement = this.createdropdown(); this.builddata(); 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[0].classlist.remove('bs-select-hidden'); if (this.options.dropdownalignright === true) this.$menu[0].classlist.add(classnames.menuright); if (typeof id !== 'undefined') { this.$button.attr('data-id', id); } this.checkdisabled(); this.clicklistener(); if (this.options.livesearch) { this.livesearchlistener(); this.focusedparent = this.$searchbox[0]; } else { this.focusedparent = this.$menuinner[0]; } this.setstyle(); this.render(); this.setwidth(); if (this.options.container) { this.selectposition(); } else { this.$element.on('hide' + event_key, function () { if (that.isvirtual()) { // empty menu on close var menuinner = that.$menuinner[0], emptymenu = menuinner.firstchild.clonenode(false); // replace the existing ul with an empty one - this is faster than $.empty() or innerhtml = '' menuinner.replacechild(emptymenu, menuinner.firstchild); menuinner.scrolltop = 0; } }); } this.$menu.data('this', this); this.$newelement.data('this', this); if (this.options.mobile) this.mobile(); this.$newelement.on({ 'hide.bs.dropdown': function (e) { that.$element.trigger('hide' + event_key, e); }, 'hidden.bs.dropdown': function (e) { that.$element.trigger('hidden' + event_key, e); }, 'show.bs.dropdown': function (e) { that.$element.trigger('show' + event_key, e); }, 'shown.bs.dropdown': function (e) { that.$element.trigger('shown' + event_key, e); } }); if (that.$element[0].hasattribute('required')) { this.$element.on('invalid' + event_key, function () { that.$button[0].classlist.add('bs-invalid'); that.$element .on('shown' + event_key + '.invalid', function () { that.$element .val(that.$element.val()) // set the value to hide the validation message in chrome when menu is opened .off('shown' + event_key + '.invalid'); }) .on('rendered' + event_key, function () { // if select is no longer invalid, remove the bs-invalid class if (this.validity.valid) that.$button[0].classlist.remove('bs-invalid'); that.$element.off('rendered' + event_key); }); that.$button.on('blur' + event_key, function () { that.$element.trigger('focus').trigger('blur'); that.$button.off('blur' + event_key); }); }); } settimeout(function () { that.buildlist(); that.$element.trigger('loaded' + event_key); }); }, createdropdown: function () { // options // if we are multiple or showtick option is set, then add the show-tick class var showtick = (this.multiple || this.options.showtick) ? ' show-tick' : '', multiselectable = this.multiple ? ' aria-multiselectable="true"' : '', inputgroup = '', autofocus = this.autofocus ? ' autofocus' : ''; if (version.major < 4 && this.$element.parent().hasclass('input-group')) { inputgroup = ' input-group-btn'; } // elements var drop, header = '', searchbox = '', actionsbox = '', donebutton = ''; if (this.options.header) { header = '' + '×' + this.options.header + ''; } if (this.options.livesearch) { searchbox = '' + '' + ''; } if (this.multiple && this.options.actionsbox) { actionsbox = '' + '' + '' + this.options.selectalltext + '' + '' + this.options.deselectalltext + '' + '' + ''; } if (this.multiple && this.options.donebutton) { donebutton = '' + '' + '' + this.options.donebuttontext + '' + '' + ''; } drop = '' + '' + '' + '' + '' + ' ' + '' + ( version.major === '4' ? '' : '' + this.options.template.caret + '' ) + '' + '' + header + searchbox + actionsbox + '' + '' + '' + '' + donebutton + '' + ''; return $(drop); }, setpositiondata: function () { this.selectpicker.view.canhighlight = []; this.selectpicker.view.size = 0; for (var i = 0; i < this.selectpicker.current.data.length; i++) { var li = this.selectpicker.current.data[i], canhighlight = true; if (li.type === 'divider') { canhighlight = false; li.height = this.sizeinfo.dividerheight; } else if (li.type === 'optgroup-label') { canhighlight = false; li.height = this.sizeinfo.dropdownheaderheight; } else { li.height = this.sizeinfo.liheight; } if (li.disabled) canhighlight = false; this.selectpicker.view.canhighlight.push(canhighlight); if (canhighlight) { this.selectpicker.view.size++; li.posinset = this.selectpicker.view.size; } 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, setsize, refresh) { var that = this, scrolltop = 0, active = [], selected, prevactive; this.selectpicker.issearching = issearching; this.selectpicker.current = issearching ? this.selectpicker.search : this.selectpicker.main; this.setpositiondata(); if (setsize) { if (refresh) { scrolltop = this.$menuinner[0].scrolltop; } else if (!that.multiple) { var element = that.$element[0], selectedindex = (element.options[element.selectedindex] || {}).liindex; if (typeof selectedindex === 'number' && that.options.size !== false) { var selecteddata = that.selectpicker.main.data[selectedindex], position = selecteddata && selecteddata.position; if (position) { scrolltop = position - ((that.sizeinfo.menuinnerheight + that.sizeinfo.liheight) / 2); } } } } scroll(scrolltop, true); this.$menuinner.off('scroll.createview').on('scroll.createview', function (e, updatevalue) { if (!that.noscroll) scroll(this.scrolltop, updatevalue); that.noscroll = false; }); function scroll (scrolltop, init) { var size = that.selectpicker.current.elements.length, chunks = [], chunksize, chunkcount, firstchunk, lastchunk, currentchunk, prevpositions, positionisdifferent, previouselements, menuisdifferent = true, isvirtual = that.isvirtual(); that.selectpicker.view.scrolltop = scrolltop; 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 endofchunk = (i + 1) * chunksize; if (i === chunkcount - 1) { endofchunk = size; } chunks[i] = [ (i) * chunksize + (!i ? 0 : 1), endofchunk ]; if (!size) break; if (currentchunk === undefined && scrolltop - 1 <= that.selectpicker.current.data[endofchunk - 1].position - that.sizeinfo.menuinnerheight) { currentchunk = i; } } if (currentchunk === undefined) currentchunk = 0; prevpositions = [that.selectpicker.view.position0, that.selectpicker.view.position1]; // always display previous, current, and next chunks firstchunk = math.max(0, currentchunk - 1); lastchunk = math.min(chunkcount - 1, currentchunk + 1); that.selectpicker.view.position0 = isvirtual === false ? 0 : (math.max(0, chunks[firstchunk][0]) || 0); that.selectpicker.view.position1 = isvirtual === false ? size : (math.min(size, chunks[lastchunk][1]) || 0); positionisdifferent = prevpositions[0] !== that.selectpicker.view.position0 || prevpositions[1] !== that.selectpicker.view.position1; if (that.activeindex !== undefined) { prevactive = that.selectpicker.main.elements[that.prevactiveindex]; active = that.selectpicker.main.elements[that.activeindex]; selected = that.selectpicker.main.elements[that.selectedindex]; if (init) { if (that.activeindex !== that.selectedindex) { that.defocusitem(active); } that.activeindex = undefined; } if (that.activeindex && that.activeindex !== that.selectedindex) { that.defocusitem(selected); } } if (that.prevactiveindex !== undefined && that.prevactiveindex !== that.activeindex && that.prevactiveindex !== that.selectedindex) { that.defocusitem(prevactive); } if (init || positionisdifferent) { previouselements = that.selectpicker.view.visibleelements ? that.selectpicker.view.visibleelements.slice() : []; if (isvirtual === false) { that.selectpicker.view.visibleelements = that.selectpicker.current.elements; } else { that.selectpicker.view.visibleelements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1); } that.setoptionstatus(); // if searching, check to make sure the list has actually been updated before updating dom // this prevents unnecessary repaints if (issearching || (isvirtual === false && init)) menuisdifferent = !isequal(previouselements, that.selectpicker.view.visibleelements); // if virtual scroll is disabled and not searching, // menu should never need to be updated more than once if ((init || isvirtual === true) && menuisdifferent) { var menuinner = that.$menuinner[0], menufragment = document.createdocumentfragment(), emptymenu = menuinner.firstchild.clonenode(false), margintop, marginbottom, elements = that.selectpicker.view.visibleelements, tosanitize = []; // replace the existing ul with an empty one - this is faster than $.empty() menuinner.replacechild(emptymenu, menuinner.firstchild); for (var i = 0, visibleelementslen = elements.length; i < visibleelementslen; i++) { var element = elements[i], eltext, elementdata; if (that.options.sanitize) { eltext = element.lastchild; if (eltext) { elementdata = that.selectpicker.current.data[i + that.selectpicker.view.position0]; if (elementdata && elementdata.content && !elementdata.sanitized) { tosanitize.push(eltext); elementdata.sanitized = true; } } } menufragment.appendchild(element); } if (that.options.sanitize && tosanitize.length) { sanitizehtml(tosanitize, that.options.whitelist, that.options.sanitizefn); } if (isvirtual === true) { margintop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position); marginbottom = (that.selectpicker.view.position1 > size - 1 ? 0 : that.selectpicker.current.data[size - 1].position - that.selectpicker.current.data[that.selectpicker.view.position1 - 1].position); menuinner.firstchild.style.margintop = margintop + 'px'; menuinner.firstchild.style.marginbottom = marginbottom + 'px'; } else { menuinner.firstchild.style.margintop = 0; menuinner.firstchild.style.marginbottom = 0; } menuinner.firstchild.appendchild(menufragment); // if an option is encountered that is wider than the current menu width, update the menu width accordingly // switch to resizeobserver with increased browser support if (isvirtual === true && that.sizeinfo.hasscrollbar) { var menuinnerinnerwidth = menuinner.firstchild.offsetwidth; if (init && menuinnerinnerwidth < that.sizeinfo.menuinnerinnerwidth && that.sizeinfo.totalmenuwidth > that.sizeinfo.selectwidth) { menuinner.firstchild.style.minwidth = that.sizeinfo.menuinnerinnerwidth + 'px'; } else if (menuinnerinnerwidth > that.sizeinfo.menuinnerinnerwidth) { // set to 0 to get actual width of menu that.$menu[0].style.minwidth = 0; var actualmenuwidth = menuinner.firstchild.offsetwidth; if (actualmenuwidth > that.sizeinfo.menuinnerinnerwidth) { that.sizeinfo.menuinnerinnerwidth = actualmenuwidth; menuinner.firstchild.style.minwidth = that.sizeinfo.menuinnerinnerwidth + 'px'; } // reset to default css styling that.$menu[0].style.minwidth = ''; } } } } that.prevactiveindex = that.activeindex; if (!that.options.livesearch) { that.$menuinner.trigger('focus'); } else if (issearching && init) { var index = 0, newactive; if (!that.selectpicker.view.canhighlight[index]) { index = 1 + that.selectpicker.view.canhighlight.slice(1).indexof(true); } newactive = that.selectpicker.view.visibleelements[index]; that.defocusitem(that.selectpicker.view.currentactive); that.activeindex = (that.selectpicker.current.data[index] || {}).index; that.focusitem(newactive); } } $(window) .off('resize' + event_key + '.' + this.selectid + '.createview') .on('resize' + event_key + '.' + this.selectid + '.createview', function () { var isactive = that.$newelement.hasclass(classnames.show); if (isactive) scroll(that.$menuinner[0].scrolltop); }); }, focusitem: function (li, lidata, nostyle) { if (li) { lidata = lidata || this.selectpicker.main.data[this.activeindex]; var a = li.firstchild; if (a) { a.setattribute('aria-setsize', this.selectpicker.view.size); a.setattribute('aria-posinset', lidata.posinset); if (nostyle !== true) { this.focusedparent.setattribute('aria-activedescendant', a.id); li.classlist.add('active'); a.classlist.add('active'); } } } }, defocusitem: function (li) { if (li) { li.classlist.remove('active'); if (li.firstchild) li.firstchild.classlist.remove('active'); } }, setplaceholder: function () { var updateindex = false; if (this.options.title && !this.multiple) { if (!this.selectpicker.view.titleoption) this.selectpicker.view.titleoption = document.createelement('option'); // this option doesn't create a new element, but does add a new option at the start, // so startindex should increase to prevent having to check every option for the bs-title-option class updateindex = true; var element = this.$element[0], 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; } return updateindex; }, builddata: function () { var optionselector = ':not([hidden]):not([data-hidden="true"])', maindata = [], optid = 0, startindex = this.setplaceholder() ? 1 : 0; // append the titleoption if necessary and skip the first option in the loop if (this.options.hidedisabled) optionselector += ':not(:disabled)'; var selectoptions = this.$element[0].queryselectorall('select > *' + optionselector); function adddivider (config) { var previousdata = maindata[maindata.length - 1]; // ensure optgroup doesn't create back-to-back dividers if ( previousdata && previousdata.type === 'divider' && (previousdata.optid || config.optid) ) { return; } config = config || {}; config.type = 'divider'; maindata.push(config); } function addoption (option, config) { config = config || {}; config.divider = option.getattribute('data-divider') === 'true'; if (config.divider) { adddivider({ optid: config.optid }); } else { var liindex = maindata.length, csstext = option.style.csstext, inlinestyle = csstext ? htmlescape(csstext) : '', optionclass = (option.classname || '') + (config.optgroupclass || ''); if (config.optid) optionclass = 'opt ' + optionclass; config.optionclass = optionclass.trim(); config.inlinestyle = inlinestyle; config.text = option.textcontent; config.content = option.getattribute('data-content'); config.tokens = option.getattribute('data-tokens'); config.subtext = option.getattribute('data-subtext'); config.icon = option.getattribute('data-icon'); option.liindex = liindex; config.display = config.content || config.text; config.type = 'option'; config.index = liindex; config.option = option; config.selected = !!option.selected; config.disabled = config.disabled || !!option.disabled; maindata.push(config); } } function addoptgroup (index, selectoptions) { var optgroup = selectoptions[index], previous = selectoptions[index - 1], next = selectoptions[index + 1], options = optgroup.queryselectorall('option' + optionselector); if (!options.length) return; var config = { display: htmlescape(optgroup.label), subtext: optgroup.getattribute('data-subtext'), icon: optgroup.getattribute('data-icon'), type: 'optgroup-label', optgroupclass: ' ' + (optgroup.classname || '') }, headerindex, lastindex; optid++; if (previous) { adddivider({ optid: optid }); } config.optid = optid; maindata.push(config); for (var j = 0, len = options.length; j < len; j++) { var option = options[j]; if (j === 0) { headerindex = maindata.length - 1; lastindex = headerindex + len; } addoption(option, { headerindex: headerindex, lastindex: lastindex, optid: config.optid, optgroupclass: config.optgroupclass, disabled: optgroup.disabled }); } if (next) { adddivider({ optid: optid }); } } for (var len = selectoptions.length; startindex < len; startindex++) { var item = selectoptions[startindex]; if (item.tagname !== 'optgroup') { addoption(item, {}); } else { addoptgroup(startindex, selectoptions); } } this.selectpicker.main.data = this.selectpicker.current.data = maindata; }, buildlist: function () { var that = this, selectdata = this.selectpicker.main.data, mainelements = [], widestoptionlength = 0; if ((that.options.showtick || that.multiple) && !elementtemplates.checkmark.parentnode) { elementtemplates.checkmark.classname = this.options.iconbase + ' ' + that.options.tickicon + ' check-mark'; elementtemplates.a.appendchild(elementtemplates.checkmark); } function buildelement (item) { var lielement, combinedlength = 0; switch (item.type) { case 'divider': lielement = generateoption.li( false, classnames.divider, (item.optid ? item.optid + 'div' : undefined) ); break; case 'option': lielement = generateoption.li( generateoption.a( generateoption.text.call(that, item), item.optionclass, item.inlinestyle ), '', item.optid ); if (lielement.firstchild) { lielement.firstchild.id = that.selectid + '-' + item.index; } break; case 'optgroup-label': lielement = generateoption.li( generateoption.label.call(that, item), 'dropdown-header' + item.optgroupclass, item.optid ); break; } mainelements.push(lielement); // count the number of characters in the option - not perfect, but should work in most cases if (item.display) combinedlength += item.display.length; if (item.subtext) combinedlength += item.subtext.length; // if there is an icon, ensure this option's width is checked if (item.icon) combinedlength += 1; if (combinedlength > widestoptionlength) { widestoptionlength = combinedlength; // guess which option is the widest // use this when calculating menu width // not perfect, but it's fast, and the width will be updating accordingly when scrolling that.selectpicker.view.widestoption = mainelements[mainelements.length - 1]; } } for (var len = selectdata.length, i = 0; i < len; i++) { var item = selectdata[i]; buildelement(item); } this.selectpicker.main.elements = this.selectpicker.current.elements = mainelements; }, findlis: function () { return this.$menuinner.find('.inner > li'); }, render: function () { var that = this, element = this.$element[0], // ensure titleoption is appended and selected (if necessary) before getting selectedoptions placeholderselected = this.setplaceholder() && element.selectedindex === 0, selectedoptions = getselectedoptions(element, this.options.hidedisabled), selectedcount = selectedoptions.length, button = this.$button[0], buttoninner = button.queryselector('.filter-option-inner-inner'), multipleseparator = document.createtextnode(this.options.multipleseparator), titlefragment = elementtemplates.fragment.clonenode(false), showcount, countmax, hascontent = false; button.classlist.toggle('bs-placeholder', that.multiple ? !selectedcount : !getselectvalues(element, selectedoptions)); this.tabindex(); if (this.options.selectedtextformat === 'static') { titlefragment = generateoption.text.call(this, { text: this.options.title }, true); } else { showcount = this.multiple && this.options.selectedtextformat.indexof('count') !== -1 && selectedcount > 1; // determine if the number of selected options will be shown (showcount === true) if (showcount) { countmax = this.options.selectedtextformat.split('>'); showcount = (countmax.length > 1 && selectedcount > countmax[1]) || (countmax.length === 1 && selectedcount >= 2); } // only loop through all selected options if the count won't be shown if (showcount === false) { if (!placeholderselected) { for (var selectedindex = 0; selectedindex < selectedcount; selectedindex++) { if (selectedindex < 50) { var option = selectedoptions[selectedindex], thisdata = this.selectpicker.main.data[option.liindex], titleoptions = {}; if (this.multiple && selectedindex > 0) { titlefragment.appendchild(multipleseparator.clonenode(false)); } if (option.title) { titleoptions.text = option.title; } else if (thisdata) { if (thisdata.content && that.options.showcontent) { titleoptions.content = thisdata.content.tostring(); hascontent = true; } else { if (that.options.showicon) { titleoptions.icon = thisdata.icon; } if (that.options.showsubtext && !that.multiple && thisdata.subtext) titleoptions.subtext = ' ' + thisdata.subtext; titleoptions.text = option.textcontent.trim(); } } titlefragment.appendchild(generateoption.text.call(this, titleoptions, true)); } else { break; } } // add ellipsis if (selectedcount > 49) { titlefragment.appendchild(document.createtextnode('...')); } } } else { var optionselector = ':not([hidden]):not([data-hidden="true"]):not([data-divider="true"])'; if (this.options.hidedisabled) optionselector += ':not(:disabled)'; // if this is a multiselect, and selectedtextformat is count, then show 1 of 2 selected, etc. var totalcount = this.$element[0].queryselectorall('select > option' + optionselector + ', optgroup' + optionselector + ' option' + optionselector).length, tr8ntext = (typeof this.options.countselectedtext === 'function') ? this.options.countselectedtext(selectedcount, totalcount) : this.options.countselectedtext; titlefragment = generateoption.text.call(this, { text: tr8ntext.replace('{0}', selectedcount.tostring()).replace('{1}', totalcount.tostring()) }, true); } } if (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 the select doesn't have a title, then use the default, or if nothing is set at all, use noneselectedtext if (!titlefragment.childnodes.length) { titlefragment = generateoption.text.call(this, { text: typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneselectedtext }, true); } // strip all html tags and trim the result, then unescape any escaped tags button.title = titlefragment.textcontent.replace(/<[^>]*>?/g, '').trim(); if (this.options.sanitize && hascontent) { sanitizehtml([titlefragment], that.options.whitelist, that.options.sanitizefn); } buttoninner.innerhtml = ''; buttoninner.appendchild(titlefragment); if (version.major < 4 && this.$newelement[0].classlist.contains('bs3-has-addon')) { var filterexpand = button.queryselector('.filter-expand'), clone = buttoninner.clonenode(true); clone.classname = 'filter-expand'; if (filterexpand) { button.replacechild(clone, filterexpand); } else { button.appendchild(clone); } } this.$element.trigger('rendered' + event_key); }, /** * @param [style] * @param [status] */ setstyle: function (newstyle, status) { var button = this.$button[0], newelement = this.$newelement[0], style = this.options.style.trim(), buttonclass; if (this.$element.attr('class')) { this.$newelement.addclass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); } if (version.major < 4) { newelement.classlist.add('bs3'); if (newelement.parentnode.classlist.contains('input-group') && (newelement.previouselementsibling || newelement.nextelementsibling) && (newelement.previouselementsibling || newelement.nextelementsibling).classlist.contains('input-group-addon') ) { newelement.classlist.add('bs3-has-addon'); } } if (newstyle) { buttonclass = newstyle.trim(); } else { buttonclass = style; } if (status == 'add') { if (buttonclass) button.classlist.add.apply(button.classlist, buttonclass.split(' ')); } else if (status == 'remove') { if (buttonclass) button.classlist.remove.apply(button.classlist, buttonclass.split(' ')); } else { if (style) button.classlist.remove.apply(button.classlist, style.split(' ')); if (buttonclass) button.classlist.add.apply(button.classlist, buttonclass.split(' ')); } }, liheight: function (refresh) { if (!refresh && (this.options.size === false || object.keys(this.sizeinfo).length)) return; var newelement = 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 = 0; // ensure button width doesn't affect natural width of menu when calculating if (this.options.width === 'auto') menu.style.minwidth = 0; menu.classname = classnames.menu + ' ' + classnames.show; menuinner.classname = 'inner ' + classnames.show; menuinnerinner.classname = classnames.menu + ' inner ' + (version.major === '4' ? classnames.show : ''); divider.classname = classnames.divider; dropdownheader.classname = 'dropdown-header'; text.appendchild(document.createtextnode('\u200b')); a.appendchild(text); li.appendchild(a); dropdownheader.appendchild(text.clonenode(true)); if (this.selectpicker.view.widestoption) { menuinnerinner.appendchild(this.selectpicker.view.widestoption.clonenode(true)); } menuinnerinner.appendchild(li); menuinnerinner.appendchild(divider); menuinnerinner.appendchild(dropdownheader); if (header) menu.appendchild(header); if (search) { var input = document.createelement('input'); search.classname = 'bs-searchbox'; input.classname = 'form-control'; search.appendchild(input); menu.appendchild(search); } if (actions) menu.appendchild(actions); menuinner.appendchild(menuinnerinner); menu.appendchild(menuinner); if (donebutton) menu.appendchild(donebutton); newelement.appendchild(menu); document.body.appendchild(newelement); var liheight = li.offsetheight, dropdownheaderheight = dropdownheader ? dropdownheader.offsetheight : 0, headerheight = header ? header.offsetheight : 0, searchheight = search ? search.offsetheight : 0, actionsheight = actions ? actions.offsetheight : 0, donebuttonheight = donebutton ? donebutton.offsetheight : 0, dividerheight = $(divider).outerheight(true), // 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.menuinnerinnerwidth = menuwidth - menupadding.horiz; this.sizeinfo.totalmenuwidth = this.sizeinfo.menuwidth; this.sizeinfo.scrollbarwidth = scrollbarwidth; this.sizeinfo.selectheight = this.$newelement[0].offsetheight; this.setpositiondata(); }, getselectposition: function () { var that = this, $window = $(window), pos = that.$newelement.offset(), $container = $(that.options.container), containerpos; if (that.options.container && $container.length && !$container.is('body')) { containerpos = $container.offset(); containerpos.top += parseint($container.css('bordertopwidth')); containerpos.left += parseint($container.css('borderleftwidth')); } else { containerpos = { top: 0, left: 0 }; } var winpad = that.options.windowpadding; this.sizeinfo.selectoffsettop = pos.top - containerpos.top - $window.scrolltop(); this.sizeinfo.selectoffsetbot = $window.height() - this.sizeinfo.selectoffsettop - this.sizeinfo.selectheight - containerpos.top - winpad[2]; this.sizeinfo.selectoffsetleft = pos.left - containerpos.left - $window.scrollleft(); this.sizeinfo.selectoffsetright = $window.width() - this.sizeinfo.selectoffsetleft - this.sizeinfo.selectwidth - containerpos.left - winpad[1]; this.sizeinfo.selectoffsettop -= winpad[0]; this.sizeinfo.selectoffsetleft -= winpad[3]; }, setmenusize: function (isauto) { this.getselectposition(); var selectwidth = this.sizeinfo.selectwidth, liheight = this.sizeinfo.liheight, headerheight = this.sizeinfo.headerheight, searchheight = this.sizeinfo.searchheight, actionsheight = this.sizeinfo.actionsheight, donebuttonheight = this.sizeinfo.donebuttonheight, divheight = this.sizeinfo.dividerheight, menupadding = this.sizeinfo.menupadding, menuinnerheight, menuheight, divlength = 0, minheight, _minheight, maxheight, menuinnerminheight, estimate, isdropup; if (this.options.dropupauto) { // get the estimated height of the menu without scrollbars. // this is useful for smaller menus, where there might be plenty of room // below the button without setting dropup, but we can't know // the exact height of the menu until createview is called later estimate = liheight * this.selectpicker.current.elements.length + menupadding.vert; isdropup = this.sizeinfo.selectoffsettop - this.sizeinfo.selectoffsetbot > this.sizeinfo.menuextras.vert && estimate + this.sizeinfo.menuextras.vert + 50 > this.sizeinfo.selectoffsetbot; // ensure dropup doesn't change while searching (so menu doesn't bounce back and forth) if (this.selectpicker.issearching === true) { isdropup = this.selectpicker.dropup; } this.$newelement.toggleclass(classnames.dropup, isdropup); this.selectpicker.dropup = isdropup; } if (this.options.size === 'auto') { _minheight = this.selectpicker.current.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 = ''; } 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' }); // ensure menuinnerheight is always a positive number to prevent issues calculating chunksize in createview this.sizeinfo.menuinnerheight = math.max(menuinnerheight, 1); if (this.selectpicker.current.data.length && this.selectpicker.current.data[this.selectpicker.current.data.length - 1].position > this.sizeinfo.menuinnerheight) { this.sizeinfo.hasscrollbar = true; this.sizeinfo.totalmenuwidth = this.sizeinfo.menuwidth + this.sizeinfo.scrollbarwidth; } if (this.options.dropdownalignright === 'auto') { this.$menu.toggleclass(classnames.menuright, this.sizeinfo.selectoffsetleft > this.sizeinfo.selectoffsetright && this.sizeinfo.selectoffsetright < (this.sizeinfo.totalmenuwidth - selectwidth)); } if (this.dropdown && this.dropdown._popper) this.dropdown._popper.update(); }, setsize: function (refresh) { this.liheight(refresh); if (this.options.header) this.$menu.css('padding-top', 0); if (this.options.size !== false) { var that = this, $window = $(window); this.setmenusize(); if (this.options.livesearch) { this.$searchbox .off('input.setmenusize propertychange.setmenusize') .on('input.setmenusize propertychange.setmenusize', function () { return that.setmenusize(); }); } if (this.options.size === 'auto') { $window .off('resize' + event_key + '.' + this.selectid + '.setmenusize' + ' scroll' + event_key + '.' + this.selectid + '.setmenusize') .on('resize' + event_key + '.' + this.selectid + '.setmenusize' + ' scroll' + event_key + '.' + this.selectid + '.setmenusize', function () { return that.setmenusize(); }); } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { $window.off('resize' + event_key + '.' + this.selectid + '.setmenusize' + ' scroll' + event_key + '.' + this.selectid + '.setmenusize'); } } this.createview(false, true, refresh); }, setwidth: function () { var that = this; if (this.options.width === 'auto') { requestanimationframe(function () { that.$menu.css('min-width', '0'); that.$element.on('loaded' + event_key, function () { that.liheight(); that.setmenusize(); // get correct width if element is hidden var $selectclone = that.$newelement.clone().appendto('body'), btnwidth = $selectclone.css('width', 'auto').children('button').outerwidth(); $selectclone.remove(); // set width to whatever's larger, button title or longest option that.sizeinfo.selectwidth = math.max(that.sizeinfo.totalmenuwidth, btnwidth); that.$newelement.css('width', that.sizeinfo.selectwidth + 'px'); }); }); } else if (this.options.width === 'fit') { // remove inline min-width so width can be changed from 'auto' this.$menu.css('min-width', ''); this.$newelement.css('width', '').addclass('fit-width'); } else if (this.options.width) { // remove inline min-width so width can be changed from 'auto' this.$menu.css('min-width', ''); this.$newelement.css('width', this.options.width); } else { // remove inline min-width/width so width can be changed this.$menu.css('min-width', ''); this.$newelement.css('width', ''); } // remove fit-width class if width is changed programmatically if (this.$newelement.hasclass('fit-width') && this.options.width !== 'fit') { this.$newelement[0].classlist.remove('fit-width'); } }, selectposition: function () { this.$bscontainer = $(''); var that = this, $container = $(this.options.container), pos, containerpos, actualheight, getplacement = function ($element) { var containerposition = {}, // fall back to dropdown's default display setting if display is not manually set display = that.options.display || ( // bootstrap 3 doesn't have $.fn.dropdown.constructor.default $.fn.dropdown.constructor.default ? $.fn.dropdown.constructor.default.display : false ); that.$bscontainer.addclass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleclass(classnames.dropup, $element.hasclass(classnames.dropup)); pos = $element.offset(); if (!$container.is('body')) { containerpos = $container.offset(); containerpos.top += parseint($container.css('bordertopwidth')) - $container.scrolltop(); containerpos.left += parseint($container.css('borderleftwidth')) - $container.scrollleft(); } else { containerpos = { top: 0, left: 0 }; } actualheight = $element.hasclass(classnames.dropup) ? 0 : $element[0].offsetheight; // bootstrap 4+ uses popper for menu positioning if (version.major < 4 || display === 'static') { containerposition.top = pos.top - containerpos.top + actualheight; containerposition.left = pos.left - containerpos.left; } containerposition.width = $element[0].offsetwidth; that.$bscontainer.css(containerposition); }; this.$button.on('click.bs.dropdown.data-api', function () { if (that.isdisabled()) { return; } getplacement(that.$newelement); that.$bscontainer .appendto(that.options.container) .toggleclass(classnames.show, !that.$button.hasclass(classnames.show)) .append(that.$menu); }); $(window) .off('resize' + event_key + '.' + this.selectid + ' scroll' + event_key + '.' + this.selectid) .on('resize' + event_key + '.' + this.selectid + ' scroll' + event_key + '.' + this.selectid, function () { var isactive = that.$newelement.hasclass(classnames.show); if (isactive) getplacement(that.$newelement); }); this.$element.on('hide' + event_key, function () { that.$menu.data('height', that.$menu.height()); that.$bscontainer.detach(); }); }, setoptionstatus: function (selectedonly) { var that = this; that.noscroll = false; if (that.selectpicker.view.visibleelements && that.selectpicker.view.visibleelements.length) { for (var i = 0; i < that.selectpicker.view.visibleelements.length; i++) { var lidata = that.selectpicker.current.data[i + that.selectpicker.view.position0], option = lidata.option; if (option) { if (selectedonly !== true) { that.setdisabled( lidata.index, lidata.disabled ); } that.setselected( lidata.index, option.selected ); } } } }, /** * @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) { var li = this.selectpicker.main.elements[index], lidata = this.selectpicker.main.data[index], activeindexisset = this.activeindex !== undefined, thisisactive = this.activeindex === index, 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); lidata.selected = selected; a = li.firstchild; if (selected) { this.selectedindex = index; } li.classlist.toggle('selected', selected); if (keepactive) { this.focusitem(li, lidata); this.selectpicker.view.currentactive = li; this.activeindex = index; } else { this.defocusitem(li); } if (a) { a.classlist.toggle('selected', selected); if (selected) { a.setattribute('aria-selected', true); } else { if (this.multiple) { a.setattribute('aria-selected', false); } else { a.removeattribute('aria-selected'); } } } if (!keepactive && !activeindexisset && selected && this.prevactiveindex !== undefined) { prevactive = this.selectpicker.main.elements[this.prevactiveindex]; this.defocusitem(prevactive); } }, /** * @param {number} index - the index of the option that is being disabled * @param {boolean} disabled - true if the option is being disabled, false if being enabled */ setdisabled: function (index, disabled) { var li = this.selectpicker.main.elements[index], a; this.selectpicker.main.data[index].disabled = disabled; a = li.firstchild; li.classlist.toggle(classnames.disabled, disabled); if (a) { if (version.major === '4') a.classlist.toggle(classnames.disabled, disabled); if (disabled) { a.setattribute('aria-disabled', disabled); a.setattribute('tabindex', -1); } else { a.removeattribute('aria-disabled'); a.setattribute('tabindex', 0); } } }, isdisabled: function () { return this.$element[0].disabled; }, checkdisabled: function () { if (this.isdisabled()) { this.$newelement[0].classlist.add(classnames.disabled); this.$button.addclass(classnames.disabled).attr('tabindex', -1).attr('aria-disabled', true); } else { if (this.$button[0].classlist.contains(classnames.disabled)) { this.$newelement[0].classlist.remove(classnames.disabled); this.$button.removeclass(classnames.disabled).attr('aria-disabled', false); } if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) { this.$button.removeattr('tabindex'); } } }, 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.trigger('focus'); } else { that.$menuinner.trigger('focus'); } } function checkpopperexists () { if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state.iscreated) { setfocus(); } else { requestanimationframe(checkpopperexists); } } this.$element.on('shown' + event_key, function () { if (that.$menuinner[0].scrolltop !== that.selectpicker.view.scrolltop) { that.$menuinner[0].scrolltop = that.selectpicker.view.scrolltop; } if (version.major > 3) { requestanimationframe(checkpopperexists); } else { setfocus(); } }); // ensure posinset and setsize are correct before selecting an option via a click this.$menuinner.on('mouseenter', 'li a', function (e) { var hoverli = this.parentelement, position0 = that.isvirtual() ? that.selectpicker.view.position0 : 0, index = array.prototype.indexof.call(hoverli.parentelement.children, hoverli), hoverdata = that.selectpicker.current.data[index + position0]; that.focusitem(hoverli, hoverdata, true); }); this.$menuinner.on('click', 'li a', function (e, retainactive) { var $this = $(this), element = that.$element[0], position0 = that.isvirtual() ? that.selectpicker.view.position0 : 0, clickeddata = that.selectpicker.current.data[$this.parent().index() + position0], clickedindex = clickeddata.index, prevvalue = getselectvalues(element), previndex = element.selectedindex, prevoption = element.options[previndex], triggerchange = true; // don't close on multi choice menu if (that.multiple && that.options.maxoptions !== 1) { e.stoppropagation(); } e.preventdefault(); // don't run if the select is disabled if (!that.isdisabled() && !$this.parent().hasclass(classnames.disabled)) { var option = clickeddata.option, $option = $(option), state = option.selected, $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 if (prevoption) prevoption.selected = false; option.selected = true; that.setselected(clickedindex, true); } else { // toggle the one we have chosen if we are multi select. option.selected = !state; that.setselected(clickedindex, !state); $this.trigger('blur'); if (maxoptions !== false || maxoptionsgrp !== false) { var maxreached = maxoptions < getselectedoptions(element).length, maxreachedgrp = maxoptionsgrp < $optgroup.find('option:selected').length; if ((maxoptions && maxreached) || (maxoptionsgrp && maxreachedgrp)) { if (maxoptions && maxoptions == 1) { element.selectedindex = -1; option.selected = true; that.setoptionstatus(true); } else if (maxoptionsgrp && maxoptionsgrp == 1) { for (var i = 0; i < $optgroupoptions.length; i++) { var _option = $optgroupoptions[i]; _option.selected = false; that.setselected(_option.liindex, false); } option.selected = true; 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.selected = false; that.$menu.append($notify); if (maxoptions && maxreached) { $notify.append($('' + maxtxt + '')); triggerchange = false; that.$element.trigger('maxreached' + event_key); } if (maxoptionsgrp && maxreachedgrp) { $notify.append($('' + maxtxtgrp + '')); triggerchange = false; that.$element.trigger('maxreachedgrp' + event_key); } settimeout(function () { that.setselected(clickedindex, false); }, 10); $notify[0].classlist.add('fadeout'); settimeout(function () { $notify.remove(); }, 1050); } } } } if (!that.multiple || (that.multiple && that.options.maxoptions === 1)) { that.$button.trigger('focus'); } else if (that.options.livesearch) { that.$searchbox.trigger('focus'); } // trigger select 'change' if (triggerchange) { if (that.multiple || previndex !== element.selectedindex) { // $option.prop('selected') is current option state (selected/unselected). prevvalue is the value of the select prior to being changed. changedarguments = [option.index, $option.prop('selected'), prevvalue]; that.$element .triggernative('change'); } } } }); this.$menu.on('click', 'li.' + classnames.disabled + ' a, .' + classnames.popoverheader + ', .' + classnames.popoverheader + ' :not(.close)', function (e) { if (e.currenttarget == this) { e.preventdefault(); e.stoppropagation(); if (that.options.livesearch && !$(e.target).hasclass('close')) { that.$searchbox.trigger('focus'); } else { that.$button.trigger('focus'); } } }); this.$menuinner.on('click', '.divider, .dropdown-header', function (e) { e.preventdefault(); e.stoppropagation(); if (that.options.livesearch) { that.$searchbox.trigger('focus'); } else { that.$button.trigger('focus'); } }); this.$menu.on('click', '.' + classnames.popoverheader + ' .close', function () { that.$button.trigger('click'); }); this.$searchbox.on('click', function (e) { e.stoppropagation(); }); this.$menu.on('click', '.actions-btn', function (e) { if (that.options.livesearch) { that.$searchbox.trigger('focus'); } else { that.$button.trigger('focus'); } e.preventdefault(); e.stoppropagation(); if ($(this).hasclass('bs-select-all')) { that.selectall(); } else { that.deselectall(); } }); this.$element .on('change' + event_key, function () { that.render(); that.$element.trigger('changed' + event_key, changedarguments); changedarguments = null; }) .on('focus' + event_key, function () { if (!that.options.mobile) that.$button.trigger('focus'); }); }, livesearchlistener: function () { var that = this, noresults = 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.elements = []; that.selectpicker.search.data = []; if (searchvalue) { var i, searchmatch = [], q = searchvalue.touppercase(), cache = {}, cachearr = [], searchstyle = that._searchstyle(), normalizesearch = that.options.livesearchnormalize; if (normalizesearch) q = normalizetobase(q); for (var i = 0; i < that.selectpicker.main.data.length; i++) { var li = that.selectpicker.main.data[i]; if (!cache[i]) { cache[i] = stringsearch(li, q, searchstyle, normalizesearch); } if (cache[i] && li.headerindex !== undefined && cachearr.indexof(li.headerindex) === -1) { if (li.headerindex > 0) { cache[li.headerindex - 1] = true; cachearr.push(li.headerindex - 1); } cache[li.headerindex] = true; cachearr.push(li.headerindex); cache[li.lastindex + 1] = true; } if (cache[i] && li.type !== 'optgroup-label') cachearr.push(i); } for (var i = 0, cachelen = cachearr.length; i < cachelen; i++) { var index = cachearr[i], previndex = cachearr[i - 1], li = that.selectpicker.main.data[index], liprev = that.selectpicker.main.data[previndex]; if (li.type !== 'divider' || (li.type === 'divider' && liprev && liprev.type !== 'divider' && cachelen - 1 !== i)) { that.selectpicker.search.data.push(li); searchmatch.push(that.selectpicker.main.elements[index]); } } that.activeindex = undefined; that.noscroll = true; that.$menuinner.scrolltop(0); that.selectpicker.search.elements = searchmatch; that.createview(true); if (!searchmatch.length) { noresults.classname = 'no-results'; noresults.innerhtml = that.options.noneresultstext.replace('{0}', '"' + htmlescape(searchvalue) + '"'); that.$menuinner[0].firstchild.appendchild(noresults); } } else { that.$menuinner.scrolltop(0); that.createview(false); } }); }, _searchstyle: function () { return this.options.livesearchstyle || 'contains'; }, val: function (value) { var element = this.$element[0]; if (typeof value !== 'undefined') { var prevvalue = getselectvalues(element); changedarguments = [null, null, prevvalue]; this.$element .val(value) .trigger('changed' + event_key, changedarguments); if (this.$newelement.hasclass(classnames.show)) { if (this.multiple) { this.setoptionstatus(true); } else { var liselectedindex = (element.options[element.selectedindex] || {}).liindex; if (typeof liselectedindex === 'number') { this.setselected(this.selectedindex, false); this.setselected(liselectedindex, true); } } } this.render(); changedarguments = null; return this.$element; } else { return this.$element.val(); } }, changeall: function (status) { if (!this.multiple) return; if (typeof status === 'undefined') status = true; var element = this.$element[0], previousselected = 0, currentselected = 0, prevvalue = getselectvalues(element); element.classlist.add('bs-select-hidden'); for (var i = 0, data = this.selectpicker.current.data, len = data.length; i < len; i++) { var lidata = data[i], option = lidata.option; if (option && !lidata.disabled && lidata.type !== 'divider') { if (lidata.selected) previousselected++; option.selected = status; if (status === true) currentselected++; } } element.classlist.remove('bs-select-hidden'); if (previousselected === currentselected) return; this.setoptionstatus(); changedarguments = [null, null, prevvalue]; this.$element .triggernative('change'); }, selectall: function () { return this.changeall(true); }, deselectall: function () { return this.changeall(false); }, toggle: function (e) { 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; // do nothing if a function key is pressed if (e.which >= 112 && e.which <= 123) return; 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 (that.options.livesearch) { that.$searchbox.trigger('focus'); return; } } if (e.which === keycodes.escape && isactive) { e.preventdefault(); that.$button.trigger('click.bs.dropdown.data-api').trigger('focus'); } if (isarrowkey) { // if up or down if (!$items.length) return; liactive = that.selectpicker.main.elements[that.activeindex]; index = liactive ? array.prototype.indexof.call(liactive.parentelement.children, liactive) : -1; if (index !== -1) { that.defocusitem(liactive); } if (e.which === keycodes.arrow_up) { // up if (index !== -1) index--; if (index + position0 < 0) index += $items.length; if (!that.selectpicker.view.canhighlight[index + position0]) { index = that.selectpicker.view.canhighlight.slice(0, index + position0).lastindexof(true) - position0; if (index === -1) index = $items.length - 1; } } else if (e.which === keycodes.arrow_down || downontab) { // down index++; if (index + position0 >= that.selectpicker.view.canhighlight.length) index = 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]; that.activeindex = that.selectpicker.current.data[liactiveindex].index; that.focusitem(liactive); that.selectpicker.view.currentactive = liactive; if (updatescroll) that.$menuinner[0].scrolltop = offset; if (that.options.livesearch) { that.$searchbox.trigger('focus'); } else { $this.trigger('focus'); } } else if ( (!$this.is('input') && !regexp_tab_or_escape.test(e.which)) || (e.which === keycodes.space && that.selectpicker.keydown.keyhistory) ) { var searchmatch, matches = [], keyhistory; e.preventdefault(); that.selectpicker.keydown.keyhistory += keycodemap[e.which]; if (that.selectpicker.keydown.resetkeyhistory.cancel) cleartimeout(that.selectpicker.keydown.resetkeyhistory.cancel); that.selectpicker.keydown.resetkeyhistory.cancel = that.selectpicker.keydown.resetkeyhistory.start(); keyhistory = that.selectpicker.keydown.keyhistory; // if all letters are the same, set keyhistory to just the first character when searching if (/^(.)\1+$/.test(keyhistory)) { keyhistory = keyhistory.charat(0); } // find matches for (var i = 0; i < that.selectpicker.current.data.length; i++) { var li = that.selectpicker.current.data[i], hasmatch; hasmatch = stringsearch(li, keyhistory, 'startswith', true); if (hasmatch && that.selectpicker.view.canhighlight[i]) { matches.push(li.index); } } 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 = matches[matchindex]; activeli = that.selectpicker.main.data[searchmatch]; if (scrolltop - activeli.position > 0) { offset = activeli.position - activeli.height; updatescroll = true; } else { offset = activeli.position - that.sizeinfo.menuinnerheight; // if the option is already visible at the current scroll position, just keep it the same updatescroll = activeli.position > scrolltop + that.sizeinfo.menuinnerheight; } liactive = that.selectpicker.main.elements[searchmatch]; that.activeindex = matches[matchindex]; that.focusitem(liactive); if (liactive) liactive.firstchild.focus(); if (updatescroll) that.$menuinner[0].scrolltop = offset; $this.trigger('focus'); } } // select focused option if "enter", "spacebar" or "tab" (when selectontab is true) are pressed inside the menu. if ( isactive && ( (e.which === keycodes.space && !that.selectpicker.keydown.keyhistory) || e.which === keycodes.enter || (e.which === keycodes.tab && that.options.selectontab) ) ) { if (e.which !== keycodes.space) e.preventdefault(); if (!that.options.livesearch || e.which !== keycodes.space) { that.$menuinner.find('.active a').trigger('click', true); // retain active class $this.trigger('focus'); if (!that.options.livesearch) { // prevent screen from scrolling if the user hits the spacebar e.preventdefault(); // fixes spacebar selection of dropdown items in ff & ie $(document).data('spaceselect', true); } } } }, mobile: function () { this.$element[0].classlist.add('mobile-device'); }, refresh: function () { // update options if data attributes have been changed var config = $.extend({}, this.options, this.$element.data()); this.options = config; this.checkdisabled(); this.setstyle(); this.render(); this.builddata(); this.buildlist(); this.setwidth(); this.setsize(true); this.$element.trigger('refreshed' + event_key); }, hide: function () { this.$newelement.hide(); }, show: function () { this.$newelement.show(); }, remove: function () { this.$newelement.remove(); this.$element.remove(); }, destroy: function () { this.$newelement.before(this.$element).remove(); if (this.$bscontainer) { this.$bscontainer.remove(); } else { this.$menu.remove(); } this.$element .off(event_key) .removedata('selectpicker') .removeclass('bs-select-hidden selectpicker'); $(window).off(event_key + '.' + this.selectid); } }; // selectpicker plugin definition // ============================== function plugin (option) { // get the args of the outer function.. var args = arguments; // the arguments of the function are explicitly re-defined from the argument list, because the shift causes them // to get lost/corrupted in android 2.3 and ie9 #715 #775 var _option = option; [].shift.apply(args); // if the version was not set successfully if (!version.success) { // try to retreive it again try { version.full = ($.fn.dropdown.constructor.version || '').split(' ')[0].split('.'); } catch (err) { // fall back to use bootstrapversion if set if (selectpicker.bootstrapversion) { version.full = selectpicker.bootstrapversion.split(' ')[0].split('.'); } else { version.full = [version.major, '0', '0']; console.warn( 'there was an issue retrieving bootstrap\'s version. ' + 'ensure bootstrap is being loaded before bootstrap-select and there is no namespace collision. ' + 'if loading bootstrap asynchronously, the version may need to be manually specified via $.fn.selectpicker.constructor.bootstrapversion.', err ); } } version.major = version.full[0]; version.success = true; } if (version.major === '4') { // some defaults need to be changed if using bootstrap 4 // check to see if they have already been manually changed before forcing them to update var toupdate = []; if (selectpicker.defaults.style === classnames.buttonclass) toupdate.push({ name: 'style', classname: 'buttonclass' }); if (selectpicker.defaults.iconbase === classnames.iconbase) toupdate.push({ name: 'iconbase', classname: 'iconbase' }); if (selectpicker.defaults.tickicon === classnames.tickicon) toupdate.push({ name: 'tickicon', classname: 'tickicon' }); classnames.divider = 'dropdown-divider'; classnames.show = 'show'; classnames.buttonclass = 'btn-light'; classnames.popoverheader = 'popover-header'; classnames.iconbase = ''; classnames.tickicon = 'bs-ok-default'; for (var i = 0; i < toupdate.length; i++) { var option = toupdate[i]; selectpicker.defaults[option.name] = classnames[option.classname]; } } 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 dataattributes = $this.data(); for (var dataattr in dataattributes) { if (dataattributes.hasownproperty(dataattr) && $.inarray(dataattr, disallowed_attributes) !== -1) { delete dataattributes[dataattr]; } } var config = $.extend({}, selectpicker.defaults, $.fn.selectpicker.defaults || {}, dataattributes, options); config.template = $.extend({}, selectpicker.defaults.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), dataattributes.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; }; // get bootstrap's keydown event handler for either bootstrap 4 or bootstrap 3 var bootstrapkeydown = $.fn.dropdown.constructor._dataapikeydownhandler || $.fn.dropdown.constructor.prototype.keydown; $(document) .off('keydown.bs.dropdown.data-api') .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > [data-toggle="dropdown"]', bootstrapkeydown) .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > .dropdown-menu', bootstrapkeydown) .on('keydown' + event_key, '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', selectpicker.prototype.keydown) .on('focusin.modal', '.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', function (e) { e.stoppropagation(); }); // selectpicker data-api // ===================== $(window).on('load' + event_key + '.data-api', function () { $('.selectpicker').each(function () { var $selectpicker = $(this); plugin.call($selectpicker, $selectpicker.data()); }) }); })(jquery);