PageSourceSearch

https://satruck.org/scripts/jquery.multiselect.js?t=134347155875283972

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