PageSourceSearch

https://www.hba.org/assets/common/javascript/jQueryAddons/multiselect/jquery.multiselect-1.13a.js

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