diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index c389bad..cf7a7b3 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -46,7 +46,7 @@ noneSelectedText: 'Select options', // (str | null) The text to show in the button where nothing is selected. Set to null to use the native select's placeholder text. 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. + maxSelected: null, // (int | null) If selected count > maxSelected, 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. @@ -104,7 +104,7 @@ var options = this.options; var classes = options.classes; var headerOn = options.header; - var checkAllText = options.checkAllText; + var checkAllText = options.maxSelected ? null : options.checkAllText; // Do an extend here to address icons missing from options.iconSet--missing icons default to those in defaultIcons. var iconSet = $.extend({}, defaultIcons, options.iconSet || {}); var uncheckAllText = options.uncheckAllText; @@ -119,10 +119,9 @@ this.speed = $.fx.speeds._default; this._isOpen = false; - // create a unique namespace for events that the widget - // factory cannot unbind automatically. Use eventNamespace if on - // jQuery UI 1.9+, and otherwise fallback to a custom string. - this._namespaceID = this.eventNamespace || ('multiselect' + multiselectID); + // Create a unique namespace for events that the widget + // factory cannot unbind automatically. + this._namespaceID = this.eventNamespace.slice(1); // bump unique ID after assigning it to the widget instance this.multiselectID = multiselectID++; @@ -511,6 +510,12 @@ var $inputs = $this.next('ul').find('input').filter(':visible:not(:disabled)'); var nodes = $inputs.get(); var label = this.textContent; + + // if maxSelected is in use, cannot exceed it + var maxSelected = self.options.maxSelected; + if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) { + return; + } // trigger before callback and bail if the return is false if (self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) { @@ -593,11 +598,10 @@ var $tags = $element.find('option'); var isMultiple = $element[0].multiple; var $allInputs = self.$inputs; - var inputCount = $allInputs.length; var numChecked = $allInputs.filter(":checked").length; var options = self.options; var optionText = $input.parent().find("span")[options.htmlOptionText ? 'html' : 'text'](); - var selectedMax = options.selectedMax; + var maxSelected = options.maxSelected; // bail if this input is disabled or the event is cancelled if (input.disabled || self._trigger('click', e, { value: val, text: optionText, checked: checked }) === false) { @@ -605,18 +609,10 @@ return; } - if ( selectedMax && checked - && ( typeof selectedMax === 'function' ? !!selectedMax.call(input, $allInputs) : numChecked > selectedMax ) ) { - var saveText = options.selectedText; - - // The following warning is shown in the button and then cleared after a second. - options.selectedText = "
LIMIT OF " + (numChecked - 1) + " REACHED!
"; - self.update(); - setTimeout( function() { - options.selectedText = saveText; - self.update(); - }, 1000 ); - + if ( maxSelected && checked && numChecked > maxSelected) { + if ( self._trigger('maxselected', e, { labels: self.$labels, inputs: $allInputs }) !== false ) { + self.buttonMessage("
LIMIT OF " + (numChecked - 1) + " REACHED!
"); + } input.checked = false; e.preventDefault(); return false; @@ -1280,22 +1276,47 @@ }, checkAll: function(e) { + this._trigger('beforeCheckAll'); this._toggleChecked(true); this._trigger('checkAll'); }, uncheckAll: function() { + this._trigger('beforeUncheckAll'); + this._toggleChecked(false); if ( !this.element[0].multiple ) { // Forces the underlying single-select to have no options selected. this.element[0].selectedIndex = -1; } + this._trigger('uncheckAll'); }, flipAll: function() { - this._toggleChecked('!'); - this._trigger('flipAll'); + this._trigger('beforeFlipAll'); + + var maxSelected = this.options.maxSelected; + if (maxSelected === null || maxSelected > (this.$inputs.length - this.$inputs.filter(':checked').length) ) { + this._toggleChecked('!'); + this._trigger('flipAll'); + } + else { + this.buttonMessage("
Flip All Not Permitted.
"); + } + }, + + /** + * Flashes a message in the button caption for 1 second. + * Useful for very short warning messages to the user. + * @param {string} HTML message to show in the button. + */ + buttonMessage: function(message) { + var self = this; + self.$buttonlabel.html(message); + setTimeout( function() { + self.update(); + }, 1000 ); }, /** @@ -1347,6 +1368,13 @@ return this.$menu; }, + /** + * @returns {string} namespaceID for use with external event handlers. + */ + getNamespaceID: function() { + return this._namespaceID; + }, + /** * @returns {object} jQuery object for button */ @@ -1466,12 +1494,16 @@ 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 + if (key !== 'checkAllText' || !this.options.maxSelected) { + $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 + if (key !== 'checkAllIcon' || !this.options.maxSelected) { + $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); @@ -1491,7 +1523,7 @@ break; case 'selectedText': case 'selectedList': - case 'selectedMax': + case 'maxSelected': case 'noneSelectedText': case 'selectedListSeparator': this.options[key] = value; // these all need to update immediately for the update() call diff --git a/tests/unit/options.js b/tests/unit/options.js index cb2889f..cff7899 100644 --- a/tests/unit/options.js +++ b/tests/unit/options.js @@ -82,11 +82,11 @@ el.multiselect("destroy").remove(); }); - QUnit.test("selectedMax", function (assert) { + QUnit.test("maxSelected", function (assert) { var html = ''; el = $(html).appendTo("body").multiselect({ - selectedMax: 2 + maxSelected: 2 }); var checkboxes = el.multiselect("widget").find(":checkbox"); @@ -94,7 +94,7 @@ checkboxes.eq(1).trigger('click'); checkboxes.eq(2).trigger('click'); - assert.equal(menu().find("input").filter(":checked").length, 2, 'after clicking each checkbox, count of checked restored to selectedMax of 2'); + assert.equal(menu().find("input").filter(":checked").length, 2, 'after clicking each checkbox, count of checked restored to maxSelected of 2'); el.multiselect("destroy").remove(); }); @@ -202,7 +202,7 @@ width = "3in"; el.multiselect("option", "menuWidth", width).multiselect('refresh'); assert.equal(menu().parent().find(".ui-multiselect-menu").outerWidth(), 3 * 96.0, 'menuWidth supports strings suffixed with "in" unit as well as integer "px" values'); - + el.multiselect("destroy"); });