Completed
Push — master ( 10ff09...c7ef41 )
by Andreas
33:56
created

jquery.multiselect.js ➔ parse2px   B

Complexity

Conditions 8

Size

Total Lines 34
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 27
dl 0
loc 34
rs 7.3333
c 0
b 0
f 0
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) {
0 ignored issues
show
Unused Code introduced by
The parameter undefined is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
20
   // Counter used to prevent collisions
21
   var multiselectID = 0;
22
23
   // The following information can be overridden via the linkInfo option.
24
   // An $.extend is used to allow just specifying a partial object in linkInfo.
25
   var linkDefaults = {
26
      'open': {
27
         'class': 'ui-multiselect-open',
28
         'icon': '<span class="ui-icon ui-icon-triangle-1-s"></span',
29
         'title': 'Open'
30
      },
31
      'close': {
32
         'class': 'ui-multiselect-close',
33
         'icon': '<span class="ui-icon ui-icon-circle-close"></span>',
34
         'title': 'Close'
35
      },
36
      'checkAll': {
37
         'class': 'ui-multiselect-all',
38
         'icon': '<span class="ui-icon ui-icon-check"></span>',
39
         'text': 'Check all',
40
         'title': 'Check all'
41
      },
42
      'uncheckAll': {
43
         'class': 'ui-multiselect-none',
44
         'icon': '<span class="ui-icon ui-icon-closethick"></span>',
45
         'text': 'Uncheck all',
46
         'title': 'Uncheck all'
47
      },
48
      'flipAll': {
49
         'class': 'ui-multiselect-flip',
50
         'icon': '<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>',
51
         'text': 'Flip all',
52
         'title': 'Flip all'
53
      },
54
      'collapse': {
55
         'icon': '<span class="ui-icon ui-icon-minusthick"></span>',
56
         'title': 'Collapse'
57
      },
58
      'expand': {
59
         'icon': '<span class="ui-icon ui-icon-plusthick"></span>',
60
         'title': 'Expand'
61
      },
62
      'collapseAll': {
63
         'class': 'ui-multiselect-collapseall',
64
         'icon': '<span class="ui-icon ui-icon-minus"></span>',
65
         'text': 'Collapse all',
66
         'title': 'Collapse all'
67
      },
68
      'expandAll': {
69
         'class': 'ui-multiselect-expandall',
70
         'icon': '<span class="ui-icon ui-icon-plus"></span>',
71
         'text': 'Expand all',
72
         'title': 'Expand all'
73
      }
74
   };
75
76
   /**
77
    * Checks an option element for data-image-src
78
    * and adds that as an image tag within the widget option
79
    * 
80
    * @param {Node} option to pull an image from
81
    * @param {Node} span to insert image tag into
82
    */
83
   function insertImage(option, span) {
84
    var optionImageSrc = option.getAttribute('data-image-src');
85
    if (optionImageSrc) {
86
      var img = document.createElement('img');
87
      img.setAttribute('src', optionImageSrc);
88
      span.insertBefore(img, span.firstChild);
89
    }
90
  }
91
92
  /**
93
   * Retrieves the font size of the document
94
   * Defaults to 16px
95
   * @returns {string} pixel string for font size
96
   */
97
  function determineFontSize() {
98
    if(window.getComputedStyle) {
99
      return getComputedStyle(document.body).fontSize;
100
    }
101
    return '16px';
102
  }
103
104
  /**
105
   * Creates a jQuery object from the input element
106
   * This can be a string selector, Node, or jQuery object
107
   * @param {(object|string)} elem 
108
   */
109
  function getjQueryFromElement(elem) {
110
    if(!!elem.jquery) {
111
      return elem;
112
    }
113
    if(!!elem.nodeType) {
114
      return $(elem);
115
    }
116
117
    return $(elem).eq(0);
118
  }
119
120
  /**
121
     * Converts dimensions specified in options to pixel values.
122
     * Determines if specified value is a minimum, maximum or exact value.
123
     * The value can be a number or a string with px, pts, ems, in, cm, mm, or % units.
124
     * Number/Numeric string treated as pixel measurements
125
     *  - 30
126
     *  - '30'
127
     *  - '>30px'
128
     *  - '1.3em'
129
     *  - '20 pt'
130
     *  - '30%'
131
     * @param {string} dimText Option text (or number) containing possibly < or >, number, and a unit.
132
     * @param {object} $elem jQuery object (or node) to reference for % calculations.
133
     * @param {boolean} isHeight T/F to change from using width in % calculations.
134
     * @returns {pixels, minimax} object containing pixels and -1/1/0 indicating min/max/exact.
135
     */
136
    function parse2px(dimText, $elem, isHeight) {
137
      if (typeof dimText !== 'string') {
138
         return {px: dimText, minimax: 0};
139
      }
140
141
      var parts = dimText.match(/([<>])?=?\s*([.\d]+)\s*([eimnptx%]*)s?/i);
142
      var minimax = parts[1];
143
      var value = parseFloat(parts[2]);
144
      var unit = parts[3].toLowerCase();
145
      var pixels = -1;
146
      switch (unit) {
147
         case 'pt':
148
         case 'in':
149
         case 'cm':
150
         case 'mm':
151
            pixels = {'pt': 4.0 / 3.0, 'in': 96.0, 'cm': 96.0 / 2.54, 'mm': 96.0 / 25.4}[unit] * value;
152
            break;
153
         case 'em':
154
            pixels = parseFloat(determineFontSize()) * value;
155
            break;
156
         case '%':
157
            if ( !!$elem ) {
158
               if (typeof $elem === 'string' || !$elem.jquery) {
159
                  $elem = $($elem);
160
               }
161
               pixels = ( !!isHeight ? $elem.parent().height() : $elem.parent().width() ) * (value / 100.0);
162
            } // else returns -1 default value from above.
163
            break;
164
         default:
165
            pixels = value;
166
      }
167
      // minimax:  -1 => minimum value, 1 => maximum value, 0 => exact value
168
      return {px: pixels, minimax: minimax == '>' ? -1 : ( minimax == '<' ? 1 : 0 ) };
169
    }
170
171
   $.widget("ech.multiselect", {
172
173
   // default options
174
   options: {
175
      buttonWidth: 225,                   // (integer | string | 'auto' | null) Sets the min/max/exact width of the button.
176
      menuWidth: null,                    // (integer | string | 'auto' | null) If a number is provided, sets the exact menu width.
177
      menuHeight: 200,                    // (integer | string | 'auto' | 'size') Sets the height of the menu or determines it using native select's size setting.
178
      resizableMenu: false,               // (true | false) Enables the use of jQuery UI resizable if it is loaded.
179
      appendTo: null,                     // (jQuery | DOM element | selector string)  If provided, this specifies what element to append the widget to in the DOM.
180
      position: {},                       // (object) A jQuery UI position object that constrains how the pop-up menu is positioned.
181
      zIndex: null,                       // (integer) Overrides the z-index set for the menu container.
182
      classes: '',                        // (string) Classes that you can provide to be applied to the elements making up the widget.
183
      header: ['checkAll','uncheckAll'],  // (false | string | array) False, custom string or array indicating which links to show in the header & in what order.
184
      linkInfo: null,                     // (object | null) Supply an obect of link information to use alternative icons, icon labels, or icon title text.  See linkDefaults above for object structure.
185
      noneSelectedText: 'Select options', // (string | null) The text to show in the button where nothing is selected.  Set to null to use the native select's placeholder text.
186
      selectedText: '# of # selected',    // (string) A "template" that indicates how to show the count of selections in the button.  The "#'s" are replaced by the selection count & option count.
187
      selectedList: 0,                    // (integer) The actual list selections will be shown in the button when the count of selections is <= than this number.
188
      selectedListSeparator: ', ',        // (string) This allows customization of the list separator.  Use ',<br/>' to make the button grow vertically showing 1 selection per line.
189
      maxSelected: null,                  // (integer | null)  If selected count > maxSelected, then message is displayed, and new selection is undone.
190
      openEffect: null,                   // (array) An array containing menu opening effect information.
191
      closeEffect: null,                  // (array) An array containing menu closing effect information.
192
      autoOpen: false,                    // (true | false) If true, then the menu will be opening immediately after initialization.
193
      htmlText: [],                       // (array) List of 'button' &/or 'options' indicating in which parts of the widget to treat text as html.
194
      wrapText: ['button','header','options'],  // (array) List of 'button', 'header', &/or 'options' indicating in which parts of the widget to wrap text.
195
      listbox: false,                     // (true | false) Omits the button and instead of a pop-up inserts the open menu directly after the native select as a list box.
196
      addInputNames: true,                // (true | false) If true, names are created for each option input in the multi-select.
197
      disableInputsOnToggle: true,        // (true | false)  If true, each individual checkbox input is also disabled when the widget is disabled.
198
      groupsSelectable: true,             // (true | false) Determines if clicking on an option group heading selects all of its options.
199
      groupsCollapsable: false,           // (true | false) Determines if option groups can be collapsed.
200
      groupColumns: false                 // (true | false)  Displays groups in a horizonal column layout.
201
    },
202
203
    /**
204
     * This method determines which DOM element to append the menu to.   Determination process:
205
     * 1. Look up the jQuery object, DOM element, or string selector provided in the options.
206
     * 2. If nothing provided in options or lookup in #1 failed, then look for .ui-front or dialog.  (dialog case)
207
     * 3. If still do not have a valid DOM element to append to, then append to the document body.
208
     *
209
     * NOTE:  this.element and this.document are jQuery objects per the jQuery UI widget API.
210
    * @returns {object} jQuery object for the DOM element to append to.
211
     */
212
    _getAppendEl: function() {
213
      var elem = this.options.appendTo;         // jQuery object or selector, DOM element or null.
214
215
      if (elem) {                               // NOTE: The find below handles the jQuery selector case
216
        elem = getjQueryFromElement(elem);
217
      }
218
      if (!elem || !elem[0]) {
219
        elem = this.element.closest('.ui-front, dialog');
220
      }
221
      if (!elem.length) {
222
        elem = $(document.body);                 // Position at end of body.  Note that this returns a DOM element.
223
      }
224
      return elem;
225
    },
226
227
    /**
228
     * Constructs the button element for the widget
229
     * Stores the result in this.$button
230
     * @returns{object} jQuery object for button
231
     */
232
     _buildButton: function () {
233
       var wrapText = this.options.wrapText || [];
234
       var $button = (this.$button = $(document.createElement('button')))
235
         .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all'
236
           + (wrapText.indexOf('button') > -1 ? '' : ' ui-multiselect-nowrap')
237
           + (this.options.classes ? ' ' + this.options.classes : '')
238
         )
239
         .attr({
240
           'type': 'button',
241
           'title': this.element[0].title,
242
           'tabIndex': this.element[0].tabIndex,
243
           'id': this.element[0].id ? this.element[0].id + '_ms' : null
244
         })
245
         .prop('aria-haspopup', true)
246
         .html(this._linkHTML('<span class="{{class}}" title="{{title}}">{{icon}}</span>', 'open'));
247
248
       this.$buttonlabel = $(document.createElement('span'))
249
         .html(this.options.noneSelectedText || $element[0].placeholder)
250
         .appendTo($button);
251
       return $button;
252
     },
253
254
     /**
255
      * Constructs HTML string for menu header
256
      * @returns {string}
257
      */
258
     _buildHeaderHtml: function () {
259
       // Header controls will contain the links & ordering specified by the header option.
260
       // Depending on how the options are set, this may be empty or simply plain text
261
       if (!this.options.header) {
262
         return '';
263
       }
264
       if (typeof this.options.header === 'string') {
265
         return '<li>' + this.options.header + '</li>';
266
       }
267
       var headerLinksHTML = '';
268
       if (this.options.header.constructor == Array) {
269
         for (var x = 0; x < this.options.header.length; x++) {
270
           var linkInfoKey = this.options.header[x];
271
           if (linkInfoKey && linkInfoKey in this.linkInfo
272
             && !(this.options.maxSelected && linkInfoKey === 'checkAll')
273
             && ['open', 'close', 'collapse', 'expand'].indexOf(linkInfoKey) === -1) {
274
             headerLinksHTML += this._linkHTML('<li><a class="{{class}}" title="{{title}}">{{icon}}<span>{{text}}</span></a></li>', linkInfoKey);
275
           }
276
         }
277
       }
278
       return headerLinksHTML;
279
     },
280
281
   /**
282
    * Performs initial widget creation
283
    * Widget API has already set this.element and this.options for us
284
    * All inserts into the DOM are performed at the end to limit performance impact
285
    *   - Build header links based on options and linkInfo object
286
    *   - Set UI effect speeds
287
    *   - Sets the multiselect ID using the global counter
288
    *   - Creates the button, header, and menu
289
    *   - Binds events for the widget
290
    *   - Calls refresh to populate the menu
291
    */
292
   _create: function() {
293
      var $element = this.element;
294
      var options = this.options;
295
296
      // Do an extend here to address link info missing from options.linkInfo--missing info defaults to that in linkDefaults.
297
      var linkInfo = ( this.linkInfo = $.extend(true, {}, linkDefaults, options.linkInfo || {}) );
0 ignored issues
show
Unused Code introduced by
The assignment to variable linkInfo seems to be never used. Consider removing it.
Loading history...
298
299
      // grab select width before hiding it
300
      this._selectWidth = $element.outerWidth();
301
      $element.hide();
302
303
      // Convert null/falsely option values to empty arrays for fewer problems
304
      options.htmlText = options.htmlText || [];
305
      var wrapText = ( options.wrapText = options.wrapText || [] );
306
307
      // default speed for effects
308
      this.speed = $.fx.speeds._default;
309
      this._isOpen = false;
310
311
      // Create a unique namespace for events that
312
      // the widget factory cannot unbind automatically.
313
      this._namespaceID = this.eventNamespace;
314
      // bump unique ID after assigning it to the widget instance
315
      this.multiselectID = multiselectID++;
316
317
      
318
      this.$headerLinkContainer = $( document.createElement('ul') )
319
            .addClass('ui-helper-reset')
320
            .html( this._buildHeaderHtml()
321
                  + ( !options.listbox
322
                     ? this._linkHTML('<li class="{{class}}"><a class="{{class}}" title="{{title}}">{{icon}}</a></li>', 'close')
323
                     : '' ) );
324
325
      // Menu header to hold controls for the menu
326
      var $header = ( this.$header = $( document.createElement('div') ) )
327
            .addClass('ui-multiselect-header ui-widget-header ui-corner-all ui-helper-clearfix')
328
            .append( this.$headerLinkContainer );
329
330
      // Holds the actual check boxes for inputs
331
      var $checkboxes = ( this.$checkboxes = $( document.createElement('ul') ) )
332
            .addClass('ui-multiselect-checkboxes ui-helper-reset' + (wrapText.indexOf('options') > -1 ? '' : ' ui-multiselect-nowrap'));
333
334
      // This is the menu container that will hold all the options added via refresh().
335
      var $menu = ( this.$menu = $( document.createElement('div') ) )
336
            .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all'
337
                      + ($element[0].multiple ? '' : ' ui-multiselect-single')
338
                      + (!options.listbox ? '' : ' ui-multiselect-listbox')
339
                      + (this.options.classes ? ' ' + this.options.classes : ''))
340
            .append($header, $checkboxes);
341
342
      if (!options.listbox) {
343
        var $button = this._buildButton();
344
         $button.insertAfter($element);
345
         var $appendEl = this._getAppendEl();
346
         $appendEl.append($menu);
347
         // Set z-index of menu appropriately when it is not appended to a dialog and no z-index specified.
348
         if ( !options.zIndex && !$appendEl.hasClass('ui-front') ) {
349
            var $uiFront = this.element.closest('.ui-front, dialog');
350
            options.zIndex = Math.max( $uiFront && parseInt($uiFront.css('z-index'), 10) + 1 || 0,
351
                                                   $appendEl && parseInt($appendEl.css('z-index'), 10) + 1 || 0);
352
         }
353
354
         if (options.zIndex) {
355
            $menu.css('z-index', options.zIndex);
356
         }
357
         // Use $.extend below since the "of" position property may not be able to be supplied via the option.
358
         options.position = $.extend({'my': 'left top', 'at': 'left bottom', 'of': $button}, options.position || {});
359
      }
360
      else {
361
         $menu.insertAfter($element);  // No button
362
      }
363
364
      this._bindEvents();
365
366
      // build menu
367
      this.refresh(true);
368
   },
369
370
    /**
371
     * Helper function used in _create()
372
    * @param {string} linkTemplate HTML link template string
373
    * @param {string} linkID key string to look up in linkInfo object.
374
    * @returns {object} link HTML
375
     */
376
   _linkHTML: function(linkTemplate, linkID) {
377
      var self = this;
378
      return linkTemplate.replace(/{{(.*?)}}/ig, function(m, p1){ return self.linkInfo[linkID][p1] } )
379
                                 .replace('<span></span>', '');
380
   },
381
382
    /**
383
     * https://api.jqueryui.com/jquery.widget/#method-_init
384
     * Performed every time the widget is instantiated, or called with only an options object
385
     *  - Set visibility of header links
386
     *  - Auto open menu if appropriate
387
     *  - Set disabled status
388
     */
389
    _init: function() {
390
      var elSelect = this.element[0];
391
392
      if (this.options.header !== false) {
393
         this.$headerLinkContainer
394
              .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')
395
              .toggle( !!elSelect.multiple );
396
      }
397
      else {
398
         this.$header.hide();
399
      }
400
401
      if (this.options.autoOpen && !this.options.listbox) {
402
        this.open();
403
      }
404
405
      if (elSelect.disabled) {
406
        this.disable();
407
      }
408
    },
409
410
    /**
411
    * Builds an option item for the menu.  (Mostly plain JS for speed.)
412
    * <li>
413
    *   <label>
414
    *     <input /> checkbox or radio depending on single/multiple select
415
    *     <span /> option text
416
    *   </label>
417
    * </li>
418
    * @param {node} option Option from select to be added to menu
419
    * @returns {object} jQuery object for menu option
420
    */
421
   _makeOption: function(option) {
422
      var elSelect = this.element.get(0);
423
      // Determine unique ID for the label & option tags
424
      var id = elSelect.id || this.multiselectID;
425
      var inputID = 'ui-multiselect-' + this.multiselectID + '-' + (option.id || id + '-option-' + this.inputIdCounter++);
426
      // Pick up the select type from the underlying element
427
      var isMultiple = elSelect.multiple;
428
      var isDisabled = option.disabled;
429
      var isSelected = option.selected;
430
431
      var input = document.createElement('input');
432
      var inputAttribs = {
433
        "type": isMultiple ? 'checkbox' : 'radio',
434
        "id": inputID,
435
        "title": option.title || null,
436
        "value": option.value,
437
        "name": this.options.addInputNames ? "multiselect_" + id : null,
438
        "checked": isSelected ? "checked" : null,
439
        "aria-selected": isSelected ? "true" : null,
440
        "disabled": isDisabled ? "disabled" : null,
441
        "aria-disabled": isDisabled ? "true" : null
442
      };
443
      for (var name in inputAttribs) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
444
        if (inputAttribs[name] !== null) {
445
          input.setAttribute(name,inputAttribs[name]);
446
        }
447
      }
448
      // Clone data attributes
449
      var optionAttribs = option.attributes;
450
      var len = optionAttribs.length;
451
      for (var x = 0; x < len; x++) {
452
        var attribute = optionAttribs[x];
453
        if ( /^data\-.+/.test(attribute.name) ) {
454
          input.setAttribute(attribute.name, attribute.value);
455
        }
456
      }
457
458
      // Option text or html
459
      var span = document.createElement('span');
460
      if (this.htmlAllowedFor('options')) {
461
        span.innerHTML = option.innerHTML;
462
      }
463
      else {
464
        span.textContent = option.textContent;
465
      }
466
467
      // Icon images for each item.
468
      insertImage(option, span);
469
470
      var label = document.createElement('label');
471
      label.setAttribute('for', inputID);
472
      if (option.title) {
473
        label.setAttribute('title', option.title);
474
      }
475
      label.className += (isDisabled ? ' ui-state-disabled' : '')
476
                          + (isSelected && !isMultiple ? ' ui-state-active' : '')
477
                          + ' ui-corner-all';
478
      label.appendChild(input);
479
      label.appendChild(span);
480
481
      var item = document.createElement('li');
482
      item.className = (isDisabled ? 'ui-multiselect-disabled ' : '')
483
                        + (option.className || '');
484
      item.appendChild(label);
485
486
      return item;
487
    },
488
489
    /**
490
     * Processes option and optgroup tags from underlying select to construct the menu's option list
491
     * If groupsCollapsable option is set, adds collapse/expand buttons for each option group.
492
     * This replaces the current contents of this.$checkboxes
493
     * Defers to _makeOption to actually build the options
494
     * Resets the input ID counter
495
     */
496
    _buildOptionList: function() {
497
      var self = this;
498
      var list = [];
499
500
      this.inputIdCounter = 0;
501
502
      this.element.children().each( function() {
503
        var elem = this;
504
505
        if (elem.tagName.toUpperCase() === 'OPTGROUP') {
506
          var options = [];
507
508
          $(elem).children().each( function() {
509
            options.push(self._makeOption(this));
510
          });
511
512
          // Build the list section for this optgroup, complete w/ option inputs...
513
          var $collapseButton = !!self.options.groupsCollapsable
514
                                 ? $( document.createElement('button') )
515
                                    .attr({'title': self.linkInfo.collapse.title})
516
                                    .addClass('ui-state-default ui-corner-all ui-multiselect-collapser')
517
                                    .html(self.linkInfo.collapse.icon)
518
                                 : null;
519
          var $optGroupLabel = $( document.createElement('a') )
520
                                    .addClass('ui-multiselect-grouplabel'
521
                                      + (self.options.groupsSelectable ? ' ui-multiselect-selectable' : ''))
522
                                    .html( elem.getAttribute('label') );
523
          var $optionGroup = $( document.createElement('ul') ).append(options);
524
          var $optGroupItem = $( document.createElement('li') )
525
                                 .addClass('ui-multiselect-optgroup'
526
                                    + (self.options.groupColumns ? ' ui-multiselect-columns' : '')
527
                                    + (elem.className ? ' ' + elem.className : ''))
528
                                 .append($collapseButton, $optGroupLabel, $optionGroup)
529
          list.push($optGroupItem);
530
        }
531
        else {
532
          list.push(self._makeOption(elem));
533
        }
534
      });
535
536
      this.$checkboxes.empty().append(list);
537
   },
538
539
    /**
540
     * Refreshes the widget's menu
541
     *  - Refresh header links if required
542
     *  - Rebuild option list
543
     *  - Update the cached values for height, width, and cached elements
544
     *  - If listbox option is set, shows the menu and sets menu size.
545
     * @param {boolean} init If false, broadcasts a refresh event
546
     */
547
    refresh: function(init) {
548
      var $element = this.element;
549
550
      // update header link container visibility if needed
551
      if (this.options.header !== false) {
552
         this.$headerLinkContainer
553
              .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip')
554
              .toggle( !!$element[0].multiple );
555
      }
556
557
      this._buildOptionList();                                  // Clear and rebuild the menu.
558
      this._updateCache();                                      // cache some more useful elements
559
560
      if (!this.options.listbox) {
561
         this._setButtonWidth();
562
         this.update(true);
563
      }
564
      else {
565
         if (!this._isOpen) {
566
            this.$menu.show();
567
            this._isOpen = true;
568
         }
569
         this._setMenuWidth();
570
         this._setMenuHeight();
571
      }
572
573
      // broadcast refresh event; useful for widgets
574
      if (!init) {
575
        this._trigger('refresh');
576
      }
577
    },
578
579
    /**
580
     * Updates cached values used elsewhere in the widget
581
     * Causes the filter to also update its cache if the filter is loaded
582
     */
583
    _updateCache: function() {
584
      // Invalidate cached dimensions to force recalcs.
585
      this._savedButtonWidth = 0;
586
      this._savedMenuWidth = 0;
587
      this._savedMenuHeight = 0;
588
589
      // Recreate important cached jQuery objects
590
      this.$header = this.$menu.children('.ui-multiselect-header');
591
      this.$checkboxes = this.$menu.children('.ui-multiselect-checkboxes');
592
593
      // Update saved labels and inputs
594
      this.$labels = this.$menu.find('label:not(.ui-multiselect-filter-label)');
595
      this.$inputs = this.$labels.children('input');
596
597
      // If the filter widget is in use, then also update its cache.
598
      if ( this.element.is(':data("ech-multiselectfilter")') ) {
599
            this.element.data('ech-multiselectfilter').updateCache(true);
600
      }
601
    },
602
603
    /**
604
     * Updates the widget checkboxes' checked states
605
     * from the native select options' selected states.
606
     * @param {boolean} skipDisabled If true, disabled options in either are skipped.
607
     */
608
    resync : function(skipDisabled) {
609
      var $inputs = this.$inputs;
610
      var $options = this.element.find('option');
611
612
      if ($inputs.length === $options.length) {
613
         var inputValues = {};
614
         $inputs.not(!!skipDisabled ? ':disabled' : '').each( function() {
615
            inputValues[this.value] = this;
616
         });
617
         $options.not(!!skipDisabled ? ':disabled' : '').each( function() {
618
            if (this.value in inputValues) {
619
               inputValues[this.value].checked = this.selected;
620
            }
621
         });
622
         this._trigger('resync');
623
         this.update();
624
      }
625
      else {
626
         this.refresh();
627
      }
628
    },
629
630
   /**
631
    * Updates the button text
632
    * If selectedText option is a function, simply call it
633
    * The selectedList option determines how many options to display
634
    *   before switching to # of # selected
635
    * This does not apply in listbox mode
636
    * @param {boolean} isDefault true if value is default value for the button
637
    */
638
    update: function(isDefault) {
639
      if (!!this.options.listbox) {
640
         return;
641
      }
642
      var options = this.options;
643
      var selectedList = options.selectedList;
644
      var selectedText = options.selectedText;
645
      var $inputs = this.$inputs;
646
      var inputCount = $inputs.length;
647
      var $checked = $inputs.filter(':checked');
648
      var numChecked = $checked.length;
649
      var value;
650
651
      if (numChecked) {
652
        if (typeof selectedText === 'function') {
653
          value = selectedText.call(this, numChecked, inputCount, $checked.get());
654
        }
655
        else if (/\d/.test(selectedList) && selectedList > 0 && numChecked <= selectedList) {
656
          value = $checked.map(function() { return $(this).next().text().replace(/\n$/, '') })
657
                          .get().join(options.selectedListSeparator);
658
        }
659
        else {
660
          value = selectedText.replace('#', numChecked).replace('#', inputCount);
661
        }
662
      }
663
      else {
664
        value = options.noneSelectedText;
665
      }
666
667
      this._setButtonValue(value, isDefault);
668
669
      if ( options.wrapText.indexOf('button') === -1 ) {
670
         this._setButtonWidth(true);
671
      }
672
673
      // Check if the menu needs to be repositioned due to button height changing from adding/removing selections.
674
      if (this._isOpen && this._savedButtonHeight != this.$button.outerHeight(false)) {
675
         this.position();
676
      }
677
    },
678
679
    /**
680
     * Sets the button text
681
     * @param {string} value content to be assigned to the button
682
     * @param {boolean} isDefault true if value is default value for the button
683
     */
684
    _setButtonValue: function(value, isDefault) {
685
      this.$buttonlabel[this.htmlAllowedFor('button') ? 'html' : 'text'](value);
686
687
      if (!!isDefault) {
688
        this.$button[0].defaultValue = value;
689
      }
690
    },
691
692
    /**
693
     * Sets button events for mouse and keyboard interaction
694
     * Called by _bindEvents
695
     */
696
    _bindButtonEvents: function() {
697
      var self = this;
698
      var $button = this.$button;
699
      function buttonClickHandler() {
700
         self[ self._isOpen ? 'close' : 'open' ]();
701
         return false;
702
      }
703
704
      $button
705
        .on({
706
          click: buttonClickHandler,
707
          keydown: $.proxy(self._handleButtonKeyboardNav, self),
708
          mouseenter: function() {
709
            if (!this.classList.contains('ui-state-disabled')) {
710
              this.classList.add('ui-state-hover');
711
            }
712
          },
713
          mouseleave: function() {
714
            this.classList.remove('ui-state-hover');
715
          },
716
          focus: function() {
717
            if (!this.classList.contains('ui-state-disabled')) {
718
              this.classList.add('ui-state-focus');
719
            }
720
          },
721
          blur: function() {
722
            this.classList.remove('ui-state-focus');
723
          }
724
        })
725
        // webkit doesn't like it when you click on the span :(
726
        .find('span')
727
        .on('click.multiselect,click', buttonClickHandler);
728
    },
729
730
    // Handle keyboard events for the multiselect button.
731
    _handleButtonKeyboardNav: function(e) {
732
       // Change selection via up/down on a closed single select.
733
       if (!this._isOpen && !this.element[0].multiple && (e.which === 38 || e.which === 40) ) {
734
         var $inputs = this.$inputs;
735
         var index = $inputs.index( $inputs.filter(':checked') );
736
         if (e.which === 38 && index) {
737
            $inputs.eq(index - 1).trigger('click');
738
         }
739
         else if (e.which === 40 && index < $inputs.length - 1) {
740
            $inputs.eq(index + 1).trigger('click');
741
         }
742
         return;
743
      }
744
745
      switch(e.which) {
746
         case 27: // esc
747
         case 37: // left
748
         case 38: // up
749
            this.close();
750
            break;
751
         case 40: // down
752
         case 39: // right
753
            this.open();
754
            break;
755
      }
756
    },
757
758
    /**
759
     * Bind events to the checkboxes for options and option groups
760
     * Must be bound to the checkboxes container.
761
     * This method scopes actions to filtered options
762
     * Called by _bindEvents
763
     */
764
    _bindCheckboxEvents: function() {
765
      var self = this;
766
767
      // optgroup label toggle support
768
      self.$checkboxes.on('click.multiselect', '.ui-multiselect-grouplabel', function(e) {
769
        e.preventDefault();
770
771
        if (!self.options.groupsSelectable) {
772
           return false;
773
        }
774
775
        var $this = $(this);
776
        var $inputs = $this.next('ul').children(':not(.ui-multiselect-excluded)').find('input').not(':disabled');
777
        var nodes = $inputs.get();
778
        var label = this.textContent;
779
780
        // trigger before callback and bail if the return is false
781
        if (self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) {
782
          return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
783
        }
784
785
        // if maxSelected is in use, cannot exceed it
786
        var maxSelected = self.options.maxSelected;
787
        if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) {
788
          return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
789
        }
790
791
        // toggle inputs
792
        self._toggleChecked(
793
          $inputs.filter(':checked').length !== $inputs.length,
794
          $inputs
795
        );
796
797
        self._trigger('optgrouptoggle', e, {
798
          inputs: nodes,
799
          label: label,
800
          checked: nodes.length ? nodes[0].checked : null
801
        });
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
802
      })
803
      // collapse button
804
      .on('click.multiselect', '.ui-multiselect-collapser', function(e) {
805
        var $this = $(this),
806
              $parent = $this.parent(),
807
              optgroupLabel = $parent.find('.ui-multiselect-grouplabel').first().html(),
808
              linkInfo = self.linkInfo,
809
              collapsedClass = 'ui-multiselect-collapsed',
810
              isCollapsed = $parent.hasClass(collapsedClass);
811
812
        if (self._trigger('beforecollapsetoggle', e, { label: optgroupLabel , collapsed: isCollapsed }) === false) {
813
          return;
814
        }
815
        $parent.toggleClass(collapsedClass);
816
817
        $this.attr('title', isCollapsed ? linkInfo.collapse.title : linkInfo.expand.title)
818
               .html(isCollapsed ? linkInfo.collapse.icon : linkInfo.expand.icon );
819
820
        if (!self.options.listbox) {
821
           self._setMenuHeight(true);
822
        }
823
824
        self._trigger('collapsetoggle', e, { label: optgroupLabel, collapsed: !isCollapsed });
825
      })
826
      // collapse button
827
      .on('mouseenter.multiselect', '.ui-multiselect-collapser', function(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
828
         this.classList.add('ui-state-hover');
829
      })
830
      // collapse button
831
      .on('mouseleave.multiselect', '.ui-multiselect-collapser', function(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
832
         this.classList.remove('ui-state-hover');
833
      })
834
      // option label
835
      .on('mouseenter.multiselect', 'label', function(e, param) {
836
        if (!this.classList.contains('ui-state-disabled')) {
837
          var checkboxes = self.$checkboxes[0];
838
          var scrollLeft = checkboxes.scrollLeft;
839
          var scrollTop = checkboxes.scrollTop;
840
          var scrollX = window.pageXOffset;
841
          var scrollY = window.pageYOffset;
842
843
          self.$labels.removeClass('ui-state-hover');
844
          $(this).addClass('ui-state-hover').find('input').focus();
845
846
          // Restore scroll positions if altered by setting input focus
847
          if ( !param || !param.allowScroll ) {
848
            checkboxes.scrollLeft = scrollLeft;
849
            checkboxes.scrollTop = scrollTop;
850
            window.scrollTo(scrollX, scrollY);
851
          }
852
        }
853
      })
854
      // Keyboard navigation of the menu
855
      .on('keydown.multiselect', 'label', function(e) {
856
        // Don't capture function keys or 'r'
857
        if (e.which === 82) {
858
          return; // r
859
        }
860
861
        if (e.which > 111 && e.which < 124) {
862
          return; // Function keys.
863
        }
864
865
        e.preventDefault();
866
        switch(e.which) {
867
          case 9: // tab
868
            if (e.shiftKey) {
869
              self.$menu.find(".ui-state-hover").removeClass("ui-state-hover");
870
              self.$header.find("li").last().find("a").focus();
871
            }
872
            else {
873
              self.close();
874
            }
875
            break;
876
          case 27: // esc
877
            self.close();
878
            break;
879
          case 38: // up
880
          case 40: // down
881
          case 37: // left
882
          case 39: // right
883
            self._traverse(e.which, this);
884
            break;
885
          case 13: // enter
886
          case 32: // space
887
            $(this).find('input')[0].click();
888
            break;
889
          case 65:   // Alt-A
890
            if (e.altKey) {
891
              self.checkAll();
892
            }
893
            break;
894
          case 70:   // Alt-F
895
            if (e.altKey) {
896
              self.flipAll();
897
            }
898
            break;
899
          case 85:   // Alt-U
900
            if (e.altKey) {
901
              self.uncheckAll();
902
            }
903
            break;
904
        }
905
      })
906
      .on('click.multiselect', 'input', function(e) {
907
        // Reference to this checkbox / radio input
908
        var input = this;
909
        var $input = $(input);
910
        var val = input.value;
911
        var checked = input.checked;
912
        // self is cached from outer scope above
913
        var $element = self.element;
914
        var $tags = $element.find('option');
915
        var isMultiple = $element[0].multiple;
916
        var $allInputs = self.$inputs;
917
        var numChecked = $allInputs.filter(":checked").length;
918
        var options = self.options;
919
        var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text';
920
        var optionText = $input.parent().find("span")[textFxn]();
921
        var maxSelected = options.maxSelected;
922
923
        // bail if this input is disabled or the event is cancelled
924
        if (input.disabled || self._trigger('click', e, { value: val, text: optionText, checked: checked }) === false) {
925
          e.preventDefault();
926
          return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
927
        }
928
929
        if (maxSelected && checked && numChecked > maxSelected) {
930
         if (self._trigger('maxselected', e, { labels: self.$labels, inputs: $allInputs }) !== false) {
931
            self.buttonMessage("<center><b>LIMIT OF " + (numChecked - 1) + " REACHED!</b></center>");
932
         }
933
          input.checked = false;
934
          e.preventDefault();
935
          return false;
936
        }
937
938
        // make sure the input has focus. otherwise, the esc key
939
        // won't close the menu after clicking an item.
940
        input.focus();
941
942
        // toggle aria state
943
        $input.prop('aria-selected', checked);
944
945
        // change state on the original option tags
946
        $tags.each(function() {
947
          this.selected = (this.value === val ? checked : isMultiple && this.selected);
948
        });
949
950
        // some additional single select-specific logic
951
        if (!isMultiple) {
952
          self.$labels.removeClass('ui-state-active');
953
          $input.closest('label').toggleClass('ui-state-active', checked);
954
955
          // close menu
956
          self.close();
957
        }
958
959
        // fire change on the select box
960
        $element.trigger("change");
961
962
        // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
963
        // http://bugs.jquery.com/ticket/3827
964
        setTimeout($.proxy(self.update, self), 10);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
965
      });
966
    },
967
968
    /**
969
     * Binds keyboard and mouse events to the header
970
     * Called by _bindEvents
971
     */
972
    _bindHeaderEvents: function() {
973
      var self = this;
974
975
      // header links
976
      self.$header
977
      .on('click.multiselect', 'a', function(e) {
978
        var headerLinks = {
979
          'ui-multiselect-close' : 'close',
980
          'ui-multiselect-all' : 'checkAll',
981
          'ui-multiselect-none' : 'uncheckAll',
982
          'ui-multiselect-flip' : 'flipAll',
983
          'ui-multiselect-collapseall' : 'collapseAll',
984
          'ui-multiselect-expandall' : 'expandAll'
985
        };
986
        for (hdgClass in headerLinks) {
0 ignored issues
show
Bug introduced by
The variable hdgClass seems to be never declared. Assigning variables without defining them first makes them global. If this was intended, consider making it explicit like using window.hdgClass.
Loading history...
987
          if ( this.classList.contains(hdgClass) ) {
988
            // headerLinks[hdgClass] is the click handler name
989
              self[ headerLinks[hdgClass] ]();
990
              e.preventDefault();
991
              return false;
992
          }
993
        }
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
994
      }).
995
      on('keydown.multiselect', 'a', function(e) {
996
        switch(e.which) {
997
          case 27:
998
            self.close();
999
            break;
1000
          case 9: //tab
1001
            var $target = $(e.target);
1002
            if ((e.shiftKey
1003
                && !$target.parent().prev().length
1004
                && !self.$header.find(".ui-multiselect-filter").length)
1005
               || (!$target.parent().next().length && !self.$labels.length && !e.shiftKey)) {
1006
              self.close();
1007
              e.preventDefault();
1008
            }
1009
            break;
1010
        }
1011
      });
1012
    },
1013
1014
    /**
1015
     * Allows the widget to be resized if the option is set and resizable is
1016
     * included in jQuery UI
1017
     */
1018
     _setResizable: function () {
1019
       if (!this.options.resizableMenu || !('resizable' in $.ui)) {
1020
         return;
1021
       }
1022
       this.$menu.show();
1023
       this.$menu.resizable({
1024
         containment: 'parent',
1025
         handles: 's',
1026
         helper: 'ui-multiselect-resize',
1027
         stop: function (e, ui) {
1028
           // Force consistent width
1029
           ui.size.width = ui.originalSize.width;
1030
           $(this).outerWidth(ui.originalSize.width);
1031
           if (this._trigger('resize', e, ui) !== false) {
1032
             this.options.menuHeight = ui.size.height;
1033
           }
1034
           this._setMenuHeight(true);
1035
         }
1036
       });
1037
       this.$menu.hide();
1038
     },
1039
1040
    /**
1041
     * Binds all events used in the widget
1042
     * This calls the menu, button, and header event binding methods
1043
     */
1044
    _bindEvents: function() {
1045
      if (!this.options.listbox) {
1046
         this._bindButtonEvents();
1047
      }
1048
      this._bindHeaderEvents();
1049
      this._bindCheckboxEvents();
1050
      this._setResizable();
1051
1052
      // Close each widget when clicking on any other element/anywhere else on the page,
1053
      // another widget instance, or when scrolling w/ the mouse wheel outside the menu button.
1054
      this.document.on('mousedown' + this._namespaceID
1055
                       + ' wheel' + this._namespaceID
1056
                       + ' mousewheel' + this._namespaceID, function(event) {
1057
        var target = event.target;
1058
1059
        if ( this._isOpen
1060
            && (!!this.$button ? target !== this.$button[0] && !$.contains(this.$button[0], target) : true)
1061
            && target !== this.$menu[0] && !$.contains(this.$menu[0], target) ) {
1062
          this.close();
1063
        }
1064
      }.bind(this));
1065
1066
      // deal with form resets.  the problem here is that buttons aren't
1067
      // restored to their defaultValue prop on form reset, and the reset
1068
      // handler fires before the form is actually reset.  delaying it a bit
1069
      // gives the form inputs time to clear.
1070
      $(this.element[0].form).on('reset' + this._namespaceID, function() {
1071
        setTimeout(this.refresh.bind(this), 10);
1072
      }.bind(this));
1073
    },
1074
1075
    /**
1076
     * Sets and caches the width of the button
1077
     * Can set a minimum value if less than calculated width of native select.
1078
     * @param {boolean} recalc true if cached width needs to be re-calculated
1079
     */
1080
    _setButtonWidth: function(recalc) {
1081
      if (this._savedButtonWidth && !recalc) {
1082
         return;
1083
      }
1084
1085
      // this._selectWidth set in _create() for native select element before hiding it.
1086
      var width = this._selectWidth || this._getBCRWidth( this.element );
1087
      var buttonWidth = this.options.buttonWidth || '';
1088
      if (/\d/.test(buttonWidth)) {
1089
         var parsed = parse2px(buttonWidth, this.element);
1090
         var pixels = parsed.px;
1091
         var minimax = parsed.minimax;
1092
         width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels );
1093
      }
1094
      else  { // keywords
1095
         buttonWidth = buttonWidth.toLowerCase();
1096
      }
1097
1098
      // The button width is set to auto in the CSS,
1099
      // so we only need to change it for a specific width.
1100
      if (buttonWidth !== 'auto') {
1101
         this.$button.outerWidth(width);
1102
      }
1103
      this._savedButtonWidth = width;
1104
    },
1105
1106
    /**
1107
     * Sets and caches the width of the menu
1108
     * Will use the width in options if provided, otherwise matches the button
1109
     * @param {boolean} recalc true if cached width needs to be re-calculated
1110
     */
1111
    _setMenuWidth: function(recalc) {
1112
      if (this._savedMenuWidth && !recalc) {
1113
         return;
1114
      }
1115
1116
      // Note that it is assumed that the button width was set prior.
1117
      var width = !!this.options.listbox ? this._selectWidth : (this._savedButtonWidth || this._getBCRWidth( this.$button ));
1118
1119
      var menuWidth = this.options.menuWidth || '';
1120
      if ( /\d/.test(menuWidth) ) {
1121
         var parsed = parse2px(menuWidth, this.element);
1122
         var pixels = parsed.px;
1123
         var minimax = parsed.minimax;
1124
         width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels );
1125
      }
1126
      else { // keywords
1127
         menuWidth = menuWidth.toLowerCase();
1128
      }
1129
1130
      // Note that the menu width defaults to the button width if menuWidth option is null or blank.
1131
      if (menuWidth !== 'auto') {
1132
         this.$menu.outerWidth(width);
1133
         this._savedMenuWidth = width;
1134
         return;
1135
      }
1136
1137
      // Auto width determination: get intrinsic / "shrink-wrapped" outer widths w/ margins by applying floats.
1138
      // cbWidth includes the width of the vertical scrollbar & ui-hover-state width increase per the applied CSS.
1139
      // Note that a correction is made for jQuery floating point round-off errors below.
1140
      this.$menu.addClass('ui-multiselect-measure');
1141
      var headerWidth = this.$header.outerWidth(true) + this._jqWidthFix(this.$header);
1142
      var cbWidth = this.$checkboxes.outerWidth(true) + this._jqWidthFix(this.$checkboxes);
1143
      this.$menu.removeClass('ui-multiselect-measure');
1144
1145
      var contentWidth = Math.max(this.options.wrapText.indexOf('header') > -1 ? 0 : headerWidth, cbWidth);
1146
1147
      // Use $().width() to set menu width not including padding or border.
1148
      this.$menu.width(contentWidth);
1149
      // Save width including padding and border (no margins) for consistency w/ normal width setting.
1150
      this._savedMenuWidth = this.$menu.outerWidth(false);
1151
    },
1152
1153
    /**
1154
     * Sets and caches the height of the menu
1155
     * Will use the height provided in the options unless using the select size
1156
     *  option or the option exceeds the available height for the menu
1157
     * Will set a scrollbar if the options can't all be visible at once
1158
     * @param {boolean} recalc true if cached value needs to be re-calculated
1159
     */
1160
    _setMenuHeight: function(recalc) {
1161
      var self = this;
1162
      if (self._savedMenuHeight && !recalc) {
1163
         return;
1164
      }
1165
1166
      var maxHeight = $(window).height();
1167
      var optionHeight = self.options.menuHeight || '';
1168
      var useSelectSize = false;
1169
      var elSelectSize = 4;
1170
1171
      if ( /\d/.test(optionHeight) ) {
1172
         // Deduct height of header & border/padding to find height available for checkboxes.
1173
         var $header = self.$header.filter(':visible');
1174
         var headerHeight = $header.outerHeight(true);
1175
         var menuBorderPaddingHt = this.$menu.outerHeight(false) - this.$menu.height();
1176
         var cbBorderPaddingHt = this.$checkboxes.outerHeight(false) - this.$checkboxes.height();
1177
1178
         optionHeight = parse2px(optionHeight, self.element, true).px;
1179
         maxHeight = Math.min(optionHeight, maxHeight) - headerHeight - menuBorderPaddingHt - cbBorderPaddingHt;
1180
      }
1181
      else if (optionHeight.toLowerCase() === 'size') {
1182
         // Overall height based on native select 'size' attribute
1183
         useSelectSize = true;
1184
         // Retrieves native select's size attribute or defaults to 4 (like native select).
1185
         elSelectSize = self.element[0].size || elSelectSize;
1186
      }
1187
1188
      var overflowSetting = 'hidden';
1189
      var itemCount = 0;
1190
      var hoverAdjust = 4;  // Adjustment for hover height included here.
1191
      var ulHeight = hoverAdjust;
1192
      var ulTop = -1;
1193
1194
      // The following determines the how many items are visible per the menuHeight option.
1195
      //   If the visible height calculation exceeds the calculated maximum height or if the number
1196
      //   of item heights summed equal or exceed the native select size attribute, the loop is aborted.
1197
      // If the loop is aborted, this means that the menu must be scrolled to see all the items.
1198
      self.$checkboxes.find('li:not(.ui-multiselect-optgroup),a').filter(':visible').each( function() {
1199
        if (ulTop < 0) {
1200
           ulTop = this.offsetTop;
1201
        }
1202
        ulHeight =  this.offsetTop + this.offsetHeight - ulTop + hoverAdjust;
1203
        if (useSelectSize && ++itemCount >= elSelectSize || ulHeight > maxHeight) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if useSelectSize && ++itemC...|| ulHeight > maxHeight is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
1204
          overflowSetting = 'auto';
1205
          if (!useSelectSize) {
1206
            ulHeight = maxHeight;
1207
          }
1208
          return false;
1209
        }
1210
      });
1211
1212
      // We actually only set the height of the checkboxes as the outer menu container is height:auto.
1213
      // The _savedMenuHeight value below can be compared to optionHeight as an accuracy check.
1214
      self.$checkboxes.css('overflow', overflowSetting).height(ulHeight);
1215
      self._savedMenuHeight = this.$menu.outerHeight(false);
1216
    },
1217
1218
    /**
1219
     * Calculate accurate outerWidth(false) using getBoundingClientRect()
1220
     * Note that this presumes that the element is visible in the layout.
1221
     * @param {node} elem DOM node or jQuery equivalent to get width for.
1222
     * @returns {float} Decimal floating point value for the width.
1223
     */
1224
   _getBCRWidth: function(elem) {
1225
      if (!elem || !!elem.jquery && !elem[0]) {
1226
         return null;
1227
      }
1228
      var domRect = !!elem.jquery ? elem[0].getBoundingClientRect() : elem.getBoundingClientRect();
1229
      return domRect.right - domRect.left;
1230
    },
1231
1232
    /**
1233
     * Calculate jQuery width correction factor to fix floating point round-off errors.
1234
     * Note that this presumes that the element is visible in the layout.
1235
     * @param {node} DOM node or jQuery equivalent to get width for.
1236
     * @returns {float} Correction value for the width--typically a decimal < 1.0
1237
     */
1238
    _jqWidthFix: function(elem) {
1239
      if (!elem || !!elem.jquery && !elem[0]) {
1240
         return null;
1241
      }
1242
      return !!elem.jquery
1243
                  ? this._getBCRWidth(elem[0]) - elem.outerWidth(false)
1244
                  :  this._getBCRWidth(elem) - $(elem).outerWidth(false);
1245
    },
1246
1247
    /**
1248
     * Moves focus up or down the options list
1249
     * @param {number} which key that triggered the traversal
1250
     * @param {node} start element event was triggered from
1251
     */
1252
    _traverse: function(which, start) {
1253
      var $start = $(start);
1254
      var moveToLast = which === 38 || which === 37;
1255
1256
      // select the first li that isn't an optgroup label / disabled
1257
      var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(:disabled, .ui-multiselect-optgroup):visible').first();
1258
      // we might have to jump to the next/previous option group
1259
      if (!$next.length) {
1260
        $next = $start.parents(".ui-multiselect-optgroup")[moveToLast ? "prev" : "next" ]();
1261
      }
1262
1263
      // if at the first/last element
1264
      if (!$next.length) {
1265
        var $container = this.$checkboxes;
1266
1267
        // move to the first/last
1268
        $container.find('label').filter(':visible')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover', {allowScroll: true});
1269
1270
        // set scroll position
1271
        $container.scrollTop(moveToLast ? $container.height() : 0);
1272
      }
1273
      else {
1274
        $next.find('label').filter(':visible')[ moveToLast ? "last" : "first" ]().trigger('mouseover', {allowScroll: true});
1275
      }
1276
    },
1277
1278
    /**
1279
     * Internal function to toggle checked property and related attributes on a checkbox
1280
     * The context of this function should be a checkbox; do not proxy it.
1281
     * @param {string} prop Property being toggled on the checkbox
1282
     * @param {string} flag Flag to set for the property
1283
     */
1284
    _toggleState: function(prop, flag) {
1285
      return function() {
1286
         var state = (flag === '!') ? !this[prop] : flag;
1287
1288
         if ( !this.disabled ) {
1289
          this[ prop ] = state;
1290
         }
1291
1292
        if (state) {
1293
          this.setAttribute('aria-' + prop, true);
1294
        }
1295
        else {
1296
          this.removeAttribute('aria-' + prop);
1297
        }
1298
      };
1299
    },
1300
1301
    /**
1302
     * Toggles the checked state on options within the menu
1303
     * Potentially scoped down to visible elements from filteredInputs
1304
     * @param {boolean} flag checked property to set
1305
     * @param {object} group option group that was clicked, if any
1306
     * @param {boolean} filteredInputs does not toggle hidden inputs if filtering.
1307
     */
1308
    _toggleChecked: function(flag, group, filteredInputs) {
1309
      var self = this;
1310
      var $element = self.element;
1311
      var $inputs = (group && group.length) ? group : self.$inputs;
1312
1313
      if (filteredInputs) {
1314
         $inputs = self._isOpen
1315
                     ? $inputs.closest('li').not('.ui-multiselect-excluded').find('input').not(':disabled')
1316
                     : $inputs.not(':disabled');
1317
      }
1318
1319
      // toggle state on inputs
1320
      $inputs.each(self._toggleState('checked', flag));
1321
1322
      // Give the first input focus
1323
      $inputs.eq(0).focus();
1324
1325
      // update button text
1326
      self.update();
1327
1328
      // Create a plain object of the values that actually changed
1329
      var inputValues = {};
1330
      $inputs.each( function() {
1331
        inputValues[ this.value ] = true;
1332
      });
1333
1334
      // toggle state on original option tags
1335
      $element.find('option')
1336
              .each( function() {
1337
                if (!this.disabled && inputValues[this.value]) {
1338
                  self._toggleState('selected', flag).call(this);
1339
                }
1340
              });
1341
1342
      // trigger the change event on the select
1343
      if ($inputs.length) {
1344
        $element.trigger("change");
1345
      }
1346
    },
1347
1348
   /**
1349
    * Toggles disabled state on the widget and underlying select or for just one option group.
1350
    * Will also disable all individual options if the disableInputsOnToggle option is set
1351
    * @param {boolean} flag true if disabling widget
1352
    * @param {number | string} groupID index or label of option group to disable
1353
    */
1354
    _toggleDisabled: function(flag, groupID) {
1355
      var disabledClass = 'ui-state-disabled';  // used for styling only
1356
1357
      if (this.$button) {
1358
         this.$button.prop({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ](disabledClass);
1359
	  }
1360
	  
1361
      if (this.options.disableInputsOnToggle) {
1362
         // Apply the ui-multiselect-disabled class name to identify which
1363
         // input elements this widget disabled (not pre-disabled)
1364
         // so that they can be restored if the widget is re-enabled.
1365
         var $inputs = (typeof groupID === 'undefined') ? this.$inputs : this._multiselectOptgroupFilter(groupID).find('input'),
1366
               msDisabledClass = 'ui-multiselect-disabled';
1367
         if (flag) {
1368
            var matchedInputs = $inputs.filter(':enabled').get();
1369
            for (var x = 0, len = matchedInputs.length; x < len; x++) {
1370
               matchedInputs[x].setAttribute('disabled', 'disabled');
1371
               matchedInputs[x].setAttribute('aria-disabled', 'disabled');
1372
               matchedInputs[x].classList.add(msDisabledClass);
1373
               matchedInputs[x].parentNode.classList.add(disabledClass);
1374
             }
1375
         }
1376
         else {
1377
            var matchedInputs = $inputs.filter('.' + msDisabledClass + ':disabled').get();
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable matchedInputs already seems to be declared on line 1368. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
1378
            for (var x = 0, len = matchedInputs.length; x < len; x++) {
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable len already seems to be declared on line 1369. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
Comprehensibility Naming Best Practice introduced by
The variable x already seems to be declared on line 1369. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
1379
              matchedInputs[x].removeAttribute("disabled");
1380
              matchedInputs[x].removeAttribute("aria-disabled");
1381
              matchedInputs[x].classList.remove(msDisabledClass);
1382
              matchedInputs[x].parentNode.classList.remove(disabledClass);
1383
            }
1384
         }
1385
      }
1386
1387
      var $select = (typeof groupID === 'undefined') ? this.element : this._nativeOptgroupFilter(groupID).find('option');
1388
      $select.prop({
1389
        'disabled': flag,
1390
        'aria-disabled': flag
1391
      });
1392
    },
1393
1394
    /**
1395
     * Opens the menu, possibly with effects
1396
     * Calls methods to set position and resize as well
1397
     */
1398
    open: function() {
1399
      var $button = this.$button;
1400
1401
      // bail if the multiselect open event returns false, this widget is disabled, or is already open
1402
      if (this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen || !!this.options.listbox) {
1403
        return;
1404
      }
1405
1406
      var $menu = this.$menu;
1407
      var $header = this.$header;
1408
      var $labels = this.$labels;
1409
      var $inputs = this.$inputs.filter(':checked:not(.ui-state-disabled)');
1410
      var options = this.options;
1411
      var effect = options.openEffect;
1412
      var scrollX = window.pageXOffset;
1413
      var scrollY = window.pageYOffset;
1414
1415
      // set the scroll of the checkbox container
1416
      this.$checkboxes.scrollTop(0);
1417
1418
      // Show the menu, set its dimensions, and position it.
1419
      $menu.css('display','block');
1420
      this._setMenuWidth();
1421
      this._setMenuHeight();
1422
      this.position();
1423
1424
      // Do any specified open animation effect after positioning the menu.
1425 View Code Duplication
      if (!!effect) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1426
         // Menu must be hidden for some effects (e.g. fade) to work.
1427
         $menu.css('display','none');
1428
         if (typeof effect == 'string') {
1429
            $menu.show(effect, this.speed);
1430
         }
1431
         else if (typeof effect == 'object' && effect.constructor == Array) {
1432
            $menu.show(effect[0], effect[1] || this.speed);
1433
         }
1434
         else if (typeof effect == 'object' && effect.constructor == Object) {
1435
            $menu.show(effect);
1436
         }
1437
      }
1438
1439
      // focus the first not disabled option or filter input if available
1440
      var filter = $header.find(".ui-multiselect-filter");
1441
      if (filter.length) {
1442
        filter.first().find('input').trigger('focus');
1443
      }
1444
      else if ($inputs.length) {
1445
         $inputs.eq(0).trigger('focus').parent('label').eq(0).trigger('mouseover').trigger('mouseenter');
1446
      }
1447
      else if ($labels.length) {
1448
        $labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
1449
      }
1450
      else {
1451
        $header.find('a').first().trigger('focus');
1452
      }
1453
1454
      // Restore window scroll position if altered by setting element focus
1455
      window.scrollTo(scrollX, scrollY);
1456
1457
      $button.addClass('ui-state-active');
1458
      this._isOpen = true;
1459
      this._trigger('open');
1460
    },
1461
1462
    // Close the menu
1463
    close: function() {
1464
      // bail if the multiselect close event returns false
1465
      if (this._trigger('beforeclose') === false || !!this.options.listbox) {
1466
        return;
1467
      }
1468
1469
      var $menu = this.$menu;
1470
      var options = this.options;
1471
      var effect = options.closeEffect;
1472
      var $button = this.$button;
1473
1474
      // hide the menu, maybe with a speed/effect combo
1475 View Code Duplication
      if (!!effect) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1476
         if (typeof effect == 'string') {
1477
            $menu.hide(effect, this.speed);
1478
         }
1479
         else if (typeof effect == 'object' && effect.constructor == Array) {
1480
            $menu.hide(effect[0], effect[1] || this.speed);
1481
         }
1482
         else if (typeof effect == 'object' && effect.constructor == Object) {
1483
            $menu.hide(effect);
1484
         }
1485
      }
1486
      else {
1487
         $menu.css('display','none');
1488
      }
1489
1490
      $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
1491
      this.element.trigger('blur');    // For jQuery Validate
1492
      this._isOpen = false;
1493
      this._trigger('close');
1494
      $button.trigger('focus');
1495
    },
1496
1497
    /**
1498
     * Positions the menu relative to the button.
1499
     */
1500
    position: function() {
1501
      var $button = this.$button;
1502
1503
      // Save the button height so that we can determine when it has changed due to adding/removing selections.
1504
      this._savedButtonHeight = $button.outerHeight(false);
1505
1506
      if ($.ui && $.ui.position) {
1507
        this.$menu.position(this.options.position);
1508
      }
1509
      else {
1510
        var pos = {};
1511
1512
        pos.top = $button.offset().top + this._savedButtonHeight;
1513
        pos.left = $button.offset().left;
1514
1515
        this.$menu.offset(pos);
1516
      }
1517
    },
1518
1519
    // Enable widget
1520
    enable: function(groupID) {
1521
      this._toggleDisabled(false, groupID);
1522
    },
1523
1524
    // Disable widget
1525
    disable: function(groupID) {
1526
      this._toggleDisabled(true, groupID);
1527
    },
1528
1529
    /**
1530
    * Checks all options or those in an option group
1531
    * Accounts for maxSelected possibly being set.
1532
    * @param {(number|string)} groupID index or label of option group to check all for.
1533
    */
1534
    checkAll: function(groupID) {
1535
      this._trigger('beforeCheckAll');
1536
1537
      if (this.options.maxSelected) {
1538
         return;
1539
      }
1540
1541
      if (typeof groupID === 'undefined') {  // groupID could be 0
1542
         this._toggleChecked(true);
1543
      }
1544
      else {
1545
         this._toggleChecked(true, this._multiselectOptgroupFilter(groupID).find('input'));
1546
      }
1547
1548
      this._trigger('checkAll');
1549
    },
1550
1551
    /**
1552
    * Unchecks all options or those in an option group
1553
    * @param {(number|string)} groupID index or label of option group to uncheck all for.
1554
    */
1555
    uncheckAll: function(groupID) {
1556
      this._trigger('beforeUncheckAll');
1557
1558
      if (typeof groupID === 'undefined') {  // groupID could be 0
1559
         this._toggleChecked(false);
1560
      }
1561
      else {
1562
         this._toggleChecked(false, this._multiselectOptgroupFilter(groupID).find('input'));
1563
      }
1564
      if ( !this.element[0].multiple && !this.$inputs.filter(':checked').length) {
1565
        // Forces the underlying single-select to have no options selected.
1566
        this.element[0].selectedIndex = -1;
1567
      }
1568
1569
      this._trigger('uncheckAll');
1570
    },
1571
1572
    /**
1573
    * Flips all options or those in an option group.
1574
    * Accounts for maxSelected possibly being set.
1575
    * @param {(number|string)} groupID index or label of option group to flip all for.
1576
    */
1577
    flipAll: function(groupID) {
1578
      this._trigger('beforeFlipAll');
1579
1580
      var gotID = (typeof groupID !== 'undefined');  // groupID could be 0
1581
      var maxSelected = this.options.maxSelected;
1582
      var inputCount = this.$inputs.length;
1583
      var checkedCount = this.$inputs.filter(':checked').length;
1584
      var $filteredOptgroupInputs = gotID ? this._multiselectOptgroupFilter(groupID).find('input') : null;
1585
      var gInputCount = gotID ? $filteredOptgroupInputs.length : 0;
1586
      var gCheckedCount = gotID ? $filteredOptgroupInputs.filter(':checked').length : 0;
1587
1588
      if (!maxSelected
1589
          || maxSelected >= (gotID ? checkedCount - gCheckedCount + gInputCount - gCheckedCount : inputCount - checkedCount ) ) {
1590
         if (gotID) {
1591
            this._toggleChecked('!', $filteredOptgroupInputs);
1592
         }
1593
         else {
1594
            this._toggleChecked('!');
1595
         }
1596
         this._trigger('flipAll');
1597
      }
1598
      else {
1599
         this.buttonMessage("<center><b>Flip All Not Permitted.</b></center>");
1600
      }
1601
    },
1602
1603
    /**
1604
    * Collapses all option groups or just the one specified.
1605
    * @param {(number|string)} groupID index or label of option group to collapse.
1606
    */
1607
    collapseAll: function(groupID) {
1608
      this._trigger('beforeCollapseAll');
1609
1610
      var $optgroups = (typeof groupID === 'undefined')  // groupID could be 0
1611
                              ? this.$checkboxes.find('.ui-multiselect-optgroup')
1612
                              : this._multiselectOptgroupFilter(groupID);
1613
1614
      $optgroups.addClass('ui-multiselect-collapsed')
1615
                     .children('.ui-multiselect-collapser').attr('title', this.linkInfo.expand.title ).html( this.linkInfo.expand.icon );
1616
1617
      this._trigger('collapseAll');
1618
    },
1619
1620
    /**
1621
    * Expands all option groups or just the one specified.
1622
    * @param {(number|string)} groupID index or label of option group to expand.
1623
    */
1624
    expandAll: function(groupID) {
1625
      this._trigger('beforeExpandAll');
1626
1627
      var $optgroups = (typeof groupID === 'undefined')  // groupID could be 0
1628
                              ? this.$checkboxes.find('.ui-multiselect-optgroup')
1629
                              : this._multiselectOptgroupFilter(groupID);
1630
1631
      $optgroups.removeClass('ui-multiselect-collapsed')
1632
                     .children('.ui-multiselect-collapser').attr('title', this.linkInfo.collapse.title ).html( this.linkInfo.collapse.icon );
1633
1634
      this._trigger('expandAll');
1635
    },
1636
1637
    /**
1638
     * Flashes a message in the button caption for 1 second.
1639
     * Useful for very short warning messages to the user.
1640
     * @param {string} message HTML to show in the button.
1641
     */
1642
    buttonMessage: function(message) {
1643
       var self = this;
1644
       self.$buttonlabel.html(message);
1645
       setTimeout( function() {
1646
         self.update();
1647
       }, 1000 );
1648
    },
1649
1650
    /**
1651
     * Provides a list of all checked options
1652
     * @returns {array} list of inputs
1653
     */
1654
    getChecked: function() {
1655
      return this.$inputs.filter(":checked");
1656
    },
1657
1658
    /**
1659
     * Provides a list of all options that are not checked
1660
     * @returns {array} list of inputs
1661
     */
1662
    getUnchecked: function() {
1663
      return this.$inputs.filter(":not(:checked)");
1664
    },
1665
1666
    /**
1667
     * Destroys the widget instance
1668
     * @returns {object} reference to widget
1669
     */
1670
    destroy: function() {
1671
      // remove classes + data
1672
      $.Widget.prototype.destroy.call(this);
1673
1674
      // unbind events
1675
      this.document.off(this._namespaceID);
1676
      $(this.element[0].form).off(this._namespaceID);
1677
1678
      if (!this.options.listbox) {
1679
         this.$button.remove();
1680
      }
1681
      this.$menu.remove();
1682
      this.element.show();
1683
1684
      return this;
1685
    },
1686
1687
    /**
1688
     * @returns {boolean} indicates whether the menu is open
1689
     */
1690
    isOpen: function() {
1691
      return this._isOpen;
1692
    },
1693
1694
    /**
1695
     * @returns {object} jQuery object for menu
1696
     */
1697
    widget: function() {
1698
      return this.$menu;
1699
    },
1700
1701
    /**
1702
     * @returns {string} namespaceID for use with external event handlers.
1703
     */
1704
    getNamespaceID: function() {
1705
      return this._namespaceID;
1706
    },
1707
1708
    /**
1709
     * @returns {object} jQuery object for button
1710
     */
1711
    getButton: function() {
1712
      return this.$button;
1713
    },
1714
1715
    /**
1716
     * Essentially an alias for widget
1717
     * @returns {object} jQuery object for menu
1718
     */
1719
    getMenu: function() {
1720
      return this.$menu;
1721
    },
1722
1723
    /**
1724
     * @returns {array} List of the option labels
1725
     */
1726
    getLabels: function() {
1727
      return this.$labels;
1728
    },
1729
1730
    /**
1731
     * @returns {array} List of option groups that are collapsed
1732
     */
1733
    getCollapsed: function() {
1734
       return this.$checkboxes.find('.ui-multiselect-collapsed');
1735
    },
1736
1737
    /**
1738
    * Sets the value of the underlying select then resyncs the menu.
1739
    * @param {(string|array)} newValue value(s) to set the underlying select to.
1740
     */
1741
    value: function(newValue) {
1742
      if (typeof newValue !== 'undefined') {
1743
         this.element.val(newValue);
1744
         this.resync();
1745
         return this.element;
1746
      }
1747
      else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
1748
         return this.element.val();
1749
      }
1750
    },
1751
1752
    /**
1753
     * Determines if HTML content is allowed for the given element type
1754
     * @param {string} element to check
1755
     * @return {boolean} true if html content is allowed
1756
     */
1757
    htmlAllowedFor: function(element) {
1758
      return this.options.htmlText.indexOf(element) > -1;
1759
    },
1760
1761
    /**
1762
    * Adds an option to the widget and underlying select
1763
    * @param {object} attributes hash to be added to the option
1764
    * @param {string} text label for the option
1765
    * @param {(number|string)} groupID index or label of option group to add the option to
1766
    */
1767
    addOption: function(attributes, text, groupID) {
1768
      var self = this;
1769
      var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text';
1770
      var $option = $( document.createElement('option') ).attr(attributes)[textFxn](text);
1771
      var optionNode = $option.get(0);
1772
1773
      if (typeof groupID === 'undefined') {  // groupID could be 0
1774
         self.element.append($option);
1775
         self.$checkboxes.append(self._makeOption(optionNode));
1776
      }
1777
      else {
1778
         self._nativeOptgroupFilter(groupID).append($option);
1779
         self._multiselectOptgroupFilter(groupID).append(self._makeOption(optionNode));
1780
      }
1781
1782
      self._updateCache();
1783
    },
1784
1785
    /**
1786
    * Finds an optgroup in the native select by index or label
1787
    * @param {(number|string)} groupID index or label of option group to find
1788
    */
1789
    _nativeOptgroupFilter: function(groupID) {
1790
       return this.element.children("OPTGROUP").filter( function(index) {
1791
          return (typeof groupID === 'number' ? index === groupID : this.getAttribute('label') === groupID);
1792
       });
1793
    },
1794
1795
    /**
1796
    * Finds an optgroup in the multiselect widget by index or label
1797
    * @param {(number|string)} groupID index or label of option group to find
1798
    */
1799
    _multiselectOptgroupFilter: function(groupID) {
1800
       return this.$menu.find(".ui-multiselect-optgroup").filter( function(index) {
1801
          return (typeof groupID === 'number' ? index === groupID : this.getElementsByClassName('ui-multiselect-grouplabel')[0].textContent === groupID);
1802
       });
1803
    },
1804
1805
    /**
1806
     * Removes an option from the widget and underlying select
1807
     * @param {string} value attribute corresponding to option being removed
1808
     */
1809
    removeOption: function(value) {
1810
      if (!value) {
1811
        return;
1812
      }
1813
      this.element.find("option[value=" + value + "]").remove();
1814
      this.$labels.find("input[value=" + value + "]").parents("li").remove();
1815
1816
      this._updateCache();
1817
    },
1818
1819
    /**
1820
     * Reacts to options being changed
1821
     * Delegates to various handlers
1822
     * @param {string} key into the options hash
1823
     * @param {any} value to be assigned to that option
1824
     */
1825
    _setOption: function(key, value) {
1826
      var $header = this.$header,
1827
            $menu = this.$menu;
1828
1829
      switch(key) {
1830
        case 'header':
1831
          if (typeof value === 'boolean') {
1832
            $header.toggle( value );
1833
          }
1834
          else if (typeof value === 'string') {
1835
            this.$headerLinkContainer.children('li:not(:last-child)').remove();
1836
            this.$headerLinkContainer.prepend('<li>' + value + '</li>');
1837
          }
1838
          break;
1839
        case 'checkAllText':
1840
        case 'uncheckAllText':
1841
        case 'flipAllText':
1842
        case 'collapseAllText':
1843
        case 'expandAllText':
1844
          if (key !== 'checkAllText' || !this.options.maxSelected) {
1845
            // eq(-1) finds the last span
1846
            $header.find('a.' + this.linkInfo[key.replace('Text','')]['class'] + ' span').eq(-1).html(value);
1847
          }
1848
          break;
1849
        case 'checkAllIcon':
1850
        case 'uncheckAllIcon':
1851
        case 'flipAllIcon':
1852
        case 'collapseAllIcon':
1853
        case 'expandAllIcon':
1854
          if (key !== 'checkAllIcon' || !this.options.maxSelected) {
1855
            // eq(0) finds the first span
1856
            $header.find('a.' + this.linkInfo[key.replace('Icon','')]['class'] + ' span').eq(0).replaceWith(value);
1857
          }
1858
          break;
1859
        case 'openIcon':
1860
          $menu.find('span.ui-multiselect-open').html(value);
1861
          break;
1862
        case 'closeIcon':
1863
          $menu.find('a.ui-multiselect-close').html(value);
1864
          break;
1865
        case 'buttonWidth':
1866
        case 'menuWidth':
1867
          this.options[key] = value;
1868
          this._setButtonWidth(true);        // true forces recalc of cached value.
1869
          this._setMenuWidth(true);          // true forces recalc of cached value.
1870
          break;
1871
        case 'menuHeight':
1872
          this.options[key] = value;
1873
          this._setMenuHeight(true);         // true forces recalc of cached value.
1874
          break;
1875
        case 'selectedText':
1876
        case 'selectedList':
1877
        case 'maxSelected':
1878
        case 'noneSelectedText':
1879
        case 'selectedListSeparator':
1880
          this.options[key] = value;            // these all need to update immediately for the update() call
1881
          this.update(true);
1882
          break;
1883
        case 'classes':
1884
          $menu.add(this.$button).removeClass(this.options.classes).addClass(value);
1885
          break;
1886
        case 'multiple':
1887
          var $element = this.element;
1888
          if (!!$element[0].multiple !== value) {
1889
             $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value);
1890
             $element[0].multiple = value;
1891
             this.uncheckAll();
1892
             this.refresh();
1893
          }
1894
          break;
1895
       case 'position':
1896
         if (value !== null && !$.isEmptyObject(value) ) {
1897
            this.options.position = value;
1898
         }
1899
         this.position();
1900
         break;
1901
       case 'zIndex':
1902
         this.options.zIndex = value;
1903
         this.$menu.css('z-index', value);
1904
         break;
1905
      default:
1906
         this.options[key] = value;
1907
     }
1908
     $.Widget.prototype._setOption.apply(this, arguments);
1909
   },
1910
1911
  });
1912
1913
   // Fix for jQuery UI modal dialogs
1914
   // https://api.jqueryui.com/dialog/#method-_allowInteraction
1915
   // https://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/
1916
   if ($.ui && 'dialog' in $.ui) {
1917
      $.widget( "ui.dialog", $.ui.dialog, {
1918
         _allowInteraction: function( event ) {
1919
             if ( this._super( event ) || $( event.target ).closest('.ui-multiselect-menu' ).length ) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if this._super(event) || $(...ltiselect-menu").length is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
1920
               return true;
1921
             }
1922
         }
1923
      });
1924
   }
1925
1926
})(jQuery);
1927