PageSourceSearch

https://service.essbay.de/_assets/4aa87cbc97b19002293e590a434cf15d…/jquery.multiselect.js?1750255479

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