PageSourceSearch

https://dyreklinikkenrungstedvej.dk/functions/jquery/jquery.multiselect.js?version=278202601

js dyreklinikkenrungstedvej.dk collected 2026-10-05 23:37:05 UTC 72,935 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': '<div class="ui-icon ui-icon-triangle-1-s"></div>',
29         'title': 'Open',
30      },
31      'close': {
32         'class': 'ui-multiselect-close',
33         'icon': '<div class="ui-icon ui-icon-circle-close"></div>',
34         'title': 'Close',
35      },
36      'checkAll': {
37         'class': 'ui-multiselect-all',
38         'icon': '<div class="ui-icon ui-icon-check"></div>',
39         'text': 'Check all',
40         'title': 'Check all',
41      },
42      'uncheckAll': {
43         'class': 'ui-multiselect-none',
44         'icon': '<div class="ui-icon ui-icon-closethick"></div>',
45         'text': 'Uncheck all',
46         'title': 'Uncheck all',
47      },
48      'flipAll': {
49         'class': 'ui-multiselect-flip',
50         'icon': '<div class="ui-icon ui-icon-arrowrefresh-1-w"></div>',
51         'text': 'Flip all',
52         'title': 'Flip all',
53      },
54      'collapse': {
55         'icon': '<div class="ui-icon ui-icon-minusthick"></div>',
56         'title': 'Collapse',
57      },
58      'expand': {
59         'icon': '<div class="ui-icon ui-icon-plusthick"></div>',
60         'title': 'Expand',
61      },
62      'collapseAll': {
63         'class': 'ui-multiselect-collapseall',
64         'icon': '<div class="ui-icon ui-icon-minus"></div>',
65         'text': 'Collapse all',
66         'title': 'Collapse all',
67      },
68      'expandAll': {
69         'class': 'ui-multiselect-expandall',
70         'icon': '<div class="ui-icon ui-icon-plus"></div>',
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} div to insert image tag into
82    */
83   function insertImage(option, div) {
84    var optionImageSrc = option.getAttribute('data-image-src');
85    if (optionImageSrc) {
86      var img = document.createElement('img');
87      img.setAttribute('src', optionImageSrc);
88      div.insertBefore(img, div.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('<div class="{{class}}" title="{{title}}">{{icon}}</div>', 'open'));
249
250       this.$buttonlabel = $(document.createElement('div'))
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) {
393 return self.linkInfo[linkID][p1];
394} )
395                                 .replace('<div></div>', '');
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    *     <div /> 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 div = document.createElement('div');
475      if (this.htmlAllowedFor('options')) {
476        div.innerHTML = option.innerHTML;
477      } else {
478        div.textContent = option.textContent;
479      }
480
481      // Icon images for each item.
482      insertImage(option, div);
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(div);
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() {
678 return $(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 div :(
750        .find('div')
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').trigger('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').trigger("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].trigger("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('div')[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).trigger("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 div
1850            $header.find('a.' + this.linkInfo[key.replace('Text', '')]['class'] + ' div').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 div
1860            $header.find('a.' + this.linkInfo[key.replace('Icon', '')]['class'] + ' div').eq(0).replaceWith(value);
1861          }
1862          break;
1863        case 'openIcon':
1864          $menu.find('div.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.