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 = $('')) - .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 = $('