diff --git a/jquery.multiselect.css b/jquery.multiselect.css
index 735fed3..41de11e 100644
--- a/jquery.multiselect.css
+++ b/jquery.multiselect.css
@@ -7,7 +7,7 @@
.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; }
+.ui-multiselect-header a:hover { text-decoration:underline; cursor: pointer;}
.ui-multiselect-header span.ui-icon { float:left; }
.ui-multiselect-header .ui-multiselect-close { float:right; padding-right:0; text-align:right; }
diff --git a/src/jquery.multiselect.filter.js b/src/jquery.multiselect.filter.js
index 9c873d6..a9bdaaf 100644
--- a/src/jquery.multiselect.filter.js
+++ b/src/jquery.multiselect.filter.js
@@ -1,12 +1,11 @@
/* 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 3.0.0
+ * jQuery MultiSelect UI Widget Filtering Plugin 2.0.0
* Copyright (c) 2012 Eric Hynds
*
* http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
*
* Depends:
- * - jQuery 1.7+
* - jQuery UI MultiSelect widget
*
* Dual licensed under the MIT and GPL licenses:
@@ -49,45 +48,48 @@
_create: function() {
var opts = this.options;
- var $elem = this.element;
+ var $element = this.element;
// get the multiselect instance
- this.instance = $elem.multiselect('instance');
+ this.instance = $element.multiselect('instance');
// store header; add filter class so the close/check all/uncheck all links can be positioned correctly
this.$header = this.instance.$menu.find('.ui-multiselect-header').addClass('ui-multiselect-hasfilter');
- // wrapper $elem
- this.$input = $(" ").attr({
+ // wrapper $element
+ this.$input = $(document.createElement('input'))
+ .attr({
placeholder: opts.placeholder,
type: "search"
- }).css({
- width: (/\d/.test(opts.width) ? opts.width + 'px' : null)
- }).on({
+ })
+ .css({ width: (/\d/.test(opts.width) ? opts.width + 'px' : null) })
+ .on({
keydown: function(e) {
// prevent the enter key from submitting the form / closing the widget
- if(e.which === 13) {
+ if(e.which === 13)
e.preventDefault();
- } else if(e.which === 27) {
- $elem.multiselect('close');
+ else if(e.which === 27) {
+ $element.multiselect('close');
e.preventDefault();
- } else if(e.which === 9 && e.shiftKey) {
- $elem.multiselect('close');
+ }
+ else if(e.which === 9 && e.shiftKey) {
+ $element.multiselect('close');
e.preventDefault();
- } else if(e.altKey) {
+ }
+ else if(e.altKey) {
switch(e.which) {
case 82:
e.preventDefault();
$(this).val('').trigger('input', '');
break;
case 65:
- $elem.multiselect('checkAll');
+ $element.multiselect('checkAll');
break;
case 85:
- $elem.multiselect('uncheckAll');
+ $element.multiselect('uncheckAll');
break;
case 76:
- $elem.multiselect('instance').$labels.first().trigger("mouseenter");
+ $element.multiselect('instance').$labels.first().trigger("mouseenter");
break;
}
}
@@ -96,15 +98,19 @@
search: $.proxy(this._handler, this)
});
// automatically reset the widget on close?
- if(this.options.autoReset) {
- $elem.on('multiselectclose', $.proxy(this._reset, this));
- }
+ if (this.options.autoReset)
+ $element.on('multiselectclose', $.proxy(this._reset, this));
+
// rebuild cache when multiselect is updated
- $elem.on('multiselectrefresh', $.proxy(function() {
+ $element.on('multiselectrefresh', $.proxy(function() {
this.updateCache();
this._handler();
}, this));
- this.$wrapper = $("
").addClass("ui-multiselect-filter").text(opts.label).append(this.$input).prependTo(this.$header);
+ this.$wrapper = $(document.createElement('div'))
+ .addClass(' ui-multiselect-filter')
+ .text(opts.label)
+ .append(this.$input)
+ .prependTo(this.$header);
// reference to the actual inputs
this.$inputs = this.instance.$menu.find('input[type="checkbox"], input[type="radio"]');
@@ -116,10 +122,11 @@
// 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 self = this;
+ var $element = this.element;
// do not include hidden elems if the menu isn't open.
- var selector = _self._isOpen ? ':disabled, :hidden' : ':disabled';
+ var selector = self._isOpen ? ':disabled, :hidden' : ':disabled';
$inputs = $inputs
.not(selector)
@@ -135,16 +142,16 @@
});
// select option tags
- this.element.find('option').filter(function() {
+ $element.find('option').filter(function() {
if(!this.disabled && values[this.value]) {
- _self._toggleState('selected', flag).call(this);
+ self._toggleState('selected', flag).call(this);
}
});
// trigger the change event on the select
- if($inputs.length) {
- this.element.trigger('change');
- }
+ if($inputs.length)
+ $element.trigger('change');
+
};
},
@@ -153,19 +160,19 @@
var term = $.trim(this.$input[0].value.toLowerCase()),
// speed up lookups
- rows = this.rows, $inputs = this.$inputs, $cache = this.$cache;
+ $rows = this.$rows, $inputs = this.$inputs, $cache = this.$cache;
var $groups = this.instance.$menu.find(".ui-multiselect-optgroup");
$groups.show();
if(!term) {
- rows.show();
+ $rows.show();
} else {
- rows.hide();
+ $rows.hide();
var regex = new RegExp(term.replace(rEscape, "\\$&"), 'gi');
this._trigger("filter", e, $.map($cache, function(v, i) {
if(v.search(regex) !== -1) {
- rows.eq(i).show();
+ $rows.eq(i).show();
return $inputs.get(i);
}
@@ -176,9 +183,8 @@
// show/hide optgroups
$groups.each(function() {
var $this = $(this);
- if(!$this.children("li:visible").length) {
+ if (!$this.children("li:visible").length)
$this.hide();
- }
});
this.instance._setMenuHeight();
},
@@ -189,18 +195,18 @@
updateCache: function() {
// each list item
- this.rows = this.instance.$labels.parent();
+ this.$rows = this.instance.$labels.parent();
// cache
this.$cache = this.element.children().map(function() {
- var $elem = $(this);
+ var $element = $(this);
// account for optgroups
if(this.tagName.toLowerCase() === "optgroup") {
- $elem = $elem.children();
+ $element = $element.children();
}
- return $elem.map(function() {
+ return $element.map(function() {
return this.innerHTML.toLowerCase();
}).get();
}).get();
diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js
index f0ba55f..df04314 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.7+ (http://api.jquery.com/)
* - jQuery UI 1.11 widget factory (http://api.jqueryui.com/jQuery.widget/)
*
* Optional:
@@ -15,6 +15,19 @@
* 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
+ * - http://www.jedi.be/blog/2008/10/10/is-your-jquery-or-javascript-getting-slow-or-bad-performance/
+ *
*/
(function($, undefined) {
// Counter used to prevent collisions
@@ -25,58 +38,67 @@
// 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.
- 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.
- 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.
+ 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)
},
// 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 is a jQuery object per http://api.jqueryui.com/jQuery.widget/
- var o = this.options;
-
- this.speed = $.fx.speeds._default; // default speed for effects
+ 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 checkAllText = options.checkAllText;
+ var uncheckAllText = options.uncheckAllText;
+ var flipAllText = options.flipAllText;
+
+ this.speed = $.fx.speeds._default; // default speed for effects
this._isOpen = false; // assume no
this.inputIdCounter = 0;
@@ -87,93 +109,69 @@
// bump unique ID after assigning it to the widget instance
this.multiselectID = multiselectID++;
- // The button that opens the widget menu.
- // The ui-multiselect-open span is necessary below to simplify dynamically changing the open icon.
- var $button = (this.$button = $('' + o.openIcon + ' '))
- .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all ' + o.classes)
- .attr({ title: $element.attr('title'),
- tabIndex: $element.attr('tabIndex'),
- id: $element.attr('id') ? $element.attr('id') + '_ms' : null
- })
- .prop('aria-haspopup', true)
- .insertAfter($element);
-
- this.$buttonlabel = $(' ')
- .html(o.noneSelectedText)
- .appendTo($button);
-
- // This is the menu that will hold all the options
- var $menu = (this.$menu = $('
'))
- .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all ' + o.classes)
- .appendTo(this._getAppendEl());
+ // The button that opens the widget menu. Note that this 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('' + options.openIcon + ' '); // Necessary to simplify dynamically changing the open icon.
+
+ 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 = $('
'))
- .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
- .appendTo($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
- this.$headerLinkContainer = $('')
- .addClass('ui-helper-reset')
- .html(function() {
- if(o.header === true) {
- var header_lis = '';
- if(o.showCheckAll && o.checkAllText) {
- header_lis = '' + o.checkAllIcon + '' + o.checkAllText + ' ';
- }
- if(o.showUncheckAll && o.uncheckAllText) {
- header_lis += '' + o.uncheckAllIcon + '' + o.uncheckAllText + ' ';
- }
- if(o.showFlipAll && o.flipAllText) {
- header_lis += '' + o.flipAllIcon + '' + o.flipAllText + ' ';
- }
- return header_lis;
- } else if(typeof o.header === "string") {
- return '' + o.header + ' ';
- } else {
- return '';
- }
- })
- .append('' + o.closeIcon + ' ')
- .appendTo($header);
+ var headerCtlsHTML = ( headerOn === true
+ ? (options.showCheckAll && checkAllText ? '' + options.checkAllIcon + '' + checkAllText + ' ' : '')
+ + (options.showUncheckAll && uncheckAllText ? '' + options.uncheckAllIcon+'' + uncheckAllText + ' ' : '')
+ + (options.showFlipAll && flipAllText ? '' + options.flipAllIcon + '' + flipAllText + ' ' : '')
+ : (typeof headerOn === 'string' ? '' + headerOn + ' ' : '') );
+
+ this.$headerLinkContainer = $( document.createElement('ul') )
+ .addClass('ui-helper-reset')
+ .html(headerCtlsHTML + '' + options.closeIcon + ' ')
+ .appendTo($header);
// Holds the actual check boxes for inputs
- this.$checkboxContainer = $('')
- .addClass('ui-multiselect-checkboxes ui-helper-reset')
- .appendTo($menu);
+ this.$checkboxContainer = $( document.createElement('ul') )
+ .addClass('ui-multiselect-checkboxes ui-helper-reset')
+ .appendTo($menu);
+
+ // 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();
// build menu
this.refresh(true);
-
- // If this is a single select widget, add the appropriate class
- if(!$element[0].multiple) {
- $menu.addClass('ui-multiselect-single');
- }
},
// https://api.jqueryui.com/jquery.widget/#method-_init
_init: function() {
- var $element = this.element; // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/
- var $headerLinks = this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip');
+ var elSelect = this.element.get(0); // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/
- if(this.options.header === false) {
- this.$header.hide();
- }
- if(!!$element[0].multiple) {
- $headerLinks.show();
- } else {
- $headerLinks.hide();
- }
- if(this.options.autoOpen) {
+ if (!!this.options.header)
+ this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')[ !!elSelect.multiple ? 'show' : 'hide' ]();
+ else
+ this.$header.hide();
+
+ if(this.options.autoOpen)
this.open();
- }
- if(!!$element[0].disabled) {
+
+ if(elSelect.disabled)
this.disable();
- }
},
/*
@@ -187,48 +185,39 @@
*/
_makeOption: function(option) {
var title = option.title || null;
- var $element = this.element; // element is a jQuery object per http://api.jqueryui.com/jQuery.widget/
- var id = $element[0].id || this.multiselectID; // unique ID for the label & option tags
+ 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 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 isDisabled = option.disabled;
var isSelected = option.selected;
- var labelClasses = [ 'ui-corner-all' ];
- var liClasses = [];
- var o = this.options;
- var isMultiple = !!$element[0].multiple; // Pick up the select type from the underlying element
-
- if(isDisabled) {
- liClasses.push('ui-multiselect-disabled');
- labelClasses.push('ui-state-disabled');
- }
- if(option.className) {
- liClasses.push(option.className);
- }
- if(isSelected && !isMultiple) {
- labelClasses.push('ui-state-active');
- }
- var $item = $(" ").addClass(liClasses.join(' '));
- var $label = $(" ").attr({
- "for": inputID,
- "title": title
- }).addClass(labelClasses.join(' ')).appendTo($item);
- var $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);
-
- var $span = $(" ").text($(option).text());
- if($input.data("image-src")) {
- $span.prepend($(" ").attr({"src": $input.data("image-src")}));
- }
+ 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);
+
+ 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);
return $item;
@@ -237,19 +226,20 @@
// 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;
+ var self = this; // Save this => widget reference
+
$element.children().each(function() {
- var $this = $(this);
if(this.tagName === 'OPTGROUP') {
- var $optionGroup = $("").addClass('ui-multiselect-optgroup ' + this.className).appendTo($appendTo);
- if(self.options.groupColumns) {
- $optionGroup.addClass("ui-multiselect-columns");
- }
- $(" ").text(this.getAttribute('label')).appendTo($optionGroup);
- self._buildOptionList($this, $optionGroup);
- } else {
- var $listItem = self._makeOption(this).appendTo($appendTo);
+
+ 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);
+
+ self._buildOptionList($(this), $optionGroup);
}
+ else
+ $appendTo.append(self._makeOption(this));
});
},
@@ -257,72 +247,70 @@
// 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 $menu = this.$menu;
- var $headerLinks = this.$headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip');
- var $dropdown = $("").addClass('ui-multiselect-checkboxes ui-helper-reset');
+ 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;
// update header link container visibility if needed
- if (this.options.header) {
- if(!!$element[0].multiple) {
- $headerLinks.show();
- } else {
- $headerLinks.hide();
- }
- }
+ 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);
+ this._buildOptionList($element, $dropdown); // Rebuild the menu.
+ this.$menu.find('.ui-multiselect-checkboxes').replaceWith($dropdown); // Insert updated check list.
- this.$menu.find(".ui-multiselect-checkboxes").remove();
- this.$menu.append($dropdown);
-
- // cache some more useful elements
- this.$labels = $menu.find('label');
- this.$inputs = this.$labels.children('input');
+ this._updateCache(); // cache some more useful elements
this._setButtonWidth();
-
this.update(true);
// broadcast refresh event; useful for widgets
- if(!init) {
+ if (!init)
this._trigger('refresh');
- }
},
- // updates the button text. call refresh() to rebuild
+ // cache some more useful elements
+ _updateCache: function() {
+ 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 o = this.options;
+ var options = this.options;
+ var selectedList = options.selectedList;
+ var selectedText = options.selectedText;
var $inputs = this.$inputs;
+ var inputCount = $inputs.length;
var $checked = $inputs.filter(':checked');
var numChecked = $checked.length;
var value;
- if(numChecked === 0) {
- value = o.noneSelectedText;
- } else {
- if($.isFunction(o.selectedText)) {
- value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get());
- } else if(/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) {
- value = $checked.map(function() { return $(this).next().text(); }).get().join(o.selectedListSeparator);
- } else {
- value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length);
- }
+ if (numChecked) {
+ 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);
+ else
+ value = selectedText.replace('#', numChecked).replace('#', inputCount);
}
+ else
+ value = options.noneSelectedText;
- this._setButtonValue(value);
- if(isDefault) {
- this.$button[0].defaultValue = value;
- }
+ this._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();
},
// this exists as a separate method so that the developer
// can easily override it.
- _setButtonValue: function(value) {
- this.$buttonlabel.text(value);
+ _setButtonValue: function(value, isDefault) {
+ this.$buttonlabel[this.options.htmlButtonText ? 'html' : 'text'](value);
+
+ if (!!isDefault)
+ this.$button[0].defaultValue = value;
},
_bindButtonEvents: function() {
@@ -377,9 +365,9 @@
e.preventDefault();
var $this = $(this);
- var $inputs = $this.parent().find('input:visible:not(:disabled)');
+ var $inputs = $this.parent().find('input').filter(':visible:not(:disabled)');
var nodes = $inputs.get();
- var label = $this.text();
+ var label = this.textContent;
// trigger event and bail if the return is false
if(self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) {
@@ -405,21 +393,21 @@
}
})
.on('keydown.multiselect', 'label', function(e) {
- if(e.which === 82) {
+ if(e.which === 82)
return; //"r" key, often used for reload.
- }
- if(e.which > 111 && e.which < 124) {
+
+ if(e.which > 111 && e.which < 124)
return; //Keyboard function keys.
- }
+
e.preventDefault();
switch(e.which) {
case 9: // tab
if(e.shiftKey) {
self.$menu.find(".ui-state-hover").removeClass("ui-state-hover");
self.$header.find("li").last().find("a").focus();
- } else {
- self.close();
}
+ else
+ self.close();
break;
case 27: // esc
self.close();
@@ -434,25 +422,29 @@
case 32: // space
$(this).find('input')[0].click();
break;
- case 65: // Alt-A
- if(e.altKey) {
+ case 65: // Ctrl-A
+ if (e.altKey)
self.checkAll();
- }
break;
- case 85: // Alt-U
- if(e.altKey) {
+ case 85: // Ctrl-U
+ if (e.altKey)
self.uncheckAll();
- }
break;
}
})
.on('click.multiselect', 'input[type="checkbox"], input[type="radio"]', function(e) {
var $this = $(this);
var val = this.value;
- var optionText = $this.parent().find("span").text();
var checked = this.checked;
- var $tags = self.element.find('option'); // "element" is a jQuery object representing the underlying select
- var isMultiple = !!self.element[0].multiple;
+ var $element = self.element;
+ var options = self.options;
+ 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 inputCount = $inputs.length;
+ var numChecked = $inputs.filter(":checked").length;
+ 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) {
@@ -460,6 +452,22 @@
return;
}
+ if ( selectedMax && checked && ( $.isFunction(selectedMax) ? !!selectedMax.call(this, $inputs) : numChecked > selectedMax ) ) {
+ var saveText = options.selectedText;
+
+ // The following warning is shown in the button and then cleared after a second.
+ options.selectedText = "LIMIT OF " + (numChecked - 1) + " REACHED! ";
+ self.update();
+ setTimeout( function() {
+ options.selectedText = saveText;
+ self.update();
+ }, 1000 );
+
+ this.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();
@@ -469,11 +477,7 @@
// change state on the original option tags
$tags.each(function() {
- if(this.value === val) {
- this.selected = checked;
- } else if(!isMultiple) {
- this.selected = false;
- }
+ this.selected = (this.value === val ? checked : (isMultiple ? this.selected : false) );
});
// some additional single select-specific logic
@@ -486,7 +490,7 @@
}
// fire change on the select box
- self.element.trigger("change");
+ $element.trigger("change");
// setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
// http://bugs.jquery.com/ticket/3827
@@ -497,19 +501,18 @@
_bindHeaderEvents: function() {
var self = this;
// header links
- this.$header.on('click.multiselect', 'a', function(e) {
+ this.$header
+ .on('click.multiselect', 'a', function(e) {
var $this = $(this);
- if($this.hasClass('ui-multiselect-close')) {
- self.close();
- } else if($this.hasClass("ui-multiselect-all")) {
- self.checkAll();
- } else if($this.hasClass("ui-multiselect-none")) {
- self.uncheckAll();
- } else if($this.hasClass("ui-multiselect-flip")) {
- self.flipAll();
- }
- e.preventDefault();
- }).on('keydown.multiselect', 'a', function(e) {
+ $.each({'ui-multiselect-close' : 'close', 'ui-multiselect-all' : 'checkAll', 'ui-multiselect-none' : 'uncheckAll', 'ui-multiselect-flip' : 'flipAll'}, function( hdgClass, clickHandler) {
+ if ( $this.hasClass(hdgClass) ) {
+ self[ clickHandler ]();
+ e.preventDefault();
+ return false; // Break out of loop early
+ }
+ });
+ })
+ .on('keydown.multiselect', 'a', function(e) {
switch(e.which) {
case 27:
self.close();
@@ -536,22 +539,18 @@
// close each widget when clicking on any other element/anywhere else on the page
$doc.on('mousedown.' + self._namespaceID, function(event) {
var target = event.target;
+ var button = self.$button.get(0);
+ var menu = self.$menu.get(0);
- if(self._isOpen &&
- target !== self.$button[0] &&
- target !== self.$menu[0] &&
- !$.contains(self.$menu[0], target) &&
- !$.contains(self.$button[0], target)
- ) {
+ if ( self._isOpen && button !== target && !$.contains(button, target) && menu !== target && !$.contains(menu, target) )
self.close();
- }
});
// deal with form resets. the problem here is that buttons aren't
// 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).bind('reset.' + this._namespaceID, function() {
+ $(this.element[0].form).on('reset.' + this._namespaceID, function() {
setTimeout($.proxy(self.refresh, self), 10);
});
},
@@ -592,28 +591,30 @@
// set menu width
_setMenuWidth: function() {
- var m = this.$menu;
var width = (this.$button.outerWidth() <= 0) ? this._getMinWidth() : this.$button.outerWidth();
- m.outerWidth(this.options.menuWidth || width);
+ this.$menu.outerWidth(this.options.menuWidth || 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 headerHeight = this.$menu.children(".ui-multiselect-header:visible").outerHeight(true);
+ var $menu = this.$menu;
+ var headerHeight = $menu.children(".ui-multiselect-header:visible").outerHeight(true);
var ulHeight = 0;
- this.$menu.find(".ui-multiselect-checkboxes li, .ui-multiselect-checkboxes a").each(function(idx, li) {
+
+ $menu.find(".ui-multiselect-checkboxes li, .ui-multiselect-checkboxes a").each(function(idx, li) {
ulHeight += $(li).outerHeight(true);
});
+
if(ulHeight > this.options.height) {
- this.$menu.children(".ui-multiselect-checkboxes").css("overflow", "auto");
+ $menu.children(".ui-multiselect-checkboxes").css("overflow", "auto");
ulHeight = this.options.height;
- } else {
- this.$menu.children(".ui-multiselect-checkboxes").css("overflow", "hidden");
}
+ else
+ $menu.children(".ui-multiselect-checkboxes").css("overflow", "hidden");
- this.$menu.children(".ui-multiselect-checkboxes").height(ulHeight);
- this.$menu.height(ulHeight + headerHeight);
+ $menu.children(".ui-multiselect-checkboxes").height(ulHeight);
+ $menu.height(ulHeight + headerHeight);
},
// Resizes the menu, called every time the menu is opened
@@ -630,23 +631,23 @@
// select the first li that isn't an optgroup label / disabled
var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup):visible').first();
// we might have to jump to the next/previous option group
- if(!$next.length) {
+ if (!$next.length)
$next = $start.parents(".ui-multiselect-optgroup")[moveToLast ? "prev" : "next" ]();
- }
// if at the first/last element
- if(!$next.length) {
+ if (!$next.length) {
var $container = this.$menu.find('ul').last();
// move to the first/last
- this.$menu.find('label:visible')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover');
+ this.$menu.find('label').filter(':visible')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover');
// set scroll position
$container.scrollTop(moveToLast ? $container.height() : 0);
- } else {
- $next.find('label:visible')[ moveToLast ? "last" : "first" ]().trigger('mouseover');
}
+ else
+ $next.find('label').filter(':visible')[ moveToLast ? "last" : "first" ]().trigger('mouseover');
+
},
// This is an internal function to toggle the checked property and
@@ -657,23 +658,22 @@
return function() {
var state = (flag === '!') ? !this[prop] : flag;
- if( !this.disabled ) {
+ if( !this.disabled )
this[ prop ] = state;
- }
- if(state) {
+ if (state)
this.setAttribute('aria-' + prop, true);
- } else {
+ else
this.removeAttribute('aria-' + prop);
- }
+
};
},
// Toggles checked state on either an option group or all inputs
_toggleChecked: function(flag, group) {
- var $inputs = (group && group.length) ? group : this.$inputs;
var self = this;
- var $element = this.element; // element is a jQuery object
+ var $element = this.element; // element is a jQuery object
+ var $inputs = (group && group.length) ? group : this.$inputs;
// toggle state on inputs
$inputs.each(this._toggleState('checked', flag));
@@ -684,26 +684,24 @@
// update button text
this.update();
- // gather an array of the values that actually changed
+ // Create a plain object of the values that actually changed
var values = {};
$inputs.each(function() {
values[this.value] = true;
});
// toggle state on original option tags
- $element.selectedIndex = -1;
+ $element[0].selectedIndex = -1;
$element
.find('option')
.each(function() {
- if(!this.disabled && values[this.value]) {
+ if(!this.disabled && values[this.value])
self._toggleState('selected', flag).call(this);
- }
});
// trigger the change event on the select
- if($inputs.length) {
+ if ($inputs.length)
$element.trigger("change");
- }
},
// Toggle disable state on the widget and underlying select
@@ -738,7 +736,7 @@
}
}
- this.element.prop({
+ this.element.prop({ // element is a jQuery object
'disabled':flag,
'aria-disabled':flag
});
@@ -749,50 +747,42 @@
var self = this;
var $button = this.$button;
var $menu = this.$menu;
+ var $header = this.$header;
+ var $labels = this.$labels;
var speed = this.speed;
- var o = this.options;
- var args = [];
+ var options = this.options;
+ var effect = options.show;
+ var $container = $menu.find('.ui-multiselect-checkboxes');
// 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) {
+ if(this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen)
return;
- }
-
- var $container = $menu.find('.ui-multiselect-checkboxes');
- var effect = o.show;
// figure out opening effects/speeds
- if($.isArray(o.show)) {
- effect = o.show[0];
- speed = o.show[1] || self.speed;
- }
-
- // if there's an effect, assume jQuery UI is in use
- // build the arguments to pass to show()
- if(effect) {
- args = [ effect, speed ];
+ if ($.isArray(options.show)) {
+ effect = options.show[0];
+ speed = options.show[1] || self.speed;
}
// set the scroll of the checkbox container
$container.scrollTop(0);
// show the menu, maybe with a speed/effect combo
- $.fn.show.apply($menu, args);
+ // if there's an effect, assume jQuery UI is in use
+ $.fn.show.apply($menu, effect ? [ effect, speed ] : []);
this._resizeMenu();
// positon
this.position();
-
// select the first not disabled option or the filter input if available
- var filter = this.$header.find(".ui-multiselect-filter");
- if(filter.length) {
+ var filter = $header.find(".ui-multiselect-filter");
+ if (filter.length)
filter.first().find('input').trigger('focus');
- } else if(this.$labels.length){
- this.$labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
- } else {
- this.$header.find('a').first().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;
@@ -801,30 +791,25 @@
// close the menu
close: function() {
- if(this._trigger('beforeclose') === false) {
+ if (this._trigger('beforeclose') === false)
return;
- }
- var o = this.options;
- var effect = o.hide;
+ var options = this.options;
+ var effect = options.hide;
var speed = this.speed;
- var args = [];
+ var $button = this.$button;
// figure out closing effects/speeds
- if($.isArray(o.hide)) {
- effect = o.hide[0];
- speed = o.hide[1] || this.speed;
- }
-
- if(effect) {
- args = [ effect, speed ];
+ if ($.isArray(options.hide)) {
+ effect = options.hide[0];
+ speed = options.hide[1] || this.speed;
}
- $.fn.hide.apply(this.$menu, args);
- this.$button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
+ $.fn.hide.apply(this.$menu, effect ? [ effect, speed ] : []);
+ $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
this._isOpen = false;
this._trigger('close');
- this.$button.trigger('focus');
+ $button.trigger('focus');
},
enable: function() {
@@ -842,7 +827,7 @@
uncheckAll: function() {
this._toggleChecked(false);
- if ( !this.element[0].multiple ) // element is a jQuery object
+ if ( !this.element[0].multiple ) // element is a jQuery object
this.element[0].selectedIndex = -1; // Forces the underlying single-select to have no options selected.
this._trigger('uncheckAll');
},
@@ -902,37 +887,38 @@
* groupLabel: Option Group to add the option to
*/
addOption: function(attributes, text, groupLabel) {
- var $option = $(" ").attr(attributes).text(text);
+ var $element = this.element;
+ var $menu = this.$menu;
+ var $option = $( document.createElement('option') ).attr(attributes).text(text);
var optionNode = $option.get(0);
- if(groupLabel) {
- this.element.children("OPTGROUP").filter(function() {
- return $(this).prop("label") === groupLabel;
- }).append($option);
- this.$menu.find(".ui-multiselect-optgroup").filter(function() {
- return $(this).find("a").text() === groupLabel;
- }).append(this._makeOption(optionNode));
- } else {
- this.element.append($option);
- this.$menu.find(".ui-multiselect-checkboxes").append(this._makeOption(optionNode));
+
+ if (groupLabel) {
+ $element.children("OPTGROUP").filter(function() {
+ return $(this).prop("label") === groupLabel;
+ }).append($option);
+ $menu.find(".ui-multiselect-optgroup").filter(function() {
+ return $(this).find("a").text() === groupLabel;
+ }).append(this._makeOption(optionNode));
}
- //update cached elements
- this.$labels = this.$menu.find('label');
- this.$inputs = this.$labels.children('input');
+ else {
+ $element.append($option);
+ $menu.find(".ui-multiselect-checkboxes").append(this._makeOption(optionNode));
+ }
+
+ this._updateCache(); //update cached elements
},
removeOption: function(value) {
- if(!value) {
+ if (!value)
return;
- }
this.element.find("option[value=" + value + "]").remove();
this.$labels.find("input[value=" + value + "]").parents("li").remove();
- //update cached elements
- this.$labels = this.$menu.find('label');
- this.$inputs = this.$labels.children('input');
+ 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",
@@ -943,11 +929,11 @@
pos.at = this.options.position.at || pos.at;
pos.of = this.options.position.of || pos.of;
}
- if($.ui && $.ui.position) {
+ if($.ui && $.ui.position)
this.$menu.position(pos);
- } else {
+ else {
pos = this.$button.position();
- pos.top += this.$button.outerHeight(false);
+ pos.top += this._savedButtonHeight;
this.$menu.offset(pos);
}
},
@@ -958,9 +944,9 @@
switch(key) {
case 'header':
- if(typeof value === 'boolean') {
+ if (typeof value === 'boolean')
this.$header[value ? 'show' : 'hide']();
- } else if(typeof value === 'string') {
+ else if(typeof value === 'string') {
this.$headerLinkContainer.children("li:not(:last-child)").remove();
this.$headerLinkContainer.prepend("" + value + " ");
}
@@ -968,12 +954,12 @@
case 'checkAllText':
case 'uncheckAllText':
case 'flipAllText':
- $menu.find('a.ui-multiselect-' + {checkAllText: 'all', uncheckAllText: 'none', flipAllText: 'flip'}[key] + ' span').eq(-1).text(value); // eq(-1) finds the last span
+ $menu.find('a.ui-multiselect-' + {'checkAllText': 'all', 'uncheckAllText': 'none', 'flipAllText': 'flip'}[key] + ' span').eq(-1).text(value); // eq(-1) finds the last span
break;
case 'checkAllIcon':
case 'uncheckAllIcon':
case 'flipAllIcon':
- $menu.find('a.ui-multiselect-' + {checkAllIcon: 'all', uncheckAllIcon: 'none', flipAllIcon: 'flip'}[key] + ' span').eq(0).replaceWith(value); // eq(0) finds the first span
+ $menu.find('a.ui-multiselect-' + {'checkAllIcon': 'all', 'uncheckAllIcon': 'none', 'flipAllIcon': 'flip'}[key] + ' span').eq(0).replaceWith(value); // eq(0) finds the first span
break;
case 'openIcon':
$menu.find('span.ui-multiselect-open').html(value);
@@ -993,19 +979,20 @@
break;
case 'selectedText':
case 'selectedList':
+ case 'selectedMax':
case 'noneSelectedText':
- this.options[key] = value; // these all needs to update immediately for the update() call
+ this.options[key] = value; // these all need to update immediately for the update() call
this.update();
break;
case 'classes':
$menu.add(this.$button).removeClass(this.options.classes).addClass(value);
break;
case 'multiple':
- var el_0 = this.element[0];
- if (!!el_0.multiple != value) {
+ var $element = this.element;
+ if (!!$element[0].multiple != value) {
$menu.toggleClass('ui-multiselect-multiple', value);
$menu.toggleClass('ui-multiselect-single', !value);
- el_0.multiple = value;
+ $element[0].multiple = value;
this.uncheckAll();
this.refresh();
}
diff --git a/tests/unit/core.js b/tests/unit/core.js
index 1830d95..1bb244e 100644
--- a/tests/unit/core.js
+++ b/tests/unit/core.js
@@ -2,192 +2,213 @@ var el;
var body = document.body;
function button(){
- return el.next();
+ return el.next();
}
function menu(){
- return el.multiselect("widget");
+ return el.multiselect("widget");
}
function header(){
- return menu().find('.ui-multiselect-header');
+ return menu().find('.ui-multiselect-header');
}
QUnit.done = function(){
- $("select").hide();
+ $("select").hide();
};
(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' );
- ok( $header.find('a.ui-multiselect-close').css('display') !== 'none', 'close link is visible' );
- ok( menu().is(':hidden'), 'menu is hidden');
- ok( el.is(":hidden"), 'the original select is hidden');
- ok( el.attr('tabIndex') == 2, 'button inherited the correct tab index');
- el.multiselect("destroy");
- });
-
- test("form submission", function(){
- expect(3);
-
- var form = $('').appendTo(body),
- data;
-
- el = $('foo bar ')
- .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 = $('foo bar baz bax ')
- .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 = $('foo bar baz ')
- .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 = $('foo bar ')
- .appendTo(form)
- .multiselect({ noneSelectedText: noneSelected })
- .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");
- el.multiselect('destroy');
- form.remove();
- start();
- }, 10);
- });
-
- asyncTest("form reset, pre-selected options", function(){
- expect(2);
-
- var form = $('').appendTo(body);
-
- el = $('foo bar ')
- .appendTo(form)
- .multiselect({ selectedText: '# of # selected' })
- .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" );
- el.multiselect('destroy');
- form.remove();
- start();
- }, 10);
- });
-
+
+ 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' );
+ ok( $header.find('a.ui-multiselect-close').css('display') !== 'none', 'close link is visible' );
+ ok( menu().is(':hidden'), 'menu is hidden');
+ ok( el.is(":hidden"), 'the original select is hidden');
+ 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 = $('foo bar baz bax ')
+ .appendTo(form)
+ .multiselect();
+
+ el2 = $('foo bar baz bax ')
+ .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');
+
+ el1.multiselect('destroy');
+ el2.multiselect('destroy');
+ form.remove();
+ });
+
+ test("form submission", function(){
+ expect(3);
+
+ var form = $('').appendTo(body),
+ data;
+
+ el = $('foo bar ')
+ .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 = $('foo bar baz bax ')
+ .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 = $('foo bar baz ')
+ .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 = $('foo bar ')
+ .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");
+ el.multiselect('destroy');
+ form.remove();
+ start();
+ }, 10);
+ });
+
+ asyncTest("form reset, pre-selected options", function(){
+ expect(2);
+
+ var form = $('').appendTo(body);
+
+ el = $('foo bar ')
+ .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" );
+ el.multiselect('destroy');
+ form.remove();
+ start();
+ }, 10);
+ });
+
})(jQuery);
diff --git a/tests/unit/options.js b/tests/unit/options.js
index 5ca530a..14577c3 100644
--- a/tests/unit/options.js
+++ b/tests/unit/options.js
@@ -41,7 +41,8 @@
var numOptions = $("select option").length;
el = $("select").multiselect({
- selectedText: '# of # selected'
+ selectedText: '# of # selected',
+ selectedList: 0
});
el.multiselect("checkAll");
@@ -67,7 +68,8 @@
var html = 'foo "with quotes" bar baz ';
el = $(html).appendTo("body").multiselect({
- selectedList: 3
+ selectedList: 3,
+ selectedListSeparator: ', '
});
el.multiselect("checkAll");
@@ -75,7 +77,8 @@
el.multiselect("destroy").remove();
el = $(html).appendTo("body").multiselect({
- selectedList: 2
+ selectedList: 2,
+ selectedListSeparator: ', '
});
el.multiselect("checkAll");
@@ -83,6 +86,25 @@
el.multiselect("destroy").remove();
});
+ test("selectedMax", function(){
+ expect(1);
+
+ var html = 'foo "with quotes" bar baz ';
+
+ 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();
@@ -91,7 +113,8 @@
checkboxes;
el = $(html).appendTo(body).multiselect({
- selectedList: 2
+ selectedList: 2,
+ selectedListSeparator: ', '
});
checkboxes = el.multiselect("widget").find(":checkbox");
@@ -165,11 +188,12 @@
var outerWidth = button().outerWidth();
ok( minWidth !== outerWidth, 'changing value through api to '+minWidth+' (too small), outerWidth is actually ' + outerWidth);
+ // Reference: https://www.wired.com/2010/12/why-percentage-based-designs-dont-work-in-every-browser/
minWidth = "50%";
el.multiselect("option", "minWidth", minWidth);
- outerWidth = Math.floor(button().outerWidth());
- var halfParent = Math.floor(el.parent().outerWidth()/2);
- ok(outerWidth === halfParent, 'changing value to 50%');
+ var outerWidthX2 = Math.floor(button().outerWidth() * 2); // Double to reduce chance of fractions
+ var parentWidth = Math.floor(el.parent().outerWidth());
+ ok(Math.abs(outerWidthX2 - parentWidth) <= 1, 'changing value to 50%'); // Off by 1 is ok due to floating point rounding discrepancies between browsers.
minWidth = "351px";
el.multiselect("option", "minWidth", minWidth);