From 804cf1a7e13be92fc54f13e92077084cfa490d79 Mon Sep 17 00:00:00 2001
From: Steve James <4stevejames@gmail.com>
Date: Wed, 10 Jan 2018 00:39:01 -0800
Subject: [PATCH] Improve options for header text/links and add Flip All
feature
This breaks out the icons to allow them to be something other than the fixed size jQuery UI icons--they could be HTML entities or Font-Awesome icons instead.
Additionally, this adds support for a "flip all" feature that toggles the state of every check box in the menu. This is useful for quickly setting a lot of check-boxes in a long list.
Lastly, tests have been added and modified to accommodate these changes.
---
src/jquery.multiselect.js | 172 ++++++++++++++++++++++++--------------
tests/unit/options.js | 30 ++++++-
2 files changed, 134 insertions(+), 68 deletions(-)
diff --git a/src/jquery.multiselect.js b/src/jquery.multiselect.js
index 78e650d..7bd32d5 100644
--- a/src/jquery.multiselect.js
+++ b/src/jquery.multiselect.js
@@ -29,14 +29,20 @@
height: 175,
minWidth: 225,
classes: '',
- checkAllText: 'Check all',
- uncheckAllText: 'Uncheck all',
- noneSelectedText: 'Select options',
+ openIcon: '', // Scaleable HTML Entities or Font-Awesome icons can be used here instead of the default jQuery UI icons.
+ closeIcon: '',
+ checkAllIcon: '',
+ uncheckAllIcon: '',
+ flipAllIcon: '',
+ checkAllText: 'Check all', // If blank or null, link not shown.
+ uncheckAllText: 'Uncheck all', // If blank or null, link not shown.
+ flipAllText: 'Flip all', // If blank or null, link not shown.
showCheckAll: true,
showUncheckAll: true,
+ showFlipAll: false,
+ noneSelectedText: 'Select options',
selectedText: '# selected',
selectedList: 0,
- closeIcon: 'ui-icon-circle-close',
show: null,
hide: null,
autoOpen: false,
@@ -48,10 +54,9 @@
groupColumns: 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
+ // Otherwise appends to the body
_getAppendEl: function() {
var element = this.options.appendTo;
if(element) {
@@ -66,7 +71,7 @@
return element;
},
- // Performs the initial creation of the widget
+ // Performs the initial creation of the widget
_create: function() {
var el = this.element;
var o = this.options;
@@ -82,8 +87,8 @@
// bump unique ID after assigning it to the widget instance
this.multiselectID = multiselectID++;
- // The button that opens the widget menu
- var button = (this.button = $(''))
+ // The button that opens the widget menu
+ var button = (this.button = $(''))
.addClass('ui-multiselect ui-widget ui-state-default ui-corner-all ' + o.classes)
.attr({ 'title':el.attr('title'), 'tabIndex':el.attr('tabIndex'), 'id': el.attr('id') ? el.attr('id') + '_ms' : null })
.prop('aria-haspopup', true)
@@ -93,28 +98,31 @@
.html(o.noneSelectedText)
.appendTo(button);
- // This is the menu that will hold all the options
+ // This is the menu that will hold all the options
this.menu = $('
')
.addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all ' + o.classes)
.appendTo(this._getAppendEl());
- // Menu header to hold controls for the menu
+ // Menu header to hold controls for the menu
this.header = $('')
.addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
.appendTo(this.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
+ // 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 = $('
')
.appendTo(this.header);
- // Holds the actual check boxes for inputs
+ // Holds the actual check boxes for inputs
var checkboxContainer = (this.checkboxContainer = $('
+ */
_makeOption: function(option) {
var title = option.title ? option.title : null;
var value = option.value;
- var el = this.element;
- var id = el.attr('id') || this.multiselectID; // unique ID for the label & option tags
+ var el = this.element;
+ var id = el.attr('id') || this.multiselectID; // unique ID for the label & option tags
var inputID = 'ui-multiselect-' + this.multiselectID + '-' + (option.id || id + '-option-' + this.inputIdCounter++);
var isDisabled = option.disabled;
var isSelected = option.selected;
var labelClasses = [ 'ui-corner-all' ];
var liClasses = [];
var o = this.options;
- var isMultiple = !!el[0].multiple; // Pick up the select type from the underlying element
+ var isMultiple = !!el[0].multiple; // Pick up the select type from the underlying element
if(isDisabled) {
liClasses.push('ui-multiselect-disabled');
@@ -212,8 +229,8 @@
return $item;
},
- // Builds a menu item for each option in the underlying select
- // Option groups are built here as well
+ // 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;
element.children().each(function() {
@@ -232,8 +249,8 @@
},
- // Refreshes the widget to pick up changes to the underlying select
- // Rebuilds the menu, sets button width
+ // Refreshes the widget to pick up changes to the underlying select
+ // Rebuilds the menu, sets button width
refresh: function(init) {
var self = this;
var el = this.element;
@@ -247,9 +264,9 @@
// update header link container visibility if needed
if (this.options.header) {
if(!!el[0].multiple) {
- this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').show();
+ this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip').show();
} else {
- this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();
+ this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip').hide();
}
}
@@ -414,15 +431,15 @@
self._traverse(e.which, this);
break;
case 13: // enter
- case 32:
+ case 32: // space
$(this).find('input')[0].click();
break;
- case 65:
+ case 65: // Alt-A
if(e.altKey) {
self.checkAll();
}
break;
- case 85:
+ case 85: // Alt-U
if(e.altKey) {
self.uncheckAll();
}
@@ -435,7 +452,7 @@
var optionText = $this.parent().find("span").text();
var checked = this.checked;
var tags = self.element.find('option');
- var isMultiple = !!self.element[0].multiple;
+ var isMultiple = !!self.element[0].multiple;
// 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) {
@@ -488,6 +505,8 @@
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) {
@@ -538,7 +557,7 @@
},
// Determines the minimum width for the button and menu
- // Can be a number, a digit string, or a percentage
+ // Can be a number, a digit string, or a percentage
_getMinWidth: function() {
var minVal = this.options.minWidth;
var width = 0;
@@ -558,7 +577,7 @@
}
return width;
},
-
+
// set button width
_setButtonWidth: function() {
var width = this.element.outerWidth();
@@ -578,8 +597,8 @@
m.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
+ // 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 ulHeight = 0;
@@ -597,7 +616,7 @@
this.menu.height(ulHeight + headerHeight);
},
- // Resizes the menu, called every time the menu is opened
+ // Resizes the menu, called every time the menu is opened
_resizeMenu: function() {
this._setMenuWidth();
this._setMenuHeight();
@@ -636,19 +655,21 @@
// The context of this function should be a checkbox; do not proxy it.
_toggleState: function(prop, flag) {
return function() {
- if(!this.disabled) {
- this[ prop ] = flag;
+ var state = (flag === '!') ? !this[prop] : flag;
+
+ if( !this.disabled ) {
+ this[ prop ] = state;
}
- if(flag) {
- this.setAttribute('aria-selected', true);
+ if(state) {
+ this.setAttribute('aria-' + prop, true);
} else {
- this.removeAttribute('aria-selected');
+ this.removeAttribute('aria-' + prop);
}
};
},
- // Toggles checked state on either an option group or all inputs
+ // 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;
@@ -657,7 +678,7 @@
$inputs.each(this._toggleState('checked', flag));
// Give the first input focus
- $inputs.eq(0).focus();
+ $inputs.eq(0).focus();
// update button text
this.update();
@@ -669,7 +690,7 @@
});
// toggle state on original option tags
- this.element.selectedIndex = -1;
+ this.element.selectedIndex = -1;
this.element
.find('option')
.each(function() {
@@ -684,7 +705,7 @@
}
},
- // Toggle disable state on the widget and underlying select
+ // Toggle disable state on the widget and underlying select
_toggleDisabled: function(flag) {
this.button.prop({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
@@ -821,11 +842,16 @@
uncheckAll: function() {
this._toggleChecked(false);
- if ( !this.element[0].multiple )
- this.element[0].selectedIndex = -1; // Forces the underlying single-select to have no options selected.
+ if ( !this.element[0].multiple )
+ this.element[0].selectedIndex = -1; // Forces the underlying single-select to have no options selected.
this._trigger('uncheckAll');
},
+ flipAll: function() {
+ this._toggleChecked('!');
+ this._trigger('flipAll');
+ },
+
getChecked: function() {
return this.menu.find('input').filter(':checked');
},
@@ -869,7 +895,7 @@
return this.labels;
},
- /*
+ /*
* Adds an option to the widget and underlying select
* attributes: Attributes hash to add to the option
* text: text of the option
@@ -940,11 +966,29 @@
}
break;
case 'checkAllText':
- menu.find('a.ui-multiselect-all span').eq(-1).text(value);
+ menu.find('a.ui-multiselect-all span').eq(-1).text(value); // eq(-1) finds the last span
break;
case 'uncheckAllText':
menu.find('a.ui-multiselect-none span').eq(-1).text(value);
break;
+ case 'flipAllText':
+ menu.find('a.ui-multiselect-flip span').eq(-1).text(value);
+ break;
+ case 'checkAllIcon':
+ menu.find('a.ui-multiselect-all span').eq(0).replaceWith(value);
+ break;
+ case 'uncheckAllIcon':
+ menu.find('a.ui-multiselect-none span').eq(0).replaceWith(value);
+ break;
+ case 'flipAllIcon':
+ menu.find('a.ui-multiselect-flip span').eq(0).replaceWith(value);
+ break;
+ case 'openIcon':
+ menu.find('span.ui-multiselect-open').html(value);
+ break;
+ case 'closeIcon':
+ menu.find('a.ui-multiselect-close').html(value);
+ break;
case 'height':
this.options[key] = value;
this._setMenuHeight();
@@ -965,14 +1009,14 @@
menu.add(this.button).removeClass(this.options.classes).addClass(value);
break;
case 'multiple':
- var el_0 = this.element[0];
- if (!!el_0.multiple != value) {
- menu.toggleClass('ui-multiselect-multiple', value);
- menu.toggleClass('ui-multiselect-single', !value);
- el_0.multiple = value;
- this.uncheckAll();
- this.refresh();
- }
+ var el_0 = this.element[0];
+ if (!!el_0.multiple != value) {
+ menu.toggleClass('ui-multiselect-multiple', value);
+ menu.toggleClass('ui-multiselect-single', !value);
+ el_0.multiple = value;
+ this.uncheckAll();
+ this.refresh();
+ }
break;
case 'position':
this.position();
diff --git a/tests/unit/options.js b/tests/unit/options.js
index 4d38352..3388170 100644
--- a/tests/unit/options.js
+++ b/tests/unit/options.js
@@ -198,7 +198,7 @@
expect(2);
var text = "foo";
- el = $("select").multiselect({ checkAllText:text });
+ el = $("select").multiselect({ checkAllText:text, showCheckAll:true });
equals( text, menu().find(".ui-multiselect-all").text(), 'check all link reads '+text );
// set through option
@@ -213,7 +213,7 @@
expect(2);
var text = "foo";
- el = $("select").multiselect({ uncheckAllText:text });
+ el = $("select").multiselect({ uncheckAllText:text, showUncheckAll:true });
equals( text, menu().find(".ui-multiselect-none").text(), 'check all link reads '+text );
// set through option
@@ -224,6 +224,21 @@
el.multiselect("destroy");
});
+ test("flipAllText", function(){
+ expect(2);
+ var text = "foo";
+
+ el = $("select").multiselect({ flipAllText:text, showFlipAll:true });
+ equals( text, menu().find(".ui-multiselect-flip").text(), 'flip all link reads '+text );
+
+ // set through option
+ text = "bar";
+ el.multiselect("option","flipAllText","bar");
+ equals( text, menu().find(".ui-multiselect-flip").text(), 'changing value through api to '+text );
+
+ el.multiselect("destroy");
+ });
+
test("autoOpen", function(){
expect(2);
@@ -352,11 +367,18 @@
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 = "ui-icon-search";
+ var icon = '';
el = $("select").multiselect({ autoOpen:true, closeIcon:icon });
- equals(menu().find(".ui-multiselect-close").find("."+icon).length, 1);
+ equals(menu().find(".ui-multiselect-close").find(".ui-icon-search").length, 1);
el.multiselect("destroy");
});
test("selectedListSeparator", function(){