From 15446a0a253195f0b830f169d591dedd41d6477c Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Sat, 27 Jan 2018 20:40:26 -0800 Subject: [PATCH 1/4] Peformance 3 and Dimensions 1. Converted _makeOption to plain JS for speed. This is the last specific jQuery-to-plain-JS conversion that I am planning. 2. Added addInputNames option to allow opting out (false value) of creating name properties for inputs. These names sometimes duplicate what is already in use for the native select and just cause more inputs to be serialized and posted to the server than is needed. (slower) 3. Added caching of button width value, menu width value, menu height value, and positioned status and mechanisms to invalidate the cached status for each. The caching minimizes the recalculation of each of these values when the menu is successively opened. (Just calculated on first open and re-used.) 4. Re-wrote _setMenuHeight: - a. Implement a new "size" keyword for the menuHeight option that sets the menu height based on the number of rows specified in the native select's "size" attribute. If "size" is specified, but no attribute is found on the native select, a default size value of 4 rows is used like is the default for native multiple selects. - b. Also, _setMenuHeight is optimized to execute the least loop iterations to determine the appropriate menu height to use. - c. Lastly, the menu height is limited to the available window.innerHeight value. 5. Changed from using self.element.parent().outerWidth() to using self.element.parent().innerWidth() in _getMinWidth() to fix errors in % width calculations. 6. Tweaked the css file to remove unnecessary tag qualifications that degrade layout efficiency. 7. Tweaked the css file to replace descendent selectors with child selectors where possible for improved layout efficiency. --- CHANGELOG.MD | 13 + README.markdown => README.MD | 0 css/jquery.multiselect.css | 18 +- src/jquery.multiselect.js | 679 ++++++++++++++++++++--------------- 4 files changed, 407 insertions(+), 303 deletions(-) create mode 100644 CHANGELOG.MD rename README.markdown => README.MD (100%) diff --git a/CHANGELOG.MD b/CHANGELOG.MD new file mode 100644 index 0000000..e951626 --- /dev/null +++ b/CHANGELOG.MD @@ -0,0 +1,13 @@ +Summary of Code Changes for jQuery UI Multiselect Widget, Version 3.0.0: + +**Enhancements** +1. + +**Feature & Functional Changes** +1. + +**Feature & Functional Deletions** +1. + +**Bug Fixes** +1. diff --git a/README.markdown b/README.MD similarity index 100% rename from README.markdown rename to README.MD diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css index 41de11e..65ad3c4 100644 --- a/css/jquery.multiselect.css +++ b/css/jquery.multiselect.css @@ -1,25 +1,29 @@ +/* References: +* - https://css-tricks.com/efficiently-rendering-css/ +* - https://csswizardry.com/2011/09/writing-efficient-css-selectors/ +*/ .ui-multiselect { padding:2px 0 2px 4px; text-align:left } -.ui-multiselect span.ui-icon { float:right } +.ui-multiselect .ui-icon { float:right } .ui-multiselect-single .ui-multiselect-checkboxes input { left:-9999px; position:absolute !important; top: auto !important; } .ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important } .ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px; } -.ui-multiselect-header ul { font-size:0.9em } -.ui-multiselect-header ul li { float:left; padding:0 10px 0 0; } +.ui-multiselect-header > ul { font-size:0.9em } +.ui-multiselect-header > ul > li { float:left; padding:0 10px 0 0; } .ui-multiselect-header a { text-decoration:none; } .ui-multiselect-header a:hover { text-decoration:underline; cursor: pointer;} -.ui-multiselect-header span.ui-icon { float:left; } +.ui-multiselect-header .ui-icon { float:left; } .ui-multiselect-header .ui-multiselect-close { float:right; padding-right:0; text-align:right; } .ui-multiselect-menu { display:none; padding:3px; position:absolute; text-align: left; } .ui-multiselect-checkboxes { overflow-y:auto; position:relative; } .ui-multiselect-checkboxes label { border:1px solid transparent; cursor:default; display:block; padding:3px 1px; } -.ui-multiselect-checkboxes label input { position:relative; top:1px } +.ui-multiselect-checkboxes label > input { position:relative; top:1px } .ui-multiselect-checkboxes label img { height: 30px; vertical-align: middle; padding-right: 3px;} .ui-multiselect-checkboxes li { clear:both; font-size:0.9em; list-style: none; padding-right:3px; } -.ui-multiselect-checkboxes .ui-multiselect-optgroup { padding: 3px; } +.ui-multiselect-checkboxes > .ui-multiselect-optgroup { padding: 3px; } .ui-multiselect-columns { display: inline-block; vertical-align: top; } -.ui-multiselect-checkboxes .ui-multiselect-optgroup a { border-bottom:1px solid; cursor: pointer; display:block; font-weight:bold; margin:1px 0; padding:3px; text-align:center; text-decoration:none; } +.ui-multiselect-checkboxes > .ui-multiselect-optgroup > a { border-bottom:1px solid; cursor: pointer; display:block; font-weight:bold; margin:1px 0; padding:3px; text-align:center; text-decoration:none; } @media print{ .ui-multiselect-menu {display: none;} diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index ca006d4..7f69824 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -1,6 +1,6 @@ /* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ /* - * jQuery MultiSelect UI Widget 3.0.0 + * jQuery UI MultiSelect Widget 3.0.0 * Copyright (c) 2012 Eric Hynds * * Depends: @@ -28,12 +28,16 @@ * - https://blog.kevin-brown.com/select2/2014/12/15/jquery-js-performance.html * - https://jsperf.com/append-array-of-jquery-elements * - https://gist.github.com/adrienne/5341713 + * - https://www.sitepoint.com/10-ways-minimize-reflows-improve-performance/ + * - https://gist.github.com/paulirish/5d52fb081b3570c81e3a (List of reflow triggers.) + * - http://hueypetersen.com/posts/2012/08/23/having-fun-with-reflows-and-infinityjs/ (jQuery outerHeight() causes reflows.) + * - http://youmightnotneedjquery.com/ + * - https://plainjs.com/ * */ (function($, undefined) { - // Counter used to prevent collisions - var multiselectID = 0; - var $doc = $(document); + // Counter used to prevent collisions + var multiselectID = 0; var defaultIcons = { 'open': '' }; - $.widget("ech.multiselect", { - - // default options - options: { - header: true, // (true | false) If true, the header is shown. - height: 175, // (int) Sets the height of the menu. - minWidth: 225, // (int) Sets the minimum width of the menu. - classes: '', // Classes that you can provide to be applied to the elements making up the widget. - iconSet: null, // (plain object | null) Supply an object of icons to use alternative icon sets, or null for default set. Reference defaultIcons above for object structure. - checkAllText: 'Check all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. - uncheckAllText: 'Uncheck all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. - flipAllText: null, //'Flip all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. - noneSelectedText: 'Select options', // (str) The text to show in the button where nothing is selected. - selectedText: '# of # selected', // (str) A "template" that indicates how to show the count of selections in the button. The "#'s" are replaced by the selection count & option count. - selectedList: 0, // (int) The actual list selections will be shown in the button when the count of selections is <= than this number. - selectedMax: null, // (int | function) If selected count > selectedMax or if function returns 1, then message is displayed, and new selection is undone. - show: null, // (array) An array containing menu opening effects. - hide: null, // (array) An array containing menu closing effects. - autoOpen: false, // (true | false) If true, then the menu will be opening immediately after initialization. - position: {}, // (object) A jQuery UI position object that constrains how the pop-up menu is positioned. - appendTo: null, // (jQuery | DOM element | selector str) If provided, this specifies what element to append the widget to in the DOM. - menuWidth:null, // (int | null) If a number is provided, sets the menu width. - selectedListSeparator: ', ', // (str) This allows customization of the list separator. Use ',
' to make the button grow vertically showing 1 selection per line. - htmlButtonText: false, // (true | false) If true, then the text used for the button's label is treated as html rather than plain text. - htmlOptionText: false, // (true | false) If true, then the text for option label is treated as html rather than plain text. - disableInputsOnToggle: true, // (true | false) - groupColumns: false // (true | false) + $.widget("ech.multiselect", { + + // default options + options: { + header: true, // (true | false) If true, the header is shown. + height: 175, // (int | 'size') Sets the height of the menu in pixels. If 'size' is instead specified, the native select's size attribute is instead for height. + minWidth: 225, // (int) Sets the minimum width of the menu. + classes: '', // Classes that you can provide to be applied to the elements making up the widget. + iconSet: null, // (plain object | null) Supply an object of icons to use alternative icon sets, or null for default set. Reference defaultIcons above for object structure. + checkAllText: 'Check all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. + uncheckAllText: 'Uncheck all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. + flipAllText: null, //'Flip all', // (str | blank | null) If blank, only icon shown. If null, no icon, text or link is shown. + noneSelectedText: 'Select options', // (str) The text to show in the button where nothing is selected. + selectedText: '# of # selected', // (str) A "template" that indicates how to show the count of selections in the button. The "#'s" are replaced by the selection count & option count. + selectedList: 0, // (int) The actual list selections will be shown in the button when the count of selections is <= than this number. + selectedMax: null, // (int | function) If selected count > selectedMax or if function returns 1, then message is displayed, and new selection is undone. + show: null, // (array) An array containing menu opening effects. + hide: null, // (array) An array containing menu closing effects. + autoOpen: false, // (true | false) If true, then the menu will be opening immediately after initialization. + position: {}, // (object) A jQuery UI position object that constrains how the pop-up menu is positioned. + appendTo: null, // (jQuery | DOM element | selector str) If provided, this specifies what element to append the widget to in the DOM. + menuWidth: null, // (int | null) If a number is provided, sets the exact menu width. + selectedListSeparator: ', ', // (str) This allows customization of the list separator. Use ',
' to make the button grow vertically showing 1 selection per line. + htmlButtonText: false, // (true | false) If true, then the text used for the button's label is treated as html rather than plain text. + htmlOptionText: false, // (true | false) If true, then the text for option label is treated as html rather than plain text. + addInputNames: true, // (true | false) If true, names are created for each option input in the multi-select. + disableInputsOnToggle: true, // (true | false) + groupColumns: false // (true | false) }, - // This method determines which element to append the menu to - // Uses the element provided in the options first, then looks for ui-front / dialog - // Otherwise appends to the body + /* This method determines which DOM element to append the menu to. Determination process: + * 1. Look up the jQuery object, DOM element, or string selector provided in the options. + * 2. If nothing provided in options or lookup in #1 failed, then look for .ui-front or dialog. (dialog case) + * 3. If still do not have a valid DOM element to append to, then append to the document body. + * + * NOTE: this.element and this.document are jQuery objects per the jQuery UI widget API. + */ _getAppendEl: function() { - var elem = this.options.appendTo; // jQuery object, DOM element, OR selector str. - if(elem) { - elem = elem.jquery || elem.nodeType ? $(elem) : this.document.find(elem).eq(0); // Note that the find handles the selector case. + var elem = this.options.appendTo; // jQuery object or selector, DOM element or null. + + if (elem) { // NOTE: The find below handles the jQuery selector case + elem = !!elem.jquery ? elem : ( !!elem.nodeType ? $(elem) : this.document.find(elem).eq(0) ); } if(!elem || !elem[0]) { - elem = this.element.closest(".ui-front, dialog"); // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/ + elem = this.element.closest(".ui-front, dialog"); } if(!elem.length) { - elem = this.document[0].body; // Position at end of body. + elem = this.document[0].body; // Position at end of body. Note that this returns a DOM element. } return elem; }, - // Performs the initial creation of the widget - _create: function() { - var $element = this.element.hide(); // element property is a jQuery object - var elSelect = $element.get(0); // This would be expected to be the underlying native select element. + // Performs the initial creation of the widget + _create: function() { + var $element = this.element.hide(); // element property is a jQuery object per http://api.jqueryui.com/jQuery.widget/ + var elSelect = $element.get(0); // This would be expected to be the underlying native select element. var options = this.options; var classes = options.classes; var headerOn = options.header; - var iconSet = $.extend({}, defaultIcons, options.iconSet || {}); // Do an extend here to handle icons missing from options.iconSet + var iconSet = $.extend({}, defaultIcons, options.iconSet || {}); // Do an extend here to address icons missing from options.iconSet--missing icons default to those in msIcons. var checkAllText = options.checkAllText; var uncheckAllText = options.uncheckAllText; var flipAllText = options.flipAllText; @@ -113,66 +123,67 @@ // The button that opens the widget menu. Note that this is inserted later below. var $button = (this.$button = $( document.createElement('button') ) ) - .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all' + (classes ? ' ' + classes : '')) - .attr({ - 'type': 'button', - 'title': elSelect.title, - 'tabIndex': elSelect.tabIndex, - 'id': elSelect.id ? elSelect.id + '_ms' : null - }) - .prop('aria-haspopup', true) - .html('' + iconSet.open + ''); // Necessary to simplify dynamically changing the open icon. - - this.$buttonlabel = $( document.createElement('span') ) + .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all' + (classes ? ' ' + classes : '')) + .attr({ 'type': 'button', 'title': elSelect.title, 'tabIndex': elSelect.tabIndex, 'id': elSelect.id ? elSelect.id + '_ms' : null }) + .prop('aria-haspopup', true) + .html('' + iconSet.open + ''); // Necessary to simplify dynamically changing the open icon. + + this.$buttonlabel = $( document.createElement('span') ) .html(options.noneSelectedText) .appendTo( $button ); - // Header controls, will contain the check all/uncheck all buttons - // Depending on how the options are set, this may be empty or simply plain text - var headerLinksHTML = ( headerOn === true + // Header controls, will contain the check all/uncheck all buttons + // Depending on how the options are set, this may be empty or simply plain text + var headerLinksHTML = ( headerOn === true ? (checkAllText === null ? '' : '
  • ' + iconSet.checkAll + (checkAllText ? '' + checkAllText + '' : '' ) + '
  • ') + (uncheckAllText === null ? '' : '
  • ' + iconSet.uncheckAll + (uncheckAllText ? '' + uncheckAllText + '' : '' ) + '
  • ') + (flipAllText === null ? '' : '
  • ' + iconSet.flipAll + '' + (flipAllText ? '' + flipAllText + '' : '' ) + '
  • ') : (typeof headerOn === 'string' ? '
  • ' + headerOn + '
  • ' : '') ); - this.$headerLinkContainer = $( document.createElement('ul') ) + this.$headerLinkContainer = $( document.createElement('ul') ) .addClass('ui-helper-reset') - .html(headerLinksHTML + '
  • ' + iconSet.close + '
  • '); + .html(headerLinksHTML + + '
  • ' + + iconSet.close + + '
  • '); - // Menu header to hold controls for the menu - var $header = (this.$header = $( document.createElement('div') ) ) + // Menu header to hold controls for the menu + var $header = (this.$header = $( document.createElement('div') ) ) .addClass('ui-multiselect-header ui-widget-header ui-corner-all ui-helper-clearfix') .append( this.$headerLinkContainer ); - // Holds the actual check boxes for inputs - var $checkboxContainer = (this.$checkboxContainer = $( document.createElement('ul') ) ) + // Holds the actual check boxes for inputs + var $checkboxContainer = (this.$checkboxContainer = $( document.createElement('ul') ) ) .addClass('ui-multiselect-checkboxes ui-helper-reset'); - // This is the menu that will hold all the options. If this is a single select widget, add the appropriate class. Note that this is inserted below. - var $menu = (this.$menu = $( document.createElement('div') ) ) + // This is the menu that will hold all the options. + // If this is a single select widget, add the appropriate class. + // Note that this is inserted below. + var $menu = (this.$menu = $( document.createElement('div') ) ) .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all ' + (elSelect.multiple ? '' : 'ui-multiselect-single ') + classes) .append($header, $checkboxContainer); - // We wait until everything is built before we insert in the DOM to limit browser re-flowing (an optimization). - $button.insertAfter($element); - $menu.appendTo( this._getAppendEl() ); // This is an empty menu at this point. + // We wait until everything is built before we insert in the DOM to limit browser re-flowing (an optimization). + $button.insertAfter($element); + $menu.appendTo( this._getAppendEl() ); // This is an empty menu at this point. - // perform event bindings - this._bindEvents(); + // perform event bindings + this._bindEvents(); - // build menu - this.refresh(true); - }, + // build menu + this.refresh(true); + }, // https://api.jqueryui.com/jquery.widget/#method-_init _init: function() { - var elSelect = this.element.get(0); // element is a jQuery object + var elSelect = this.element.get(0); // element is a jQuery object per widget API if (this.options.header) - this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') - .toggle( !!elSelect.multiple ); + this.$headerLinkContainer + .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') + .toggle( !!elSelect.multiple ); else this.$header.hide(); @@ -184,7 +195,7 @@ }, /* - * Builds an option item for the menu + * Builds an option item for the menu. (Mostly plain JS for speed.) *
  • * *
  • */ - _makeOption: function(option) { + _makeOption: function(option) { + var self = this; var title = option.title || null; - var elSelect = this.element.get(0); // element is a jQuery object - var id = elSelect.id || this.multiselectID; // unique ID for the label & option tags - var inputID = 'ui-multiselect-' + this.multiselectID + '-' + (option.id || id + '-option-' + this.inputIdCounter++); - var isMultiple = elSelect.multiple; // Pick up the select type from the underlying element + var elSelect = self.element.get(0); // element is a jQuery object per widget API + var id = elSelect.id || self.multiselectID; // unique ID for the label & option tags + var inputID = 'ui-multiselect-' + self.multiselectID + '-' + (option.id || id + '-option-' + self.inputIdCounter++); + var isMultiple = elSelect.multiple; // Pick up the select type from the underlying element var isDisabled = option.disabled; var isSelected = option.selected; - var $input = $( document.createElement('input') ) - .attr({ - "name": "multiselect_" + id, - "type": isMultiple ? 'checkbox' : 'radio', - "value": option.value, - "title": title, - "id": inputID, - "checked": isSelected ? "checked" : null, - "aria-selected": isSelected ? "true" : null, - "disabled": isDisabled ? "disabled" : null, - "aria-disabled": isDisabled ? "true" : null - }) - .data(option.dataset); - - var $span = this.options.htmlOptionText - ? $( document.createElement('span') ).html( option.innerHTML ) - : $( document.createElement('span') ).text( option.textContent ); - var optionImageSrc = option.getAttribute('data-image-src'); - if (optionImageSrc) - $span.prepend( $( document.createElement('img') ).attr('src', optionImageSrc) ); - - var $label = $( document.createElement('label') ) - .attr({ 'for': inputID, 'title': title}) - .addClass( (isDisabled ? 'ui-state-disabled ' : '') - + (isSelected && !isMultiple ? 'ui-state-active ' : '') - + 'ui-corner-all') - .append($input, $span); - - var $item = $( document.createElement('li') ) - .addClass( (isDisabled ? 'ui-multiselect-disabled ' : '') + (option.className || '') ) - .append($label); - - return $item; + var input = document.createElement('input'); + var inputAttribs = { + "type": isMultiple ? 'checkbox' : 'radio', + "id": inputID, + "title": title, + "value": option.value, + "name": self.options.addInputNames ? "multiselect_" + id : null, + "checked": isSelected ? "checked" : null, + "aria-selected": isSelected ? "true" : null, + "disabled": isDisabled ? "disabled" : null, + "aria-disabled": isDisabled ? "true" : null + }; + for (var name in inputAttribs) { + if (inputAttribs[name] !== null) + input.setAttribute(name,inputAttribs[name]); + } + if ('dataset' in option) { + for (var key in option.dataset) { // Clone data attributes + input.dataset[key] = option.dataset[key]; + } + } + + var span = document.createElement('span'); + if (self.options.htmlOptionText) + span.innerHTML = option.innerHTML; + else + span.textContent = option.textContent; + var optionImageSrc = option.getAttribute('data-image-src'); // Icon images for each item. + if (optionImageSrc) { + var img = document.createElement('img'); + img.setAttribute('src', optionImageSrc); + span.insertBefore(img, span.firstChild); + } + + var label = document.createElement('label'); + label.setAttribute('for', inputID); + if (title !== null) + label.setAttribute('title', title); + label.className += (isDisabled ? ' ui-state-disabled' : '') + + (isSelected && !isMultiple ? ' ui-state-active' : '') + + ' ui-corner-all'; + label.appendChild(input); + label.appendChild(span); + + var item = document.createElement('li'); + item.className += (isDisabled ? ' ui-multiselect-disabled' : '') + + (' ' + option.className || '') + + ' ui-multiselect-nowrap'; + item.appendChild(label); + + return item; }, // Builds a menu item for each option in the underlying select // Option groups are built here as well _buildOptionList: function($element, $checkboxContainer) { - var self = this; // Save this => widget reference + var self = this; var list = []; this.inputIdCounter = 0; $element.children().each( function() { - var elem = this; + var elem = this; - if (elem.tagName === 'OPTGROUP') { - var options = []; + if (elem.tagName === 'OPTGROUP') { + var options = []; - $(elem).children().each( function() { - options.push(self._makeOption(this)); - }); - - // Build the list section for this optgroup, complete w/ option inputs. - var $optionGroup = $( document.createElement('ul') ) + $(elem).children().each( function() { + options.push(self._makeOption(this)); + }); + + // Build the list section for this optgroup, complete w/ option inputs... + var $optionGroup = $( document.createElement('ul') ) .addClass('ui-multiselect-optgroup' + (self.options.groupColumns ? ' ui-multiselect-columns' : '') + (elem.className && ' ') + elem.className) .append( $( document.createElement('a') ).text( elem.getAttribute('label') ), options); - list.push($optionGroup); - } - else { - list.push(self._makeOption(elem)); - } + list.push($optionGroup); + } + else { + list.push(self._makeOption(elem)); + } }); $checkboxContainer.empty().append(list); - }, + }, // Refreshes the widget to pick up changes to the underlying select // Rebuilds the menu, sets button width refresh: function(init) { - var $element = this.element; // "element" is a jQuery object + var $element = this.element; // "element" is a jQuery object representing the underlying select // update header link container visibility if needed if (this.options.header) - this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') - .toggle( !!$element[0].multiple ); + this.$headerLinkContainer + .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') + .toggle( !!$element[0].multiple ); - this._buildOptionList($element, this.$checkboxContainer); - this._updateCache(); + this._buildOptionList($element, this.$checkboxContainer); // Clear and rebuild the menu. + this._updateCache(); // cache some more useful elements this._setButtonWidth(); this.update(true); @@ -292,18 +323,26 @@ this._trigger('refresh'); }, - // cache some more useful elements + // Cache key data for good performance. _updateCache: function() { + // Invalidate cached dimensions and positioning state. + this._savedButtonWidth = 0; + this._savedMenuWidth = 0; + this._ulHeight = 0; + this._positioned = false; + + // Update saved labels and inputs this.$labels = this.$menu.find('label'); this.$inputs = this.$labels.children('input'); }, // Update the button text. Call refresh() to rebuild the menu update: function(isDefault) { - var options = this.options; + var self = this; + var options = self.options; var selectedList = options.selectedList; var selectedText = options.selectedText; - var $inputs = this.$inputs; + var $inputs = self.$inputs; var inputCount = $inputs.length; var $checked = $inputs.filter(':checked'); var numChecked = $checked.length; @@ -311,7 +350,7 @@ if (numChecked) { if (typeof selectedText === 'function') - value = selectedText.call(this, numChecked, inputCount, $checked.get()); + value = selectedText.call(self, numChecked, inputCount, $checked.get()); else if(/\d/.test(selectedList) && selectedList > 0 && numChecked <= selectedList) value = $checked.map(function() { return $(this).next().text() }).get().join(options.selectedListSeparator); else @@ -320,11 +359,11 @@ else value = options.noneSelectedText; - this._setButtonValue(value, isDefault); + self._setButtonValue(value, isDefault); // Check if the menu needs to be repositioned due to button height changing from adding/removing selections. - if (this._isOpen && this._savedButtonHeight != this.$button.outerHeight(false)) - this.position(); + if (self._isOpen && self._savedButtonHeight != self.$button.outerHeight(false)) + self._position(true); }, // this exists as a separate method so that the developer @@ -344,7 +383,7 @@ return false; } - $button // button events + $button // button events .on({ click: clickHandler, keypress: function(e) { @@ -377,8 +416,8 @@ $button.removeClass('ui-state-focus'); } }) - .find('span') // webkit doesn't like it when you click on the span :( - .on('click.multiselect', clickHandler); + .find('span') // webkit doesn't like it when you click on the span :( + .on('click.multiselect,click', clickHandler); }, _bindMenuEvents: function() { @@ -456,26 +495,28 @@ } }) .on('click.multiselect', 'input[type="checkbox"], input[type="radio"]', function(e) { - var $this = $(this); - var val = this.value; - var checked = this.checked; - var $element = self.element; - var options = self.options; - var $inputs = self.$inputs; - var optionText = $this.parent().find("span")[options.htmlOptionText ? 'html' : 'text'](); + var input = this; // Reference to this checkbox / radio input + var $input = $(input); + var val = input.value; + var checked = input.checked; + var $element = self.element; // self is set above in _bindMenuEvents var $tags = $element.find('option'); var isMultiple = $element[0].multiple; - var inputCount = $inputs.length; - var numChecked = $inputs.filter(":checked").length; + var $allInputs = self.$inputs; + var inputCount = $allInputs.length; + var numChecked = $allInputs.filter(":checked").length; + var options = self.options; + var optionText = $input.parent().find("span")[options.htmlOptionText ? 'html' : 'text'](); var selectedMax = options.selectedMax; // bail if this input is disabled or the event is cancelled - if(this.disabled || self._trigger('click', e, { value: val, text: optionText, checked: checked }) === false) { + if(input.disabled || self._trigger('click', e, { value: val, text: optionText, checked: checked }) === false) { e.preventDefault(); return; } - if ( selectedMax && checked && ( typeof selectedMax === 'function' ? !!selectedMax.call(this, $inputs) : numChecked > selectedMax ) ) { + if ( selectedMax && checked + && ( typeof selectedMax === 'function' ? !!selectedMax.call(input, $allInputs) : numChecked > selectedMax ) ) { var saveText = options.selectedText; // The following warning is shown in the button and then cleared after a second. @@ -486,17 +527,17 @@ self.update(); }, 1000 ); - this.checked = false; // Kill the event. + input.checked = false; // Kill the event. e.preventDefault(); return false; } // make sure the input has focus. otherwise, the esc key // won't close the menu after clicking an item. - $this.focus(); + input.focus(); // toggle aria state - $this.prop('aria-selected', checked); + $input.prop('aria-selected', checked); // change state on the original option tags $tags.each( function() { @@ -506,7 +547,7 @@ // some additional single select-specific logic if(!isMultiple) { self.$labels.removeClass('ui-state-active'); - $this.closest('label').toggleClass('ui-state-active', checked); + $input.closest('label').toggleClass('ui-state-active', checked); // close menu self.close(); @@ -523,15 +564,15 @@ _bindHeaderEvents: function() { var self = this; + // header links - this.$header + self.$header .on('click.multiselect', 'a', function(e) { - var $this = $(this); - var headerLinks = { - 'ui-multiselect-close' : 'close', - 'ui-multiselect-all' : 'checkAll', - 'ui-multiselect-none' : 'uncheckAll', - 'ui-multiselect-flip' : 'flipAll' + var $this = $(this); // Reference to this anchor element + var headerLinks = {'ui-multiselect-close' : 'close', + 'ui-multiselect-all' : 'checkAll', + 'ui-multiselect-none' : 'uncheckAll', + 'ui-multiselect-flip' : 'flipAll' }; for (hdgClass in headerLinks) { if ( $this.hasClass(hdgClass) ) { @@ -548,7 +589,10 @@ break; case 9: var $target = $(e.target); - if((e.shiftKey && !$target.parent().prev().length && !self.$header.find(".ui-multiselect-filter").length) || (!$target.parent().next().length && !self.$labels.length && !e.shiftKey)) { + if((e.shiftKey + && !$target.parent().prev().length + && !self.$header.find(".ui-multiselect-filter").length) + || (!$target.parent().next().length && !self.$labels.length && !e.shiftKey)) { self.close(); e.preventDefault(); } @@ -561,12 +605,12 @@ _bindEvents: function() { var self = this; - this._bindButtonEvents(); - this._bindMenuEvents(); - this._bindHeaderEvents(); + self._bindButtonEvents(); + self._bindMenuEvents(); + self._bindHeaderEvents(); // close each widget when clicking on any other element/anywhere else on the page - $doc.on('mousedown.' + self._namespaceID, function(event) { + self.document.on('mousedown.' + self._namespaceID, function(event) { // self.document is a jQuery object per widget API var target = event.target; var button = self.$button.get(0); var menu = self.$menu.get(0); @@ -579,7 +623,7 @@ // restored to their defaultValue prop on form reset, and the reset // handler fires before the form is actually reset. delaying it a bit // gives the form inputs time to clear. - $(this.element[0].form).on('reset.' + this._namespaceID, function() { + $(self.element[0].form).on('reset.' + self._namespaceID, function() { setTimeout($.proxy(self.refresh, self), 10); }); }, @@ -587,63 +631,108 @@ // Determines the minimum width for the button and menu // Can be a number, a digit string, or a percentage _getMinWidth: function() { - var minVal = this.options.minWidth; + var self = this; + var minWidth = self.options.minWidth; var width = 0; - switch (typeof minVal) { + + switch (typeof minWidth) { case 'number': - width = minVal; + width = minWidth; break; case 'string': - var lastChar = minVal[ minVal.length -1 ]; - width = minVal.match(/\d+/); - if(lastChar === '%') { - width = this.element.parent().outerWidth() * (width/100); // element is a jQuery object - } else { - width = parseInt(minVal, 10); - } - break; + if (minWidth === 'auto') { + $menu = self.$menu; + var appendEl = self._getAppendEl(); + var cssVisibility = $menu.css('visibility'); + var cssWidth = $menu.css('width'); + var cssDisplay = $menu.css('display'); + var cssulDisplay = $menu.find('ul:eq(0)').css('display'); + $menu.appendTo('body').css({visibility:'hidden', width:'auto', display:'inline'}).find('ul').css('display', 'inline'); + var autoWidth = $menu.width(); + $menu.css({visibility:cssVisibility, width:cssWidth, display:cssDisplay}).find('ul').css('display', cssulDisplay).appendTo( appendEl ); + console.log('AUTO WIDTH: ', cssWidth, autoWidth); + return autoWidth; + } + width = parseInt(minWidth, 10); + + if ( minWidth.slice(-1) === '%' ) + width = self.element.parent().innerWidth() * (width/100); // element is a jQuery object. } + return width; }, - // set button width - _setButtonWidth: function() { - var width = this.element.outerWidth(); // element is a jQuery object - var minVal = this._getMinWidth(); + // set button width. jQuery seems to be required here, as getting the outerWidth by the non-jQuery way is unreliable. + _setButtonWidth: function(recalc) { + var self = this; + if (self._savedButtonWidth && !recalc) + return; + + self._positioned = false; // Force menu positioning to be adjusted on next open. + var width = self.element.outerWidth(); // element is a jQuery object + var minWidth = self._getMinWidth(); - if(width < minVal) { - width = minVal; - } // set widths - this.$button.outerWidth(width); + self._savedButtonWidth = width < minWidth ? minWidth : width; // The button width is cached. + self.$button.outerWidth(self._savedButtonWidth); }, // set menu width - _setMenuWidth: function() { - var width = (this.$button.outerWidth() <= 0) ? this._getMinWidth() : this.$button.outerWidth(); - this.$menu.outerWidth(this.options.menuWidth || width); + _setMenuWidth: function(recalc) { + var self = this; + if (self._savedMenuWidth && !recalc) + return; + + self._positioned = false; // Force menu positioning to be adjusted on next open. + var width = self.options.menuWidth; + + if (!width) { // Exact width not provided; determine appropriate width. + width = self._savedButtonWidth || self.$button.outerWidth(); + if (width <= 0) + width = self._getMinWidth(); + } + + self._savedMenuWidth = width; // The menu width is cached. + self.$menu.outerWidth(width); }, // Sets the height of the menu // Will set a scroll bar if the menu height exceeds that of the height in options - _setMenuHeight: function() { - var $menu = this.$menu; + // Generally, we cache the height value and do not set the height again unless we have to. + _setMenuHeight: function(recalc) { + var self = this; + if (self._ulHeight && !recalc) // If height is cached and no forced reset, then exit. + return; + + self._positioned = false; // Force menu positioning to be adjusted on next open. + var $menu = self.$menu; var headerHeight = $menu.children('.ui-multiselect-header').filter(':visible').outerHeight(true); + var $checkboxes = $menu.children(".ui-multiselect-checkboxes"); + var elSelectSize = self.element[0].size || 4; // Retrieves native select's size attribute or defaults to 4 (like native select). + var optionHeight = self.options.height; + var useSelectSize = (optionHeight === 'size'); // Determine overall height based on native select 'size' attribute? + var availableHeight = window.innerHeight - headerHeight; // The maximum available height for the $checkboxes. + var maxHeight = (useSelectSize || optionHeight > availableHeight ? availableHeight : optionHeight); + var overflowSetting = 'hidden'; + var itemCount = 0; var ulHeight = 0; - $menu.find('.ui-multiselect-checkboxes li, .ui-multiselect-checkboxes a').each( function() { + // The following adds up item heights. If the height sum exceeds the option height or if the number + // of item heights summed equal or exceed the native select size attribute, the loop is aborted. + // If the loop is aborted, this means that the menu must be scrolled to see all the items. + $checkboxes.find('li,a').each( function() { ulHeight += $(this).outerHeight(true); + if (useSelectSize && ++itemCount >= elSelectSize || ulHeight > maxHeight) { + overflowSetting = 'auto'; + if (!useSelectSize) + ulHeight = maxHeight; + return false; + } }); - if(ulHeight > this.options.height) { - $menu.children(".ui-multiselect-checkboxes").css("overflow", "auto"); - ulHeight = this.options.height; - } - else - $menu.children(".ui-multiselect-checkboxes").css("overflow", "hidden"); - - $menu.children(".ui-multiselect-checkboxes").height(ulHeight); - $menu.height(ulHeight + headerHeight); + $checkboxes.css("overflow", overflowSetting).height(ulHeight); + $menu.height(headerHeight + ulHeight); + self._ulHeight = ulHeight; // Cache the height. }, // Resizes the menu, called every time the menu is opened @@ -672,7 +761,6 @@ // set scroll position $container.scrollTop(moveToLast ? $container.height() : 0); - } else $next.find('label').filter(':visible')[ moveToLast ? "last" : "first" ]().trigger('mouseover'); @@ -699,11 +787,10 @@ }, // Toggles checked state on either an option group or all inputs - _toggleChecked: function(flag, group) { + _toggleChecked: function(flag, group, filteredInputs) { var self = this; var $element = this.element; // element is a jQuery object - var $inputs = (group && group.length) ? group : this.$inputs; - var inputCount = $inputs.length; + var $inputs = (group && group.length) ? group : (filteredInputs || this.$inputs); // toggle state on inputs $inputs.each(this._toggleState('checked', flag)); @@ -716,9 +803,9 @@ // Create a plain object of the values that actually changed var values = {}; - for (var x = 0; x < inputCount; x++) { - values[ $inputs.get(x).value ] = true; - }; + $inputs.each( function() { + values[ this.value ] = true; + }); // toggle state on original option tags $element[0].selectedIndex = -1; @@ -730,7 +817,7 @@ }); // trigger the change event on the select - if (inputCount) + if ($inputs.length) $element.trigger("change"); }, @@ -766,7 +853,7 @@ } } - this.element.prop({ // element is a jQuery object + this.element.prop({ // element is a jQuery object 'disabled':flag, 'aria-disabled':flag }); @@ -775,72 +862,72 @@ // open the menu open: function(e) { var self = this; - var $button = this.$button; - var $menu = this.$menu; - var $header = this.$header; - var $labels = this.$labels; - var speed = this.speed; - var options = this.options; - var effect = options.show; - var $container = $menu.find('.ui-multiselect-checkboxes'); + var $button = self.$button; - // bail if the multiselectopen event returns false, this widget is disabled, or is already open - if(this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen) + // bail if the multiselect open event returns false, this widget is disabled, or is already open + if(self._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || self._isOpen) return; + var $menu = self.$menu; + var $header = self.$header; + var $labels = self.$labels; + var speed = self.speed; + var options = self.options; + var effect = options.show; + // figure out opening effects/speeds - if ($.isArray(options.show)) { + if (options.show && options.show.constructor == Array) { effect = options.show[0]; speed = options.show[1] || self.speed; } // set the scroll of the checkbox container - $container.scrollTop(0); + $menu.find('.ui-multiselect-checkboxes').scrollTop(0); // show the menu, maybe with a speed/effect combo // if there's an effect, assume jQuery UI is in use $.fn.show.apply($menu, effect ? [ effect, speed ] : []); - this._resizeMenu(); - // positon - this.position(); + self._resizeMenu(); + self._position(); // select the first not disabled option or the filter input if available var filter = $header.find(".ui-multiselect-filter"); if (filter.length) filter.first().find('input').trigger('focus'); - else if ($labels.length) { - $labels.filter(':not(.ui-state-disabled)').eq(0) - .trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); - } + else if ($labels.length) + $labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); else $header.find('a').first().trigger('focus'); $button.addClass('ui-state-active'); - this._isOpen = true; - this._trigger('open'); + self._isOpen = true; + self._trigger('open'); }, // close the menu close: function() { - if (this._trigger('beforeclose') === false) + var self = this; + + // bail if the multiselect close event returns false + if (self._trigger('beforeclose') === false) return; - var options = this.options; + var options = self.options; var effect = options.hide; - var speed = this.speed; - var $button = this.$button; + var speed = self.speed; + var $button = self.$button; // figure out closing effects/speeds - if ($.isArray(options.hide)) { + if (options.hide && options.hide.constructor == Array) { effect = options.hide[0]; - speed = options.hide[1] || this.speed; + speed = options.hide[1] || self.speed; } - $.fn.hide.apply(this.$menu, effect ? [ effect, speed ] : []); + $.fn.hide.apply(self.$menu, effect ? [ effect, speed ] : []); $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); - this._isOpen = false; - this._trigger('close'); + self._isOpen = false; + self._trigger('close'); $button.trigger('focus'); }, @@ -859,7 +946,7 @@ uncheckAll: function() { this._toggleChecked(false); - if ( !this.element[0].multiple ) // element is a jQuery object + if ( !this.element[0].multiple ) // this.element is a jQuery object this.element[0].selectedIndex = -1; // Forces the underlying single-select to have no options selected. this._trigger('uncheckAll'); }, @@ -882,8 +969,8 @@ $.Widget.prototype.destroy.call(this); // unbind events - $doc.off(this._namespaceID); - $(this.element[0].form).off(this._namespaceID); // element is a jQuery object + this.document.off(this._namespaceID); // this.document is a jQuery object per widget API + $(this.element[0].form).off(this._namespaceID); // this.element is a jQuery object this.$button.remove(); this.$menu.remove(); @@ -919,8 +1006,9 @@ * groupLabel: Option Group to add the option to */ addOption: function(attributes, text, groupLabel) { - var $element = this.element; - var $menu = this.$menu; + var self = this; + var $element = self.element; + var $menu = self.$menu; var $option = $( document.createElement('option') ).attr(attributes).text(text); var optionNode = $option.get(0); @@ -930,14 +1018,14 @@ }).append($option); $menu.find(".ui-multiselect-optgroup").filter(function() { return $(this).find("a").text() === groupLabel; - }).append(this._makeOption(optionNode)); + }).append(self._makeOption(optionNode)); } else { $element.append($option); - $menu.find(".ui-multiselect-checkboxes").append(this._makeOption(optionNode)); + $menu.find(".ui-multiselect-checkboxes").append(self._makeOption(optionNode)); } - this._updateCache(); //update cached elements + self._updateCache(); // update cached elements }, removeOption: function(value) { @@ -946,41 +1034,43 @@ this.element.find("option[value=" + value + "]").remove(); this.$labels.find("input[value=" + value + "]").parents("li").remove(); - this._updateCache(); //update cached elements + this._updateCache(); // update cached elements }, - position: function() { - this._savedButtonHeight = this.$button.outerHeight(false); // Save this so that we can determine when the button height has changed due adding/removing selections. - var pos = { - my: "top", - at: "bottom", - of: this.$button - }; - if(!$.isEmptyObject(this.options.position)) { - pos.my = this.options.position.my || pos.my; - pos.at = this.options.position.at || pos.at; - pos.of = this.options.position.of || pos.of; - } + position: function(){ this._position.call(this, true) }, // Public function call always ignores cached status. + + _position: function(reposition) { + var self = this; + if (!!self._positioned && !reposition) + return; + + var $button = self.$button; + self._savedButtonHeight = self.$button.outerHeight(false); // Save this so that we can determine when the button height has changed due adding/removing selections. + + var pos = $.extend({'my': 'top', 'at': 'bottom', 'of': $button}, self.options.position || {}); + if($.ui && $.ui.position) - this.$menu.position(pos); + self.$menu.position(pos); else { - pos = this.$button.position(); - pos.top += this._savedButtonHeight; - this.$menu.offset(pos); + pos = $button.position(); + pos.top += self._savedButtonHeight; + self.$menu.offset(pos); } + self._positioned = true; }, // react to option changes after initialization _setOption: function(key, value) { - var $menu = this.$menu; + var self = this; + var $menu = self.$menu; switch(key) { case 'header': if (typeof value === 'boolean') - this.$header.toggle( value ); - else if(typeof value === 'string') { - this.$headerLinkContainer.children('li:not(:last-child)').remove(); - this.$headerLinkContainer.prepend('
  • ' + value + '
  • '); + self.$header.toggle( value ); + else if (typeof value === 'string') { + self.$headerLinkContainer.children('li:not(:last-child)').remove(); + self.$headerLinkContainer.prepend('
  • ' + value + '
  • '); } break; case 'checkAllText': @@ -1000,44 +1090,41 @@ $menu.find('a.ui-multiselect-close').html(value); break; case 'height': - this.options[key] = value; - this._setMenuHeight(); + self.options[key] = value; + self._setMenuHeight(true); // true forces recalc of cached value. break; case 'minWidth': case 'menuWidth': - this.options[key] = value; - this._setButtonWidth(); - this._setMenuWidth(); + self.options[key] = value; + self._setButtonWidth(true); // true forces recalc of cached value. + self._setMenuWidth(true); // true forces recalc of cached value. break; case 'selectedText': case 'selectedList': case 'selectedMax': case 'noneSelectedText': - this.options[key] = value; // these all need to update immediately for the update() call - this.update(); + case 'selectedListSeparator': + self.options[key] = value; // these all need to update immediately for the update() call + self.update(true); break; case 'classes': - $menu.add(this.$button).removeClass(this.options.classes).addClass(value); + $menu.add(self.$button).removeClass(self.options.classes).addClass(value); break; case 'multiple': - var $element = this.element; + var $element = self.element; if (!!$element[0].multiple !== value) { $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value); $element[0].multiple = value; - this.uncheckAll(); - this.refresh(); + self.uncheckAll(); + self.refresh(); } break; case 'position': - this.position(); - break; - case 'selectedListSeparator': - this.options[key] = value; - this.update(true); + self._position(true); // true ignores cached setting break; } - $.Widget.prototype._setOption.apply(this, arguments); - } + $.Widget.prototype._setOption.apply(self, arguments); + }, }); From 2b16ce36a8d372df78dff663b3c52bae00634581 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Sun, 28 Jan 2018 13:49:42 -0800 Subject: [PATCH 2/4] Remove references --- css/jquery.multiselect.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css index 65ad3c4..bf9daa9 100644 --- a/css/jquery.multiselect.css +++ b/css/jquery.multiselect.css @@ -1,7 +1,3 @@ -/* References: -* - https://css-tricks.com/efficiently-rendering-css/ -* - https://csswizardry.com/2011/09/writing-efficient-css-selectors/ -*/ .ui-multiselect { padding:2px 0 2px 4px; text-align:left } .ui-multiselect .ui-icon { float:right } .ui-multiselect-single .ui-multiselect-checkboxes input { left:-9999px; position:absolute !important; top: auto !important; } From ed9840b0e9d694d4778ba19ea40eb39de1d04760 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Sun, 28 Jan 2018 16:47:12 -0800 Subject: [PATCH 3/4] Deleted as requested --- CHANGELOG.MD | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 CHANGELOG.MD diff --git a/CHANGELOG.MD b/CHANGELOG.MD deleted file mode 100644 index e951626..0000000 --- a/CHANGELOG.MD +++ /dev/null @@ -1,13 +0,0 @@ -Summary of Code Changes for jQuery UI Multiselect Widget, Version 3.0.0: - -**Enhancements** -1. - -**Feature & Functional Changes** -1. - -**Feature & Functional Deletions** -1. - -**Bug Fixes** -1. From d3dcd1b8c0650df1f5d378948724567241da23e9 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Sun, 28 Jan 2018 18:00:14 -0800 Subject: [PATCH 4/4] Changes as requested - Removed extra self variables where possible. - Removed reference URLs (against my better judgement). - Used a simple alternative to dataset in _makeOptions (reference _getDataset() in (https://gist.github.com/ShirtlessKirk/6cdc2c32ddd97dc9c706)[https://gist.github.com/ShirtlessKirk/6cdc2c32ddd97dc9c706]) - Deleted spurious console.log statement - Ditched 'auto' setting for minWidth... not really done yet. - Deleted jQuery object comments. --- src/jquery.multiselect.js | 206 ++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 119 deletions(-) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index 7f69824..cd36a76 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -15,25 +15,6 @@ * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * - * References: - * - http://api.jquery.com/jquery/ - * - http://api.jqueryui.com/jQuery.widget/ - * - http://learn.jquery.com/performance/optimize-selectors/ - * - https://www.audero.it/blog/2013/09/16/15-tips-to-improve-your-jquery-selectors - * - https://stackoverflow.com/questions/327047/what-is-the-most-efficient-way-to-create-html-elements-using-jquery - * - https://jsperf.com/jquery-vs-createelement - * - https://jsperf.com/jquery-element-creationyay/19 - * - https://howchoo.com/g/mmu0nguznjg/learn-the-slow-and-fast-way-to-append-elements-to-the-dom - * - https://stackoverflow.com/questions/1357118/event-preventdefault-vs-return-false - * - https://blog.kevin-brown.com/select2/2014/12/15/jquery-js-performance.html - * - https://jsperf.com/append-array-of-jquery-elements - * - https://gist.github.com/adrienne/5341713 - * - https://www.sitepoint.com/10-ways-minimize-reflows-improve-performance/ - * - https://gist.github.com/paulirish/5d52fb081b3570c81e3a (List of reflow triggers.) - * - http://hueypetersen.com/posts/2012/08/23/having-fun-with-reflows-and-infinityjs/ (jQuery outerHeight() causes reflows.) - * - http://youmightnotneedjquery.com/ - * - https://plainjs.com/ - * */ (function($, undefined) { // Counter used to prevent collisions @@ -52,7 +33,7 @@ // default options options: { header: true, // (true | false) If true, the header is shown. - height: 175, // (int | 'size') Sets the height of the menu in pixels. If 'size' is instead specified, the native select's size attribute is instead for height. + height: 175, // (int | 'size') Sets the height of the menu in pixels or determines it using native select's size setting. minWidth: 225, // (int) Sets the minimum width of the menu. classes: '', // Classes that you can provide to be applied to the elements making up the widget. iconSet: null, // (plain object | null) Supply an object of icons to use alternative icon sets, or null for default set. Reference defaultIcons above for object structure. @@ -101,8 +82,8 @@ // Performs the initial creation of the widget _create: function() { - var $element = this.element.hide(); // element property is a jQuery object per http://api.jqueryui.com/jQuery.widget/ - var elSelect = $element.get(0); // This would be expected to be the underlying native select element. + var $element = this.element.hide(); + var elSelect = $element.get(0); var options = this.options; var classes = options.classes; var headerOn = options.header; @@ -124,7 +105,11 @@ // The button that opens the widget menu. Note that this is inserted later below. var $button = (this.$button = $( document.createElement('button') ) ) .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all' + (classes ? ' ' + classes : '')) - .attr({ 'type': 'button', 'title': elSelect.title, 'tabIndex': elSelect.tabIndex, 'id': elSelect.id ? elSelect.id + '_ms' : null }) + .attr({'type': 'button', + 'title': elSelect.title, + 'tabIndex': elSelect.tabIndex, + 'id': elSelect.id ? elSelect.id + '_ms' : null + }) .prop('aria-haspopup', true) .html('' + iconSet.open + ''); // Necessary to simplify dynamically changing the open icon. @@ -178,7 +163,7 @@ // https://api.jqueryui.com/jquery.widget/#method-_init _init: function() { - var elSelect = this.element.get(0); // element is a jQuery object per widget API + var elSelect = this.element.get(0); if (this.options.header) this.$headerLinkContainer @@ -206,7 +191,7 @@ _makeOption: function(option) { var self = this; var title = option.title || null; - var elSelect = self.element.get(0); // element is a jQuery object per widget API + var elSelect = self.element.get(0); var id = elSelect.id || self.multiselectID; // unique ID for the label & option tags var inputID = 'ui-multiselect-' + self.multiselectID + '-' + (option.id || id + '-option-' + self.inputIdCounter++); var isMultiple = elSelect.multiple; // Pick up the select type from the underlying element @@ -229,10 +214,12 @@ if (inputAttribs[name] !== null) input.setAttribute(name,inputAttribs[name]); } - if ('dataset' in option) { - for (var key in option.dataset) { // Clone data attributes - input.dataset[key] = option.dataset[key]; - } + var optionAttribs = option.attributes; + var len = optionAttribs.length; + for (x = 0; x < len; x++) { // Clone data attributes + attribute = optionAttribs[x]; + if ( /^data\-.+/.test(attribute.name) ) + input.setAttribute(attribute.name, attribute.value) } var span = document.createElement('span'); @@ -304,7 +291,7 @@ // Refreshes the widget to pick up changes to the underlying select // Rebuilds the menu, sets button width refresh: function(init) { - var $element = this.element; // "element" is a jQuery object representing the underlying select + var $element = this.element; // update header link container visibility if needed if (this.options.header) @@ -610,7 +597,7 @@ self._bindHeaderEvents(); // close each widget when clicking on any other element/anywhere else on the page - self.document.on('mousedown.' + self._namespaceID, function(event) { // self.document is a jQuery object per widget API + self.document.on('mousedown.' + self._namespaceID, function(event) { var target = event.target; var button = self.$button.get(0); var menu = self.$menu.get(0); @@ -631,8 +618,7 @@ // Determines the minimum width for the button and menu // Can be a number, a digit string, or a percentage _getMinWidth: function() { - var self = this; - var minWidth = self.options.minWidth; + var minWidth = this.options.minWidth; var width = 0; switch (typeof minWidth) { @@ -640,60 +626,45 @@ width = minWidth; break; case 'string': - if (minWidth === 'auto') { - $menu = self.$menu; - var appendEl = self._getAppendEl(); - var cssVisibility = $menu.css('visibility'); - var cssWidth = $menu.css('width'); - var cssDisplay = $menu.css('display'); - var cssulDisplay = $menu.find('ul:eq(0)').css('display'); - $menu.appendTo('body').css({visibility:'hidden', width:'auto', display:'inline'}).find('ul').css('display', 'inline'); - var autoWidth = $menu.width(); - $menu.css({visibility:cssVisibility, width:cssWidth, display:cssDisplay}).find('ul').css('display', cssulDisplay).appendTo( appendEl ); - console.log('AUTO WIDTH: ', cssWidth, autoWidth); - return autoWidth; - } width = parseInt(minWidth, 10); if ( minWidth.slice(-1) === '%' ) - width = self.element.parent().innerWidth() * (width/100); // element is a jQuery object. + width = this.element.parent().innerWidth() * (width/100); } return width; }, - // set button width. jQuery seems to be required here, as getting the outerWidth by the non-jQuery way is unreliable. + // set button width. _setButtonWidth: function(recalc) { - var self = this; - if (self._savedButtonWidth && !recalc) + if (this._savedButtonWidth && !recalc) return; - self._positioned = false; // Force menu positioning to be adjusted on next open. - var width = self.element.outerWidth(); // element is a jQuery object - var minWidth = self._getMinWidth(); + this._positioned = false; // Force menu positioning to be adjusted on next open. + var width = this.element.outerWidth(); + var minWidth = this._getMinWidth(); // set widths - self._savedButtonWidth = width < minWidth ? minWidth : width; // The button width is cached. - self.$button.outerWidth(self._savedButtonWidth); + this._savedButtonWidth = width < minWidth ? minWidth : width; // The button width is cached. + this.$button.outerWidth(this._savedButtonWidth); }, // set menu width _setMenuWidth: function(recalc) { - var self = this; - if (self._savedMenuWidth && !recalc) + if (this._savedMenuWidth && !recalc) return; - self._positioned = false; // Force menu positioning to be adjusted on next open. - var width = self.options.menuWidth; + this._positioned = false; // Force menu positioning to be adjusted on next open. + var width = this.options.menuWidth; if (!width) { // Exact width not provided; determine appropriate width. - width = self._savedButtonWidth || self.$button.outerWidth(); + width = this._savedButtonWidth || this.$button.outerWidth(); // Make width match button's width. if (width <= 0) - width = self._getMinWidth(); + width = this._getMinWidth(); } - self._savedMenuWidth = width; // The menu width is cached. - self.$menu.outerWidth(width); + this._savedMenuWidth = width; // The menu width is cached. + this.$menu.outerWidth(width); }, // Sets the height of the menu @@ -789,17 +760,17 @@ // Toggles checked state on either an option group or all inputs _toggleChecked: function(flag, group, filteredInputs) { var self = this; - var $element = this.element; // element is a jQuery object - var $inputs = (group && group.length) ? group : (filteredInputs || this.$inputs); + var $element = self.element; + var $inputs = (group && group.length) ? group : (filteredInputs || self.$inputs); // toggle state on inputs - $inputs.each(this._toggleState('checked', flag)); + $inputs.each(self._toggleState('checked', flag)); // Give the first input focus $inputs.eq(0).focus(); // update button text - this.update(); + self.update(); // Create a plain object of the values that actually changed var values = {}; @@ -853,7 +824,7 @@ } } - this.element.prop({ // element is a jQuery object + this.element.prop({ 'disabled':flag, 'aria-disabled':flag }); @@ -861,24 +832,23 @@ // open the menu open: function(e) { - var self = this; - var $button = self.$button; + var $button = this.$button; // bail if the multiselect open event returns false, this widget is disabled, or is already open - if(self._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || self._isOpen) + if(this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen) return; - var $menu = self.$menu; - var $header = self.$header; - var $labels = self.$labels; - var speed = self.speed; - var options = self.options; + var $menu = this.$menu; + var $header = this.$header; + var $labels = this.$labels; + var speed = this.speed; + var options = this.options; var effect = options.show; // figure out opening effects/speeds if (options.show && options.show.constructor == Array) { effect = options.show[0]; - speed = options.show[1] || self.speed; + speed = options.show[1] || this.speed; } // set the scroll of the checkbox container @@ -888,8 +858,8 @@ // if there's an effect, assume jQuery UI is in use $.fn.show.apply($menu, effect ? [ effect, speed ] : []); - self._resizeMenu(); - self._position(); + this._resizeMenu(); + this._position(); // select the first not disabled option or the filter input if available var filter = $header.find(".ui-multiselect-filter"); @@ -901,8 +871,8 @@ $header.find('a').first().trigger('focus'); $button.addClass('ui-state-active'); - self._isOpen = true; - self._trigger('open'); + this._isOpen = true; + this._trigger('open'); }, // close the menu @@ -910,24 +880,24 @@ var self = this; // bail if the multiselect close event returns false - if (self._trigger('beforeclose') === false) + if (this._trigger('beforeclose') === false) return; - var options = self.options; + var options = this.options; var effect = options.hide; - var speed = self.speed; - var $button = self.$button; + var speed = this.speed; + var $button = this.$button; // figure out closing effects/speeds if (options.hide && options.hide.constructor == Array) { effect = options.hide[0]; - speed = options.hide[1] || self.speed; + speed = options.hide[1] || this.speed; } - $.fn.hide.apply(self.$menu, effect ? [ effect, speed ] : []); + $.fn.hide.apply(this.$menu, effect ? [ effect, speed ] : []); $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); - self._isOpen = false; - self._trigger('close'); + this._isOpen = false; + this._trigger('close'); $button.trigger('focus'); }, @@ -946,7 +916,7 @@ uncheckAll: function() { this._toggleChecked(false); - if ( !this.element[0].multiple ) // this.element is a jQuery object + if ( !this.element[0].multiple ) this.element[0].selectedIndex = -1; // Forces the underlying single-select to have no options selected. this._trigger('uncheckAll'); }, @@ -969,8 +939,8 @@ $.Widget.prototype.destroy.call(this); // unbind events - this.document.off(this._namespaceID); // this.document is a jQuery object per widget API - $(this.element[0].form).off(this._namespaceID); // this.element is a jQuery object + this.document.off(this._namespaceID); + $(this.element[0].form).off(this._namespaceID); this.$button.remove(); this.$menu.remove(); @@ -1040,37 +1010,35 @@ position: function(){ this._position.call(this, true) }, // Public function call always ignores cached status. _position: function(reposition) { - var self = this; - if (!!self._positioned && !reposition) + if (!!this._positioned && !reposition) return; - var $button = self.$button; - self._savedButtonHeight = self.$button.outerHeight(false); // Save this so that we can determine when the button height has changed due adding/removing selections. + var $button = this.$button; + this._savedButtonHeight = this.$button.outerHeight(false); // Save this so that we can determine when the button height has changed due adding/removing selections. - var pos = $.extend({'my': 'top', 'at': 'bottom', 'of': $button}, self.options.position || {}); + var pos = $.extend({'my': 'top', 'at': 'bottom', 'of': $button}, this.options.position || {}); if($.ui && $.ui.position) - self.$menu.position(pos); + this.$menu.position(pos); else { pos = $button.position(); - pos.top += self._savedButtonHeight; - self.$menu.offset(pos); + pos.top += this._savedButtonHeight; + this.$menu.offset(pos); } - self._positioned = true; + this._positioned = true; }, // react to option changes after initialization _setOption: function(key, value) { - var self = this; - var $menu = self.$menu; + var $menu = this.$menu; switch(key) { case 'header': if (typeof value === 'boolean') - self.$header.toggle( value ); + this.$header.toggle( value ); else if (typeof value === 'string') { - self.$headerLinkContainer.children('li:not(:last-child)').remove(); - self.$headerLinkContainer.prepend('
  • ' + value + '
  • '); + this.$headerLinkContainer.children('li:not(:last-child)').remove(); + this.$headerLinkContainer.prepend('
  • ' + value + '
  • '); } break; case 'checkAllText': @@ -1090,40 +1058,40 @@ $menu.find('a.ui-multiselect-close').html(value); break; case 'height': - self.options[key] = value; - self._setMenuHeight(true); // true forces recalc of cached value. + this.options[key] = value; + this._setMenuHeight(true); // true forces recalc of cached value. break; case 'minWidth': case 'menuWidth': - self.options[key] = value; - self._setButtonWidth(true); // true forces recalc of cached value. - self._setMenuWidth(true); // true forces recalc of cached value. + this.options[key] = value; + this._setButtonWidth(true); // true forces recalc of cached value. + this._setMenuWidth(true); // true forces recalc of cached value. break; case 'selectedText': case 'selectedList': case 'selectedMax': case 'noneSelectedText': case 'selectedListSeparator': - self.options[key] = value; // these all need to update immediately for the update() call - self.update(true); + this.options[key] = value; // these all need to update immediately for the update() call + this.update(true); break; case 'classes': - $menu.add(self.$button).removeClass(self.options.classes).addClass(value); + $menu.add(this.$button).removeClass(this.options.classes).addClass(value); break; case 'multiple': - var $element = self.element; + var $element = this.element; if (!!$element[0].multiple !== value) { $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value); $element[0].multiple = value; - self.uncheckAll(); - self.refresh(); + this.uncheckAll(); + this.refresh(); } break; case 'position': - self._position(true); // true ignores cached setting + this._position(true); // true ignores cached setting break; } - $.Widget.prototype._setOption.apply(self, arguments); + $.Widget.prototype._setOption.apply(this, arguments); }, });