From 406a6ba544310e43571d90fe1a333c734a2f7e1b Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Thu, 25 Jan 2018 06:31:27 -0800 Subject: [PATCH] Performance tweaks 2 (#732) 1. Added css/ folder and moved css files there. Folder references updated. 2. Added iconSet option to be used for alternate icon sets. Removed individual icon options. 3. Ditched showCheckAll, showUncheckAll, and showFlipAll. Just set the respective text options to null to hide the respective links. 4. Various performance tweaks: - Minimized $().append() - Switched from $().appendTo() to $().append()--the latter is faster. - Reordered appends to minimize reflowing and combine appends. - Eliminated recursion in _buildOptionList by implementing a secondary loop. - Implemented the use of $().toggle() more widely. --- .../jquery.multiselect.css | 0 .../jquery.multiselect.filter.css | 0 demos/animations.htm | 20 +- demos/basic.htm | 58 +-- demos/callbacks.htm | 80 ++--- demos/enabledisable.htm | 12 +- demos/filter.htm | 80 ++--- demos/headers.htm | 98 +++--- demos/maxchecked.htm | 50 +-- demos/position.htm | 66 ++-- demos/preselected.htm | 28 +- demos/refresh.htm | 72 ++-- demos/selectedlist.htm | 32 +- demos/single.htm | 38 +- package.json | 9 +- src/jquery.multiselect.filter.js | 30 +- src/jquery.multiselect.js | 331 ++++++++++-------- tests/unit/core.js | 94 ++--- tests/unit/index.htm | 22 +- tests/unit/options.js | 24 +- tests/visual/form-reset.htm | 26 +- tests/visual/formsubmission.cfm | 4 +- tests/visual/formsubmission.htm | 6 +- tests/visual/widget-containers.htm | 108 +++--- 24 files changed, 654 insertions(+), 634 deletions(-) rename jquery.multiselect.css => css/jquery.multiselect.css (100%) rename jquery.multiselect.filter.css => css/jquery.multiselect.filter.css (100%) diff --git a/jquery.multiselect.css b/css/jquery.multiselect.css similarity index 100% rename from jquery.multiselect.css rename to css/jquery.multiselect.css diff --git a/jquery.multiselect.filter.css b/css/jquery.multiselect.filter.css similarity index 100% rename from jquery.multiselect.filter.css rename to css/jquery.multiselect.filter.css diff --git a/demos/animations.htm b/demos/animations.htm index 08c95a0..aef50e8 100644 --- a/demos/animations.htm +++ b/demos/animations.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -14,15 +14,15 @@ diff --git a/demos/basic.htm b/demos/basic.htm index c48ed11..f2b05f1 100644 --- a/demos/basic.htm +++ b/demos/basic.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -13,7 +13,7 @@ @@ -30,38 +30,38 @@

Basic Demos

Basic

- +

With Optgroups

Click on an optgroup's heading to toggle the checked state of the entire group.

- +

diff --git a/demos/callbacks.htm b/demos/callbacks.htm index 234977d..f4f93cb 100644 --- a/demos/callbacks.htm +++ b/demos/callbacks.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -14,38 +14,38 @@ @@ -60,15 +60,15 @@

Callbacks & Events

diff --git a/demos/enabledisable.htm b/demos/enabledisable.htm index e090eff..59bcfbd 100644 --- a/demos/enabledisable.htm +++ b/demos/enabledisable.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -14,11 +14,11 @@ diff --git a/demos/filter.htm b/demos/filter.htm index a20d0c0..4da9c8c 100644 --- a/demos/filter.htm +++ b/demos/filter.htm @@ -3,8 +3,8 @@ jQuery MultiSelect Widget Demo - - + + @@ -25,34 +25,34 @@

Filter Plugin

-

- -

-

- -

+

+ +

+

+ +

Options:

@@ -68,10 +68,10 @@

Options:

Events:

diff --git a/demos/headers.htm b/demos/headers.htm index 6e11660..8e71765 100644 --- a/demos/headers.htm +++ b/demos/headers.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -11,22 +11,22 @@ @@ -37,38 +37,38 @@

Headers

The header option can be used in three ways:

-

header: true (default)

- - -

header: false

- - -

header: "Choose options below"

- +

header: true (default)

+ + +

header: false

+ + +

header: "Choose options below"

+
diff --git a/demos/maxchecked.htm b/demos/maxchecked.htm index 39a0e36..bf4bf89 100644 --- a/demos/maxchecked.htm +++ b/demos/maxchecked.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Plugin Demo - + @@ -13,21 +13,21 @@ @@ -40,15 +40,15 @@

Max Checked Test

Check a few boxes.

- +
diff --git a/demos/position.htm b/demos/position.htm
index b8d2c2d..8a197ca 100644
--- a/demos/position.htm
+++ b/demos/position.htm
@@ -3,7 +3,7 @@
 
 
 jQuery MultiSelect Widget Demo
-
+
 
 
 
@@ -14,22 +14,22 @@
 
 
@@ -44,7 +44,7 @@ 

Position Utility

-

Center the menu over the button

+

Center the menu over the button

 $("select").multiselect({
    position: {
@@ -58,14 +58,14 @@ 

Center the menu over the button

} });
-

- -

- +

+ +

+

Open the menu upwards

 $("select").multiselect({
@@ -75,13 +75,13 @@ 

Open the menu upwards

} });
-

- -

+

+ +

diff --git a/demos/preselected.htm b/demos/preselected.htm index fb7c000..d80f5fd 100644 --- a/demos/preselected.htm +++ b/demos/preselected.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -19,19 +19,19 @@

Pre-selected & pre-disabled options

When the widget is initialized on the select, both attribute types are honored.

-

- -

+

+ +

diff --git a/demos/refresh.htm b/demos/refresh.htm index 8d90bea..5039ee2 100644 --- a/demos/refresh.htm +++ b/demos/refresh.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -13,28 +13,28 @@ @@ -52,22 +52,22 @@

Refresh Method

-

Add an item:

-

Type in the text of a new option tag to add dynamically.

- - -

- - -

+

Add an item:

+

Type in the text of a new option tag to add dynamically.

+ + +

+ + +

- +
diff --git a/demos/selectedlist.htm b/demos/selectedlist.htm index 2c08503..3f3a8d9 100644 --- a/demos/selectedlist.htm +++ b/demos/selectedlist.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -14,10 +14,10 @@ @@ -34,17 +34,17 @@

Using the selectedList Parameter

});
-

- -

+

+ +

Passing a Function to selectedText

diff --git a/demos/single.htm b/demos/single.htm index 716cf80..3e0532d 100644 --- a/demos/single.htm +++ b/demos/single.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Widget Demo - + @@ -14,13 +14,13 @@ @@ -42,17 +42,17 @@

Single Select

-

- -

+

+ +

diff --git a/package.json b/package.json index 96281d5..57196b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jquery-ui-multiselect-widget", "description": "MultiSelect progessively enhances an ordinary multiple select control into elegant drop down list of checkboxes, stylable with ThemeRoller.", - "version": "2.0.1", + "version": "3.0.0", "license": "MIT or GPL-2.0", "author": "Eric Hynds", "contributors": [ @@ -10,6 +10,8 @@ }, { "name": "AB Zainuddin", "email": "burhan@codeyellow.nl" + }, { + "name": "Steve James", } ], "repository": { @@ -17,6 +19,9 @@ "url": "https://github.com/ehynds/jquery-ui-multiselect-widget" }, "main": "src/jquery.multiselect.js", - "dependencies": {}, + "dependencies": { + "jquery": "^1.8.0", + "jquery-ui": "^1.11.0" + }, "devDependencies": {} } diff --git a/src/jquery.multiselect.filter.js b/src/jquery.multiselect.filter.js index a9bdaaf..6bd48ef 100644 --- a/src/jquery.multiselect.filter.js +++ b/src/jquery.multiselect.filter.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 Filtering Plugin 2.0.0 + * jQuery MultiSelect UI Widget Filtering Plugin 3.0.0 * Copyright (c) 2012 Eric Hynds * * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ @@ -71,11 +71,11 @@ else if(e.which === 27) { $element.multiselect('close'); e.preventDefault(); - } + } else if(e.which === 9 && e.shiftKey) { $element.multiselect('close'); e.preventDefault(); - } + } else if(e.altKey) { switch(e.which) { case 82: @@ -100,7 +100,7 @@ // automatically reset the widget on close? if (this.options.autoReset) $element.on('multiselectclose', $.proxy(this._reset, this)); - + // rebuild cache when multiselect is updated $element.on('multiselectrefresh', $.proxy(function() { this.updateCache(); @@ -121,9 +121,9 @@ // rewrite internal _toggleChecked fn so that when checkAll/uncheckAll is fired, // only the currently filtered $elements are checked this.instance._toggleChecked = function(flag, group) { - var $inputs = (group && group.length) ? group : this.$labels.find('input'); var self = this; var $element = this.element; + var $inputs = (group && group.length) ? group : this.$inputs; // do not include hidden elems if the menu isn't open. var selector = self._isOpen ? ':disabled, :hidden' : ':disabled'; @@ -137,9 +137,10 @@ // gather an array of the values that actually changed var values = {}; - $inputs.each(function() { - values[this.value] = true; - }); + var inputCount = $inputs.length; + for (var x = 0; x < inputCount; x++) { + values[ $inputs.get(x).value ] = true; + } // select option tags $element.find('option').filter(function() { @@ -149,9 +150,9 @@ }); // trigger the change event on the select - if($inputs.length) + if(inputCount) $element.trigger('change'); - + }; }, @@ -163,11 +164,8 @@ $rows = this.$rows, $inputs = this.$inputs, $cache = this.$cache; var $groups = this.instance.$menu.find(".ui-multiselect-optgroup"); $groups.show(); - if(!term) { - $rows.show(); - } else { - $rows.hide(); - + $rows.toggle(!term); + if(term) { var regex = new RegExp(term.replace(rEscape, "\\$&"), 'gi'); this._trigger("filter", e, $.map($cache, function(v, i) { @@ -183,7 +181,7 @@ // show/hide optgroups $groups.each(function() { var $this = $(this); - if (!$this.children("li:visible").length) + if (!$this.children('li').filter(':visible').length) $this.hide(); }); this.instance._setMenuHeight(); diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index df04314..ca006d4 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -4,7 +4,7 @@ * Copyright (c) 2012 Eric Hynds * * Depends: - * - jQuery 1.7+ (http://api.jquery.com/) + * - jQuery 1.8+ (http://api.jquery.com/) * - jQuery UI 1.11 widget factory (http://api.jqueryui.com/jQuery.widget/) * * Optional: @@ -26,7 +26,8 @@ * - 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 - * - http://www.jedi.be/blog/2008/10/10/is-your-jquery-or-javascript-getting-slow-or-bad-performance/ + * - https://jsperf.com/append-array-of-jquery-elements + * - https://gist.github.com/adrienne/5341713 * */ (function($, undefined) { @@ -34,73 +35,74 @@ var multiselectID = 0; var $doc = $(document); + var defaultIcons = { + 'open': '', + 'checkAll': '', + 'uncheckAll': '', + 'flipAll': '' + }; + $.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. - openIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be specified here instead of the default jQuery UI icons. - closeIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be specified here instead of the default jQuery UI icons. - checkAllIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be specified here instead of the default jQuery UI icons. - uncheckAllIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be specified here instead of the default jQuery UI icons. - flipAllIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be specified here instead of the default jQuery UI icons. - checkAllText: 'Check all', // (str | blank | null) If blank or null, link not shown. - uncheckAllText: 'Uncheck all', // (str | blank | null) If blank or null, link not shown. - flipAllText: 'Flip all', // (str | blank | null) If blank or null, link not shown. - showCheckAll: true, // (true | false) Show or hide the Check All link without blanking the text. - showUncheckAll: true, // (true | false) Show or hide the Uncheck All link without blanking the text. - showFlipAll: false, // (true | false) Show or hide the Flip All link without blanking the text. - 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) + 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) }, // 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 _getAppendEl: function() { - var elem = this.options.appendTo; // jQuery object, DOM element, OR selector str. + 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. + elem = elem.jquery || elem.nodeType ? $(elem) : this.document.find(elem).eq(0); // Note that the find handles the selector case. } 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"); // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/ } if(!elem.length) { - elem = this.document[0].body; // Position at end of body. + elem = this.document[0].body; // Position at end of body. } return elem; }, // 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(); // element property is a jQuery object + 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 checkAllText = options.checkAllText; var uncheckAllText = options.uncheckAllText; var flipAllText = options.flipAllText; - this.speed = $.fx.speeds._default; // default speed for effects + this.speed = $.fx.speeds._default; // default speed for effects this._isOpen = false; // assume no - this.inputIdCounter = 0; // create a unique namespace for events that the widget // factory cannot unbind automatically. Use eventNamespace if on @@ -109,47 +111,53 @@ // bump unique ID after assigning it to the widget instance this.multiselectID = multiselectID++; - // The button that opens the widget menu. Note that this inserted later below. + // 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 }) + .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('' + options.openIcon + ''); // Necessary to simplify dynamically changing the open icon. + .html('' + iconSet.open + ''); // Necessary to simplify dynamically changing the open icon. - this.$buttonlabel = $( document.createElement('span')) + this.$buttonlabel = $( document.createElement('span') ) .html(options.noneSelectedText) .appendTo( $button ); - // This is the menu that will hold all the options. If this is a single select widget, add the appropriate class. Note that this 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); - - // 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') - .appendTo( $menu ); - // 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 headerCtlsHTML = ( headerOn === true - ? (options.showCheckAll && checkAllText ? '
  • ' + options.checkAllIcon + '' + checkAllText + '
  • ' : '') - + (options.showUncheckAll && uncheckAllText ? '
  • ' + options.uncheckAllIcon+'' + uncheckAllText + '
  • ' : '') - + (options.showFlipAll && flipAllText ? '
  • ' + options.flipAllIcon + '' + flipAllText + '
  • ' : '') + 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') ) .addClass('ui-helper-reset') - .html(headerCtlsHTML + '
  • ' + options.closeIcon + '
  • ') - .appendTo($header); + .html(headerLinksHTML + '
  • ' + iconSet.close + '
  • '); + + // 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 - this.$checkboxContainer = $( document.createElement('ul') ) - .addClass('ui-multiselect-checkboxes ui-helper-reset') - .appendTo($menu); + 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') ) ) + .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. + $menu.appendTo( this._getAppendEl() ); // This is an empty menu at this point. // perform event bindings this._bindEvents(); @@ -160,10 +168,11 @@ // https://api.jqueryui.com/jquery.widget/#method-_init _init: function() { - var elSelect = this.element.get(0); // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/ + var elSelect = this.element.get(0); // element is a jQuery object - if (!!this.options.header) - this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')[ !!elSelect.multiple ? 'show' : 'hide' ](); + if (this.options.header) + this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') + .toggle( !!elSelect.multiple ); else this.$header.hide(); @@ -185,81 +194,95 @@ */ _makeOption: function(option) { var title = option.title || null; - var elSelect = this.element.get(0); // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/ - var id = elSelect.id || this.multiselectID; // unique ID for the label & option tags + 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 isMultiple = elSelect.multiple; // Pick up the select type from the underlying element var isDisabled = option.disabled; var isSelected = option.selected; - var $item = $( document.createElement('li') ) - .addClass( (isDisabled ? 'ui-multiselect-disabled ' : '') + (option.className || '') ); - - var $label = $( document.createElement('label') ) - .attr({ 'for': inputID, 'title': title}) - .addClass( (isDisabled ? 'ui-state-disabled ' : '') + (isSelected && !isMultiple ? 'ui-state-active ' : '') + 'ui-corner-all') - .appendTo($item); - 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).data()) - .appendTo($label); + .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 $span = this.options.htmlOptionText ? $( document.createElement('span') ).html($(option).html()) : $( document.createElement('span') ).text($(option).text()); - if ($input.data("image-src")) - $span.prepend( $(document.createElement('img')).attr('src', $input.data("image-src")) ); - $span.appendTo($label); + var $item = $( document.createElement('li') ) + .addClass( (isDisabled ? 'ui-multiselect-disabled ' : '') + (option.className || '') ) + .append($label); return $item; }, // Builds a menu item for each option in the underlying select // Option groups are built here as well - _buildOptionList: function($element, $appendTo) { - var self = this; // Save this => widget reference + _buildOptionList: function($element, $checkboxContainer) { + var self = this; // Save this => widget reference + var list = []; + + this.inputIdCounter = 0; + + $element.children().each( function() { + var elem = this; - $element.children().each(function() { - if(this.tagName === 'OPTGROUP') { + 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') ) - .addClass('ui-multiselect-optgroup' + (self.options.groupColumns ? ' ui-multiselect-columns' : '') + (this.className && ' ') + this.className) - .append( $( document.createElement('a') ).text( this.getAttribute('label') ) ) - .appendTo($appendTo); + .addClass('ui-multiselect-optgroup' + + (self.options.groupColumns ? ' ui-multiselect-columns' : '') + + (elem.className && ' ') + elem.className) + .append( $( document.createElement('a') ).text( elem.getAttribute('label') ), options); - self._buildOptionList($(this), $optionGroup); + list.push($optionGroup); + } + else { + list.push(self._makeOption(elem)); } - else - $appendTo.append(self._makeOption(this)); }); + $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 representing the underlying select - var $dropdown = $( document.createElement('ul') ).addClass('ui-multiselect-checkboxes ui-helper-reset'); // Checklist built in memory and inserted later. - - this.inputIdCounter = 0; + var $element = this.element; // "element" is a jQuery object // update header link container visibility if needed if (this.options.header) - this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')[ !!$element[0].multiple ? 'show' : 'hide' ](); - - this._buildOptionList($element, $dropdown); // Rebuild the menu. - this.$menu.find('.ui-multiselect-checkboxes').replaceWith($dropdown); // Insert updated check list. + this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') + .toggle( !!$element[0].multiple ); - this._updateCache(); // cache some more useful elements + this._buildOptionList($element, this.$checkboxContainer); + this._updateCache(); this._setButtonWidth(); this.update(true); @@ -290,7 +313,7 @@ if (typeof selectedText === 'function') value = selectedText.call(this, 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); + value = $checked.map(function() { return $(this).next().text() }).get().join(options.selectedListSeparator); else value = selectedText.replace('#', numChecked).replace('#', inputCount); } @@ -321,7 +344,7 @@ return false; } - $button // button events + $button // button events .on({ click: clickHandler, keypress: function(e) { @@ -354,7 +377,7 @@ $button.removeClass('ui-state-focus'); } }) - .find('span') // webkit doesn't like it when you click on the span :( + .find('span') // webkit doesn't like it when you click on the span :( .on('click.multiselect', clickHandler); }, @@ -441,7 +464,7 @@ var $inputs = self.$inputs; var optionText = $this.parent().find("span")[options.htmlOptionText ? 'html' : 'text'](); var $tags = $element.find('option'); - var isMultiple = !!$element[0].multiple; + var isMultiple = $element[0].multiple; var inputCount = $inputs.length; var numChecked = $inputs.filter(":checked").length; var selectedMax = options.selectedMax; @@ -452,7 +475,7 @@ return; } - if ( selectedMax && checked && ( $.isFunction(selectedMax) ? !!selectedMax.call(this, $inputs) : numChecked > selectedMax ) ) { + if ( selectedMax && checked && ( typeof selectedMax === 'function' ? !!selectedMax.call(this, $inputs) : numChecked > selectedMax ) ) { var saveText = options.selectedText; // The following warning is shown in the button and then cleared after a second. @@ -476,8 +499,8 @@ $this.prop('aria-selected', checked); // change state on the original option tags - $tags.each(function() { - this.selected = (this.value === val ? checked : (isMultiple ? this.selected : false) ); + $tags.each( function() { + this.selected = (this.value === val ? checked : isMultiple && this.selected); }); // some additional single select-specific logic @@ -504,13 +527,19 @@ this.$header .on('click.multiselect', 'a', function(e) { var $this = $(this); - $.each({'ui-multiselect-close' : 'close', 'ui-multiselect-all' : 'checkAll', 'ui-multiselect-none' : 'uncheckAll', 'ui-multiselect-flip' : 'flipAll'}, function( hdgClass, clickHandler) { + 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) ) { - self[ clickHandler ](); + self[ headerLinks[hdgClass] ](); // headerLinks[hdgClass] is the click handler name e.preventDefault(); - return false; // Break out of loop early + return false; // Break out of loop early } - }); + } }) .on('keydown.multiselect', 'a', function(e) { switch(e.which) { @@ -579,7 +608,7 @@ // set button width _setButtonWidth: function() { - var width = this.element.outerWidth(); // element is a jQuery object + var width = this.element.outerWidth(); // element is a jQuery object var minVal = this._getMinWidth(); if(width < minVal) { @@ -599,11 +628,11 @@ // Will set a scroll bar if the menu height exceeds that of the height in options _setMenuHeight: function() { var $menu = this.$menu; - var headerHeight = $menu.children(".ui-multiselect-header:visible").outerHeight(true); + var headerHeight = $menu.children('.ui-multiselect-header').filter(':visible').outerHeight(true); var ulHeight = 0; - $menu.find(".ui-multiselect-checkboxes li, .ui-multiselect-checkboxes a").each(function(idx, li) { - ulHeight += $(li).outerHeight(true); + $menu.find('.ui-multiselect-checkboxes li, .ui-multiselect-checkboxes a').each( function() { + ulHeight += $(this).outerHeight(true); }); if(ulHeight > this.options.height) { @@ -674,6 +703,7 @@ var self = this; var $element = this.element; // element is a jQuery object var $inputs = (group && group.length) ? group : this.$inputs; + var inputCount = $inputs.length; // toggle state on inputs $inputs.each(this._toggleState('checked', flag)); @@ -686,21 +716,21 @@ // Create a plain object of the values that actually changed var values = {}; - $inputs.each(function() { - values[this.value] = true; - }); + for (var x = 0; x < inputCount; x++) { + values[ $inputs.get(x).value ] = true; + }; // toggle state on original option tags $element[0].selectedIndex = -1; $element - .find('option') - .each(function() { - if(!this.disabled && values[this.value]) - self._toggleState('selected', flag).call(this); - }); + .find('option') + .each( function() { + if(!this.disabled && values[this.value]) + self._toggleState('selected', flag).call(this); + }); // trigger the change event on the select - if ($inputs.length) + if (inputCount) $element.trigger("change"); }, @@ -736,7 +766,7 @@ } } - this.element.prop({ // element is a jQuery object + this.element.prop({ // element is a jQuery object 'disabled':flag, 'aria-disabled':flag }); @@ -779,8 +809,10 @@ 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'); @@ -838,11 +870,11 @@ }, getChecked: function() { - return this.$menu.find('input').filter(':checked'); + return this.$menu.find('input:checked'); }, getUnchecked: function() { - return this.$menu.find('input').not(':checked'); + return this.$menu.find('input:not(:checked)'); }, destroy: function() { @@ -945,10 +977,10 @@ switch(key) { case 'header': if (typeof value === 'boolean') - this.$header[value ? 'show' : 'hide'](); + this.$header.toggle( value ); else if(typeof value === 'string') { - this.$headerLinkContainer.children("li:not(:last-child)").remove(); - this.$headerLinkContainer.prepend("
  • " + value + "
  • "); + this.$headerLinkContainer.children('li:not(:last-child)').remove(); + this.$headerLinkContainer.prepend('
  • ' + value + '
  • '); } break; case 'checkAllText': @@ -989,9 +1021,8 @@ break; case 'multiple': var $element = this.element; - if (!!$element[0].multiple != value) { - $menu.toggleClass('ui-multiselect-multiple', value); - $menu.toggleClass('ui-multiselect-single', !value); + if (!!$element[0].multiple !== value) { + $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value); $element[0].multiple = value; this.uncheckAll(); this.refresh(); @@ -1005,9 +1036,9 @@ this.update(true); break; } - $.Widget.prototype._setOption.apply(this, arguments); } + }); })(jQuery); diff --git a/tests/unit/core.js b/tests/unit/core.js index 1bb244e..f5fd824 100644 --- a/tests/unit/core.js +++ b/tests/unit/core.js @@ -18,12 +18,12 @@ QUnit.done = function(){ }; (function($){ - + module("core"); - + test("init", function(){ expect(6); - + el = $("select").multiselect(), $header = header(); ok( $header.find('a.ui-multiselect-all').css('display') !== 'none', 'select all is visible' ); ok( $header.find('a.ui-multiselect-none').css('display') !== 'none', 'select none is visible' ); @@ -33,153 +33,153 @@ QUnit.done = function(){ ok( el.attr('tabIndex') == 2, 'button inherited the correct tab index'); el.multiselect("destroy"); }); - + test("name space separation", function(){ expect(1); - + var form = $('
    ').appendTo(body), data; - + el1 = $('') .appendTo(form) .multiselect(); - + el2 = $('') .appendTo(form) .multiselect(); - - notEqual(el1.multiselect('widget').find('input').eq(0).attr('id'), el2.multiselect('widget').find('input').eq(0).attr('id'), 'name spaces for multiple widgets are different'); - + + notEqual(el1.multiselect('widget').find('input').eq(0).attr('id'), el2.multiselect('widget').find('input').eq(0).attr('id'), 'name spaces for multiple widgets are different'); + el1.multiselect('destroy'); el2.multiselect('destroy'); form.remove(); }); - + test("form submission", function(){ expect(3); - + var form = $('
    ').appendTo(body), data; - + el = $('') .appendTo(form) .multiselect() .multiselect("checkAll"); - + data = form.serialize(); equals( data, 'test=foo&test=bar', 'after checking all and serializing the form, the correct keys were serialized'); - + el.multiselect("uncheckAll"); data = form.serialize(); equals( data.length, 0, 'after unchecking all and serializing the form, nothing was serialized'); - + // re-check all and destroy, exposing original select el.multiselect("checkAll").multiselect("destroy"); data = form.serialize(); equals( data, 'test=foo&test=bar', 'after checking all, destroying the widget, and serializing the form, the correct keys were serialized'); - + form.remove(); }); test("form submission, optgroups", function(){ expect(4); - + var form = $('
    ').appendTo(body), data; - + el = $('') .appendTo(form) .multiselect() .multiselect("checkAll"); - + data = form.serialize(); equals( data, 'test=foo&test=bar&test=baz&test=bax', 'after checking all and serializing the form, the correct keys were serialized'); - + el.multiselect("uncheckAll"); data = form.serialize(); equals( data.length, 0, 'after unchecking all and serializing the form, nothing was serialized'); - + // re-check all and destroy, exposing original select el.multiselect("checkAll").multiselect("destroy"); data = form.serialize(); equals( data, 'test=foo&test=bar&test=baz&test=bax', 'after checking all, destroying the widget, and serializing the form, the correct keys were serialized'); - + // reset option tags el.find("option").each(function(){ this.selected = false; }); - + // test checking one option in both optgroups el.multiselect(); - + // finds the first input in each optgroup (assumes 2 options per optgroup) el.multiselect("widget").find('.ui-multiselect-checkboxes li:not(.ui-multiselect-optgroup-label) input:even').each(function( i ){ this.click(); }); - + data = form.serialize(); equals( data, 'test=foo&test=baz', 'after manually checking one input in each group, the correct two are serialized'); - + el.multiselect('destroy'); form.remove(); }); - + test("form submission, single select", function(){ expect(7); - + var form = $('
    ').appendTo("body"), radios, data; - + // Use an underlying single-select here. el = $('') .appendTo(form) .multiselect(); - + // select multiple radios to ensure that, in the underlying select, only one // will remain selected radios = menu().find(":radio"); radios[0].click(); radios[2].click(); radios[1].click(); - + data = form.serialize(); equals( data, 'test=bar', 'the form serializes correctly after clicking on multiple radio buttons'); equals( radios.filter(":checked").length, 1, 'Only one radio button is selected'); - + // uncheckAll method el.multiselect("uncheckAll"); data = form.serialize(); equals( data.length, 0, 'After unchecking all, nothing was serialized'); equals( radios.filter(":checked").length, 0, 'No radio buttons are selected'); - + // checkAll method el.multiselect("checkAll"); data = form.serialize(); equals( el.multiselect("getChecked").length, 1, 'After checkAll, only one radio is selected'); equals( radios.filter(":checked").length, 1, 'One radio is selected'); - + // expose original el.multiselect("destroy"); data = form.serialize(); equals( data, 'test=baz', 'after destroying the widget and serializing the form, the correct key was serialized: ' + data); - + form.remove(); }); - + asyncTest("form reset, nothing pre-selected", function(){ expect(2); - + var form = $('
    ').appendTo(body), noneSelected = 'Please check something'; - + el = $('') .appendTo(form) .multiselect({ noneSelectedText: noneSelected, selectedList: 0 }) .multiselect("checkAll"); - + // trigger reset form.trigger("reset"); - + setTimeout(function(){ equals( menu().find(":checked").length, 0, "no checked checkboxes" ); equals( button().text(), noneSelected, "none selected text"); @@ -188,20 +188,20 @@ QUnit.done = function(){ start(); }, 10); }); - + asyncTest("form reset, pre-selected options", function(){ expect(2); - + var form = $('
    ').appendTo(body); - + el = $('') .appendTo(form) .multiselect({ selectedText: '# of # selected', selectedList: 0 }) .multiselect("uncheckAll"); - + // trigger reset form.trigger("reset"); - + setTimeout(function(){ equals( menu().find(":checked").length, 2, "two checked checkboxes" ); equals( button().text(), "2 of 2 selected", "selected text" ); @@ -210,5 +210,5 @@ QUnit.done = function(){ start(); }, 10); }); - + })(jQuery); diff --git a/tests/unit/index.htm b/tests/unit/index.htm index 7dd4197..90f9316 100644 --- a/tests/unit/index.htm +++ b/tests/unit/index.htm @@ -4,8 +4,8 @@ jQuery UI MultiSelect Widget Unit Tests - - + + @@ -16,19 +16,19 @@

    diff --git a/tests/unit/options.js b/tests/unit/options.js index 14577c3..353fd9b 100644 --- a/tests/unit/options.js +++ b/tests/unit/options.js @@ -94,17 +94,17 @@ el = $(html).appendTo("body").multiselect({ selectedMax: 2 }); - + checkboxes = el.multiselect("widget").find(":checkbox"); checkboxes.eq(0).trigger('click'); checkboxes.eq(1).trigger('click'); checkboxes.eq(2).trigger('click'); - + equals( menu().find("input").filter(":checked").length, 2 , 'after clicking each checkbox, count of checked restored to selectedMax of 2'); el.multiselect("destroy").remove(); - + }); - + function asyncSelectedList( useTrigger, message ){ expect(1); stop(); @@ -262,7 +262,7 @@ el.multiselect("destroy"); }); - + test("autoOpen", function(){ expect(2); @@ -391,20 +391,6 @@ el.multiselect("destroy"); }); - test("openIcon", function(){ - expect(1); - var icon = ''; - el = $("select").multiselect({ openIcon:icon }); - equals(button().find(".ui-multiselect-open").find(".ui-icon-search").length, 1); - el.multiselect("destroy"); - }); - test("closeIcon", function(){ - expect(1); - var icon = ''; - el = $("select").multiselect({ autoOpen:true, closeIcon:icon }); - equals(menu().find(".ui-multiselect-close").find(".ui-icon-search").length, 1); - el.multiselect("destroy"); - }); test("selectedListSeparator", function(){ expect(3); el = $("select").multiselect({ selectedListSeparator: "
    ", selectedList: 15 }); diff --git a/tests/visual/form-reset.htm b/tests/visual/form-reset.htm index 963a113..6d302e8 100644 --- a/tests/visual/form-reset.htm +++ b/tests/visual/form-reset.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Plugin Tests - + @@ -11,18 +11,18 @@ diff --git a/tests/visual/formsubmission.cfm b/tests/visual/formsubmission.cfm index b8cad93..aa91925 100644 --- a/tests/visual/formsubmission.cfm +++ b/tests/visual/formsubmission.cfm @@ -6,7 +6,7 @@ jQuery MultiSelect Plugin Tests - + @@ -42,7 +42,7 @@ $("select").multiselect(); $("form").bind("submit", function(){ - alert( $(this).serialize() ); + alert( $(this).serialize() ); }); diff --git a/tests/visual/formsubmission.htm b/tests/visual/formsubmission.htm index aa511ae..a518607 100644 --- a/tests/visual/formsubmission.htm +++ b/tests/visual/formsubmission.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Plugin Tests - + @@ -38,8 +38,8 @@

    Form Submission Test

    $("#bar").multiselect({ multiple:false }); $("form").bind("submit", function(){ - alert( $(this).serialize() ); - return false; + alert( $(this).serialize() ); + return false; }); diff --git a/tests/visual/widget-containers.htm b/tests/visual/widget-containers.htm index 141cfbc..7b08c36 100644 --- a/tests/visual/widget-containers.htm +++ b/tests/visual/widget-containers.htm @@ -3,7 +3,7 @@ jQuery MultiSelect Plugin Tests - + @@ -11,16 +11,16 @@ @@ -46,60 +46,60 @@

    Widgets

    -
    - - - -

    Datepicker:

    -
    +
    + + + +

    Datepicker:

    +
    - - -
    -
    - -
    -
    - -
    - Select the first tab to view the test. -
    + + +
    +
    + +
    +
    + +
    + Select the first tab to view the test. +
    -

    Inside an accordion

    -
    -
    - -
    -
    -

    test

    -
    Click on the first item to view the test.
    +

    Inside an accordion

    +
    +
    + +
    +
    +

    test

    +
    Click on the first item to view the test.
    - +