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.
    - +