PageSourceSearch

https://www.merx.com/scripts/jquery/plugins/jquery.multiselect.js

js merx.com collected 2026-10-02 03:25:00 UTC 23,652 bytes, 776 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 MultiSelect UI Widget 1.14pre (with fixes for METS) 
4 * Copyright (c) 2012 Eric Hynds
5 *
6 * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
7 *
8 * Depends:
9 *   - jQuery 1.4.2+
10 *   - jQuery UI 1.8 widget factory
11 *
12 * Optional:
13 *   - jQuery UI effects
14 *   - jQuery UI position utility
15 *
16 * Dual licensed under the MIT and GPL licenses:
17 *   http://www.opensource.org/licenses/mit-license.php
18 *   http://www.gnu.org/licenses/gpl.html
19 *
20 *
21 * Date       Change
22 * ---------- ---------------------------------
23 * 2014-08-12 Adjusted the function _setButtonValue to use .html instead of .text (allows proper HTML formatting)
24 * 
25 * 
26 */
27(function($, undefined) {
28
29  var multiselectID = 0;
30  var $doc = $(document);
31
32  $.widget("ech.multiselect", {
33
34    // default options
35    options: {
36      header: true,
37      height: 175,
38      minWidth: 225,
39      classes: '',
40      checkAllText: 'Check all',
41      uncheckAllText: 'Uncheck all',
42      noneSelectedText: 'Select options',
43      selectedText: '# selected',
44      selectedList: 0,
45      show: null,
46      hide: null,
47      autoOpen: false,
48      multiple: true,
49      position: {},
50      appendTo: "body"
51    },
52
53    _create: function() {
54      var el = this.element.hide();
55      var o = this.options;
56
57      this.speed = $.fx.speeds._default; // default speed for effects
58      this._isOpen = false; // assume no
59
60      // create a unique namespace for events that the widget
61      // factory cannot unbind automatically. Use eventNamespace if on
62      // jQuery UI 1.9+, and otherwise fallback to a custom string.
63      this._namespaceID = this.eventNamespace || ('multiselect' + multiselectID);
64
65      var button = (this.button = $('<button type="button"><span class="ui-icon ui-icon-triangle-1-s"></span></button>'))
66        .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all')
67        .addClass(o.classes)
68        .attr({ 'title':el.attr('title'), 'aria-haspopup':true, 'tabIndex':el.attr('tabIndex') })
69        .insertAfter(el),
70
71        buttonlabel = (this.buttonlabel = $('<span />'))
72          .html(o.noneSelectedText)
73          .appendTo(button),
74
75        menu = (this.menu = $('<div />'))
76          .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all')
77          .addClass(o.classes)
78          .appendTo($(o.appendTo)),
79
80        header = (this.header = $('<div />'))
81          .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
82          .appendTo(menu),
83
84        headerLinkContainer = (this.headerLinkContainer = $('<ul />'))
85          .addClass('ui-helper-reset')
86          .html(function() {
87            if(o.header === true) {
88              return '<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>' + o.checkAllText + '</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>' + o.uncheckAllText + '</span></a></li>';
89            } else if(typeof o.header === "string") {
90              return '<li>' + o.header + '</li>';
91            } else {
92              return '';
93            }
94          })
95          .append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>')
96          .appendTo(header),
97
98        fieldset = (this.fieldset = $('<fieldset />'))
99          .addClass('accessibility-fieldset')
100          .appendTo(menu),
101
102        fieldsetLegend = (this.fieldsetLegend = $('<legend />'))
103          .text(this._getFieldsetLegend())
104          .appendTo(fieldset),
105
106        checkboxContainer = (this.checkboxContainer = $('<ul />'))
107          .addClass('ui-multiselect-checkboxes ui-helper-reset')
108          .appendTo(fieldset);
109
110        // perform event bindings
111        this._bindEvents();
112
113        // build menu
114        this.refresh(true);
115
116        // some addl. logic for single selects
117        if(!o.multiple) {
118          menu.addClass('ui-multiselect-single');
119        }
120
121        // bump unique ID
122        multiselectID++;
123    },
124
125    _getFieldsetLegend: function() {
126      var id = this.element.attr('id');
127      var associatedLabel;
128      var label = '';
129
130      if(id) {
131        associatedLabel = $('label').filter(function() {
132          return $(this).attr('for') === id;
133        }).first().clone();
134
135        associatedLabel.find('.activeComparatorFlag').each(function() {
136          var labelledBy = $(this).attr('aria-labelledby');
137
138          if(labelledBy) {
139            associatedLabel.find('#' + labelledBy).remove();
140          }
141
142          $(this).remove();
143        });
144        associatedLabel.find('.mets-field-mandatory').remove();
145        label = $.trim(associatedLabel.text());
146      }
147
148      return label || this.element.attr('title') || this.element.attr('aria-label') || this.options.noneSelectedText;
149    },
150
151    _init: function() {
152      if(this.options.header === false) {
153        this.header.hide();
154      }
155      if(!this.options.multiple) {
156        this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();
157      }
158      if(this.options.autoOpen) {
159        this.open();
160      }
161      if(this.element.is(':disabled')) {
162        this.disable();
163      }
164    },
165
166    refresh: function(init) {
167      var el = this.element;
168      var o = this.options;
169      var menu = this.menu;
170      var checkboxContainer = this.checkboxContainer;
171      var optgroups = [];
172      var html = "";
173      var id = el.attr('id') || multiselectID++; // unique ID for the label & option tags
174
175      // build items
176      el.find('option').each(function(i) {
177        var $this = $(this);
178        var parent = this.parentNode;
179        var description = this.innerHTML;
180        var title = this.title;
181        var value = this.value;
182        var inputID = 'ui-multiselect-' + (this.id || id + '-option-' + i);
183        var isDisabled = this.disabled;
184        var isSelected = this.selected;
185        var labelClasses = [ 'ui-corner-all' ];
186        var liClasses = (isDisabled ? 'ui-multiselect-disabled ' : ' ') + this.className;
187        var optLabel;
188
189        // is this an optgroup?
190        if(parent.tagName === 'OPTGROUP') {
191          optLabel = parent.getAttribute('label');
192
193          // has this optgroup been added already?
194          if($.inArray(optLabel, optgroups) === -1) {
195            html += '<li class="ui-multiselect-optgroup-label ' + parent.className + '"><a href="#">
195' + optLabel + '</a></li>';
196            optgroups.push(optLabel);
197          }
198        }
199
200        if(isDisabled) {
201          labelClasses.push('ui-state-disabled');
202        }
203
204        // browsers automatically select the first option
205        // by default with single selects
206        if(isSelected && !o.multiple) {
207          labelClasses.push('ui-state-active');
208        }
209
210        html += '<li class="' + liClasses + '">';
211
212        // create the label
213        html += '<label for="' + inputID + '" title="' + title + '" class="' + labelClasses.join(' ') + '">';
214        html += '<input id="' + inputID + '" name="multiselect_' + id + '" type="' + (o.multiple ? "checkbox" : "radio") + '" value="' + value + '" title="' + title + '"';
215
216        // pre-selected?
217        if(isSelected) {
218          html += ' checked="checked"';
219          html += ' aria-selected="true"';
220        }
221
222        // disabled?
223        if(isDisabled) {
224          html += ' disabled="disabled"';
225          html += ' aria-disabled="true"';
226        }
227
228        // add the title and close everything off
229        html += ' /><span>' + description + '</span></label></li>';
230      });
231
232      // insert into the DOM
233      checkboxContainer.html(html);
234
235      // cache some moar useful elements
236      this.labels = menu.find('label');
237      this.inputs = this.labels.children('input');
238
239      // set widths
240      this._setButtonWidth();
241      this._setMenuWidth();
242
243      // remember default value
244      this.button[0].defaultValue = this.update();
245
246      // broadcast refresh event; useful for widgets
247      if(!init) {
248        this._trigger('refresh');
249      }
250    },
251
252    // updates the button text. call refresh() to rebuild
253    update: function() {
254      var o = this.options;
255      var $inputs = this.inputs;
256      var $checked = $inputs.filter(':checked');
257      var numChecked = $checked.length;
258      var value;
259
260      if(numChecked === 0) {
261        value = o.noneSelectedText;
262      } else {
263        if($.isFunction(o.selectedText)) {
264          value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get());
265        } else if(/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) {
266          value = $checked.map(function() { return $(this).next().html(); }).get().join(', ');
267        } else {
268          value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length);
269        }
270      }
271
272      this._setButtonValue(value);
273
274      return value;
275    },
276
277    // this exists as a separate method so that the developer 
278    // can easily override it.
279    _setButtonValue: function(value) {
280      // 2014-08-12 Adjusted the function _setButtonValue to use .html instead of .text (allows proper HTML formatting)
281      this.buttonlabel.html(value);
282    },
283
284    // binds events
285    _bindEvents: function() {
286      var self = this;
287      var button = this.button;
288
289      function clickHandler() {
290        self[ self._isOpen ? 'close' : 'open' ]();
291        return false;
292      }
293
294      // webkit doesn't like it when you click on the span :(
295      button
296        .find('span')
297        .bind('click.multiselect', clickHandler);
298
299      // button events
300      button.bind({
301        click: clickHandler,
302        keypress: function(e) {
303          switch(e.which) {
304            case 27: // esc
305              case 38: // up
306              case 37: // left
307              self.close();
308            break;
309            case 39: // right
310              case 40: // down
311              self.open();
312            break;
313          }
314        },
315        mouseenter: function() {
316          if(!button.hasClass('ui-state-disabled')) {
317            $(this).addClass('ui-state-hover');
318          }
319        },
320        mouseleave: function() {
321          $(this).removeClass('ui-state-hover');
322        },
323        focus: function() {
324          if(!button.hasClass('ui-state-disabled')) {
325            $(this).addClass('ui-state-focus');
326          }
327        },
328        blur: function() {
329          $(this).removeClass('ui-state-focus');
330        }
331      });
332
333      // header links
334      this.header.delegate('a', 'click.multiselect', function(e) {
335        // close link
336        if($(this).hasClass('ui-multiselect-close')) {
337          self.close();
338
339          // check all / uncheck all
340        } else {
341          self[$(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll']();
342        }
343
344        e.preventDefault();
345      });
346
347      // optgroup label toggle support
348      this.menu.delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function(e) {
349        e.preventDefault();
350
351        var $this = $(this);
352        var $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)');
353        var nodes = $inputs.get();
354        var label = $this.parent().text();
355
356        // trigger event and bail if the return is false
357        if(self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) {
358          return;
359        }
360
361        // toggle inputs
362        self._toggleChecked(
363          $inputs.filter(':checked').length !== $inputs.length,
364          $inputs
365        );
366
367        self._trigger('optgrouptoggle', e, {
368          inputs: nodes,
369          label: label,
370          checked: nodes[0].checked
371        });
372      })
373      .delegate('label', 'mouseenter.multiselect', function() {
374        if(!$(this).hasClass('ui-state-disabled')) {
375          self.labels.removeClass('ui-state-hover');
376          $(this).addClass('ui-state-hover').find('input').focus();
377        }
378      })
379      .delegate('label', 'keydown.multiselect', function(e) {
380        e.preventDefault();
381
382        switch(e.which) {
383          case 9: // tab
384            case 27: // esc
385            self.close();
386          break;
387          case 38: // up
388            case 40: // down
389            case 37: // left
390            case 39: // right
391            self._traverse(e.which, this);
392          break;
393          case 13: // enter
394            $(this).find('input')[0].click();
395          break;
396        }
397      })
398      .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function(e) {
399        var $this = $(this);
400        var val = this.value;
401        var checked = this.checked;
402        var tags = self.element.find('option');
403
404        // bail if this input is disabled or the event is cancelled
405        if(this.disabled || self._trigger('click', e, { value: val, text: this.title, checked: checked }) === false) {
406          e.preventDefault();
407          return;
408        }
409
410        // make sure the input has focus. otherwise, the esc key
411        // won't close the menu after clicking an item.
412        $this.focus();
413
414        // toggle aria state
415        $this.attr('aria-selected', checked);
416
417        // change state on the original option tags
418        tags.each(function() {
419          if(this.value === val) {
420            this.selected = checked;
421          } else if(!self.options.multiple) {
422            this.selected = false;
423          }
424        });
425
426        // some additional single select-specific logic
427        if(!self.options.multiple) {
428          self.labels.removeClass('ui-state-active');
429          $this.closest('label').toggleClass('ui-state-active', checked);
430
431          // close menu
432          self.close();
433        }
434
435        // fire change on the select box
436        self.element.trigger("change");
437
438        // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
439        // http://bugs.jquery.com/ticket/3827
440        setTimeout($.proxy(self.update, self), 10);
441      });
442
443      // close each widget when clicking on any other element/anywhere else on the page
444      $doc.bind('mousedown.' + this._namespaceID, function(event) {
445        var target = event.target;
446
447        if(self._isOpen
448            && target !== self.button[0]
449            && target !== self.menu[0]
450            && !$.contains(self.menu[0], target)
451            && !$.contains(self.button[0], target)
452          ) {
453          self.close();
454        }
455      });
456
457      // deal with form resets.  the problem here is that buttons aren't
458      // restored to their defaultValue prop on form reset, and the reset
459      // handler fires before the form is actually reset.  delaying it a bit
460      // gives the form inputs time to clear.
461      $(this.element[0].form).bind('reset.multiselect', function() {
462        setTimeout($.proxy(self.refresh, self), 10);
463      });
464    },
465
466    // set button width
467    _setButtonWidth: function() {
468      var width = this.element.outerWidth();
469      var o = this.options;
470
471      if(/\d/.test(o.minWidth) && width < o.minWidth) {
472        width = o.minWidth;
473      }
474
475      // set widths
476      this.button.outerWidth(width);
477    },
478
479    // set menu width
480    _setMenuWidth: function() {
481      var m = this.menu;
482      m.outerWidth(this.button.outerWidth());
483    },
484
485    // move up or down within the menu
486    _traverse: function(which, start) {
487      var $start = $(start);
488      var moveToLast = which === 38 || which === 37;
489
490      // select the first li that isn't an optgroup label / disabled
491      var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)').first();
492
493      // if at the first/last element
494      if(!$next.length) {
495        var $container = this.menu.find('ul').last();
496
497        // move to the first/last
498        this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover');
499
500        // set scroll position
501        $container.scrollTop(moveToLast ? $container.height() : 0);
502
503      } else {
504        $next.find('label').trigger('mouseover');
505      }
506    },
507
508    // This is an internal function to toggle the checked property and
509    // other related attributes of a checkbox.
510    //
511    // The context of this function should be a checkbox; do not proxy it.
512    _toggleState: function(prop, flag) {
513      return function() {
514        if(!this.disabled) {
515          this[ prop ] = flag;
516        }
517
518        if(flag) {
519          this.setAttribute('aria-selected', true);
520        } else {
521          this.removeAttribute('aria-selected');
522        }
523      };
524    },
525
526    _toggleChecked: function(flag, group) {
527      var $inputs = (group && group.length) ?  group : this.inputs;
528      var self = this;
529
530      // toggle state on inputs
531      $inputs.each(this._toggleState('checked', flag));
532
533      // give the first input focus
534      $inputs.eq(0).focus();
535
536      // update button text
537      this.update();
538
539      // gather an array of the values that actually changed
540      var values = $inputs.map(function() {
541        return this.value;
542      }).get();
543
544      // toggle state on original option tags
545      this.element
546        .find('option')
547        .each(function() {
548          if(!this.disabled && $.inArray(this.value, values) > -1) {
549            self._toggleState('selected', flag).call(this);
550          }
551        });
552
553      // trigger the change event on the select
554      if($inputs.length) {
555        this.element.trigger("change");
556      }
557    },
558
559    _toggleDisabled: function(flag) {
560      this.button.attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
561
562      var inputs = this.menu.find('input');
563      var key = "ech-multiselect-disabled";
564
565      if(flag) {
566        // remember which elements this widget disabled (not pre-disabled)
567        // elements, so that they can be restored if the widget is re-enabled.
568        inputs = inputs.filter(':enabled').data(key, true)
569      } else {
570        inputs = inputs.filter(function() {
571          return $.data(this, key) === true;
572        }).removeData(key);
573      }
574
575      inputs
576        .attr({ 'disabled':flag, 'arial-disabled':flag })
577        .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
578
579      this.element.attr({
580        'disabled':flag,
581        'aria-disabled':flag
582      });
583    },
584
585    // open the menu
586    open: function(e) {
587      var self = this;
588      var button = this.button;
589      var menu = this.menu;
590      var speed = this.speed;
591      var o = this.options;
592      var args = [];
593
594      // bail if the multiselectopen event returns false, this widget is disabled, or is already open
595      if(this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen) {
596        return;
597      }
598
599      var $container = menu.find('ul').last();
600      var effect = o.show;
601
602      // figure out opening effects/speeds
603      if($.isArray(o.show)) {
604        effect = o.show[0];
605        speed = o.show[1] || self.speed;
606      }
607
608      // if there's an effect, assume jQuery UI is in use
609      // build the arguments to pass to show()
610      if(effect) {
611        args = [ effect, speed ];
612      }
613
614      // set the scroll of the checkbox container
615      $container.scrollTop(0).height(o.height);
616
617      // positon
618      this.position();
619
620      // show the menu, maybe with a speed/effect combo
621      $.fn.show.apply(menu, args);
622
623      // select the first not disabled option
624      // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover
625      // will actually trigger mouseenter.  the mouseenter trigger is there for when it's eventually fixed
626      this.labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
627
628      button.addClass('ui-state-active');
629      this._isOpen = true;
630      this._trigger('open');
631    },
632
633    // close the menu
634    close: function() {
635      if(this._trigger('beforeclose') === false) {
636        return;
637      }
638
639      var o = this.options;
640      var effect = o.hide;
641      var speed = this.speed;
642      var args = [];
643
644      // figure out opening effects/speeds
645      if($.isArray(o.hide)) {
646        effect = o.hide[0];
647        speed = o.hide[1] || this.speed;
648      }
649
650      if(effect) {
651        args = [ effect, speed ];
652      }
653
654      $.fn.hide.apply(this.menu, args);
655      this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
656      this._isOpen = false;
657      this._trigger('close');
658    },
659
660    enable: function() {
661      this._toggleDisabled(false);
662    },
663
664    disable: function() {
665      this._toggleDisabled(true);
666    },
667
668    checkAll: function(e) {
669      this._toggleChecked(true);
670      this._trigger('checkAll');
671    },
672
673    uncheckAll: function() {
674      this._toggleChecked(false);
675      this._trigger('uncheckAll');
676    },
677
678    getChecked: function() {
679      return this.menu.find('input').filter(':checked');
680    },
681
682    destroy: function() {
683      // remove classes + data
684      $.Widget.prototype.destroy.call(this);
685
686      // unbind events
687      $doc.unbind(this._namespaceID);
688
689      this.button.remove();
690      this.menu.remove();
691      this.element.show();
692
693      return this;
694    },
695
696    isOpen: function() {
697      return this._isOpen;
698    },
699
700    widget: function() {
701      return this.menu;
702    },
703
704    getButton: function() {
705      return this.button;
706    },
707
708    position: function() {
709      var o = this.options;
710
711      // use the position utility if it exists and options are specifified
712      if($.ui.position && !$.isEmptyObject(o.position)) {
713        o.position.of = o.position.of || this.button;
714
715        this.menu
716          .show()
717          .position(o.position)
718          .hide();
719
720        // otherwise fallback to custom positioning
721      } else {
722        var pos = this.button.offset();
723
724        this.menu.css({
725          top: pos.top + this.button.outerHeight(),
726          left: pos.left
727        });
728      }
729    },
730
731    // react to option changes after initialization
732    _setOption: function(key, value) {
733      var menu = this.menu;
734
735      switch(key) {
736        case 'header':
737          menu.find('div.ui-multiselect-header')[value ? 'show' : 'hide']();
738          break;
739        case 'checkAllText':
740          menu.find('a.ui-multiselect-all span').eq(-1).text(value);
741          break;
742        case 'uncheckAllText':
743          menu.find('a.ui-multiselect-none span').eq(-1).text(value);
744          break;
745        case 'height':
746          menu.find('ul').last().height(parseInt(value, 10));
747          break;
748        case 'minWidth':
749          this.options[key] = parseInt(value, 10);
750          this._setButtonWidth();
751          this._setMenuWidth();
752          break;
753        case 'selectedText':
754        case 'selectedList':
755        case 'noneSelectedText':
756          this.options[key] = value; // these all needs to update immediately for the update() call
757          this.update();
758          break;
759        case 'classes':
760          menu.add(this.button).removeClass(this.options.classes).addClass(value);
761          break;
762        case 'multiple':
763          menu.toggleClass('ui-multiselect-single', !value);
764          this.options.multiple = value;
765          this.element[0].multiple = value;
766          this.refresh();
767          break;
768        case 'position':
769          this.position();
770      }
771
772      $.Widget.prototype._setOption.apply(this, arguments);
773    }
774  });
775
776})(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.