diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php index e3eb5c7c1..addc2c19b 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php @@ -80,7 +80,7 @@ class ControllerBase extends ControllerRoot // default theme 'css/main.css', // Stylesheet for fancy select/dropdown - '/css/bootstrap-select-1.13.3.css', + '/css/bootstrap-select.css', // bootstrap dialog '/css/bootstrap-dialog.css', // Font awesome diff --git a/src/opnsense/www/css/bootstrap-select-1.13.3.css b/src/opnsense/www/css/bootstrap-select.css similarity index 82% rename from src/opnsense/www/css/bootstrap-select-1.13.3.css rename to src/opnsense/www/css/bootstrap-select.css index d35933a8e..b4d770def 100644 --- a/src/opnsense/www/css/bootstrap-select-1.13.3.css +++ b/src/opnsense/www/css/bootstrap-select.css @@ -1,10 +1,34 @@ -/*! - * 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.18 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +@-webkit-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@-o-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} select.bs-select-hidden, .bootstrap-select > select.bs-select-hidden, select.selectpicker { @@ -13,13 +37,28 @@ select.selectpicker { .bootstrap-select { width: 348px \0; /*IE9 and below*/ + vertical-align: middle; } .bootstrap-select > .dropdown-toggle { position: relative; width: 100%; - z-index: 1; text-align: right; white-space: nowrap; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} +.bootstrap-select > .dropdown-toggle:after { + margin-top: -1px; } .bootstrap-select > .dropdown-toggle.bs-placeholder, .bootstrap-select > .dropdown-toggle.bs-placeholder:hover, @@ -63,22 +102,23 @@ select.selectpicker { padding: 0 !important; opacity: 0 !important; border: none; + z-index: 0 !important; } .bootstrap-select > select.mobile-device { top: 0; left: 0; display: block !important; width: 100% !important; - z-index: 2; + z-index: 2 !important; } .has-error .bootstrap-select .dropdown-toggle, .error .bootstrap-select .dropdown-toggle, .bootstrap-select.is-invalid .dropdown-toggle, -.was-validated .bootstrap-select .selectpicker:invalid + .dropdown-toggle { +.was-validated .bootstrap-select select:invalid + .dropdown-toggle { border-color: #b94a48; } .bootstrap-select.is-valid .dropdown-toggle, -.was-validated .bootstrap-select .selectpicker:valid + .dropdown-toggle { +.was-validated .bootstrap-select select:valid + .dropdown-toggle { border-color: #28a745; } .bootstrap-select.fit-width { @@ -97,15 +137,18 @@ select.selectpicker { margin-bottom: 0; padding: 0; border: none; + height: auto; } :not(.input-group) > .bootstrap-select.form-control:not([class*="col-"]) { width: 100%; } .bootstrap-select.form-control.input-group-btn { + float: none; z-index: auto; } -.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child) > .btn { - border-radius: 0; +.form-inline .bootstrap-select, +.form-inline .bootstrap-select.form-control:not([class*="col-"]) { + width: auto; } .bootstrap-select:not(.input-group-btn), .bootstrap-select[class*="col-"] { @@ -167,28 +210,42 @@ select.selectpicker { .bootstrap-select.bs-container .dropdown-menu { z-index: 1060; } -.bootstrap-select .dropdown-toggle:before { - content: ''; - display: inline-block; -} .bootstrap-select .dropdown-toggle .filter-option { - position: absolute; + position: static; top: 0; left: 0; - padding-top: inherit; - padding-right: inherit; - padding-bottom: inherit; - padding-left: inherit; + float: left; height: 100%; width: 100%; text-align: left; + overflow: hidden; + -webkit-box-flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; } -.bootstrap-select .dropdown-toggle .filter-option-inner { +.bs3.bootstrap-select .dropdown-toggle .filter-option { + padding-right: inherit; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option { + position: absolute; + padding-top: inherit; + padding-bottom: inherit; + padding-left: inherit; + float: none; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner { padding-right: inherit; } .bootstrap-select .dropdown-toggle .filter-option-inner-inner { overflow: hidden; } +.bootstrap-select .dropdown-toggle .filter-expand { + width: 0 !important; + float: left; + opacity: 0 !important; + overflow: hidden; +} .bootstrap-select .dropdown-toggle .caret { position: absolute; top: 50%; @@ -267,6 +324,11 @@ select.selectpicker { -moz-box-sizing: border-box; box-sizing: border-box; } +.bootstrap-select .dropdown-menu .notify.fadeOut { + -webkit-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + -o-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + animation: 300ms linear 750ms forwards bs-notify-fadeOut; +} .bootstrap-select .no-results { padding: 3px; background: #f5f5f5; @@ -282,6 +344,9 @@ select.selectpicker { .bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner { display: inline; } +.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before { + content: '\00a0'; +} .bootstrap-select.fit-width .dropdown-toggle .caret { position: static; top: auto; @@ -303,6 +368,8 @@ select.selectpicker { height: 1em; border-style: solid; border-width: 0 0.26em 0.26em 0; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); @@ -390,14 +457,4 @@ select.selectpicker { width: 100%; float: none; } - -/* OPNsense edit to fix https://github.com/opnsense/core/issues/2612 : - * Move checkmarks to left hand side of the dropdown. - */ -.bootstrap-select .dropdown-menu > li > a { - padding: 3px 20px 3px 30px; -} -.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark { - left: 10px; -} -/* End OPNsense edit to fix #2612. */ +/*# sourceMappingURL=bootstrap-select.css.map */ \ No newline at end of file diff --git a/src/opnsense/www/js/bootstrap-select.js b/src/opnsense/www/js/bootstrap-select.js index 6447daca4..cd0c452be 100644 --- a/src/opnsense/www/js/bootstrap-select.js +++ b/src/opnsense/www/js/bootstrap-select.js @@ -1,7 +1,7 @@ /*! - * Bootstrap-select v1.13.3 (https://developer.snapappointments.com/bootstrap-select) + * Bootstrap-select v1.13.18 (https://developer.snapappointments.com/bootstrap-select) * - * Copyright 2012-2018 SnapAppointments, LLC + * Copyright 2012-2020 SnapAppointments, LLC * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) */ @@ -25,8 +25,198 @@ (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 @@ -34,7 +224,7 @@ if (testElement.classList.contains('c3')) { var _toggle = DOMTokenList.prototype.toggle; - DOMTokenList.prototype.toggle = function(token, force) { + DOMTokenList.prototype.toggle = function (token, force) { if (1 in arguments && !this.contains(token) === !force) { return force; } else { @@ -43,14 +233,16 @@ }; } + testElement = null; + // shallow array comparison function isEqual (array1, array2) { - return array1.length === array2.length && array1.every(function(element, index) { + 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` @@ -112,37 +304,66 @@ o, // object k, // key r // result array - ){ + ) { // initialize object and result - r=[]; + r = []; // iterate over object keys - for (k in o) - // fill result array with non-prototypical 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 (HTMLSelectElement && !HTMLSelectElement.prototype.hasOwnProperty('selectedOptions')) { + Object.defineProperty(HTMLSelectElement.prototype, 'selectedOptions', { + get: function () { + return this.querySelectorAll(':checked'); + } + }); + } - if (select.multiple) { - for (var i = 0, len = options.length; i < len; i++) { - opt = options[i]; + function getSelectedOptions (select, ignoreDisabled) { + var selectedOptions = select.selectedOptions, + options = [], + opt; - if (opt.selected) { - result.push(opt.value || opt.text); + 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); } } - } else { - result = select.value; + + return options; } - return result; + 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 @@ -159,7 +380,7 @@ return valHooks._set.apply(this, arguments); }; - var changed_arguments = null; + var changedArguments = null; var EventIsSupported = (function () { try { @@ -196,15 +417,15 @@ this.trigger(eventName); } }; - // + // - function stringSearch(li, searchString, method, normalize) { + function stringSearch (li, searchString, method, normalize) { var stringTypes = [ - 'content', - 'subtext', - 'tokens' - ], - searchSuccess = false; + 'display', + 'subtext', + 'tokens' + ], + searchSuccess = false; for (var i = 0; i < stringTypes.length; i++) { var stringType = stringTypes[i], @@ -214,7 +435,7 @@ string = string.toString(); // Strip HTML tags. This isn't perfect, but it's much faster than any other method - if (stringType === 'content') { + if (stringType === 'display') { string = string.replace(/<[^>]+>/g, ''); } @@ -234,38 +455,97 @@ return searchSuccess; } - function toInteger(value) { + 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; - } + // 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 = { @@ -277,15 +557,6 @@ '`': '`' }; - var unescapeMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", - '`': '`' - }; - // Functions for escaping and unescaping strings to/from HTML interpolation. var createEscaper = function (map) { var escaper = function (match) { @@ -302,7 +573,6 @@ }; var htmlEscape = createEscaper(escapeMap); - var htmlUnescape = createEscaper(unescapeMap); /** * ------------------------------------------------------------------------ @@ -379,14 +649,13 @@ version.full = ($.fn.dropdown.Constructor.VERSION || '').split(' ')[0].split('.'); version.major = version.full[0]; version.success = true; + } catch (err) { + // do nothing } - 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 selectId = 0; + + var EVENT_KEY = '.bs.select'; var classNames = { DISABLED: 'disabled', @@ -398,23 +667,152 @@ MENULEFT: 'dropdown-menu-left', // to-do: replace with more advanced template/customization options BUTTONCLASS: 'btn-default', - POPOVERHEADER: 'popover-title' + POPOVERHEADER: 'popover-title', + ICONBASE: 'glyphicon', + TICKICON: 'glyphicon-ok' } 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 elementTemplates = { + div: document.createElement('div'), + span: document.createElement('span'), + i: document.createElement('i'), + subtext: document.createElement('small'), + a: document.createElement('a'), + li: document.createElement('li'), + whitespace: document.createTextNode('\u00A0'), + fragment: document.createDocumentFragment() } + elementTemplates.noResults = elementTemplates.li.cloneNode(false); + elementTemplates.noResults.className = 'no-results'; + + elementTemplates.a.setAttribute('role', 'option'); + elementTemplates.a.className = 'dropdown-item'; + + elementTemplates.subtext.className = 'text-muted'; + + elementTemplates.text = elementTemplates.span.cloneNode(false); + elementTemplates.text.className = 'text'; + + elementTemplates.checkMark = elementTemplates.span.cloneNode(false); + var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); - var REGEXP_ENTER_OR_SPACE = new RegExp(keyCodes.ENTER + '|' + keyCodes.SPACE); + + var generateOption = { + li: function (content, classes, optgroup) { + var li = elementTemplates.li.cloneNode(false); + + if (content) { + if (content.nodeType === 1 || content.nodeType === 11) { + li.appendChild(content); + } else { + li.innerHTML = content; + } + } + + if (typeof classes !== 'undefined' && classes !== '') li.className = classes; + if (typeof optgroup !== 'undefined' && optgroup !== null) li.classList.add('optgroup-' + optgroup); + + return li; + }, + + a: function (text, classes, inline) { + var a = elementTemplates.a.cloneNode(true); + + if (text) { + if (text.nodeType === 11) { + a.appendChild(text); + } else { + a.insertAdjacentHTML('beforeend', text); + } + } + + if (typeof classes !== 'undefined' && classes !== '') a.classList.add.apply(a.classList, classes.split(/\s+/)); + if (inline) a.setAttribute('style', inline); + + return a; + }, + + text: function (options, useFragment) { + var textElement = elementTemplates.text.cloneNode(false), + subtextElement, + iconElement; + + if (options.content) { + textElement.innerHTML = options.content; + } else { + textElement.textContent = options.text; + + if (options.icon) { + var whitespace = elementTemplates.whitespace.cloneNode(false); + + // need to use 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; + } + } + + function showNoResults (searchMatch, searchValue) { + if (!searchMatch.length) { + elementTemplates.noResults.innerHTML = this.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); + this.$menuInner[0].firstChild.appendChild(elementTemplates.noResults); + } + } var Selectpicker = function (element, options) { var that = this; @@ -431,21 +829,11 @@ 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: {} - }, + main: {}, + search: {}, + current: {}, // current changes if a search is in progress view: {}, + isSearching: false, keydown: { keyHistory: '', resetKeyHistory: { @@ -457,6 +845,9 @@ } } }; + + 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) { @@ -469,7 +860,7 @@ this.options.windowPadding = [winPad, winPad, winPad, winPad]; } - //Expose public methods + // Expose public methods this.val = Selectpicker.prototype.val; this.render = Selectpicker.prototype.render; this.refresh = Selectpicker.prototype.refresh; @@ -484,16 +875,14 @@ this.init(); }; - Selectpicker.VERSION = '1.13.3'; - - Selectpicker.BootstrapVersion = version.major; + Selectpicker.VERSION = '1.13.18'; // 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"; + return (numSelected == 1) ? '{0} item selected' : '{0} items selected'; }, maxOptionsText: function (numAll, numGroup) { return [ @@ -524,8 +913,8 @@ liveSearchNormalize: false, liveSearchStyle: 'contains', actionsBox: false, - iconBase: 'glyphicon', - tickIcon: 'glyphicon-ok', + iconBase: classNames.ICONBASE, + tickIcon: classNames.TICKICON, showTick: false, template: { caret: '' @@ -536,40 +925,54 @@ dropdownAlignRight: false, windowPadding: 0, virtualScroll: 600, - display: false + display: false, + sanitize: true, + sanitizeFn: null, + whiteList: DefaultWhitelist }; - 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'); + id = this.$element.attr('id'), + element = this.$element[0], + form = element.form; - this.$element.addClass('bs-select-hidden'); + selectId++; + this.selectId = 'bs-select-' + selectId; + + element.classList.add('bs-select-hidden'); this.multiple = this.$element.prop('multiple'); this.autofocus = this.$element.prop('autofocus'); + + if (element.classList.contains('show-tick')) { + this.options.showTick = true; + } + this.$newElement = this.createDropdown(); - this.createLi(); + this.buildData(); this.$element .after(this.$newElement) .prependTo(this.$newElement); + + // ensure select is associated with form element if it got unlinked after moving it inside newElement + if (form && element.form === null) { + if (!form.id) form.id = 'form-' + this.selectId; + element.setAttribute('form', form.id); + } + this.$button = this.$newElement.children('button'); 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'); + element.classList.remove('bs-select-hidden'); - if (this.options.dropdownAlignRight === true) this.$menu.addClass(classNames.MENURIGHT); + if (this.options.dropdownAlignRight === true) this.$menu[0].classList.add(classNames.MENURIGHT); if (typeof id !== 'undefined') { this.$button.attr('data-id', id); @@ -577,14 +980,21 @@ this.checkDisabled(); this.clickListener(); - if (this.options.liveSearch) this.liveSearchListener(); - this.render(); + + 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.bs.select', function () { + this.$element.on('hide' + EVENT_KEY, function () { if (that.isVirtual()) { // empty menu on close var menuInner = that.$menuInner[0], @@ -602,47 +1012,45 @@ this.$newElement.on({ 'hide.bs.dropdown': function (e) { - that.$menuInner.attr('aria-expanded', false); - that.$element.trigger('hide.bs.select', e); + that.$element.trigger('hide' + EVENT_KEY, e); }, 'hidden.bs.dropdown': function (e) { - that.$element.trigger('hidden.bs.select', e); + that.$element.trigger('hidden' + EVENT_KEY, e); }, 'show.bs.dropdown': function (e) { - that.$menuInner.attr('aria-expanded', true); - that.$element.trigger('show.bs.select', e); + that.$element.trigger('show' + EVENT_KEY, e); }, 'shown.bs.dropdown': function (e) { - that.$element.trigger('shown.bs.select', e); + that.$element.trigger('shown' + EVENT_KEY, e); } }); - if (that.$element[0].hasAttribute('required')) { - this.$element.on('invalid', function () { - that.$button.addClass('bs-invalid'); + if (element.hasAttribute('required')) { + this.$element.on('invalid' + EVENT_KEY, function () { + that.$button[0].classList.add('bs-invalid'); - that.$element.on({ - 'shown.bs.select.invalid': function () { + 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.bs.select.invalid'); - }, - 'rendered.bs.select': function () { + .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.removeClass('bs-invalid'); - that.$element.off('rendered.bs.select'); - } - }); + if (this.validity.valid) that.$button[0].classList.remove('bs-invalid'); + that.$element.off('rendered' + EVENT_KEY); + }); - that.$button.on('blur.bs.select', function () { - that.$element.focus().blur(); - that.$button.off('blur.bs.select'); + that.$button.on('blur' + EVENT_KEY, function () { + that.$element.trigger('focus').trigger('blur'); + that.$button.off('blur' + EVENT_KEY); }); }); } setTimeout(function () { - that.$element.trigger('loaded.bs.select'); + that.buildList(); + that.$element.trigger('loaded' + EVENT_KEY); }); }, @@ -650,8 +1058,14 @@ // 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 = '', @@ -670,13 +1084,13 @@ if (this.options.liveSearch) { searchbox = ''; } @@ -706,8 +1120,8 @@ } drop = - '