From 07fe0204898ae0b66ea4338c236aa05cd630cc1e Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Thu, 8 Feb 2018 17:47:08 -0800 Subject: [PATCH 1/5] Max selected and misc 1. Fix/finish maxSelected functionality. 2. Fix related test. 3. Fix namespace period issue. 4. Add new event triggers requested by open PR. --- src/jquery.multiselect.js | 72 +++++++++++++++++++++++++++------------ tests/unit/options.js | 8 ++--- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index c389bad..63fe473 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 | function) If selected count > maxSelected 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. @@ -122,7 +122,7 @@ // 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); + this._namespaceID = this.eventNamespace.slice(1) || ('multiselect' + multiselectID); // bump unique ID after assigning it to the widget instance this.multiselectID = multiselectID++; @@ -593,11 +593,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 +604,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 +1271,54 @@ }, checkAll: function(e) { - this._toggleChecked(true); - this._trigger('checkAll'); + this._trigger('beforeCheckAll'); + + var maxSelected = this.options.maxSelected; + if (maxSelected === null || maxSelected > this.$inputs.length) { + this._toggleChecked(true); + this._trigger('checkAll'); + } + else { + this.buttonMessage("
Check All Not Permitted.
"); + } }, 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 +1370,13 @@ return this.$menu; }, + /** + * @returns {string} namespaceID for use with external event handlers. + */ + getNamespaceID: function() { + return this._namespaceID; + }, + /** * @returns {object} jQuery object for button */ @@ -1491,7 +1521,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"); }); From 18264668dc235d9b0c0ccc75e40305395f0eedf2 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Thu, 8 Feb 2018 18:52:41 -0800 Subject: [PATCH 2/5] Requested Changes --- src/jquery.multiselect.js | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index 63fe473..9df86b0 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -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.slice(1) || ('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++; @@ -1272,15 +1271,8 @@ checkAll: function(e) { this._trigger('beforeCheckAll'); - - var maxSelected = this.options.maxSelected; - if (maxSelected === null || maxSelected > this.$inputs.length) { - this._toggleChecked(true); - this._trigger('checkAll'); - } - else { - this.buttonMessage("
Check All Not Permitted.
"); - } + this._toggleChecked(true); + this._trigger('checkAll'); }, uncheckAll: function() { @@ -1291,7 +1283,7 @@ // Forces the underlying single-select to have no options selected. this.element[0].selectedIndex = -1; } - + this._trigger('uncheckAll'); }, @@ -1496,12 +1488,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); From e21945d52ef02effba72dad557797b468ed498a5 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Thu, 8 Feb 2018 19:20:53 -0800 Subject: [PATCH 3/5] Fix oversight --- src/jquery.multiselect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index 9df86b0..aa5fc9d 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. - maxSelected: null, // (int | function) If selected count > maxSelected 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. From e7d4b1e370cb6ef394536675cb22d5dc79f4132c Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Fri, 9 Feb 2018 00:22:42 -0800 Subject: [PATCH 4/5] Clicking optgroup w/ maxSelected in use --- src/jquery.multiselect.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index aa5fc9d..e32eb86 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -510,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 = this.options.maxSelected; + if (maxSelected && (this.$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) { From d682b5ba0041565d40c3c5e223e8d46f54cf0910 Mon Sep 17 00:00:00 2001 From: Steve James <4stevejames@gmail.com> Date: Sat, 10 Feb 2018 16:15:50 -0800 Subject: [PATCH 5/5] Fixed optgroups maxSelected checking --- src/jquery.multiselect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js index e32eb86..cf7a7b3 100644 --- a/src/jquery.multiselect.js +++ b/src/jquery.multiselect.js @@ -512,8 +512,8 @@ var label = this.textContent; // if maxSelected is in use, cannot exceed it - var maxSelected = this.options.maxSelected; - if (maxSelected && (this.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) { + var maxSelected = self.options.maxSelected; + if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) { return; }