PageSourceSearch

https://www.nsws.gov.in/themes/sws/plugins/multiselect/js/jquery.multiselect.js?v=9.4.9

js nsws.gov.in collected 2026-10-02 05:00:37 UTC 71,242 bytes, 1,930 lines download raw bytes

1/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */
2/*
3 * jQuery UI MultiSelect Widget 3.0.0
4 * Copyright (c) 2012 Eric Hynds
5 *
6 * Depends:
7 *   - jQuery 1.8+                          (http://api.jquery.com/)
8 *   - jQuery UI 1.11 widget factory   (http://api.jqueryui.com/jQuery.widget/)
9 *
10 * Optional:
11 *   - jQuery UI effects
12 *   - jQuery UI position utility
13 *
14 * Dual licensed under the MIT and GPL licenses:
15 *   http://www.opensource.org/licenses/mit-license.php
16 *   http://www.gnu.org/licenses/gpl.html
17 *
18 */
19(function($, undefined) {
20  // Counter used to prevent collisions
21  var multiselectID = 0;
22
23  // The following information can be overridden via the linkInfo option.
24  // An $.extend is used to allow just specifying a partial object in linkInfo.
25  var linkDefaults = {
26     'open': {
27        'class': 'ui-multiselect-open',
28        'icon': '<span class="ui-icon ui-icon-triangle-1-s"></span',
29        'title': 'Open',
30     },
31     'close': {
32        'class': 'ui-multiselect-close',
33        'icon': '<span class="ui-icon ui-icon-circle-close"></span>',
34        'title': 'Close',
35     },
36     'checkAll': {
37        'class': 'ui-multiselect-all',
38        'icon': '<span class="ui-icon ui-icon-check"></span>',
39        'text': 'Check all',
40        'title': 'Check all',
41     },
42     'uncheckAll': {
43        'class': 'ui-multiselect-none',
44        'icon': '<span class="ui-icon ui-icon-closethick"></span>',
45        'text': 'Uncheck all',
46        'title': 'Uncheck all',
47     },
48     'flipAll': {
49        'class': 'ui-multiselect-flip',
50        'icon': '<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>',
51        'text': 'Flip all',
52        'title': 'Flip all',
53     },
54     'collapse': {
55        'icon': '<span class="ui-icon ui-icon-minusthick"></span>',
56        'title': 'Collapse',
57     },
58     'expand': {
59        'icon': '<span class="ui-icon ui-icon-plusthick"></span>',
60        'title': 'Expand',
61     },
62     'collapseAll': {
63        'class': 'ui-multiselect-collapseall',
64        'icon': '<span class="ui-icon ui-icon-minus"></span>',
65        'text': 'Collapse all',
66        'title': 'Collapse all',
67     },
68     'expandAll': {
69        'class': 'ui-multiselect-expandall',
70        'icon': '<span class="ui-icon ui-icon-plus"></span>',
71        'text': 'Expand all',
72        'title': 'Expand all',
73     },
74  };
75
76  /**
77   * Checks an option element for data-image-src
78   * and adds that as an image tag within the widget option
79   *
80   * @param {Node} option to pull an image from
81   * @param {Node} span to insert image tag into
82   */
83  function insertImage(option, span) {
84   var optionImageSrc = option.getAttribute('data-image-src');
85   if (optionImageSrc) {
86     var img = document.createElement('img');
87     img.setAttribute('src', optionImageSrc);
88     span.insertBefore(img, span.firstChild);
89   }
90 }
91
92 /**
93  * Retrieves the font size of the document
94  * Defaults to 16px
95  * @return {string} pixel string for font size
96  */
97 function determineFontSize() {
98   if (window.getComputedStyle) {
99     return getComputedStyle(document.body).fontSize;
100   }
101   return '16px';
102 }
103
104 /**
105  * Creates a jQuery object from the input element
106  * This can be a string selector, Node, or jQuery object
107  * @param {(object|string)} elem
108  * @return {object} jquery object for element
109  */
110 function getjQueryFromElement(elem) {
111   if (!!elem.jquery) {
112     return elem;
113   }
114   if (!!elem.nodeType) {
115     return $(elem);
116   }
117
118   return $(elem).eq(0);
119 }
120
121   /**
122    * Converts dimensions specified in options to pixel values.
123    * Determines if specified value is a minimum, maximum or exact value.
124    * The value can be a number or a string with px, pts, ems, in, cm, mm, or % units.
125    * Number/Numeric string treated as pixel measurements
126    *  - 30
127    *  - '30'
128    *  - '>30px'
129    *  - '1.3em'
130    *  - '20 pt'
131    *  - '30%'
132    * @param {string} dimText Option text (or number) containing possibly < or >, number, and a unit.
133    * @param {object} $elem jQuery object (or node) to reference for % calculations.
134    * @param {boolean} isHeight T/F to change from using width in % calculations.
135    * @return {object}
135 object containing pixels and -1/1/0 indicating min/max/exact.
136    */
137   function parse2px(dimText, $elem, isHeight) {
138     if (typeof dimText !== 'string') {
139        return {px: dimText, minimax: 0};
140     }
141
142     var parts = dimText.match(/([<>])?=?\s*([.\d]+)\s*([eimnptx%]*)s?/i);
143     var minimax = parts[1];
144     var value = parseFloat(parts[2]);
145     var unit = parts[3].toLowerCase();
146     var pixels = -1;
147     switch (unit) {
148        case 'pt':
149        case 'in':
150        case 'cm':
151        case 'mm':
152           pixels = {'pt': 4.0 / 3.0, 'in': 96.0, 'cm': 96.0 / 2.54, 'mm': 96.0 / 25.4}[unit] * value;
153           break;
154        case 'em':
155           pixels = parseFloat(determineFontSize()) * value;
156           break;
157        case '%':
158           if ( !!$elem ) {
159              if (typeof $elem === 'string' || !$elem.jquery) {
160                 $elem = $($elem);
161              }
162              pixels = ( !!isHeight ? $elem.parent().height() : $elem.parent().width() ) * (value / 100.0);
163           } // else returns -1 default value from above.
164           break;
165        default:
166           pixels = value;
167     }
168     // minimax:  -1 => minimum value, 1 => maximum value, 0 => exact value
169     return {px: pixels, minimax: minimax == '>' ? -1 : ( minimax == '<' ? 1 : 0 )};
170   }
171
172  $.widget('ech.multiselect', {
173
174  // default options
175  options: {
176     buttonWidth: 225, // (integer | string | 'auto' | null) Sets the min/max/exact width of the button.
177     menuWidth: null, // (integer | string | 'auto' | null) If a number is provided, sets the exact menu width.
178     menuHeight: 200, // (integer | string | 'auto' | 'size') Sets the height of the menu or determines it using native select's size setting.
179     resizableMenu: false, // (true | false) Enables the use of jQuery UI resizable if it is loaded.
180     appendTo: null, // (jQuery | DOM element | selector string)  If provided, this specifies what element to append the widget to in the DOM.
181     position: {}, // (object) A jQuery UI position object that constrains how the pop-up menu is positioned.
182     zIndex: null, // (integer) Overrides the z-index set for the menu container.
183     classes: '', // (string) Classes that you can provide to be applied to the elements making up the widget.
184     header: ['checkAll', 'uncheckAll'], // (false | string | array) False, custom string or array indicating which links to show in the header & in what order.
185     linkInfo: null, // (object | null) Supply an obect of link information to use alternative icons, icon labels, or icon title text.  See linkDefaults above for object structure.
186     noneSelectedText: 'Select options', // (string | null) The text to show in the button where nothing is selected.  Set to null to use the native select's placeholder text.
187     selectedText: '# of # selected', // (string) A "template" that indicates how to show the count of selections in the button.  The "#'s" are replaced by the selection count & option count.
188     selectedList: 0, // (integer) The actual list selections will be shown in the button when the count of selections is <= than this number.
189     selectedListSeparator: ', ', // (string) This allows customization of the list separator.  Use ',<br/>' to make the button grow vertically showing 1 selection per line.
190     maxSelected: null, // (integer | null)  If selected count > maxSelected, then message is displayed, and new selection is undone.
191     openEffect: null, // (array) An array containing menu opening effect information.
192     closeEffect: null, // (array) An array containing menu closing effect information.
193     autoOpen: false, // (true | false) If true, then the menu will be opening immediately after initialization.
194     htmlText: [], // (array) List of 'button' &/or 'options' indicating in which parts of the widget to treat text as html.
195     wrapText: ['button', 'header', 'options'], // (array) List of 'button', 'header', &/or 'options' indicating in which parts of the widget to wrap text.
196     listbox: false, // (true | false) Omits the button and instead of a pop-up inserts the open menu directly after the native select as a list box.
197     addInputNames: true, // (true | false) If true, names are created for each option input in the multi-select.
198     disableInputsOnToggle: true, // (true | false)  If true, each individual checkbox input is also disabled 
198when the widget is disabled.
199     groupsSelectable: true, // (true | false) Determines if clicking on an option group heading selects all of its options.
200     groupsCollapsable: false, // (true | false) Determines if option groups can be collapsed.
201     groupColumns: false, // (true | false)  Displays groups in a horizonal column layout.
202     groupColumnsWidth: false, // (integer) The width of each select item in the groupColumns.
203   },
204
205   /**
206    * This method determines which DOM element to append the menu to.   Determination process:
207    * 1. Look up the jQuery object, DOM element, or string selector provided in the options.
208    * 2. If nothing provided in options or lookup in #1 failed, then look for .ui-front or dialog.  (dialog case)
209    * 3. If still do not have a valid DOM element to append to, then append to the document body.
210    *
211    * NOTE:  this.element and this.document are jQuery objects per the jQuery UI widget API.
212   * @return {object} jQuery object for the DOM element to append to.
213    */
214   _getAppendEl: function() {
215     var elem = this.options.appendTo; // jQuery object or selector, DOM element or null.
216
217     if (elem) { // NOTE: The find below handles the jQuery selector case
218       elem = getjQueryFromElement(elem);
219     }
220     if (!elem || !elem[0]) {
221       elem = this.element.closest('.ui-front, dialog');
222     }
223     if (!elem.length) {
224       elem = $(document.body); // Position at end of body.  Note that this returns a DOM element.
225     }
226     return elem;
227   },
228
229   /**
230    * Constructs the button element for the widget
231    * Stores the result in this.$button
232    * @return{object} jQuery object for button
233    */
234    _buildButton: function() {
235      var wrapText = this.options.wrapText || [];
236      var $button = (this.$button = $(document.createElement('button')))
237        .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all'
238          + (wrapText.indexOf('button') > -1 ? '' : ' ui-multiselect-nowrap')
239          + (this.options.classes ? ' ' + this.options.classes : '')
240        )
241        .attr({
242          'type': 'button',
243          'title': this.element[0].title,
244          'tabIndex': this.element[0].tabIndex,
245          'id': this.element[0].id ? this.element[0].id + '_ms' : null,
246        })
247        .prop('aria-haspopup', true)
248        .html(this._linkHTML('<span class="{{class}}" title="{{title}}">{{icon}}</span>', 'open'));
249
250      this.$buttonlabel = $(document.createElement('span'))
251        .html(this.options.noneSelectedText || this.element[0].placeholder)
252        .appendTo($button);
253      return $button;
254    },
255
256    /**
257     * Constructs HTML string for menu header
258     * @return {string}
259     */
260    _buildHeaderHtml: function() {
261      // Header controls will contain the links & ordering specified by the header option.
262      // Depending on how the options are set, this may be empty or simply plain text
263      if (!this.options.header) {
264        return '';
265      }
266      if (typeof this.options.header === 'string') {
267        return '<li>' + this.options.header + '</li>';
268      }
269      var headerLinksHTML = '';
270      if (this.options.header.constructor == Array) {
271        for (var x = 0; x < this.options.header.length; x++) {
272          var linkInfoKey = this.options.header[x];
273          if (linkInfoKey && linkInfoKey in this.linkInfo
274            && !(this.options.maxSelected && linkInfoKey === 'checkAll')
275            && ['open', 'close', 'collapse', 'expand'].indexOf(linkInfoKey) === -1) {
276            headerLinksHTML += this._linkHTML('<li><a class="{{class}}" title="{{title}}">{{icon}}<span>{{text}}</span></a></li>', linkInfoKey);
277          }
278        }
279      }
280
281      if (this.options.header.constructor == Object) {
282      var options = Object.keys(this.options.header);
283      for (var x = 0; x < options.length; x++) {
284         var displayText = options[x];
285         var linkInfoKey = this.options.header[displayText];
286         if (linkInfoKey && linkInfoKey in this.linkInfo
287             && !(this.options.maxSelected && linkInfoKey === 'checkAll')
288             && ['open', 'close', 'collapse', 'expand'].indexOf(linkInfoKey) === -1) {
289             headerLinksHTML += this._linkHTML('<li><a class="{{class}}" title="{{title}}">{{icon}}<span>'+displayText+'</span></a></li>', linkInfoKey);
290         }
291       }
292     }
293      return headerLinksHTML;
294    },
295
296  /**
297   * Performs initial widget creation
298   * Widget API has already set this.element and this.options for us
299   * All inserts into the DOM are performed at the end to limit performance impact
300   *   - Build header links based on options and linkInfo object
301   *   - Set UI effect speeds
302   *   - Sets the multiselect ID using the global counter
303   *   - Creates the button, header, and menu
304   *   - Binds events for the widget
305   *   - Calls refresh to populate the menu
306   */
307  _create: function() {
308     var $element = this.element;
309     var options = this.options;
310
311     // Do an extend here to address link info missing from options.linkInfo--missing info defaults to that in linkDefaults.
312     this.linkInfo = $.extend(true, {}, linkDefaults, options.linkInfo || {});
313
314     // grab select width before hiding it
315     this._selectWidth = $element.outerWidth();
316     $element.hide();
317
318     // Convert null/falsely option values to empty arrays for fewer problems
319     options.htmlText = options.htmlText || [];
320     var wrapText = ( options.wrapText = options.wrapText || [] );
321
322     // default speed for effects
323     this.speed = $.fx.speeds._default;
324     this._isOpen = false;
325
326     // Create a unique namespace for events that
327     // the widget factory cannot unbind automatically.
328     this._namespaceID = this.eventNamespace;
329     // bump unique ID after assigning it to the widget instance
330     this.multiselectID = multiselectID++;
331
332
333     this.$headerLinkContainer = $( document.createElement('ul') )
334           .addClass('ui-helper-reset')
335           .html( this._buildHeaderHtml()
336                 + ( !options.listbox
337                    ? this._linkHTML('<li class="{{class}}"><a class="{{class}}" title="{{title}}">{{icon}}</a></li>', 'close')
338                    : '' ) );
339
340     // Menu header to hold controls for the menu
341     var $header = ( this.$header = $( document.createElement('div') ) )
342           .addClass('ui-multiselect-header ui-widget-header ui-corner-all ui-helper-clearfix')
343           .append( this.$headerLinkContainer );
344
345     // Holds the actual check boxes for inputs
346     var $checkboxes = ( this.$checkboxes = $( document.createElement('ul') ) )
347           .addClass('ui-multiselect-checkboxes ui-helper-reset' + (wrapText.indexOf('options') > -1 ? '' : ' ui-multiselect-nowrap'));
348
349     // This is the menu container that will hold all the options added via refresh().
350     var $menu = ( this.$menu = $( document.createElement('div') ) )
351           .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all'
352                     + ($element[0].multiple ? '' : ' ui-multiselect-single')
353                     + (!options.listbox ? '' : ' ui-multiselect-listbox')
354                     + (this.options.classes ? ' ' + this.options.classes : ''))
355           .append($header, $checkboxes);
356
357     if (!options.listbox) {
358       var $button = this._buildButton();
359        $button.insertAfter($element);
360        var $appendEl = this._getAppendEl();
361        $appendEl.append($menu);
362        // Set z-index of menu appropriately when it is not appended to a dialog and no z-index specified.
363        if ( !options.zIndex && !$appendEl.hasClass('ui-front') ) {
364           var $uiFront = this.element.closest('.ui-front, dialog');
365           options.zIndex = Math.max( $uiFront && parseInt($uiFront.css('z-index'), 10) + 1 || 0,
366                                                  $appendEl && parseInt($appendEl.css('z-index'), 10) + 1 || 0);
367        }
368
369        if (options.zIndex) {
370           $menu.css('z-index', options.zIndex);
371        }
372        // Use $.extend below since the "of" position property may not be able to be supplied via the option.
373        options.position = $.extend({'my': 'left top', 'at': 'left bottom', 'of': $button}, options.position || {});
374     } else {
375        $menu.insertAfter($element); // No button
376     }
377
378     this._bindEvents();
379
380     // build menu
381     this.refresh(true);
382  },
383
384   /**
385    * Helper function used in _create()
386   * @param {string} linkTemplate HTML link template string
387   * @param {string} linkID key string to look up in linkInfo object.
388   * @return {object} link HTML
389    */
390  _linkHTML: function(linkTemplate, linkID) {
391     var self = this;
392     return linkTemplate.replace(/{{(.*?)}}/ig, function(m, p1) {
393return self.linkInfo[linkID][p1];
394} )
395                                .replace('<span></span>', '');
396  },
397
398   /**
399    * https://api.jqueryui.com/jquery.widget/#method-_init
400    * Performed every time the widget is instantiated, or called with only an options object
401    *  - Set visibility of header links
402    *  - Auto open menu if appropriate
403    *  - Set disabled status
404    */
405   _init: function() {
406     var elSelect = this.element[0];
407
408     if (this.options.header !== false) {
409        this.$headerLinkContainer
410             .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')
411             .toggle( !!elSelect.multiple );
412     } else {
413        this.$header.hide();
414     }
415
416     if (this.options.autoOpen && !this.options.listbox) {
417       this.open();
418     }
419
420     if (elSelect.disabled) {
421       this.disable();
422     }
423   },
424
425   /**
426   * Builds an option item for the menu.  (Mostly plain JS for speed.)
427   * <li>
428   *   <label>
429   *     <input /> checkbox or radio depending on single/multiple select
430   *     <span /> option text
431   *   </label>
432   * </li>
433   * @param {node} option Option from select to be added to menu
434   * @return {object} jQuery object for menu option
435   */
436  _makeOption: function(option) {
437     var elSelect = this.element.get(0);
438     // Determine unique ID for the label & option tags
439     var id = elSelect.id || this.multiselectID;
440     var inputID = 'ui-multiselect-' + this.multiselectID + '-' + (option.id || id + '-option-' + this.inputIdCounter++);
441     // Pick up the select type from the underlying element
442     var isMultiple = elSelect.multiple;
443     var isDisabled = option.disabled;
444     var isSelected = option.selected;
445
446     var input = document.createElement('input');
447     var inputAttribs = {
448       'type': isMultiple ? 'checkbox' : 'radio',
449       'id': inputID,
450       'title': option.title || null,
451       'value': option.value,
452       'name': this.options.addInputNames ? 'multiselect_' + id : null,
453       'checked': isSelected ? 'checked' : null,
454       'aria-selected': isSelected ? 'true' : null,
455       'disabled': isDisabled ? 'disabled' : null,
456       'aria-disabled': isDisabled ? 'true' : null,
457     };
458     for (var name in inputAttribs) {
459       if (inputAttribs[name] !== null) {
460         input.setAttribute(name, inputAttribs[name]);
461       }
462     }
463     // Clone data attributes
464     var optionAttribs = option.attributes;
465     var len = optionAttribs.length;
466     for (var x = 0; x < len; x++) {
467       var attribute = optionAttribs[x];
468       if ( /^data\-.+/.test(attribute.name) ) {
469         input.setAttribute(attribute.name, attribute.value);
470       }
471     }
472
473     // Option text or html
474     var span = document.createElement('span');
475     if (this.htmlAllowedFor('options')) {
476       span.innerHTML = option.innerHTML;
477     } else {
478       span.textContent = option.textContent;
479     }
480
481     // Icon images for each item.
482     insertImage(option, span);
483
484     var label = document.createElement('label');
485     label.setAttribute('for', inputID);
486     if (option.title) {
487       label.setAttribute('title', option.title);
488     }
489     label.className += (isDisabled ? ' ui-state-disabled' : '')
490                         + (isSelected && !isMultiple ? ' ui-state-active' : '')
491                         + ' ui-corner-all';
492     label.appendChild(input);
493     label.appendChild(span);
494
495     var item = document.createElement('li');
496     item.className = (isDisabled ? 'ui-multiselect-disabled ' : '')
497                       + (this.options.groupColumns ? ' ui-multiselect-columns' : '')
498                       + (option.className || '');
499
500     if (this.options.groupColumnsWidth) {
501       item.style.width = this.options.groupColumnsWidth+'px';
502     }
503
504     item.appendChild(label);
505
506     return item;
507   },
508
509   /**
510    * Processes option and optgroup tags from underlying select to construct the menu's option list
511    * If groupsCollapsable option is set, adds collapse/expand buttons for each option group.
512    * This replaces the current contents of this.$checkboxes
513    * Defers to _makeOption to actually build the options
514    * Resets the input ID counter
515    */
516   _buildOptionList: function() {
517     var self = this;
518     var list = [];
519
520     this.inputIdCounter = 0;
521
522     this.element.children().each( function() {
523       var elem = this;
524
525       if (elem.tagName.toUpperCase() === 'OPTGROUP') {
526         var options = [];
527
528         $(elem).children().each( function() {
529           options.push(self._makeOption(this));
530         });
531
532         // Build the list section for this optgroup, complete w/ option inputs...
533         var $collapseButton = !!self.options.groupsCollapsable
534                                ? $( document.createElement('button') )
535                                   .attr({'title': self.linkInfo.collapse.title, 'type': 'button'})
536                                   .addClass('ui-state-default ui-corner-all ui-multiselect-collapser')
537                                   .html(self.linkInfo.collapse.icon)
538                                : null;
539         var $optGroupLabel = $( document.createElement('a') )
540                                   .addClass('ui-multiselect-grouplabel'
541                                     + (self.options.groupsSelectable ? ' ui-multiselect-selectable' : ''))
542                                   .html( elem.getAttribute('label') );
543         var $optionGroup = $( document.createElement('ul') ).append(options);
544         var $optGroupItem = $( document.createElement('li') )
545                                .addClass('ui-multiselect-optgroup'
546                                   + (self.options.groupColumns ? ' ui-multiselect-columns' : '')
547                                   + (elem.className ? ' ' + elem.className : ''))
548                                .append($collapseButton, $optGroupLabel, $optionGroup);
549
550         if (self.options.groupColumnsWidth) {
551           $optGroupItem.css('width', self.options.groupColumnsWidth+'px');
552         }
553
554         list.push($optGroupItem);
555       } else {
556         list.push(self._makeOption(elem));
557       }
558     });
559
560     this.$checkboxes.empty().append(list);
561  },
562
563   /**
564    * Refreshes the widget's menu
565    *  - Refresh header links if required
566    *  - Rebuild option list
567    *  - Update the cached values for height, width, and cached elements
568    *  - If listbox option is set, shows the menu and sets menu size.
569    * @param {boolean} init If false, broadcasts a refresh event
570    */
571   refresh: function(init) {
572     var $element = this.element;
573
574     // update header link container visibility if needed
575     if (this.options.header !== false) {
576        this.$headerLinkContainer
577             .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')
578             .toggle( !!$element[0].multiple );
579     }
580
581     this._buildOptionList(); // Clear and rebuild the menu.
582     this._updateCache(); // cache some more useful elements
583
584     if (!this.options.listbox) {
585        this._setButtonWidth();
586        this.update(true);
587     } else {
588        if (!this._isOpen) {
589           this.$menu.show();
590           this._isOpen = true;
591        }
592        this._setMenuWidth();
593        this._setMenuHeight();
594     }
595
596     // broadcast refresh event; useful for widgets
597     if (!init) {
598       this._trigger('refresh');
599     }
600   },
601
602   /**
603    * Updates cached values used elsewhere in the widget
604    * Causes the filter to also update its cache if the filter is loaded
605    */
606   _updateCache: function() {
607     // Invalidate cached dimensions to force recalcs.
608     this._savedButtonWidth = 0;
609     this._savedMenuWidth = 0;
610     this._savedMenuHeight = 0;
611
612     // Recreate important cached jQuery objects
613     this.$header = this.$menu.children('.ui-multiselect-header');
614     this.$checkboxes = this.$menu.children('.ui-multiselect-checkboxes');
615
616     // Update saved labels and inputs
617     this.$labels = this.$menu.find('label:not(.ui-multiselect-filter-label)');
618     this.$inputs = this.$labels.children('input');
619
620     // If the filter widget is in use, then also update its cache.
621     if ( this.element.is(':data("ech-multiselectfilter")') ) {
622           this.element.data('ech-multiselectfilter').updateCache(true);
623     }
624   },
625
626   /**
627    * Updates the widget checkboxes' checked states
628    * from the native select options' selected states.
629    * @param {boolean} skipDisabled If true, disabled options in either are skipped.
630    */
631   resync: function(skipDisabled) {
632     var $inputs = this.$inputs;
633     var $options = this.element.find('option');
634
635     if ($inputs.length === $options.length) {
636        var inputValues = {};
637        $inputs.not(!!skipDisabled ? ':disabled' : '').each( function() {
638           inputValues[this.value] = this;
639        });
640        $options.not(!!skipDisabled ? ':disabled' : '').each( function() {
641           if (this.value in inputValues) {
642              inputValues[this.value].checked = this.selected;
643           }
644        });
645        this._trigger('resync');
646        this.update();
647     } else {
648        this.refresh();
649     }
650   },
651
652  /**
653   * Updates the button text
654   * If selectedText option is a function, simply call it
655   * The selectedList option determines how many options to display
656   *   before switching to # of # selected
657   * This does not apply in listbox mode
658   * @param {boolean} isDefault true if value is default value for the button
659   */
660   update: function(isDefault) {
661     if (!!this.options.listbox) {
662        return;
663     }
664     var options = this.options;
665     var selectedList = options.selectedList;
666     var selectedText = options.selectedText;
667     var $inputs = this.$inputs;
668     var inputCount = $inputs.length;
669     var $checked = $inputs.filter(':checked');
670     var numChecked = $checked.length;
671     var value;
672
673     if (numChecked) {
674       if (typeof selectedText === 'function') {
675         value = selectedText.call(this, numChecked, inputCount, $checked.get());
676       } else if (/\d/.test(selectedList) && selectedList > 0 && numChecked <= selectedList) {
677         value = $checked.map(function() {
678return $(this).next().text().replace(/\n$/, '');
679})
680                         .get().join(options.selectedListSeparator);
681       } else {
682         value = selectedText.replace('#', numChecked).replace('#', inputCount);
683       }
684     } else {
685       value = options.noneSelectedText;
686     }
687
688     this._setButtonValue(value, isDefault);
689
690     if ( options.wrapText.indexOf('button') === -1 ) {
691        this._setButtonWidth(true);
692     }
693
694     // Check if the menu needs to be repositioned due to button height changing from adding/removing selections.
695     if (this._isOpen && this._savedButtonHeight != this.$button.outerHeight(false)) {
696        this.position();
697     }
698   },
699
700   /**
701    * Sets the button text
702    * @param {string} value content to be assigned to the button
703    * @param {boolean} isDefault true if value is default value for the button
704    */
705   _setButtonValue: function(value, isDefault) {
706     this.$buttonlabel[this.htmlAllowedFor('button') ? 'html' : 'text'](value);
707
708     if (!!isDefault) {
709       this.$button[0].defaultValue = value;
710     }
711   },
712
713   /**
714    * Sets button events for mouse and keyboard interaction
715    * Called by _bindEvents
716    */
717   _bindButtonEvents: function() {
718     var self = this;
719     var $button = this.$button;
720     /**
721      * @return {Boolean} always false
722      */
723     function buttonClickHandler() {
724        self[self._isOpen ? 'close' : 'open']();
725        return false;
726     }
727
728     $button
729       .on({
730         click: buttonClickHandler,
731         keydown: $.proxy(self._handleButtonKeyboardNav, self),
732         mouseenter: function() {
733           if (!this.classList.contains('ui-state-disabled')) {
734             this.classList.add('ui-state-hover');
735           }
736         },
737         mouseleave: function() {
738           this.classList.remove('ui-state-hover');
739         },
740         focus: function() {
741           if (!this.classList.contains('ui-state-disabled')) {
742             this.classList.add('ui-state-focus');
743           }
744         },
745         blur: function() {
746           this.classList.remove('ui-state-focus');
747         },
748       })
749       // webkit doesn't like it when you click on the span :(
750       .find('span')
751       .on('click.multiselect,click', buttonClickHandler);
752   },
753
754   // Handle keyboard events for the multiselect button.
755   _handleButtonKeyboardNav: function(e) {
756      // Change selection via up/down on a closed single select.
757      if (!this._isOpen && !this.element[0].multiple && (e.which === 38 || e.which === 40) ) {
758        var $inputs = this.$inputs;
759        var index = $inputs.index( $inputs.filter(':checked') );
760        if (e.which === 38 && index) {
761           $inputs.eq(index - 1).trigger('click');
762        } else if (e.which === 40 && index < $inputs.length - 1) {
763           $inputs.eq(index + 1).trigger('click');
764        }
765        return;
766     }
767
768     switch (e.which) {
769        case 27: // esc
770        case 37: // left
771        case 38: // up
772           this.close();
773           break;
774        case 40: // down
775        case 39: // right
776           this.open();
777           break;
778     }
779   },
780
781   /**
782    * Bind events to the checkboxes for options and option groups
783    * Must be bound to the checkboxes container.
784    * This method scopes actions to filtered options
785    * Called by _bindEvents
786    */
787   _bindCheckboxEvents: function() {
788     var self = this;
789
790     // optgroup label toggle support
791     self.$checkboxes.on('click.multiselect', '.ui-multiselect-grouplabel', function(e) {
792       e.preventDefault();
793
794       if (!self.options.groupsSelectable) {
795          return false;
796       }
797
798       var $this = $(this);
799       var $inputs = $this.next('ul').children(':not(.ui-multiselect-excluded)').find('input').not(':disabled');
800       var nodes = $inputs.get();
801       var label = this.textContent;
802
803       // trigger before callback and bail if the return is false
804       if (self._trigger('beforeoptgrouptoggle', e, {inputs: nodes, label: label}) === false) {
805         return;
806       }
807
808       // if maxSelected is in use, cannot exceed it
809       var maxSelected = self.options.maxSelected;
810       if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) {
811         return;
812       }
813
814       // toggle inputs
815       self._toggleChecked(
816         $inputs.filter(':checked').length !== $inputs.length,
817         $inputs
818       );
819
820       self._trigger('optgrouptoggle', e, {
821         inputs: nodes,
822         label: label,
823         checked: nodes.length ? nodes[0].checked : null,
824       });
825     })
826     // collapse button
827     .on('click.multiselect', '.ui-multiselect-collapser', function(e) {
828       var $this = $(this);
829             var $parent = $this.parent();
830             var optgroupLabel = $parent.find('.ui-multiselect-grouplabel').first().html();
831             var linkInfo = self.linkInfo;
832             var collapsedClass = 'ui-multiselect-collapsed';
833             var isCollapsed = $parent.hasClass(collapsedClass);
834
835       if (self._trigger('beforecollapsetoggle', e, {label: optgroupLabel, collapsed: isCollapsed}) === false) {
836         return;
837       }
838       $parent.toggleClass(collapsedClass);
839
840       $this.attr('title', isCollapsed ? linkInfo.collapse.title : linkInfo.expand.title)
841              .html(isCollapsed ? linkInfo.collapse.icon : linkInfo.expand.icon );
842
843       if (!self.options.listbox) {
844          self._setMenuHeight(true);
845       }
846
847       self._trigger('collapsetoggle', e, {label: optgroupLabel, collapsed: !isCollapsed});
848     })
849     // collapse button
850     .on('mouseenter.multiselect', '.ui-multiselect-collapser', function(e) {
851        this.classList.add('ui-state-hover');
852     })
853     // collapse button
854     .on('mouseleave.multiselect', '.ui-multiselect-collapser', function(e) {
855        this.classList.remove('ui-state-hover');
856     })
857     // option label
858     .on('mouseenter.multiselect', 'label', function(e, param) {
859       if (!this.classList.contains('ui-state-disabled')) {
860         var checkboxes = self.$checkboxes[0];
861         var scrollLeft = checkboxes.scrollLeft;
862         var scrollTop = checkboxes.scrollTop;
863         var scrollX = window.pageXOffset;
864         var scrollY = window.pageYOffset;
865
866         self.$labels.removeClass('ui-state-hover');
867         $(this).addClass('ui-state-hover').find('input').focus();
868
869         // Restore scroll positions if altered by setting input focus
870         if ( !param || !param.allowScroll ) {
871           checkboxes.scrollLeft = scrollLeft;
872           checkboxes.scrollTop = scrollTop;
873           window.scrollTo(scrollX, scrollY);
874         }
875       }
876     })
877     // Keyboard navigation of the menu
878     .on('keydown.multiselect', 'label', function(e) {
879       // Don't capture function keys or 'r'
880       if (e.which === 82) {
881         return; // r
882       }
883
884       if (e.which >
884 111 && e.which < 124) {
885         return; // Function keys.
886       }
887
888       e.preventDefault();
889       switch (e.which) {
890         case 9: // tab
891           if (e.shiftKey) {
892             self.$menu.find('.ui-state-hover').removeClass('ui-state-hover');
893             self.$header.find('li').last().find('a').focus();
894           } else {
895             self.close();
896           }
897           break;
898         case 27: // esc
899           self.close();
900           break;
901         case 38: // up
902         case 40: // down
903         case 37: // left
904         case 39: // right
905           self._traverse(e.which, this);
906           break;
907         case 13: // enter
908         case 32: // space
909           $(this).find('input')[0].click();
910           break;
911         case 65: // Alt-A
912           if (e.altKey) {
913             self.checkAll();
914           }
915           break;
916         case 70: // Alt-F
917           if (e.altKey) {
918             self.flipAll();
919           }
920           break;
921         case 85: // Alt-U
922           if (e.altKey) {
923             self.uncheckAll();
924           }
925           break;
926       }
927     })
928     .on('click.multiselect', 'input', function(e) {
929       // Reference to this checkbox / radio input
930       var input = this;
931       var $input = $(input);
932       var val = input.value;
933       var checked = input.checked;
934       // self is cached from outer scope above
935       var $element = self.element;
936       var $tags = $element.find('option');
937       var isMultiple = $element[0].multiple;
938       var $allInputs = self.$inputs;
939       var numChecked = $allInputs.filter(':checked').length;
940       var options = self.options;
941       var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text';
942       var optionText = $input.parent().find('span')[textFxn]();
943       var maxSelected = options.maxSelected;
944
945       // bail if this input is disabled or the event is cancelled
946       if (input.disabled || self._trigger('click', e, {value: val, text: optionText, checked: checked}) === false) {
947         e.preventDefault();
948         return;
949       }
950
951       if (maxSelected && checked && numChecked > maxSelected) {
952        if (self._trigger('maxselected', e, {labels: self.$labels, inputs: $allInputs}) !== false) {
953           self.buttonMessage('<center><b>LIMIT OF ' + (numChecked - 1) + ' REACHED!</b></center>');
954        }
955         input.checked = false;
956         e.preventDefault();
957         return false;
958       }
959
960       // make sure the input has focus. otherwise, the esc key
961       // won't close the menu after clicking an item.
962       input.focus();
963
964       // toggle aria state
965       $input.prop('aria-selected', checked);
966
967       // change state on the original option tags
968       $tags.each(function() {
969         this.selected = (this.value === val ? checked : isMultiple && this.selected);
970       });
971
972       // some additional single select-specific logic
973       if (!isMultiple) {
974         self.$labels.removeClass('ui-state-active');
975         $input.closest('label').toggleClass('ui-state-active', checked);
976
977         // close menu
978         self.close();
979       }
980
981       // fire change on the select box
982       $element.trigger('change');
983
984       // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
985       // http://bugs.jquery.com/ticket/3827
986       setTimeout($.proxy(self.update, self), 10);
987     });
988   },
989
990   /**
991    * Binds keyboard and mouse events to the header
992    * Called by _bindEvents
993    */
994   _bindHeaderEvents: function() {
995     var self = this;
996
997     // header links
998     self.$header
999     .on('click.multiselect', 'a', function(e) {
1000       var headerLinks = {
1001         'ui-multiselect-close': 'close',
1002         'ui-multiselect-all': 'checkAll',
1003         'ui-multiselect-none': 'uncheckAll',
1004         'ui-multiselect-flip': 'flipAll',
1005         'ui-multiselect-collapseall': 'collapseAll',
1006         'ui-multiselect-expandall': 'expandAll',
1007       };
1008       for (hdgClass in headerLinks) {
1009         if ( this.classList.contains(hdgClass) ) {
1010           // headerLinks[hdgClass] is the click handler name
1011             self[headerLinks[hdgClass]]();
1012             e.preventDefault();
1013             return false;
1014         }
1015       }
1016     }).
1017     on('keydown.multiselect', 'a', function(e) {
1018       switch (e.which) {
1019         case 27:
1020           self.close();
1021           break;
1022         case 9: // tab
1023           var $target = $(e.target);
1024           if ((e.shiftKey
1025               && !$target.parent().prev().length
1026               && !self.$header.find('.ui-multiselect-filter').length)
1027              || (!$target.parent().next().length && !self.$labels.length && !e.shiftKey)) {
1028             self.close();
1029             e.preventDefault();
1030           }
1031           break;
1032       }
1033     });
1034   },
1035
1036   /**
1037    * Allows the widget to be resized if the option is set and resizable is
1038    * included in jQuery UI
1039    */
1040    _setResizable: function() {
1041      if (!this.options.resizableMenu || !('resizable' in $.ui)) {
1042        return;
1043      }
1044      this.$menu.show();
1045      this.$menu.resizable({
1046        containment: 'parent',
1047        handles: 's',
1048        helper: 'ui-multiselect-resize',
1049        stop: function(e, ui) {
1050          // Force consistent width
1051          ui.size.width = ui.originalSize.width;
1052          $(this).outerWidth(ui.originalSize.width);
1053          if (this._trigger('resize', e, ui) !== false) {
1054            this.options.menuHeight = ui.size.height;
1055          }
1056          this._setMenuHeight(true);
1057        },
1058      });
1059      this.$menu.hide();
1060    },
1061
1062   /**
1063    * Binds all events used in the widget
1064    * This calls the menu, button, and header event binding methods
1065    */
1066   _bindEvents: function() {
1067     if (!this.options.listbox) {
1068        this._bindButtonEvents();
1069     }
1070     this._bindHeaderEvents();
1071     this._bindCheckboxEvents();
1072     this._setResizable();
1073
1074     // Close each widget when clicking on any other element/anywhere else on the page,
1075     // another widget instance, or when scrolling w/ the mouse wheel outside the menu button.
1076     this.document.on('mousedown' + this._namespaceID
1077                      + ' wheel' + this._namespaceID
1078                      + ' mousewheel' + this._namespaceID, function(event) {
1079       var target = event.target;
1080
1081       if ( this._isOpen
1082           && (!!this.$button ? target !== this.$button[0] && !$.contains(this.$button[0], target) : true)
1083           && target !== this.$menu[0] && !$.contains(this.$menu[0], target) ) {
1084         this.close();
1085       }
1086     }.bind(this));
1087
1088     // deal with form resets.  the problem here is that buttons aren't
1089     // restored to their defaultValue prop on form reset, and the reset
1090     // handler fires before the form is actually reset.  delaying it a bit
1091     // gives the form inputs time to clear.
1092     $(this.element[0].form).on('reset' + this._namespaceID, function() {
1093       setTimeout(this.refresh.bind(this), 10);
1094     }.bind(this));
1095   },
1096
1097   /**
1098    * Sets and caches the width of the button
1099    * Can set a minimum value if less than calculated width of native select.
1100    * @param {boolean} recalc true if cached width needs to be re-calculated
1101    */
1102   _setButtonWidth: function(recalc) {
1103     if (this._savedButtonWidth && !recalc) {
1104        return;
1105     }
1106
1107     // this._selectWidth set in _create() for native select element before hiding it.
1108     var width = this._selectWidth || this._getBCRWidth( this.element );
1109     var buttonWidth = this.options.buttonWidth || '';
1110     if (/\d/.test(buttonWidth)) {
1111        var parsed = parse2px(buttonWidth, this.element);
1112        var pixels = parsed.px;
1113        var minimax = parsed.minimax;
1114        width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels );
1115     } else { // keywords
1116        buttonWidth = buttonWidth.toLowerCase();
1117     }
1118
1119     // The button width is set to auto in the CSS,
1120     // so we only need to change it for a specific width.
1121     if (buttonWidth !== 'auto') {
1122        this.$button.outerWidth(width);
1123     }
1124     this._savedButtonWidth = width;
1125   },
1126
1127   /**
1128    * Sets and caches the width of the menu
1129    * Will use the width in options if provided, otherwise matches the button
1130    * @param {boolean} recalc true if cached width needs to be re-calculated
1131    */
1132   _setMenuWidth: function(recalc) {
1133     if (this._savedMenuWidth && !recalc) {
1134        return;
1135     }
1136
1137     // Note that it is assumed that the button width was set prior.
1138     var width = !!this.options.listbox ? this._selectWidth : (this._savedButtonWidth || this._getBCRWidth( this.$button ));
1139
1140     var menuWidth = this.options.menuWidth || '';
1141     if ( /\d/.test(menuWidth) ) {
1142        var parsed = parse2px(menuWidth, this.element);
1143        var pixels = parsed.px;
1144        var minimax = parsed.minimax;
1145        width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels );
1146     } else { // keywords
1147        menuWidth = menuWidth.toLowerCase();
1148     }
1149
1150     // Note that the menu width defaults to the button width if menuWidth option is null or blank.
1151     if (menuWidth !== 'auto') {
1152        this.$menu.outerWidth(width);
1153        this._savedMenuWidth = width;
1154        return;
1155     }
1156
1157     // Auto width determination: get intrinsic / "shrink-wrapped" outer widths w/ margins by applying floats.
1158     // cbWidth includes the width of the vertical scrollbar & ui-hover-state width increase per the applied CSS.
1159     // Note that a correction is made for jQuery floating point round-off errors below.
1160     this.$menu.addClass('ui-multiselect-measure');
1161     var headerWidth = this.$header.outerWidth(true) + this._jqWidthFix(this.$header);
1162     var cbWidth = this.$checkboxes.outerWidth(true) + this._jqWidthFix(this.$checkboxes);
1163     this.$menu.removeClass('ui-multiselect-measure');
1164
1165     var contentWidth = Math.max(this.options.wrapText.indexOf('header') > -1 ? 0 : headerWidth, cbWidth);
1166
1167     // Use $().width() to set menu width not including padding or border.
1168     this.$menu.width(contentWidth);
1169     // Save width including padding and border (no margins) for consistency w/ normal width setting.
1170     this._savedMenuWidth = this.$menu.outerWidth(false);
1171   },
1172
1173   /**
1174    * Sets and caches the height of the menu
1175    * Will use the height provided in the options unless using the select size
1176    *  option or the option exceeds the available height for the menu
1177    * Will set a scrollbar if the options can't all be visible at once
1178    * @param {boolean} recalc true if cached value needs to be re-calculated
1179    */
1180   _setMenuHeight: function(recalc) {
1181     var self = this;
1182     if (self._savedMenuHeight && !recalc) {
1183        return;
1184     }
1185
1186     var maxHeight = $(window).height();
1187     var optionHeight = self.options.menuHeight || '';
1188     var useSelectSize = false;
1189     var elSelectSize = 4;
1190
1191     if ( /\d/.test(optionHeight) ) {
1192        // Deduct height of header & border/padding to find height available for checkboxes.
1193        var $header = self.$header.filter(':visible');
1194        var headerHeight = $header.outerHeight(true);
1195        var menuBorderPaddingHt = this.$menu.outerHeight(false) - this.$menu.height();
1196        var cbBorderPaddingHt = this.$checkboxes.outerHeight(false) - this.$checkboxes.height();
1197
1198        optionHeight = parse2px(optionHeight, self.element, true).px;
1199        maxHeight = Math.min(optionHeight, maxHeight) - headerHeight - menuBorderPaddingHt - cbBorderPaddingHt;
1200     } else if (optionHeight.toLowerCase() === 'size') {
1201        // Overall height based on native select 'size' attribute
1202        useSelectSize = true;
1203        // Retrieves native select's size attribute or defaults to 4 (like native select).
1204        elSelectSize = self.element[0].size || elSelectSize;
1205     }
1206
1207     var overflowSetting = 'hidden';
1208     var itemCount = 0;
1209     var hoverAdjust = 4; // Adjustment for hover height included here.
1210     var ulHeight = hoverAdjust;
1211     var ulTop = -1;
1212
1213     // The following determines the how many items are visible per the menuHeight option.
1214     //   If the visible height calculation exceeds the calculated maximum height or if the number
1215     //   of item heights summed equal or exceed the native select size attribute, the loop is aborted.
1216     // If the loop is aborted, this means that the menu must be scrolled to see all the items.
1217     self.$checkboxes.find('li:not(.ui-multiselect-optgroup),a').filter(':visible').each( function() {
1218       if (ulTop < 0) {
1219          ulTop = this.offsetTop;
1220       }
1221       ulHeight = this.offsetTop + this.offsetHeight - ulTop + hoverAdjust;
1222       if (useSelectSize && ++itemCount >= elSelectSize || ulHeight > maxHeight) {
1223         overflowSetting = 'auto';
1224         if (!useSelectSize) {
1225           ulHeight = maxHeight;
1226         }
1227         return false;
1228       }
1229     });
1230
1231     // We actually only set the height of the checkboxes as the outer menu container is height:auto.
1232     // The _savedMenuHeight value below can be compared to optionHeight as an accuracy check.
1233     self.$checkboxes.css('overflow', overflowSetting).height(ulHeight);
1234     self._savedMenuHeight = this.$menu.outerHeight(false);
1235   },
1236
1237   /**
1238    * Calculate accurate outerWidth(false) using getBoundingClientRect()
1239    * Note that this presumes that the element is visible in the layout.
1240    * @param {node} elem DOM node or jQuery equivalent to get width for.
1241    * @return {float} Decimal floating point value for the width.
1242    */
1243  _getBCRWidth: function(elem) {
1244     if (!elem || !!elem.jquery && !elem[0]) {
1245        return null;
1246     }
1247     var domRect = !!elem.jquery ? elem[0].getBoundingClientRect() : elem.getBoundingClientRect();
1248     return domRect.right - domRect.left;
1249   },
1250
1251   /**
1252    * Calculate jQuery width correction factor to fix floating point round-off errors.
1253    * Note that this presumes that the element is visible in the layout.
1254    * @param {node} elem node or jQuery equivalent to get width for.
1255    * @return {float} Correction value for the width--typically a decimal < 1.0
1256    */
1257   _jqWidthFix: function(elem) {
1258     if (!elem || !!elem.jquery && !elem[0]) {
1259        return null;
1260     }
1261     return !!elem.jquery
1262                 ? this._getBCRWidth(elem[0]) - elem.outerWidth(false)
1263                 : this._getBCRWidth(elem) - $(elem).outerWidth(false);
1264   },
1265
1266   /**
1267    * Moves focus up or down the options list
1268    * @param {number} which key that triggered the traversal
1269    * @param {node} start element event was triggered from
1270    */
1271   _traverse: function(which, start) {
1272     var $start = $(start);
1273     var moveToLast = which === 38 || which === 37;
1274
1275     // select the first li that isn't an optgroup label / disabled
1276     var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(:disabled, .ui-multiselect-optgroup):visible').first();
1277     // we might have to jump to the next/previous option group
1278     if (!$next.length) {
1279       $next = $start.parents('.ui-multiselect-optgroup')[moveToLast ? 'prev' : 'next']();
1280     }
1281
1282     // if at the first/last element
1283     if (!$next.length) {
1284       var $container = this.$checkboxes;
1285
1286       // move to the first/last
1287       $container.find('label').filter(':visible')[moveToLast ? 'last' : 'first']().trigger('mouseover', {allowScroll: true});
1288
1289       // set scroll position
1290       $container.scrollTop(moveToLast ? $container.height() : 0);
1291     } else {
1292       $next.find('label').filter(':visible')[moveToLast ? 'last' : 'first']().trigger('mouseover', {allowScroll: true});
1293     }
1294   },
1295
1296   /**
1297    * Internal function to toggle checked property and related attributes on a checkbox
1298    * The context of this function should be a checkbox; do not proxy it.
1299    * @param {string} prop Property being toggled on the checkbox
1300    * @param {string} flag Flag to set for the property
1301    * @return {function} function for toggling checked state
1302    */
1303   _toggleState: function(prop, flag) {
1304     return function() {
1305        var state = (flag === '!') ? !this[prop] : flag;
1306
1307        if ( !this.disabled ) {
1308         this[prop] = state;
1309        }
1310
1311       if (state) {
1312         this.setAttribute('aria-' + prop, true);
1313       } else {
1314         this.removeAttribute('aria-' + prop);
1315       }
1316     };
1317   },
1318
1319   /**
1320    * Toggles the checked state on options within the menu
1321    * Potentially scoped down to visible elements from filteredInputs
1322    * @param {boolean} flag checked property to set
1323    * @param {object} group option group that was clicked, if any
1324    * @param {boolean}
1324 filteredInputs does not toggle hidden inputs if filtering.
1325    */
1326   _toggleChecked: function(flag, group, filteredInputs) {
1327     var self = this;
1328     var $element = self.element;
1329     var $inputs = (group && group.length) ? group : self.$inputs;
1330
1331     if (filteredInputs) {
1332        $inputs = self._isOpen
1333                    ? $inputs.closest('li').not('.ui-multiselect-excluded').find('input').not(':disabled')
1334                    : $inputs.not(':disabled');
1335     }
1336
1337     // toggle state on inputs
1338     $inputs.each(self._toggleState('checked', flag));
1339
1340     // Give the first input focus
1341     $inputs.eq(0).focus();
1342
1343     // update button text
1344     self.update();
1345
1346     // Create a plain object of the values that actually changed
1347     var inputValues = {};
1348     $inputs.each( function() {
1349       inputValues[this.value] = true;
1350     });
1351
1352     // toggle state on original option tags
1353     $element.find('option')
1354             .each( function() {
1355               if (!this.disabled && inputValues[this.value]) {
1356                 self._toggleState('selected', flag).call(this);
1357               }
1358             });
1359
1360     // trigger the change event on the select
1361     if ($inputs.length) {
1362       $element.trigger('change');
1363     }
1364   },
1365
1366  /**
1367   * Toggles disabled state on the widget and underlying select or for just one option group.
1368   * Will also disable all individual options if the disableInputsOnToggle option is set
1369   * @param {boolean} flag true if disabling widget
1370   * @param {number | string} groupID index or label of option group to disable
1371   */
1372   _toggleDisabled: function(flag, groupID) {
1373     var disabledClass = 'ui-state-disabled'; // used for styling only
1374
1375     if (this.$button) {
1376        this.$button.prop({'disabled': flag, 'aria-disabled': flag})[flag ? 'addClass' : 'removeClass'](disabledClass);
1377     }
1378
1379     if (this.options.disableInputsOnToggle) {
1380        // Apply the ui-multiselect-disabled class name to identify which
1381        // input elements this widget disabled (not pre-disabled)
1382        // so that they can be restored if the widget is re-enabled.
1383        var $inputs = (typeof groupID === 'undefined') ? this.$inputs : this._multiselectOptgroupFilter(groupID).find('input');
1384              var msDisabledClass = 'ui-multiselect-disabled';
1385        if (flag) {
1386           var matchedInputs = $inputs.filter(':enabled').get();
1387           for (var x = 0, len = matchedInputs.length; x < len; x++) {
1388              matchedInputs[x].setAttribute('disabled', 'disabled');
1389              matchedInputs[x].setAttribute('aria-disabled', 'disabled');
1390              matchedInputs[x].classList.add(msDisabledClass);
1391              matchedInputs[x].parentNode.classList.add(disabledClass);
1392            }
1393        } else {
1394           var matchedInputs = $inputs.filter('.' + msDisabledClass + ':disabled').get();
1395           for (var x = 0, len = matchedInputs.length; x < len; x++) {
1396             matchedInputs[x].removeAttribute('disabled');
1397             matchedInputs[x].removeAttribute('aria-disabled');
1398             matchedInputs[x].classList.remove(msDisabledClass);
1399             matchedInputs[x].parentNode.classList.remove(disabledClass);
1400           }
1401        }
1402     }
1403
1404     var $select = (typeof groupID === 'undefined') ? this.element : this._nativeOptgroupFilter(groupID).find('option');
1405     $select.prop({
1406       'disabled': flag,
1407       'aria-disabled': flag,
1408     });
1409   },
1410
1411   /**
1412    * Opens the menu, possibly with effects
1413    * Calls methods to set position and resize as well
1414    */
1415   open: function() {
1416     var $button = this.$button;
1417
1418     // bail if the multiselect open event returns false, this widget is disabled, or is already open
1419     if (this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen || !!this.options.listbox) {
1420       return;
1421     }
1422
1423     var $menu = this.$menu;
1424     var $header = this.$header;
1425     var $labels = this.$labels;
1426     var $inputs = this.$inputs.filter(':checked:not(.ui-state-disabled)');
1427     var options = this.options;
1428     var effect = options.openEffect;
1429     var scrollX = window.pageXOffset;
1430     var scrollY = window.pageYOffset;
1431
1432     // set the scroll of the checkbox container
1433     this.$checkboxes.scrollTop(0);
1434
1435     // Show the menu, set its dimensions, and position it.
1436     $menu.css('display', 'block');
1437     this._setMenuWidth();
1438     this._setMenuHeight();
1439     this.position();
1440
1441     // Do any specified open animation effect after positioning the menu.
1442     if (!!effect) {
1443        // Menu must be hidden for some effects (e.g. fade) to work.
1444        $menu.css('display', 'none');
1445        if (typeof effect == 'string') {
1446           $menu.show(effect, this.speed);
1447        } else if (typeof effect == 'object' && effect.constructor == Array) {
1448           $menu.show(effect[0], effect[1] || this.speed);
1449        } else if (typeof effect == 'object' && effect.constructor == Object) {
1450           $menu.show(effect);
1451        }
1452     }
1453
1454     // focus the first not disabled option or filter input if available
1455     var filter = $header.find('.ui-multiselect-filter');
1456     if (filter.length) {
1457       filter.first().find('input').trigger('focus');
1458     } else if ($inputs.length) {
1459        $inputs.eq(0).trigger('focus').parent('label').eq(0).trigger('mouseover').trigger('mouseenter');
1460     } else if ($labels.length) {
1461       $labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
1462     } else {
1463       $header.find('a').first().trigger('focus');
1464     }
1465
1466     // Restore window scroll position if altered by setting element focus
1467     window.scrollTo(scrollX, scrollY);
1468
1469     $button.addClass('ui-state-active');
1470     this._isOpen = true;
1471     this._trigger('open');
1472   },
1473
1474   // Close the menu
1475   close: function() {
1476     // bail if the multiselect close event returns false
1477     if (this._trigger('beforeclose') === false || !!this.options.listbox) {
1478       return;
1479     }
1480
1481     var $menu = this.$menu;
1482     var options = this.options;
1483     var effect = options.closeEffect;
1484     var $button = this.$button;
1485
1486     // hide the menu, maybe with a speed/effect combo
1487     if (!!effect) {
1488        if (typeof effect == 'string') {
1489           $menu.hide(effect, this.speed);
1490        } else if (typeof effect == 'object' && effect.constructor == Array) {
1491           $menu.hide(effect[0], effect[1] || this.speed);
1492        } else if (typeof effect == 'object' && effect.constructor == Object) {
1493           $menu.hide(effect);
1494        }
1495     } else {
1496        $menu.css('display', 'none');
1497     }
1498
1499     $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
1500     this.element.trigger('blur'); // For jQuery Validate
1501     this._isOpen = false;
1502     this._trigger('close');
1503     $button.trigger('focus');
1504   },
1505
1506   /**
1507    * Positions the menu relative to the button.
1508    */
1509   position: function() {
1510     var $button = this.$button;
1511
1512     // Save the button height so that we can determine when it has changed due to adding/removing selections.
1513     this._savedButtonHeight = $button.outerHeight(false);
1514
1515     if ($.ui && $.ui.position) {
1516       this.$menu.position(this.options.position);
1517     } else {
1518       var pos = {};
1519
1520       pos.top = $button.offset().top + this._savedButtonHeight;
1521       pos.left = $button.offset().left;
1522
1523       this.$menu.offset(pos);
1524     }
1525   },
1526
1527   // Enable widget
1528   enable: function(groupID) {
1529     this._toggleDisabled(false, groupID);
1530   },
1531
1532   // Disable widget
1533   disable: function(groupID) {
1534     this._toggleDisabled(true, groupID);
1535   },
1536
1537   /**
1538   * Checks all options or those in an option group
1539   * Accounts for maxSelected possibly being set.
1540   * @param {(number|string)} groupID index or label of option group to check all for.
1541   */
1542   checkAll: function(groupID) {
1543     this._trigger('beforeCheckAll');
1544
1545     if (this.options.maxSelected) {
1546        return;
1547     }
1548
1549     if (typeof groupID === 'undefined') { // groupID could be 0
1550        this._toggleChecked(true);
1551     } else {
1552        this._toggleChecked(true, this._multiselectOptgroupFilter(groupID).find('input'));
1553     }
1554
1555     this._trigger('checkAll');
1556   },
1557
1558   /**
1559   * Unchecks all options or those in an option group
1560   * @param {(number|string)} groupID index or label of option group to uncheck all for.
1561   */
1562   uncheckAll: function(groupID) {
1563     this._trigger('beforeUncheckAll');
1564
1565     if (typeof groupID === 'undefined') { // groupID could be 0
1566        this._toggleChecked(false);
1567     } else {
1568        this._toggleChecked(false, this._multiselectOptgroupFilter(groupID).find('input'));
1569     }
1570     if ( !this.element[0].multiple && !this.$inputs.filter(':checked').length) {
1571       // Forces the underlying single-select to have no options selected.
1572       this.element[0].selectedIndex = -1;
1573     }
1574
1575     this._trigger('uncheckAll');
1576   },
1577
1578   /**
1579   * Flips all options or those in an option group.
1580   * Accounts for maxSelected possibly being set.
1581   * @param {(number|string)} groupID index or label of option group to flip all for.
1582   */
1583   flipAll: function(groupID) {
1584     this._trigger('beforeFlipAll');
1585
1586     var gotID = (typeof groupID !== 'undefined'); // groupID could be 0
1587     var maxSelected = this.options.maxSelected;
1588     var inputCount = this.$inputs.length;
1589     var checkedCount = this.$inputs.filter(':checked').length;
1590     var $filteredOptgroupInputs = gotID ? this._multiselectOptgroupFilter(groupID).find('input') : null;
1591     var gInputCount = gotID ? $filteredOptgroupInputs.length : 0;
1592     var gCheckedCount = gotID ? $filteredOptgroupInputs.filter(':checked').length : 0;
1593
1594     if (!maxSelected
1595         || maxSelected >= (gotID ? checkedCount - gCheckedCount + gInputCount - gCheckedCount : inputCount - checkedCount ) ) {
1596        if (gotID) {
1597           this._toggleChecked('!', $filteredOptgroupInputs);
1598        } else {
1599           this._toggleChecked('!');
1600        }
1601        this._trigger('flipAll');
1602     } else {
1603        this.buttonMessage('<center><b>Flip All Not Permitted.</b></center>');
1604     }
1605   },
1606
1607   /**
1608   * Collapses all option groups or just the one specified.
1609   * @param {(number|string)} groupID index or label of option group to collapse.
1610   */
1611   collapseAll: function(groupID) {
1612     this._trigger('beforeCollapseAll');
1613
1614     var $optgroups = (typeof groupID === 'undefined') // groupID could be 0
1615                             ? this.$checkboxes.find('.ui-multiselect-optgroup')
1616                             : this._multiselectOptgroupFilter(groupID);
1617
1618     $optgroups.addClass('ui-multiselect-collapsed')
1619                    .children('.ui-multiselect-collapser').attr('title', this.linkInfo.expand.title ).html( this.linkInfo.expand.icon );
1620
1621     this._trigger('collapseAll');
1622   },
1623
1624   /**
1625   * Expands all option groups or just the one specified.
1626   * @param {(number|string)} groupID index or label of option group to expand.
1627   */
1628   expandAll: function(groupID) {
1629     this._trigger('beforeExpandAll');
1630
1631     var $optgroups = (typeof groupID === 'undefined') // groupID could be 0
1632                             ? this.$checkboxes.find('.ui-multiselect-optgroup')
1633                             : this._multiselectOptgroupFilter(groupID);
1634
1635     $optgroups.removeClass('ui-multiselect-collapsed')
1636                    .children('.ui-multiselect-collapser').attr('title', this.linkInfo.collapse.title ).html( this.linkInfo.collapse.icon );
1637
1638     this._trigger('expandAll');
1639   },
1640
1641   /**
1642    * Flashes a message in the button caption for 1 second.
1643    * Useful for very short warning messages to the user.
1644    * @param {string} message HTML to show in the button.
1645    */
1646   buttonMessage: function(message) {
1647      var self = this;
1648      self.$buttonlabel.html(message);
1649      setTimeout( function() {
1650        self.update();
1651      }, 1000 );
1652   },
1653
1654   /**
1655    * Provides a list of all checked options
1656    * @return {array} list of inputs
1657    */
1658   getChecked: function() {
1659     return this.$inputs.filter(':checked');
1660   },
1661
1662   /**
1663    * Provides a list of all options that are not checked
1664    * @return {array} list of inputs
1665    */
1666   getUnchecked: function() {
1667     return this.$inputs.filter(':not(:checked)');
1668   },
1669
1670   /**
1671    * Destroys the widget instance
1672    * @return {object} reference to widget
1673    */
1674   destroy: function() {
1675     // remove classes + data
1676     $.Widget.prototype.destroy.call(this);
1677
1678     // unbind events
1679     this.document.off(this._namespaceID);
1680     $(this.element[0].form).off(this._namespaceID);
1681
1682     if (!this.options.listbox) {
1683        this.$button.remove();
1684     }
1685     this.$menu.remove();
1686     this.element.show();
1687
1688     return this;
1689   },
1690
1691   /**
1692    * @return {boolean} indicates whether the menu is open
1693    */
1694   isOpen: function() {
1695     return this._isOpen;
1696   },
1697
1698   /**
1699    * @return {object} jQuery object for menu
1700    */
1701   widget: function() {
1702     return this.$menu;
1703   },
1704
1705   /**
1706    * @return {string} namespaceID for use with external event handlers.
1707    */
1708   getNamespaceID: function() {
1709     return this._namespaceID;
1710   },
1711
1712   /**
1713    * @return {object} jQuery object for button
1714    */
1715   getButton: function() {
1716     return this.$button;
1717   },
1718
1719   /**
1720    * Essentially an alias for widget
1721    * @return {object} jQuery object for menu
1722    */
1723   getMenu: function() {
1724     return this.$menu;
1725   },
1726
1727   /**
1728    * @return {array} List of the option labels
1729    */
1730   getLabels: function() {
1731     return this.$labels;
1732   },
1733
1734   /**
1735    * @return {array} List of option groups that are collapsed
1736    */
1737   getCollapsed: function() {
1738      return this.$checkboxes.find('.ui-multiselect-collapsed');
1739   },
1740
1741   /**
1742   * Sets the value of the underlying select then resyncs the menu.
1743   * @param {(string|array)} newValue value(s) to set the underlying select to.
1744   * @return {any} the underlying select when a value is provied, or eles the value of the select
1745    */
1746   value: function(newValue) {
1747     if (typeof newValue !== 'undefined') {
1748        this.element.val(newValue);
1749        this.resync();
1750        return this.element;
1751     } else {
1752        return this.element.val();
1753     }
1754   },
1755
1756   /**
1757    * Determines if HTML content is allowed for the given element type
1758    * @param {string} element to check
1759    * @return {boolean} true if html content is allowed
1760    */
1761   htmlAllowedFor: function(element) {
1762     return this.options.htmlText.indexOf(element) > -1;
1763   },
1764
1765   /**
1766   * Adds an option to the widget and underlying select
1767   * @param {object} attributes hash to be added to the option
1768   * @param {string} text label for the option
1769   * @param {(number|string)} groupID index or label of option group to add the option to
1770   */
1771   addOption: function(attributes, text, groupID) {
1772     var self = this;
1773     var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text';
1774     var $option = $( document.createElement('option') ).attr(attributes)[textFxn](text);
1775     var optionNode = $option.get(0);
1776
1777     if (typeof groupID === 'undefined') { // groupID could be 0
1778        self.element.append($option);
1779        self.$checkboxes.append(self._makeOption(optionNode));
1780     } else {
1781        self._nativeOptgroupFilter(groupID).append($option);
1782        self._multiselectOptgroupFilter(groupID).append(self._makeOption(optionNode));
1783     }
1784
1785     self._updateCache();
1786   },
1787
1788   /**
1789   * Finds an optgroup in the native select by index or label using the tag name
1790   * @param {(number|string)} groupID index or label of option group to find
1791   * @return {object} matching option groups
1792   */
1793   _nativeOptgroupFilter: function(groupID) {
1794      return this.element.children('OPTGROUP').filter( function(index) {
1795         return (typeof groupID === 'number' ? index === groupID : this.getAttribute('label') === groupID);
1796      });
1797   },
1798
1799   /**
1800   * Finds an optgroup in the multiselect widget by index or label
1801   * @param {(number|string)} groupID index or label of option group to find
1802   * @return {object} matching option groups
1803   */
1804   _multiselectOptgroupFilter: function(groupID) {
1805      return this.$menu.find('.ui-multiselect-optgroup').filter( function(index) {
1806         return (typeof groupID === 'number' ? index === groupID : this.getElementsByClassName('ui-multiselect-grouplabel')[0].textContent === groupID);
1807      });
1808   },
1809
1810   /**
1811    * Removes an option from the widget and underlying select
1812    * @param {string} value attribute corresponding to option being removed
1813    */
1814   removeOption: function(value) {
1815     if (!value) {
1816       return;
1817     }
1818     this.element.find('option[value=' + value + ']').remove();
1819     this.$labels.find('input[value=' + value + ']').parents('li').remove();
1820
1821     this._updateCache();
1822   },
1823
1824   /**
1825    * Reacts to options being changed
1826    * Delegates to various handlers
1827    * @param {string} key into the options hash
1828    * @param {any} value to be assigned to that option
1829    */
1830   _setOption: function(key, value) {
1831     var $header = this.$header;
1832     var $menu = this.$menu;
1833
1834     switch (key) {
1835       case 'header':
1836         if (typeof value === 'boolean') {
1837           $header.toggle( value );
1838         } else if (typeof value === 'string') {
1839           this.$headerLinkContainer.children('li:not(:last-child)').remove();
1840           this.$headerLinkContainer.prepend('<li>' + value + '</li>');
1841         }
1842         break;
1843       case 'checkAllText':
1844       case 'uncheckAllText':
1845       case 'flipAllText':
1846       case 'collapseAllText':
1847       case 'expandAllText':
1848         if (key !== 'checkAllText' || !this.options.maxSelected) {
1849           // eq(-1) finds the last span
1850           $header.find('a.' + this.linkInfo[key.replace('Text', '')]['class'] + ' span').eq(-1).html(value);
1851         }
1852         break;
1853       case 'checkAllIcon':
1854       case 'uncheckAllIcon':
1855       case 'flipAllIcon':
1856       case 'collapseAllIcon':
1857       case 'expandAllIcon':
1858         if (key !== 'checkAllIcon' || !this.options.maxSelected) {
1859           // eq(0) finds the first span
1860           $header.find('a.' + this.linkInfo[key.replace('Icon', '')]['class'] + ' span').eq(0).replaceWith(value);
1861         }
1862         break;
1863       case 'openIcon':
1864         $menu.find('span.ui-multiselect-open').html(value);
1865         break;
1866       case 'closeIcon':
1867         $menu.find('a.ui-multiselect-close').html(value);
1868         break;
1869       case 'buttonWidth':
1870       case 'menuWidth':
1871         this.options[key] = value;
1872         this._setButtonWidth(true); // true forces recalc of cached value.
1873         this._setMenuWidth(true); // true forces recalc of cached value.
1874         break;
1875       case 'menuHeight':
1876         this.options[key] = value;
1877         this._setMenuHeight(true); // true forces recalc of cached value.
1878         break;
1879       case 'selectedText':
1880       case 'selectedList':
1881       case 'maxSelected':
1882       case 'noneSelectedText':
1883       case 'selectedListSeparator':
1884         this.options[key] = value; // these all need to update immediately for the update() call
1885         this.update(true);
1886         break;
1887       case 'classes':
1888         $menu.add(this.$button).removeClass(this.options.classes).addClass(value);
1889         break;
1890       case 'multiple':
1891         var $element = this.element;
1892         if (!!$element[0].multiple !== value) {
1893            $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value);
1894            $element[0].multiple = value;
1895            this.uncheckAll();
1896            this.refresh();
1897         }
1898         break;
1899      case 'position':
1900        if (value !== null && !$.isEmptyObject(value) ) {
1901           this.options.position = value;
1902        }
1903        this.position();
1904        break;
1905      case 'zIndex':
1906        this.options.zIndex = value;
1907        this.$menu.css('z-index', value);
1908        break;
1909     default:
1910        this.options[key] = value;
1911    }
1912    $.Widget.prototype._setOption.apply(this, arguments); // eslint-disable-line prefer-rest-params
1913  },
1914  _parse2px: parse2px,
1915
1916 });
1917
1918  // Fix for jQuery UI modal dialogs
1919  // https://api.jqueryui.com/dialog/#method-_allowInteraction
1920  // https://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/
1921  if ($.ui && 'dialog' in $.ui) {
1922     $.widget( 'ui.dialog', $.ui.dialog, {
1923        _allowInteraction: function( event ) {
1924            if ( this._super( event ) || $( event.target ).closest('.ui-multiselect-menu' ).length ) {
1925              return true;
1926            }
1927        },
1928     });
1929  }
1930})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.