From aee4ccb5a278a3986da913b88eaa9812a9f151a9 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 22 Sep 2017 16:33:04 +0100 Subject: [PATCH] Update JS dependencies --- RIGS/static/js/affix.js | 12 +- RIGS/static/js/ajax-bootstrap-select.js | 84 +- RIGS/static/js/alert.js | 8 +- RIGS/static/js/bootstrap-select.js | 1485 ++++++++++++++++------- RIGS/static/js/button.js | 37 +- RIGS/static/js/carousel.js | 14 +- RIGS/static/js/collapse.js | 17 +- RIGS/static/js/dropdown.js | 94 +- RIGS/static/js/modal.js | 10 +- RIGS/static/js/popover.js | 13 +- RIGS/static/js/scrollspy.js | 39 +- RIGS/static/js/tab.js | 12 +- RIGS/static/js/tooltip.js | 120 +- RIGS/static/js/transition.js | 4 +- 14 files changed, 1351 insertions(+), 598 deletions(-) mode change 100644 => 100755 RIGS/static/js/affix.js mode change 100755 => 100644 RIGS/static/js/ajax-bootstrap-select.js mode change 100644 => 100755 RIGS/static/js/alert.js mode change 100644 => 100755 RIGS/static/js/button.js mode change 100644 => 100755 RIGS/static/js/carousel.js mode change 100644 => 100755 RIGS/static/js/collapse.js mode change 100644 => 100755 RIGS/static/js/dropdown.js mode change 100644 => 100755 RIGS/static/js/modal.js mode change 100644 => 100755 RIGS/static/js/popover.js mode change 100644 => 100755 RIGS/static/js/scrollspy.js mode change 100644 => 100755 RIGS/static/js/tab.js mode change 100644 => 100755 RIGS/static/js/tooltip.js mode change 100644 => 100755 RIGS/static/js/transition.js diff --git a/RIGS/static/js/affix.js b/RIGS/static/js/affix.js old mode 100644 new mode 100755 index aaebc8c0..7f651681 --- a/RIGS/static/js/affix.js +++ b/RIGS/static/js/affix.js @@ -1,8 +1,8 @@ /* ======================================================================== - * Bootstrap: affix.js v3.3.2 + * Bootstrap: affix.js v3.3.7 * http://getbootstrap.com/javascript/#affix * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ @@ -21,14 +21,14 @@ .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) this.$element = $(element) - this.affixed = - this.unpin = + this.affixed = null + this.unpin = null this.pinnedOffset = null this.checkPosition() } - Affix.VERSION = '3.3.2' + Affix.VERSION = '3.3.7' Affix.RESET = 'affix affix-top affix-bottom' @@ -78,7 +78,7 @@ var offset = this.options.offset var offsetTop = offset.top var offsetBottom = offset.bottom - var scrollHeight = $('body').height() + var scrollHeight = Math.max($(document).height(), $(document.body).height()) if (typeof offset != 'object') offsetBottom = offsetTop = offset if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) diff --git a/RIGS/static/js/ajax-bootstrap-select.js b/RIGS/static/js/ajax-bootstrap-select.js old mode 100755 new mode 100644 index da91c98b..e9bb833b --- a/RIGS/static/js/ajax-bootstrap-select.js +++ b/RIGS/static/js/ajax-bootstrap-select.js @@ -3,16 +3,16 @@ * * Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON. * - * @version 1.3.1 + * @version 1.4.1 * @author Adam Heim - https://github.com/truckingsim * @link https://github.com/truckingsim/Ajax-Bootstrap-Select - * @copyright 2015 Adam Heim + * @copyright 2017 Adam Heim * @license Released under the MIT license. * * Contributors: * Mark Carver - https://github.com/markcarver * - * Last build: 2015-01-06 8:43:11 PM EST + * Last build: 2017-07-21 1:08:54 PM GMT-0400 */ !(function ($, window) { @@ -186,10 +186,25 @@ var AjaxBootstrapSelect = function (element, options) { if (dataKeys.length) { // Object containing the data attribute options. var dataOptions = {}; + var flattenedOptions = ['locale']; for (i = 0, l = dataKeys.length; i < l; i++) { var name = dataKeys[i].replace(/^abs([A-Z])/, matchToLowerCase).replace(/([A-Z])/g, '-$1').toLowerCase(); + var keys = name.split('-'); + + // Certain options should be flattened to a single object + // and not fully expanded (such as Locale). + if (keys[0] && keys.length > 1 && flattenedOptions.indexOf(keys[0]) !== -1) { + var newKeys = [keys.shift()]; + var property = ''; + // Combine the remaining keys as a single property. + for (var ii = 0; ii < keys.length; ii++) { + property += (ii === 0 ? keys[ii] : keys[ii].charAt(0).toUpperCase() + keys[ii].slice(1)); + } + newKeys.push(property); + keys = newKeys; + } this.log(this.LOG_DEBUG, 'Processing data attribute "data-abs-' + name + '":', data[dataKeys[i]]); - expandObject(name.split('-'), data[dataKeys[i]], dataOptions); + expandObject(keys, data[dataKeys[i]], dataOptions); } this.options = $.extend(true, {}, this.options, dataOptions); this.log(this.LOG_DEBUG, 'Merged in the data attribute options: ', dataOptions, this.options); @@ -315,6 +330,12 @@ AjaxBootstrapSelect.prototype.init = function () { plugin.log(plugin.LOG_DEBUG, 'Key ignored.'); return; } + + // Don't process if below minimum query length + if (query.length < plugin.options.minLength) { + plugin.list.setStatus(plugin.t('statusTooShort')); + return; + } // Clear out any existing timer. clearTimeout(requestDelayTimer); @@ -573,8 +594,25 @@ var AjaxBootstrapSelectList = function (plugin) { this.title = null; this.selectedTextFormat = plugin.selectpicker.options.selectedTextFormat; + // Save initial options + var initial_options = []; + plugin.$element.find('option').each(function() { + var $option = $(this); + var value = $option.attr('value'); + initial_options.push({ + value: value, + text: $option.text(), + 'class': $option.attr('class') || '', + data: $option.data() || {}, + preserved: plugin.options.preserveSelected, + selected: !!$option.attr('selected') + }); + }); + this.cacheSet(/*query=*/'', initial_options); + // Preserve selected options. if (plugin.options.preserveSelected) { + that.selected = initial_options; plugin.$element.on('change.abs.preserveSelected', function (e) { var $selected = plugin.$element.find(':selected'); that.selected = []; @@ -644,7 +682,7 @@ AjaxBootstrapSelectList.prototype.build = function (data) { } // Set various properties. - $option.val(item.value).text(item.text); + $option.val(item.value).text(item.text).attr('title', item.text); if (item['class'].length) { $option.attr('class', item['class']); } @@ -732,7 +770,13 @@ AjaxBootstrapSelectList.prototype.refresh = function (triggerChange) { if (!this.plugin.$element.find('option').length && emptyTitle && emptyTitle.length) { this.setTitle(emptyTitle); } - else if (this.title) { + else if ( + this.title || + ( + this.selectedTextFormat !== 'static' && + this.selectedTextFormat !== this.plugin.selectpicker.options.selectedTextFormat + ) + ) { this.restoreTitle(); } this.plugin.selectpicker.refresh(); @@ -1224,6 +1268,14 @@ $.fn.ajaxSelectPicker.defaults = { } }, + /** + * @member $.fn.ajaxSelectPicker.defaults + * @cfg {Number} minLength = 0 + * @markdown + * Invoke a request for empty search values. + */ + minLength: 0, + /** * @member $.fn.ajaxSelectPicker.defaults * @cfg {String} ajaxSearchUrl @@ -1296,8 +1348,7 @@ $.fn.ajaxSelectPicker.defaults = { * 39: "right", * 38: "up", * 40: "down", - * 91: "meta", - * 229: "unknown" + * 91: "meta" * } * ``` */ @@ -1311,8 +1362,7 @@ $.fn.ajaxSelectPicker.defaults = { 39: "right", 38: "up", 40: "down", - 91: "meta", - 229: "unknown" + 91: "meta" }, /** @@ -1406,7 +1456,7 @@ $.fn.ajaxSelectPicker.defaults = { * } * ``` */ - preprocessData: function(){}, + preprocessData: function () { }, /** * @member $.fn.ajaxSelectPicker.defaults @@ -1434,7 +1484,7 @@ $.fn.ajaxSelectPicker.defaults = { * @markdown * Process the data returned after this plugin, but before the list is built. */ - processData: function(){}, + processData: function () { }, /** * @member $.fn.ajaxSelectPicker.defaults @@ -1547,7 +1597,15 @@ $.fn.ajaxSelectPicker.locale['en-US'] = { * @markdown * The text to use in the status container when a request is being initiated. */ - statusSearching: 'Searching...' + statusSearching: 'Searching...', + + /** + * @member $.fn.ajaxSelectPicker.locale + * @cfg {String} statusToShort = 'Please enter more characters' + * @markdown + * The text used in the status container when the request returns no results. + */ + statusTooShort: 'Please enter more characters' }; $.fn.ajaxSelectPicker.locale.en = $.fn.ajaxSelectPicker.locale['en-US']; diff --git a/RIGS/static/js/alert.js b/RIGS/static/js/alert.js old mode 100644 new mode 100755 index e8212caf..db97f3b0 --- a/RIGS/static/js/alert.js +++ b/RIGS/static/js/alert.js @@ -1,8 +1,8 @@ /* ======================================================================== - * Bootstrap: alert.js v3.3.2 + * Bootstrap: alert.js v3.3.7 * http://getbootstrap.com/javascript/#alerts * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ @@ -18,7 +18,7 @@ $(el).on('click', dismiss, this.close) } - Alert.VERSION = '3.3.2' + Alert.VERSION = '3.3.7' Alert.TRANSITION_DURATION = 150 @@ -31,7 +31,7 @@ selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = $(selector) + var $parent = $(selector === '#' ? [] : selector) if (e) e.preventDefault() diff --git a/RIGS/static/js/bootstrap-select.js b/RIGS/static/js/bootstrap-select.js index caa9615d..ee2f74a4 100755 --- a/RIGS/static/js/bootstrap-select.js +++ b/RIGS/static/js/bootstrap-select.js @@ -1,27 +1,212 @@ (function ($) { 'use strict'; - // Case insensitive search - $.expr[':'].icontains = function (obj, index, meta) { - return icontains($(obj).text(), meta[3]); - }; - - // Case and accent insensitive search - $.expr[':'].aicontains = function (obj, index, meta) { - return icontains($(obj).data('normalizedText') || $(obj).text(), meta[3]); - }; - - /** - * Actual implementation of the case insensitive search. - * @access private - * @param {String} haystack - * @param {String} needle - * @returns {boolean} - */ - function icontains(haystack, needle) { - return haystack.toUpperCase().indexOf(needle.toUpperCase()) > -1; + // + if (!String.prototype.includes) { + (function () { + 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` + var toString = {}.toString; + 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 indexOf = ''.indexOf; + var includes = 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; + } + return indexOf.call(string, searchString, pos) != -1; + }; + if (defineProperty) { + defineProperty(String.prototype, 'includes', { + 'value': includes, + 'configurable': true, + 'writable': true + }); + } else { + String.prototype.includes = includes; + } + }()); } + 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; + }; + } + + // set data-selected on select element if the value has been programmatically selected + // prior to initialization of bootstrap-select + // * consider removing or replacing an alternative method * + var valHooks = { + useDefault: false, + _set: $.valHooks.select.set + }; + + $.valHooks.select.set = function(elem, value) { + if (value && !valHooks.useDefault) $(elem).data('selected', true); + + return valHooks._set.apply(this, arguments); + }; + + var changed_arguments = null; + + var EventIsSupported = (function() { + try { + new Event('change'); + return true; + } catch (e) { + return false; + } + })(); + + $.fn.triggerNative = function (eventName) { + var el = this[0], + event; + + if (el.dispatchEvent) { // for modern browsers & IE9+ + if (EventIsSupported) { + // For modern browsers + event = new Event(eventName, { + bubbles: true + }); + } else { + // For IE since it doesn't support Event constructor + event = document.createEvent('Event'); + event.initEvent(eventName, true, false); + } + + el.dispatchEvent(event); + } else if (el.fireEvent) { // for IE8 + event = document.createEventObject(); + event.eventType = eventName; + el.fireEvent('on' + eventName, event); + } else { + // fall back to jQuery.trigger + this.trigger(eventName); + } + }; + // + + // Case insensitive contains search + $.expr.pseudos.icontains = function (obj, index, meta) { + var $obj = $(obj).find('a'); + var haystack = ($obj.data('tokens') || $obj.text()).toString().toUpperCase(); + return haystack.includes(meta[3].toUpperCase()); + }; + + // Case insensitive begins search + $.expr.pseudos.ibegins = function (obj, index, meta) { + var $obj = $(obj).find('a'); + var haystack = ($obj.data('tokens') || $obj.text()).toString().toUpperCase(); + return haystack.startsWith(meta[3].toUpperCase()); + }; + + // Case and accent insensitive contains search + $.expr.pseudos.aicontains = function (obj, index, meta) { + var $obj = $(obj).find('a'); + var haystack = ($obj.data('tokens') || $obj.data('normalizedText') || $obj.text()).toString().toUpperCase(); + return haystack.includes(meta[3].toUpperCase()); + }; + + // Case and accent insensitive begins search + $.expr.pseudos.aibegins = function (obj, index, meta) { + var $obj = $(obj).find('a'); + var haystack = ($obj.data('tokens') || $obj.data('normalizedText') || $obj.text()).toString().toUpperCase(); + return haystack.startsWith(meta[3].toUpperCase()); + }; + /** * Remove all diatrics from the given text. * @access private @@ -45,34 +230,54 @@ {re: /[\xF1]/g, ch: "n"} ]; $.each(rExps, function () { - text = text.replace(this.re, this.ch); + text = text ? text.replace(this.re, this.ch) : ''; }); return text; } - function htmlEscape(html) { - var escapeMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`' - }; - var source = '(?:' + Object.keys(escapeMap).join('|') + ')', - testRegexp = new RegExp(source), - replaceRegexp = new RegExp(source, 'g'), - string = html == null ? '' : '' + html; - return testRegexp.test(string) ? string.replace(replaceRegexp, function (match) { - return escapeMap[match]; - }) : string; - } + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + var unescapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '`': '`' + }; - var Selectpicker = function (element, options, e) { - if (e) { - e.stopPropagation(); - e.preventDefault(); + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + Object.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + + var htmlEscape = createEscaper(escapeMap); + var htmlUnescape = createEscaper(unescapeMap); + + var Selectpicker = function (element, options) { + // 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); @@ -88,6 +293,12 @@ 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; @@ -95,7 +306,7 @@ this.setStyle = Selectpicker.prototype.setStyle; this.selectAll = Selectpicker.prototype.selectAll; this.deselectAll = Selectpicker.prototype.deselectAll; - this.destroy = Selectpicker.prototype.remove; + this.destroy = Selectpicker.prototype.destroy; this.remove = Selectpicker.prototype.remove; this.show = Selectpicker.prototype.show; this.hide = Selectpicker.prototype.hide; @@ -103,26 +314,27 @@ this.init(); }; - Selectpicker.VERSION = '1.6.3'; + Selectpicker.VERSION = '1.12.4'; // part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both. Selectpicker.DEFAULTS = { noneSelectedText: 'Nothing selected', - noneResultsText: 'No results match', + noneResultsText: 'No results matched {0}', countSelectedText: function (numSelected, numTotal) { return (numSelected == 1) ? "{0} item selected" : "{0} items selected"; }, maxOptionsText: function (numAll, numGroup) { - var arr = []; - - arr[0] = (numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)'; - arr[1] = (numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)'; - - return arr; + 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: 'btn-default', size: 'auto', title: null, @@ -136,14 +348,21 @@ dropupAuto: true, header: false, liveSearch: false, + liveSearchPlaceholder: null, + liveSearchNormalize: false, + liveSearchStyle: 'contains', actionsBox: false, iconBase: 'glyphicon', tickIcon: 'glyphicon-ok', + showTick: false, + template: { + caret: '' + }, maxOptions: false, mobile: false, selectOnTab: false, dropdownAlignRight: false, - searchAccentInsensitive: false + windowPadding: 0 }; Selectpicker.prototype = { @@ -154,17 +373,25 @@ var that = this, id = this.$element.attr('id'); - this.$element.hide(); + this.$element.addClass('bs-select-hidden'); + + // store originalIndex (key) and newIndex (value) in this.liObj for fast accessibility + // allows us to do this.$lis.eq(that.liObj[index]) instead of this.$lis.filter('[data-original-index="' + index + '"]') + this.liObj = {}; this.multiple = this.$element.prop('multiple'); this.autofocus = this.$element.prop('autofocus'); this.$newElement = this.createView(); - this.$element.after(this.$newElement); - this.$menu = this.$newElement.find('> .dropdown-menu'); - this.$button = this.$newElement.find('> button'); - this.$searchbox = this.$newElement.find('input'); + this.$element + .after(this.$newElement) + .appendTo(this.$newElement); + this.$button = this.$newElement.children('button'); + this.$menu = this.$newElement.children('.dropdown-menu'); + this.$menuInner = this.$menu.children('.inner'); + this.$searchbox = this.$menu.find('input'); - if (this.options.dropdownAlignRight) - this.$menu.addClass('dropdown-menu-right'); + this.$element.removeClass('bs-select-hidden'); + + if (this.options.dropdownAlignRight === true) this.$menu.addClass('dropdown-menu-right'); if (typeof id !== 'undefined') { this.$button.attr('data-id', id); @@ -178,47 +405,113 @@ this.clickListener(); if (this.options.liveSearch) this.liveSearchListener(); this.render(); - this.liHeight(); this.setStyle(); this.setWidth(); if (this.options.container) this.selectPosition(); this.$menu.data('this', this); this.$newElement.data('this', this); if (this.options.mobile) this.mobile(); + + this.$newElement.on({ + 'hide.bs.dropdown': function (e) { + that.$menuInner.attr('aria-expanded', false); + that.$element.trigger('hide.bs.select', e); + }, + 'hidden.bs.dropdown': function (e) { + that.$element.trigger('hidden.bs.select', e); + }, + 'show.bs.dropdown': function (e) { + that.$menuInner.attr('aria-expanded', true); + that.$element.trigger('show.bs.select', e); + }, + 'shown.bs.dropdown': function (e) { + that.$element.trigger('shown.bs.select', e); + } + }); + + if (that.$element[0].hasAttribute('required')) { + this.$element.on('invalid', function () { + that.$button.addClass('bs-invalid'); + + that.$element.on({ + 'focus.bs.select': function () { + that.$button.focus(); + that.$element.off('focus.bs.select'); + }, + 'shown.bs.select': 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'); + }, + 'rendered.bs.select': function () { + // if select is no longer invalid, remove the bs-invalid class + if (this.validity.valid) that.$button.removeClass('bs-invalid'); + that.$element.off('rendered.bs.select'); + } + }); + + that.$button.on('blur.bs.select', function() { + that.$element.focus().blur(); + that.$button.off('blur.bs.select'); + }); + }); + } + + setTimeout(function () { + that.$element.trigger('loaded.bs.select'); + }); }, createDropdown: function () { // Options - // If we are multiple, then add the show-tick class by default - var multiple = this.multiple ? ' show-tick' : '', + // If we are multiple or showTick option is set, then add the show-tick class + var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '', inputGroup = this.$element.parent().hasClass('input-group') ? ' input-group-btn' : '', - autofocus = this.autofocus ? ' autofocus' : '', - btnSize = this.$element.parents().hasClass('form-group-lg') ? ' btn-lg' : (this.$element.parents().hasClass('form-group-sm') ? ' btn-sm' : ''); + autofocus = this.autofocus ? ' autofocus' : ''; // Elements var header = this.options.header ? '
' + this.options.header + '
' : ''; - var searchbox = this.options.liveSearch ? '' : ''; - var actionsbox = this.options.actionsBox ? '
' + - '
' + - '' + - '' + '
' + - '
' : ''; + '' + : ''; + var donebutton = this.multiple && this.options.doneButton ? + '
' + + '
' + + '' + + '
' + + '
' + : ''; var drop = - '
' + - '' + - ''; @@ -226,111 +519,198 @@ }, createView: function () { - var $drop = this.createDropdown(); - var $li = this.createLi(); - $drop.find('ul').append($li); + var $drop = this.createDropdown(), + li = this.createLi(); + + $drop.find('ul')[0].innerHTML = li; return $drop; }, reloadLi: function () { - //Remove all children. - this.destroyLi(); - //Re build - var $li = this.createLi(); - this.$menu.find('ul').append($li); - }, - - destroyLi: function () { - this.$menu.find('li').remove(); + // rebuild + var li = this.createLi(); + this.$menuInner[0].innerHTML = li; }, createLi: function () { var that = this, _li = [], - optID = 0; + optID = 0, + titleOption = document.createElement('option'), + liIndex = -1; // increment liIndex whenever a new
  • element is created to ensure liObj is correct // Helper functions /** * @param content * @param [index] * @param [classes] + * @param [optgroup] * @returns {string} */ - var generateLI = function (content, index, classes) { + var generateLI = function (content, index, classes, optgroup) { return '' + content + '
  • '; + ((typeof classes !== 'undefined' && '' !== classes) ? ' class="' + classes + '"' : '') + + ((typeof index !== 'undefined' && null !== index) ? ' data-original-index="' + index + '"' : '') + + ((typeof optgroup !== 'undefined' && null !== optgroup) ? 'data-optgroup="' + optgroup + '"' : '') + + '>' + content + ''; }; /** * @param text * @param [classes] * @param [inline] - * @param [optgroup] + * @param [tokens] * @returns {string} */ - var generateA = function (text, classes, inline, optgroup) { - var normText = normalizeToBase(htmlEscape(text)); + var generateA = function (text, classes, inline, tokens) { return '' + text + - '' + - ''; + (typeof classes !== 'undefined' ? ' class="' + classes + '"' : '') + + (inline ? ' style="' + inline + '"' : '') + + (that.options.liveSearchNormalize ? ' data-normalized-text="' + normalizeToBase(htmlEscape($(text).html())) + '"' : '') + + (typeof tokens !== 'undefined' || tokens !== null ? ' data-tokens="' + tokens + '"' : '') + + ' role="option">' + text + + '' + + ''; }; - this.$element.find('option').each(function () { + if (this.options.title && !this.multiple) { + // this option doesn't create a new
  • element, but does add a new option, so liIndex is decreased + // since liObj is recalculated on every refresh, liIndex needs to be decreased even if the titleOption is already appended + liIndex--; + + if (!this.$element.find('.bs-title-option').length) { + // Use native JS to prepend option (faster) + var element = this.$element[0]; + titleOption.className = 'bs-title-option'; + titleOption.innerHTML = this.options.title; + titleOption.value = ''; + element.insertBefore(titleOption, element.firstChild); + // 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]); + if ($opt.attr('selected') === undefined && this.$element.data('selected') === undefined) { + titleOption.selected = true; + } + } + } + + var $selectOptions = this.$element.find('option'); + + $selectOptions.each(function (index) { var $this = $(this); + liIndex++; + + if ($this.hasClass('bs-title-option')) return; + // Get the class and text for the option - var optionClass = $this.attr('class') || '', - inline = $this.attr('style'), + var optionClass = this.className || '', + inline = htmlEscape(this.style.cssText), text = $this.data('content') ? $this.data('content') : $this.html(), - subtext = typeof $this.data('subtext') !== 'undefined' ? '' + $this.data('subtext') + '' : '', + tokens = $this.data('tokens') ? $this.data('tokens') : null, + subtext = typeof $this.data('subtext') !== 'undefined' ? '' + $this.data('subtext') + '' : '', icon = typeof $this.data('icon') !== 'undefined' ? ' ' : '', - isDisabled = $this.is(':disabled') || $this.parent().is(':disabled'), - index = $this[0].index; + $parent = $this.parent(), + isOptgroup = $parent[0].tagName === 'OPTGROUP', + isOptgroupDisabled = isOptgroup && $parent[0].disabled, + isDisabled = this.disabled || isOptgroupDisabled, + prevHiddenIndex; + if (icon !== '' && isDisabled) { icon = '' + icon + ''; } + if (that.options.hideDisabled && (isDisabled && !isOptgroup || isOptgroupDisabled)) { + // set prevHiddenIndex - the index of the first hidden option in a group of hidden options + // used to determine whether or not a divider should be placed after an optgroup if there are + // hidden options between the optgroup and the first visible option + prevHiddenIndex = $this.data('prevHiddenIndex'); + $this.next().data('prevHiddenIndex', (prevHiddenIndex !== undefined ? prevHiddenIndex : index)); + + liIndex--; + return; + } + if (!$this.data('content')) { // Prepend any icon and append any subtext to the main text. text = icon + '' + text + subtext + ''; } - if (that.options.hideDisabled && isDisabled) { - return; - } + if (isOptgroup && $this.data('divider') !== true) { + if (that.options.hideDisabled && isDisabled) { + if ($parent.data('allOptionsDisabled') === undefined) { + var $options = $parent.children(); + $parent.data('allOptionsDisabled', $options.filter(':disabled').length === $options.length); + } + + if ($parent.data('allOptionsDisabled')) { + liIndex--; + return; + } + } + + var optGroupClass = ' ' + $parent[0].className || ''; - if ($this.parent().is('optgroup') && $this.data('divider') !== true) { if ($this.index() === 0) { // Is it the first option of the optgroup? optID += 1; // Get the opt group label - var label = $this.parent().attr('label'); - var labelSubtext = typeof $this.parent().data('subtext') !== 'undefined' ? '' + $this.parent().data('subtext') + '' : ''; - var labelIcon = $this.parent().data('icon') ? ' ' : ''; - label = labelIcon + '' + label + labelSubtext + ''; + var label = $parent[0].label, + labelSubtext = typeof $parent.data('subtext') !== 'undefined' ? '' + $parent.data('subtext') + '' : '', + labelIcon = $parent.data('icon') ? ' ' : ''; + + label = labelIcon + '' + htmlEscape(label) + labelSubtext + ''; if (index !== 0 && _li.length > 0) { // Is it NOT the first option of the select && are there elements in the dropdown? - _li.push(generateLI('', null, 'divider')); + liIndex++; + _li.push(generateLI('', null, 'divider', optID + 'div')); } - - _li.push(generateLI(label, null, 'dropdown-header')); + liIndex++; + _li.push(generateLI(label, null, 'dropdown-header' + optGroupClass, optID)); } - _li.push(generateLI(generateA(text, 'opt ' + optionClass, inline, optID), index)); + if (that.options.hideDisabled && isDisabled) { + liIndex--; + return; + } + + _li.push(generateLI(generateA(text, 'opt ' + optionClass + optGroupClass, inline, tokens), index, '', optID)); } else if ($this.data('divider') === true) { _li.push(generateLI('', index, 'divider')); } else if ($this.data('hidden') === true) { - _li.push(generateLI(generateA(text, optionClass, inline), index, 'hide is-hidden')); + // set prevHiddenIndex - the index of the first hidden option in a group of hidden options + // used to determine whether or not a divider should be placed after an optgroup if there are + // hidden options between the optgroup and the first visible option + prevHiddenIndex = $this.data('prevHiddenIndex'); + $this.next().data('prevHiddenIndex', (prevHiddenIndex !== undefined ? prevHiddenIndex : index)); + + _li.push(generateLI(generateA(text, optionClass, inline, tokens), index, 'hidden is-hidden')); } else { - _li.push(generateLI(generateA(text, optionClass, inline), index)); + var showDivider = this.previousElementSibling && this.previousElementSibling.tagName === 'OPTGROUP'; + + // if previous element is not an optgroup and hideDisabled is true + if (!showDivider && that.options.hideDisabled) { + prevHiddenIndex = $this.data('prevHiddenIndex'); + + if (prevHiddenIndex !== undefined) { + // select the element **before** the first hidden element in the group + var prevHidden = $selectOptions.eq(prevHiddenIndex)[0].previousElementSibling; + + if (prevHidden && prevHidden.tagName === 'OPTGROUP' && !prevHidden.disabled) { + showDivider = true; + } + } + } + + if (showDivider) { + liIndex++; + _li.push(generateLI('', null, 'divider', optID + 'div')); + } + _li.push(generateLI(generateA(text, optionClass, inline, tokens), index)); } + + that.liObj[index] = liIndex; }); //If we are not multiple, we don't have a selected item, and we don't have a title, select the first element so something is set in the button @@ -338,7 +718,7 @@ this.$element.find('option').eq(0).prop('selected', true).attr('selected', 'selected'); } - return $(_li.join('')); + return _li.join(''); }, findLis: function () { @@ -350,33 +730,44 @@ * @param [updateLi] defaults to true */ render: function (updateLi) { - var that = this; + var that = this, + notDisabled, + $selectOptions = this.$element.find('option'); //Update the LI to match the SELECT if (updateLi !== false) { - this.$element.find('option').each(function (index) { - that.setDisabled(index, $(this).is(':disabled') || $(this).parent().is(':disabled')); - that.setSelected(index, $(this).is(':selected')); + $selectOptions.each(function (index) { + var $lis = that.findLis().eq(that.liObj[index]); + + that.setDisabled(index, this.disabled || this.parentNode.tagName === 'OPTGROUP' && this.parentNode.disabled, $lis); + that.setSelected(index, this.selected, $lis); }); } + this.togglePlaceholder(); + this.tabIndex(); - var notDisabled = this.options.hideDisabled ? ':not([disabled])' : ''; - var selectedItems = this.$element.find('option:selected' + notDisabled).map(function () { - var $this = $(this); - var icon = $this.data('icon') && that.options.showIcon ? ' ' : ''; - var subtext; - if (that.options.showSubtext && $this.attr('data-subtext') && !that.multiple) { - subtext = ' ' + $this.data('subtext') + ''; - } else { - subtext = ''; - } - if ($this.data('content') && that.options.showContent) { - return $this.data('content'); - } else if (typeof $this.attr('title') !== 'undefined') { - return $this.attr('title'); - } else { - return icon + $this.html() + subtext; + + var selectedItems = $selectOptions.map(function () { + if (this.selected) { + if (that.options.hideDisabled && (this.disabled || this.parentNode.tagName === 'OPTGROUP' && this.parentNode.disabled)) return; + + var $this = $(this), + icon = $this.data('icon') && that.options.showIcon ? ' ' : '', + subtext; + + if (that.options.showSubtext && $this.data('subtext') && !that.multiple) { + subtext = ' ' + $this.data('subtext') + ''; + } else { + subtext = ''; + } + if (typeof $this.attr('title') !== 'undefined') { + return $this.attr('title'); + } else if ($this.data('content') && that.options.showContent) { + return $this.data('content').toString(); + } else { + return icon + $this.html() + subtext; + } } }).toArray(); @@ -389,13 +780,15 @@ var max = this.options.selectedTextFormat.split('>'); if ((max.length > 1 && selectedItems.length > max[1]) || (max.length == 1 && selectedItems.length >= 2)) { notDisabled = this.options.hideDisabled ? ', [disabled]' : ''; - var totalCount = this.$element.find('option').not('[data-divider="true"], [data-hidden="true"]' + notDisabled).length, + var totalCount = $selectOptions.not('[data-divider="true"], [data-hidden="true"]' + notDisabled).length, tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedItems.length, totalCount) : this.options.countSelectedText; title = tr8nText.replace('{0}', selectedItems.length.toString()).replace('{1}', totalCount.toString()); } } - this.options.title = this.$element.attr('title'); + if (this.options.title == undefined) { + this.options.title = this.$element.attr('title'); + } if (this.options.selectedTextFormat == 'static') { title = this.options.title; @@ -406,8 +799,11 @@ title = typeof this.options.title !== 'undefined' ? this.options.title : this.options.noneSelectedText; } - this.$button.attr('title', htmlEscape(title)); - this.$newElement.find('.filter-option').html(title); + //strip all HTML tags and trim the result, then unescape any escaped tags + this.$button.attr('title', htmlUnescape($.trim(title.replace(/<[^>]*>?/g, '')))); + this.$button.children('.filter-option').html(title); + + this.$element.trigger('rendered.bs.select'); }, /** @@ -416,7 +812,7 @@ */ setStyle: function (style, status) { if (this.$element.attr('class')) { - this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|validate\[.*\]/gi, '')); + this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); } var buttonClass = style ? style : this.options.style; @@ -431,117 +827,251 @@ } }, - liHeight: function () { - if (this.options.size === false) return; + liHeight: function (refresh) { + if (!refresh && (this.options.size === false || this.sizeInfo)) return; - var $selectClone = this.$menu.parent().clone().find('> .dropdown-toggle').prop('autofocus', false).end().appendTo('body'), - $menuClone = $selectClone.addClass('open').find('> .dropdown-menu'), - liHeight = $menuClone.find('li').not('.divider').not('.dropdown-header').filter(':visible').children('a').outerHeight(), - headerHeight = this.options.header ? $menuClone.find('.popover-title').outerHeight() : 0, - searchHeight = this.options.liveSearch ? $menuClone.find('.bs-searchbox').outerHeight() : 0, - actionsHeight = this.options.actionsBox ? $menuClone.find('.bs-actionsbox').outerHeight() : 0; + var newElement = document.createElement('div'), + menu = document.createElement('div'), + menuInner = document.createElement('ul'), + divider = document.createElement('li'), + li = document.createElement('li'), + a = document.createElement('a'), + text = document.createElement('span'), + header = this.options.header && this.$menu.find('.popover-title').length > 0 ? this.$menu.find('.popover-title')[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; - $selectClone.remove(); + text.className = 'text'; + newElement.className = this.$menu[0].parentNode.className + ' open'; + menu.className = 'dropdown-menu open'; + menuInner.className = 'dropdown-menu inner'; + divider.className = 'divider'; - this.$newElement - .data('liHeight', liHeight) - .data('headerHeight', headerHeight) - .data('searchHeight', searchHeight) - .data('actionsHeight', actionsHeight); + text.appendChild(document.createTextNode('Inner text')); + a.appendChild(text); + li.appendChild(a); + menuInner.appendChild(li); + menuInner.appendChild(divider); + 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); + menu.appendChild(menuInner); + if (doneButton) menu.appendChild(doneButton); + newElement.appendChild(menu); + + document.body.appendChild(newElement); + + var liHeight = a.offsetHeight, + 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 = typeof getComputedStyle === 'function' ? getComputedStyle(menu) : false, + $menu = menuStyle ? null : $(menu), + menuPadding = { + vert: parseInt(menuStyle ? menuStyle.paddingTop : $menu.css('paddingTop')) + + parseInt(menuStyle ? menuStyle.paddingBottom : $menu.css('paddingBottom')) + + parseInt(menuStyle ? menuStyle.borderTopWidth : $menu.css('borderTopWidth')) + + parseInt(menuStyle ? menuStyle.borderBottomWidth : $menu.css('borderBottomWidth')), + horiz: parseInt(menuStyle ? menuStyle.paddingLeft : $menu.css('paddingLeft')) + + parseInt(menuStyle ? menuStyle.paddingRight : $menu.css('paddingRight')) + + parseInt(menuStyle ? menuStyle.borderLeftWidth : $menu.css('borderLeftWidth')) + + parseInt(menuStyle ? menuStyle.borderRightWidth : $menu.css('borderRightWidth')) + }, + menuExtras = { + vert: menuPadding.vert + + parseInt(menuStyle ? menuStyle.marginTop : $menu.css('marginTop')) + + parseInt(menuStyle ? menuStyle.marginBottom : $menu.css('marginBottom')) + 2, + horiz: menuPadding.horiz + + parseInt(menuStyle ? menuStyle.marginLeft : $menu.css('marginLeft')) + + parseInt(menuStyle ? menuStyle.marginRight : $menu.css('marginRight')) + 2 + } + + document.body.removeChild(newElement); + + this.sizeInfo = { + liHeight: liHeight, + headerHeight: headerHeight, + searchHeight: searchHeight, + actionsHeight: actionsHeight, + doneButtonHeight: doneButtonHeight, + dividerHeight: dividerHeight, + menuPadding: menuPadding, + menuExtras: menuExtras + }; }, setSize: function () { this.findLis(); + this.liHeight(); + + if (this.options.header) this.$menu.css('padding-top', 0); + if (this.options.size === false) return; + var that = this, - menu = this.$menu, - menuInner = menu.find('.inner'), - selectHeight = this.$newElement.outerHeight(), - liHeight = this.$newElement.data('liHeight'), - headerHeight = this.$newElement.data('headerHeight'), - searchHeight = this.$newElement.data('searchHeight'), - actionsHeight = this.$newElement.data('actionsHeight'), - divHeight = this.$lis.filter('.divider').outerHeight(true), - menuPadding = parseInt(menu.css('padding-top')) + - parseInt(menu.css('padding-bottom')) + - parseInt(menu.css('border-top-width')) + - parseInt(menu.css('border-bottom-width')), - notDisabled = this.options.hideDisabled ? ', .disabled' : '', + $menu = this.$menu, + $menuInner = this.$menuInner, $window = $(window), - menuExtras = menuPadding + parseInt(menu.css('margin-top')) + parseInt(menu.css('margin-bottom')) + 2, + selectHeight = this.$newElement[0].offsetHeight, + selectWidth = this.$newElement[0].offsetWidth, + 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'], + menuExtras = this.sizeInfo['menuExtras'], + notDisabled = this.options.hideDisabled ? '.disabled' : '', menuHeight, + menuWidth, + getHeight, + getWidth, selectOffsetTop, selectOffsetBot, - posVert = function () { - // JQuery defines a scrollTop function, but in pure JS it's a property - //noinspection JSValidateTypes - selectOffsetTop = that.$newElement.offset().top - $window.scrollTop(); - selectOffsetBot = $window.height() - selectOffsetTop - selectHeight; - }; - posVert(); - if (this.options.header) menu.css('padding-top', 0); + selectOffsetLeft, + selectOffsetRight, + getPos = function() { + var pos = that.$newElement.offset(), + $container = $(that.options.container), + containerPos; - if (this.options.size == 'auto') { + if (that.options.container && !$container.is('body')) { + containerPos = $container.offset(); + containerPos.top += parseInt($container.css('borderTopWidth')); + containerPos.left += parseInt($container.css('borderLeftWidth')); + } else { + containerPos = { top: 0, left: 0 }; + } + + var winPad = that.options.windowPadding; + selectOffsetTop = pos.top - containerPos.top - $window.scrollTop(); + selectOffsetBot = $window.height() - selectOffsetTop - selectHeight - containerPos.top - winPad[2]; + selectOffsetLeft = pos.left - containerPos.left - $window.scrollLeft(); + selectOffsetRight = $window.width() - selectOffsetLeft - selectWidth - containerPos.left - winPad[1]; + selectOffsetTop -= winPad[0]; + selectOffsetLeft -= winPad[3]; + }; + + getPos(); + + if (this.options.size === 'auto') { var getSize = function () { var minHeight, - lisVis = that.$lis.not('.hide'); + hasClass = function (className, include) { + return function (element) { + if (include) { + return (element.classList ? element.classList.contains(className) : $(element).hasClass(className)); + } else { + return !(element.classList ? element.classList.contains(className) : $(element).hasClass(className)); + } + }; + }, + lis = that.$menuInner[0].getElementsByTagName('li'), + lisVisible = Array.prototype.filter ? Array.prototype.filter.call(lis, hasClass('hidden', false)) : that.$lis.not('.hidden'), + optGroup = Array.prototype.filter ? Array.prototype.filter.call(lisVisible, hasClass('dropdown-header', true)) : lisVisible.filter('.dropdown-header'); - posVert(); - menuHeight = selectOffsetBot - menuExtras; + getPos(); + menuHeight = selectOffsetBot - menuExtras.vert; + menuWidth = selectOffsetRight - menuExtras.horiz; + + if (that.options.container) { + if (!$menu.data('height')) $menu.data('height', $menu.height()); + getHeight = $menu.data('height'); + + if (!$menu.data('width')) $menu.data('width', $menu.width()); + getWidth = $menu.data('width'); + } else { + getHeight = $menu.height(); + getWidth = $menu.width(); + } if (that.options.dropupAuto) { - that.$newElement.toggleClass('dropup', (selectOffsetTop > selectOffsetBot) && ((menuHeight - menuExtras) < menu.height())); - } - if (that.$newElement.hasClass('dropup')) { - menuHeight = selectOffsetTop - menuExtras; + that.$newElement.toggleClass('dropup', selectOffsetTop > selectOffsetBot && (menuHeight - menuExtras.vert) < getHeight); } - if ((lisVis.length + lisVis.filter('.dropdown-header').length) > 3) { - minHeight = liHeight * 3 + menuExtras - 2; + if (that.$newElement.hasClass('dropup')) { + menuHeight = selectOffsetTop - menuExtras.vert; + } + + if (that.options.dropdownAlignRight === 'auto') { + $menu.toggleClass('dropdown-menu-right', selectOffsetLeft > selectOffsetRight && (menuWidth - menuExtras.horiz) < (getWidth - selectWidth)); + } + + if ((lisVisible.length + optGroup.length) > 3) { + minHeight = liHeight * 3 + menuExtras.vert - 2; } else { minHeight = 0; } - menu.css({ + $menu.css({ 'max-height': menuHeight + 'px', 'overflow': 'hidden', - 'min-height': minHeight + headerHeight + searchHeight + actionsHeight + 'px' + 'min-height': minHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight + 'px' }); - menuInner.css({ - 'max-height': menuHeight - headerHeight - searchHeight - actionsHeight - menuPadding + 'px', + $menuInner.css({ + 'max-height': menuHeight - headerHeight - searchHeight - actionsHeight - doneButtonHeight - menuPadding.vert + 'px', 'overflow-y': 'auto', - 'min-height': Math.max(minHeight - menuPadding, 0) + 'px' + 'min-height': Math.max(minHeight - menuPadding.vert, 0) + 'px' }); }; getSize(); this.$searchbox.off('input.getSize propertychange.getSize').on('input.getSize propertychange.getSize', getSize); - $(window).off('resize.getSize').on('resize.getSize', getSize); - $(window).off('scroll.getSize').on('scroll.getSize', getSize); - } else if (this.options.size && this.options.size != 'auto' && menu.find('li' + notDisabled).length > this.options.size) { - var optIndex = this.$lis.not('.divider' + notDisabled).find(' > *').slice(0, this.options.size).last().parent().index(); - var divLength = this.$lis.slice(0, optIndex + 1).filter('.divider').length; - menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding; + $window.off('resize.getSize scroll.getSize').on('resize.getSize scroll.getSize', getSize); + } else if (this.options.size && this.options.size != 'auto' && this.$lis.not(notDisabled).length > this.options.size) { + var optIndex = this.$lis.not('.divider').not(notDisabled).children().slice(0, this.options.size).last().parent().index(), + divLength = this.$lis.slice(0, optIndex + 1).filter('.divider').length; + menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding.vert; + + if (that.options.container) { + if (!$menu.data('height')) $menu.data('height', $menu.height()); + getHeight = $menu.data('height'); + } else { + getHeight = $menu.height(); + } + if (that.options.dropupAuto) { //noinspection JSUnusedAssignment - this.$newElement.toggleClass('dropup', (selectOffsetTop > selectOffsetBot) && (menuHeight < menu.height())); + this.$newElement.toggleClass('dropup', selectOffsetTop > selectOffsetBot && (menuHeight - menuExtras.vert) < getHeight); } - menu.css({'max-height': menuHeight + headerHeight + searchHeight + actionsHeight + 'px', 'overflow': 'hidden'}); - menuInner.css({'max-height': menuHeight - menuPadding + 'px', 'overflow-y': 'auto'}); + $menu.css({ + 'max-height': menuHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight + 'px', + 'overflow': 'hidden', + 'min-height': '' + }); + $menuInner.css({ + 'max-height': menuHeight - menuPadding.vert + 'px', + 'overflow-y': 'auto', + 'min-height': '' + }); } }, setWidth: function () { - if (this.options.width == 'auto') { + if (this.options.width === 'auto') { this.$menu.css('min-width', '0'); - // Get correct width if element hidden - var selectClone = this.$newElement.clone().appendTo('body'); - var ulWidth = selectClone.find('> .dropdown-menu').css('width'); - var btnWidth = selectClone.css('width', 'auto').find('> button').css('width'); - selectClone.remove(); + // Get correct width if element is hidden + var $selectClone = this.$menu.parent().clone().appendTo('body'), + $selectClone2 = this.options.container ? this.$newElement.clone().appendTo('body') : $selectClone, + ulWidth = $selectClone.children('.dropdown-menu').outerWidth(), + btnWidth = $selectClone2.css('width', 'auto').children('button').outerWidth(); + + $selectClone.remove(); + $selectClone2.remove(); // Set width to whatever's larger, button title or longest option - this.$newElement.css('width', Math.max(parseInt(ulWidth), parseInt(btnWidth)) + 'px'); - } else if (this.options.width == 'fit') { + this.$newElement.css('width', Math.max(ulWidth, btnWidth) + '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'); @@ -561,74 +1091,108 @@ }, selectPosition: function () { + this.$bsContainer = $('
    '); + var that = this, - drop = '
    ', - $drop = $(drop), + $container = $(this.options.container), pos, + containerPos, actualHeight, getPlacement = function ($element) { - $drop.addClass($element.attr('class').replace(/form-control/gi, '')).toggleClass('dropup', $element.hasClass('dropup')); + that.$bsContainer.addClass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleClass('dropup', $element.hasClass('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('dropup') ? 0 : $element[0].offsetHeight; - $drop.css({ - 'top': pos.top + actualHeight, - 'left': pos.left, - 'width': $element[0].offsetWidth, - 'position': 'absolute' + + that.$bsContainer.css({ + 'top': pos.top - containerPos.top + actualHeight, + 'left': pos.left - containerPos.left, + 'width': $element[0].offsetWidth }); }; - this.$newElement.on('click', function () { + + this.$button.on('click', function () { + var $this = $(this); + if (that.isDisabled()) { return; } - getPlacement($(this)); - $drop.appendTo(that.options.container); - $drop.toggleClass('open', !$(this).hasClass('open')); - $drop.append(that.$menu); + + getPlacement(that.$newElement); + + that.$bsContainer + .appendTo(that.options.container) + .toggleClass('open', !$this.hasClass('open')) + .append(that.$menu); }); - $(window).resize(function () { + + $(window).on('resize scroll', function () { getPlacement(that.$newElement); }); - $(window).on('scroll', function () { - getPlacement(that.$newElement); - }); - $('html').on('click', function (e) { - if ($(e.target).closest(that.$newElement).length < 1) { - $drop.removeClass('open'); - } + + this.$element.on('hide.bs.select', function () { + that.$menu.data('height', that.$menu.height()); + that.$bsContainer.detach(); }); }, - setSelected: function (index, selected) { - this.findLis(); - this.$lis.filter('[data-original-index="' + index + '"]').toggleClass('selected', 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 + * @param {JQuery} $lis - the 'li' element that is being modified + */ + setSelected: function (index, selected, $lis) { + if (!$lis) { + this.togglePlaceholder(); // check if setSelected is being called by changing the value of the select + $lis = this.findLis().eq(this.liObj[index]); + } + + $lis.toggleClass('selected', selected).find('a').attr('aria-selected', selected); }, - setDisabled: function (index, disabled) { - this.findLis(); + /** + * @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 + * @param {JQuery} $lis - the 'li' element that is being modified + */ + setDisabled: function (index, disabled, $lis) { + if (!$lis) { + $lis = this.findLis().eq(this.liObj[index]); + } + if (disabled) { - this.$lis.filter('[data-original-index="' + index + '"]').addClass('disabled').find('a').attr('href', '#').attr('tabindex', -1); + $lis.addClass('disabled').children('a').attr('href', '#').attr('tabindex', -1).attr('aria-disabled', true); } else { - this.$lis.filter('[data-original-index="' + index + '"]').removeClass('disabled').find('a').removeAttr('href').attr('tabindex', 0); + $lis.removeClass('disabled').children('a').removeAttr('href').attr('tabindex', 0).attr('aria-disabled', false); } }, isDisabled: function () { - return this.$element.is(':disabled'); + return this.$element[0].disabled; }, checkDisabled: function () { var that = this; if (this.isDisabled()) { - this.$button.addClass('disabled').attr('tabindex', -1); + this.$newElement.addClass('disabled'); + this.$button.addClass('disabled').attr('tabindex', -1).attr('aria-disabled', true); } else { if (this.$button.hasClass('disabled')) { - this.$button.removeClass('disabled'); + this.$newElement.removeClass('disabled'); + this.$button.removeClass('disabled').attr('aria-disabled', false); } - if (this.$button.attr('tabindex') == -1) { - if (!this.$element.data('tabindex')) this.$button.removeAttr('tabindex'); + if (this.$button.attr('tabindex') == -1 && !this.$element.data('tabindex')) { + this.$button.removeAttr('tabindex'); } } @@ -637,37 +1201,62 @@ }); }, + togglePlaceholder: function () { + var value = this.$element.val(); + this.$button.toggleClass('bs-placeholder', value === null || value === '' || (value.constructor === Array && value.length === 0)); + }, + tabIndex: function () { - if (this.$element.is('[tabindex]')) { + 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; + var that = this, + $document = $(document); - this.$newElement.on('touchstart.dropdown', '.dropdown-menu', function (e) { - e.stopPropagation(); - }); + $document.data('spaceSelect', false); - this.$newElement.on('click', function () { - that.setSize(); - if (!that.options.liveSearch && !that.multiple) { - setTimeout(function () { - that.$menu.find('.selected a').focus(); - }, 10); + this.$button.on('keyup', function (e) { + if (/(32)/.test(e.keyCode.toString(10)) && $document.data('spaceSelect')) { + e.preventDefault(); + $document.data('spaceSelect', false); } }); - this.$menu.on('click', 'li a', function (e) { + this.$button.on('click', function () { + that.setSize(); + }); + + this.$element.on('shown.bs.select', function () { + if (!that.options.liveSearch && !that.multiple) { + that.$menuInner.find('.selected a').focus(); + } else if (!that.multiple) { + var selectedIndex = that.liObj[that.$element[0].selectedIndex]; + + if (typeof selectedIndex !== 'number' || that.options.size === false) return; + + // scroll to selected option + var offset = that.$lis.eq(selectedIndex)[0].offsetTop - that.$menuInner[0].offsetTop; + offset = offset - that.$menuInner[0].offsetHeight/2 + that.sizeInfo.liHeight/2; + that.$menuInner[0].scrollTop = offset; + } + }); + + this.$menuInner.on('click', 'li a', function (e) { var $this = $(this), clickedIndex = $this.parent().data('originalIndex'), prevValue = that.$element.val(), - prevIndex = that.$element.prop('selectedIndex'); + prevIndex = that.$element.prop('selectedIndex'), + triggerChange = true; // Don't close on multi choice menu - if (that.multiple) { + if (that.multiple && that.options.maxOptions !== 1) { e.stopPropagation(); } @@ -685,14 +1274,14 @@ if (!that.multiple) { // Deselect all others if not multi select box $options.prop('selected', false); $option.prop('selected', true); - that.$menu.find('.selected').removeClass('selected'); + that.$menuInner.find('.selected').removeClass('selected').find('a').attr('aria-selected', false); that.setSelected(clickedIndex, true); } else { // Toggle the one we have chosen if we are multi select. $option.prop('selected', !state); that.setSelected(clickedIndex, !state); $this.blur(); - if ((maxOptions !== false) || (maxOptionsGrp !== false)) { + if (maxOptions !== false || maxOptionsGrp !== false) { var maxReached = maxOptions < $options.filter(':selected').length, maxReachedGrp = maxOptionsGrp < $optgroup.find('option:selected').length; @@ -700,19 +1289,17 @@ if (maxOptions && maxOptions == 1) { $options.prop('selected', false); $option.prop('selected', true); - that.$menu.find('.selected').removeClass('selected'); + that.$menuInner.find('.selected').removeClass('selected'); that.setSelected(clickedIndex, true); } else if (maxOptionsGrp && maxOptionsGrp == 1) { $optgroup.find('option:selected').prop('selected', false); $option.prop('selected', true); - var optgroupID = $this.data('optgroup'); - - that.$menu.find('.selected').has('a[data-optgroup="' + optgroupID + '"]').removeClass('selected'); - + var optgroupID = $this.parent().data('optgroup'); + that.$menuInner.find('[data-optgroup="' + optgroupID + '"]').removeClass('selected'); that.setSelected(clickedIndex, true); } else { - var maxOptionsArr = (typeof that.options.maxOptionsText === 'function') ? - that.options.maxOptionsText(maxOptions, maxOptionsGrp) : that.options.maxOptionsText, + 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 = $('
    '); @@ -729,11 +1316,13 @@ if (maxOptions && maxReached) { $notify.append($('
    ' + maxTxt + '
    ')); + triggerChange = false; that.$element.trigger('maxReached.bs.select'); } if (maxOptionsGrp && maxReachedGrp) { $notify.append($('
    ' + maxTxtGrp + '
    ')); + triggerChange = false; that.$element.trigger('maxReachedGrp.bs.select'); } @@ -749,50 +1338,54 @@ } } - if (!that.multiple) { + if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) { that.$button.focus(); } else if (that.options.liveSearch) { that.$searchbox.focus(); } // Trigger select 'change' - if ((prevValue != that.$element.val() && that.multiple) || (prevIndex != that.$element.prop('selectedIndex') && !that.multiple)) { - that.$element.change(); + if (triggerChange) { + if ((prevValue != that.$element.val() && that.multiple) || (prevIndex != that.$element.prop('selectedIndex') && !that.multiple)) { + // $option.prop('selected') is current option state (selected/unselected). state is previous option state. + changed_arguments = [clickedIndex, $option.prop('selected'), state]; + that.$element + .triggerNative('change'); + } } } }); this.$menu.on('click', 'li.disabled a, .popover-title, .popover-title :not(.close)', function (e) { - if (e.target == this) { + if (e.currentTarget == this) { e.preventDefault(); e.stopPropagation(); - if (!that.options.liveSearch) { - that.$button.focus(); - } else { + if (that.options.liveSearch && !$(e.target).hasClass('close')) { that.$searchbox.focus(); + } else { + that.$button.focus(); } } }); - this.$menu.on('click', 'li.divider, li.dropdown-header', function (e) { + this.$menuInner.on('click', '.divider, .dropdown-header', function (e) { e.preventDefault(); e.stopPropagation(); - if (!that.options.liveSearch) { - that.$button.focus(); - } else { + if (that.options.liveSearch) { that.$searchbox.focus(); + } else { + that.$button.focus(); } }); this.$menu.on('click', '.popover-title .close', function () { - that.$button.focus(); + that.$button.click(); }); this.$searchbox.on('click', function (e) { e.stopPropagation(); }); - this.$menu.on('click', '.actions-btn', function (e) { if (that.options.liveSearch) { that.$searchbox.focus(); @@ -803,31 +1396,32 @@ e.preventDefault(); e.stopPropagation(); - if ($(this).is('.bs-select-all')) { + if ($(this).hasClass('bs-select-all')) { that.selectAll(); } else { that.deselectAll(); } - that.$element.change(); }); this.$element.change(function () { that.render(false); + that.$element.trigger('changed.bs.select', changed_arguments); + changed_arguments = null; }); }, liveSearchListener: function () { var that = this, - no_results = $('
  • '); + $no_results = $('
  • '); - this.$newElement.on('click.dropdown.data-api touchstart.dropdown.data-api', function () { - that.$menu.find('.active').removeClass('active'); + this.$button.on('click.dropdown.data-api', function () { + that.$menuInner.find('.active').removeClass('active'); if (!!that.$searchbox.val()) { that.$searchbox.val(''); - that.$lis.not('.is-hidden').removeClass('hide'); - if (!!no_results.parent().length) no_results.remove(); + that.$lis.not('.is-hidden').removeClass('hidden'); + if (!!$no_results.parent().length) $no_results.remove(); } - if (!that.multiple) that.$menu.find('.selected').addClass('active'); + if (!that.multiple) that.$menuInner.find('.selected').addClass('active'); setTimeout(function () { that.$searchbox.focus(); }, 10); @@ -838,33 +1432,64 @@ }); this.$searchbox.on('input propertychange', function () { + that.$lis.not('.is-hidden').removeClass('hidden'); + that.$lis.filter('.active').removeClass('active'); + $no_results.remove(); + if (that.$searchbox.val()) { - - if (that.options.searchAccentInsensitive) { - that.$lis.not('.is-hidden').removeClass('hide').find('a').not(':aicontains(' + normalizeToBase(that.$searchbox.val()) + ')').parent().addClass('hide'); + var $searchBase = that.$lis.not('.is-hidden, .divider, .dropdown-header'), + $hideItems; + if (that.options.liveSearchNormalize) { + $hideItems = $searchBase.not(':a' + that._searchStyle() + '("' + normalizeToBase(that.$searchbox.val()) + '")'); } else { - that.$lis.not('.is-hidden').removeClass('hide').find('a').not(':icontains(' + that.$searchbox.val() + ')').parent().addClass('hide'); + $hideItems = $searchBase.not(':' + that._searchStyle() + '("' + that.$searchbox.val() + '")'); } - if (!that.$menu.find('li').filter(':visible:not(.no-results)').length) { - if (!!no_results.parent().length) no_results.remove(); - no_results.html(that.options.noneResultsText + ' "' + htmlEscape(that.$searchbox.val()) + '"').show(); - that.$menu.find('li').last().after(no_results); - } else if (!!no_results.parent().length) { - no_results.remove(); - } + if ($hideItems.length === $searchBase.length) { + $no_results.html(that.options.noneResultsText.replace('{0}', '"' + htmlEscape(that.$searchbox.val()) + '"')); + that.$menuInner.append($no_results); + that.$lis.addClass('hidden'); + } else { + $hideItems.addClass('hidden'); - } else { - that.$lis.not('.is-hidden').removeClass('hide'); - if (!!no_results.parent().length) no_results.remove(); + var $lisVisible = that.$lis.not('.hidden'), + $foundDiv; + + // hide divider if first or last visible, or if followed by another divider + $lisVisible.each(function (index) { + var $this = $(this); + + if ($this.hasClass('divider')) { + if ($foundDiv === undefined) { + $this.addClass('hidden'); + } else { + if ($foundDiv) $foundDiv.addClass('hidden'); + $foundDiv = $this; + } + } else if ($this.hasClass('dropdown-header') && $lisVisible.eq(index + 1).data('optgroup') !== $this.data('optgroup')) { + $this.addClass('hidden'); + } else { + $foundDiv = null; + } + }); + if ($foundDiv) $foundDiv.addClass('hidden'); + + $searchBase.not('.hidden').first().addClass('active'); + that.$menuInner.scrollTop(0); + } } - - that.$menu.find('li.active').removeClass('active'); - that.$menu.find('li').filter(':visible:not(.divider)').eq(0).addClass('active').find('a').focus(); - $(this).focus(); }); }, + _searchStyle: function () { + var styles = { + begins: 'ibegins', + startsWith: 'ibegins' + }; + + return styles[this.options.liveSearchStyle] || 'icontains'; + }, + val: function (value) { if (typeof value !== 'undefined') { this.$element.val(value); @@ -876,29 +1501,65 @@ } }, - selectAll: function () { + changeAll: function (status) { + if (!this.multiple) return; + if (typeof status === 'undefined') status = true; + this.findLis(); - this.$lis.not('.divider').not('.disabled').not('.selected').filter(':visible').find('a').click(); + + var $options = this.$element.find('option'), + $lisVisible = this.$lis.not('.divider, .dropdown-header, .disabled, .hidden'), + lisVisLen = $lisVisible.length, + selectedOptions = []; + + if (status) { + if ($lisVisible.filter('.selected').length === $lisVisible.length) return; + } else { + if ($lisVisible.filter('.selected').length === 0) return; + } + + $lisVisible.toggleClass('selected', status); + + for (var i = 0; i < lisVisLen; i++) { + var origIndex = $lisVisible[i].getAttribute('data-original-index'); + selectedOptions[selectedOptions.length] = $options.eq(origIndex)[0]; + } + + $(selectedOptions).prop('selected', status); + + this.render(false); + + this.togglePlaceholder(); + + this.$element + .triggerNative('change'); + }, + + selectAll: function () { + return this.changeAll(true); }, deselectAll: function () { - this.findLis(); - this.$lis.not('.divider').not('.disabled').filter('.selected').filter(':visible').find('a').click(); + return this.changeAll(false); + }, + + toggle: function (e) { + e = e || window.event; + + if (e) e.stopPropagation(); + + this.$button.trigger('click'); }, keydown: function (e) { var $this = $(this), - $parent = ($this.is('input')) ? $this.parent().parent() : $this.parent(), + $parent = $this.is('input') ? $this.parent().parent() : $this.parent(), $items, that = $parent.data('this'), index, - next, - first, - last, - prev, - nextPrev, prevIndex, isActive, + selector = ':not(.disabled, .hidden, .dropdown-header, .divider)', keyCodeMap = { 32: ' ', 48: '0', @@ -950,93 +1611,58 @@ 105: '9' }; - if (that.options.liveSearch) $parent = $this.parent().parent(); - if (that.options.container) $parent = that.$menu; + isActive = that.$newElement.hasClass('open'); - $items = $('[role=menu] li a', $parent); - - isActive = that.$menu.parent().hasClass('open'); - - if (!isActive && /([0-9]|[A-z])/.test(String.fromCharCode(e.keyCode))) { + if (!isActive && (e.keyCode >= 48 && e.keyCode <= 57 || e.keyCode >= 96 && e.keyCode <= 105 || e.keyCode >= 65 && e.keyCode <= 90)) { if (!that.options.container) { that.setSize(); that.$menu.parent().addClass('open'); isActive = true; } else { - that.$newElement.trigger('click'); + that.$button.trigger('click'); } that.$searchbox.focus(); + return; } if (that.options.liveSearch) { - if (/(^9$|27)/.test(e.keyCode.toString(10)) && isActive && that.$menu.find('.active').length === 0) { + if (/(^9$|27)/.test(e.keyCode.toString(10)) && isActive) { e.preventDefault(); - that.$menu.parent().removeClass('open'); + e.stopPropagation(); + that.$menuInner.click(); that.$button.focus(); } - $items = $('[role=menu] li:not(.divider):not(.dropdown-header):visible', $parent); - if (!$this.val() && !/(38|40)/.test(e.keyCode.toString(10))) { - if ($items.filter('.active').length === 0) { - if (that.options.searchAccentInsensitive) { - $items = that.$newElement.find('li').filter(':aicontains(' + normalizeToBase(keyCodeMap[e.keyCode]) + ')'); - } else { - $items = that.$newElement.find('li').filter(':icontains(' + keyCodeMap[e.keyCode] + ')'); - } - } - } } - if (!$items.length) return; - if (/(38|40)/.test(e.keyCode.toString(10))) { - index = $items.index($items.filter(':focus')); - first = $items.parent(':not(.disabled):visible').first().index(); - last = $items.parent(':not(.disabled):visible').last().index(); - next = $items.eq(index).parent().nextAll(':not(.disabled):visible').eq(0).index(); - prev = $items.eq(index).parent().prevAll(':not(.disabled):visible').eq(0).index(); - nextPrev = $items.eq(next).parent().prevAll(':not(.disabled):visible').eq(0).index(); - - if (that.options.liveSearch) { - $items.each(function (i) { - if ($(this).is(':not(.disabled)')) { - $(this).data('index', i); - } - }); - index = $items.index($items.filter('.active')); - first = $items.filter(':not(.disabled):visible').first().data('index'); - last = $items.filter(':not(.disabled):visible').last().data('index'); - next = $items.eq(index).nextAll(':not(.disabled):visible').eq(0).data('index'); - prev = $items.eq(index).prevAll(':not(.disabled):visible').eq(0).data('index'); - nextPrev = $items.eq(next).prevAll(':not(.disabled):visible').eq(0).data('index'); - } - - prevIndex = $this.data('prevIndex'); - - if (e.keyCode == 38) { - if (that.options.liveSearch) index -= 1; - if (index != nextPrev && index > prev) index = prev; - if (index < first) index = first; - if (index == prevIndex) index = last; - } - - if (e.keyCode == 40) { - if (that.options.liveSearch) index += 1; - if (index == -1) index = 0; - if (index != nextPrev && index < next) index = next; - if (index > last) index = last; - if (index == prevIndex) index = first; - } - - $this.data('prevIndex', index); + $items = that.$lis.filter(selector); + if (!$items.length) return; if (!that.options.liveSearch) { - $items.eq(index).focus(); + index = $items.index($items.find('a').filter(':focus').parent()); + } else { + index = $items.index($items.filter('.active')); + } + + prevIndex = that.$menuInner.data('prevIndex'); + + if (e.keyCode == 38) { + if ((that.options.liveSearch || index == prevIndex) && index != -1) index--; + if (index < 0) index += $items.length; + } else if (e.keyCode == 40) { + if (that.options.liveSearch || index == prevIndex) index++; + index = index % $items.length; + } + + that.$menuInner.data('prevIndex', index); + + if (!that.options.liveSearch) { + $items.eq(index).children('a').focus(); } else { e.preventDefault(); - if (!$this.is('.dropdown-toggle')) { - $items.removeClass('active'); - $items.eq(index).addClass('active').find('a').focus(); + if (!$this.hasClass('dropdown-toggle')) { + $items.removeClass('active').eq(index).addClass('active').children('a').focus(); $this.focus(); } } @@ -1046,11 +1672,10 @@ count, prevKey; - $items.each(function () { - if ($(this).parent().is(':not(.disabled)')) { - if ($.trim($(this).text().toLowerCase()).substring(0, 1) == keyCodeMap[e.keyCode]) { - keyIndex.push($(this).parent().index()); - } + $items = that.$lis.filter(selector); + $items.each(function (i) { + if ($.trim($(this).children('a').text().toLowerCase()).substring(0, 1) == keyCodeMap[e.keyCode]) { + keyIndex.push(i); } }); @@ -1068,16 +1693,23 @@ if (count > keyIndex.length) count = 1; } - $items.eq(keyIndex[count - 1]).focus(); + $items.eq(keyIndex[count - 1]).children('a').focus(); } // Select focused option if "Enter", "Spacebar" or "Tab" (when selectOnTab is true) are pressed inside the menu. if ((/(13|32)/.test(e.keyCode.toString(10)) || (/(^9$)/.test(e.keyCode.toString(10)) && that.options.selectOnTab)) && isActive) { if (!/(32)/.test(e.keyCode.toString(10))) e.preventDefault(); if (!that.options.liveSearch) { - $(':focus').click(); + var elem = $(':focus'); + elem.click(); + // Bring back focus for multiselects + elem.focus(); + // Prevent screen from scrolling if the user hit the spacebar + e.preventDefault(); + // Fixes spacebar selection of dropdown items in FF & IE + $(document).data('spaceSelect', true); } else if (!/(32)/.test(e.keyCode.toString(10))) { - that.$menu.find('.active a').click(); + that.$menuInner.find('.active a').click(); $this.focus(); } $(document).data('keycount', 0); @@ -1085,31 +1717,27 @@ if ((/(^9$|27)/.test(e.keyCode.toString(10)) && isActive && (that.multiple || that.options.liveSearch)) || (/(27)/.test(e.keyCode.toString(10)) && !isActive)) { that.$menu.parent().removeClass('open'); + if (that.options.container) that.$newElement.removeClass('open'); that.$button.focus(); } }, mobile: function () { - this.$element.addClass('mobile-device').appendTo(this.$newElement); - if (this.options.container) this.$menu.hide(); + this.$element.addClass('mobile-device'); }, refresh: function () { this.$lis = null; + this.liObj = {}; this.reloadLi(); this.render(); - this.setWidth(); - this.setStyle(); this.checkDisabled(); - this.liHeight(); - }, + this.liHeight(true); + this.setStyle(); + this.setWidth(); + if (this.$lis) this.$searchbox.trigger('propertychange'); - update: function () { - this.reloadLi(); - this.setWidth(); - this.setStyle(); - this.checkDisabled(); - this.liHeight(); + this.$element.trigger('refreshed.bs.select'); }, hide: function () { @@ -1123,37 +1751,46 @@ remove: function () { this.$newElement.remove(); this.$element.remove(); + }, + + destroy: function () { + this.$newElement.before(this.$element).remove(); + + if (this.$bsContainer) { + this.$bsContainer.remove(); + } else { + this.$menu.remove(); + } + + this.$element + .off('.bs.select') + .removeData('selectpicker') + .removeClass('bs-select-hidden selectpicker'); } }; // SELECTPICKER PLUGIN DEFINITION // ============================== - function Plugin(option, event) { + 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 - //noinspection JSDuplicatedDeclaration - var _option = option, - option = args[0], - event = args[1]; - [].shift.apply(args); + // to get lost/corrupted in android 2.3 and IE9 #715 #775 + var _option = option; - // This fixes a bug in the js implementation on android 2.3 #715 - if (typeof option == 'undefined') { - option = _option; - } + [].shift.apply(args); var value; var chain = this.each(function () { var $this = $(this); if ($this.is('select')) { var data = $this.data('selectpicker'), - options = typeof option == 'object' && option; + options = typeof _option == 'object' && _option; if (!data) { var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, $this.data(), options); - $this.data('selectpicker', (data = new Selectpicker(this, config, event))); + config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), $this.data().template, options.template); + $this.data('selectpicker', (data = new Selectpicker(this, config))); } else if (options) { for (var i in options) { if (options.hasOwnProperty(i)) { @@ -1162,11 +1799,11 @@ } } - if (typeof option == 'string') { - if (data[option] instanceof Function) { - value = data[option].apply(data, args); + if (typeof _option == 'string') { + if (data[_option] instanceof Function) { + value = data[_option].apply(data, args); } else { - value = data.options[option]; + value = data.options[_option]; } } } @@ -1193,8 +1830,8 @@ $(document) .data('keycount', 0) - .on('keydown', '.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role=menu], .bs-searchbox input', Selectpicker.prototype.keydown) - .on('focusin.modal', '.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role=menu], .bs-searchbox input', function (e) { + .on('keydown.bs.select', '.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input', Selectpicker.prototype.keydown) + .on('focusin.modal', '.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input', function (e) { e.stopPropagation(); }); diff --git a/RIGS/static/js/button.js b/RIGS/static/js/button.js old mode 100644 new mode 100755 index 4d569017..843b39c9 --- a/RIGS/static/js/button.js +++ b/RIGS/static/js/button.js @@ -1,8 +1,8 @@ /* ======================================================================== - * Bootstrap: button.js v3.3.2 + * Bootstrap: button.js v3.3.7 * http://getbootstrap.com/javascript/#buttons * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ @@ -19,7 +19,7 @@ this.isLoading = false } - Button.VERSION = '3.3.2' + Button.VERSION = '3.3.7' Button.DEFAULTS = { loadingText: 'loading...' @@ -31,7 +31,7 @@ var val = $el.is('input') ? 'val' : 'html' var data = $el.data() - state = state + 'Text' + state += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) @@ -41,10 +41,10 @@ if (state == 'loadingText') { this.isLoading = true - $el.addClass(d).attr(d, d) + $el.addClass(d).attr(d, d).prop(d, true) } else if (this.isLoading) { this.isLoading = false - $el.removeClass(d).removeAttr(d) + $el.removeClass(d).removeAttr(d).prop(d, false) } }, this), 0) } @@ -56,15 +56,19 @@ if ($parent.length) { var $input = this.$element.find('input') if ($input.prop('type') == 'radio') { - if ($input.prop('checked') && this.$element.hasClass('active')) changed = false - else $parent.find('.active').removeClass('active') + if ($input.prop('checked')) changed = false + $parent.find('.active').removeClass('active') + this.$element.addClass('active') + } else if ($input.prop('type') == 'checkbox') { + if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false + this.$element.toggleClass('active') } - if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + $input.prop('checked', this.$element.hasClass('active')) + if (changed) $input.trigger('change') } else { this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + this.$element.toggleClass('active') } - - if (changed) this.$element.toggleClass('active') } @@ -104,10 +108,15 @@ $(document) .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + var $btn = $(e.target).closest('.btn') Plugin.call($btn, 'toggle') - e.preventDefault() + if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { + // Prevent double click on radios, and the double selections (so cancellation) on checkboxes + e.preventDefault() + // The target component still receive the focus + if ($btn.is('input,button')) $btn.trigger('focus') + else $btn.find('input:visible,button:visible').first().trigger('focus') + } }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) diff --git a/RIGS/static/js/carousel.js b/RIGS/static/js/carousel.js old mode 100644 new mode 100755 index 450e1812..6ff954c9 --- a/RIGS/static/js/carousel.js +++ b/RIGS/static/js/carousel.js @@ -1,8 +1,8 @@ /* ======================================================================== - * Bootstrap: carousel.js v3.3.2 + * Bootstrap: carousel.js v3.3.7 * http://getbootstrap.com/javascript/#carousel * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ @@ -17,10 +17,10 @@ this.$element = $(element) this.$indicators = this.$element.find('.carousel-indicators') this.options = options - this.paused = - this.sliding = - this.interval = - this.$active = + this.paused = null + this.sliding = null + this.interval = null + this.$active = null this.$items = null this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) @@ -30,7 +30,7 @@ .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) } - Carousel.VERSION = '3.3.2' + Carousel.VERSION = '3.3.7' Carousel.TRANSITION_DURATION = 600 diff --git a/RIGS/static/js/collapse.js b/RIGS/static/js/collapse.js old mode 100644 new mode 100755 index 2bc30e7b..12038693 --- a/RIGS/static/js/collapse.js +++ b/RIGS/static/js/collapse.js @@ -1,11 +1,12 @@ /* ======================================================================== - * Bootstrap: collapse.js v3.3.2 + * Bootstrap: collapse.js v3.3.7 * http://getbootstrap.com/javascript/#collapse * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +/* jshint latedef: false */ +function ($) { 'use strict'; @@ -16,7 +17,8 @@ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $(this.options.trigger).filter('[href="#' + element.id + '"], [data-target="#' + element.id + '"]') + this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + + '[data-toggle="collapse"][data-target="#' + element.id + '"]') this.transitioning = null if (this.options.parent) { @@ -28,13 +30,12 @@ if (this.options.toggle) this.toggle() } - Collapse.VERSION = '3.3.2' + Collapse.VERSION = '3.3.7' Collapse.TRANSITION_DURATION = 350 Collapse.DEFAULTS = { - toggle: true, - trigger: '[data-toggle="collapse"]' + toggle: true } Collapse.prototype.dimension = function () { @@ -172,7 +173,7 @@ var data = $this.data('bs.collapse') var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - if (!data && options.toggle && option == 'show') options.toggle = false + if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) @@ -203,7 +204,7 @@ var $target = getTargetFromTrigger($this) var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $.extend({}, $this.data(), { trigger: this }) + var option = data ? 'toggle' : $this.data() Plugin.call($target, option) }) diff --git a/RIGS/static/js/dropdown.js b/RIGS/static/js/dropdown.js old mode 100644 new mode 100755 index 200e1c67..04e9c2de --- a/RIGS/static/js/dropdown.js +++ b/RIGS/static/js/dropdown.js @@ -1,8 +1,8 @@ /* ======================================================================== - * Bootstrap: dropdown.js v3.3.2 + * Bootstrap: dropdown.js v3.3.7 * http://getbootstrap.com/javascript/#dropdowns * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ @@ -19,7 +19,41 @@ $(element).on('click.bs.dropdown', this.toggle) } - Dropdown.VERSION = '3.3.2' + Dropdown.VERSION = '3.3.7' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } Dropdown.prototype.toggle = function (e) { var $this = $(this) @@ -34,7 +68,10 @@ if (!isActive) { if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { // if mobile we use a backdrop because click events don't delegate - $('