From a575e02f036a788b603fa4efc78d4f7f25807d80 Mon Sep 17 00:00:00 2001
From: Steve James <4stevejames@gmail.com>
Date: Tue, 16 Jan 2018 22:40:16 -0800
Subject: [PATCH 1/3] Improve performance and fix issues
1. Add htmlButtonText option
2. Add htmlOptionText option
3. Use more local variables instead of frequently referencing widget properties/options.
4. Use document.createElement() to more quickly create new DOM elements.
5. Eliminate use of href="#" in header links. Add cursor: pointer to jquery.multiselect.css for these links.
6. Move insertions to end of _create to limit browser reflowing lags.
7. Use more ternary logic to eliminate if/then's where it makes sense.
8. Create _updateCache() internal function... called from multiple places.
9. Ditch use of $.isFunction. A simple typeof can be reliably used instead.
10. Add check in update() to reposition menu if button height has changed from saved value and the menu is open.
11. Save the current button height in position().
Tests Updated
12. Added test for namespace separation to core.js
13. Added selectedList:0 to tests in core.js to ensure that button text is correct for tests.
13. Fixed minWidth percentage test in options.js that was failing for non-Chrome browsers.
Files updated:
jquery.multiselect.js
jquery.multiselect.filter.js
jquery.multiselect.css
core.js
options.js
---
jquery.multiselect.css | 2 +-
src/jquery.multiselect.filter.js | 86 ++--
src/jquery.multiselect.js | 647 +++++++++++++++----------------
tests/unit/core.js | 373 +++++++++---------
tests/unit/options.js | 38 +-
5 files changed, 592 insertions(+), 554 deletions(-)
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..91c2f0e 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 = $(''))
- .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 = $('