PageSourceSearch

https://portal.colocrossing.com/js/ui.multiselect.js

js colocrossing.com collected 2026-10-03 21:19:20 UTC 28,846 bytes, 901 lines download raw bytes

1/*
2 * jQuery UI Multiselect
3 *
4 * Authors:
5 *  Michael Aufreiter (quasipartikel.at)
6 *  Yanick Rochon (yanick.rochon[at]gmail[dot]com)
7 *
8 * Dual licensed under the MIT (MIT-LICENSE.txt)
9 * and GPL (GPL-LICENSE.txt) licenses.
10 *
11 * http://yanickrochon.uuuq.com/multiselect/
12 *
13 *
14 * Depends:
15 *	 ui.core.js
16 *	 ui.draggable.js
17 *  ui.droppable.js
18 *  ui.sortable.js
19 *  jquery.blockUI (http://github.com/malsup/blockui/)
20 *  jquery.tmpl (http://andrew.hedges.name/blog/2008/09/03/introducing-jquery-simple-templates
21 *
22 * Optional:
23 *  localization (http://plugins.jquery.com/project/localisation)
24 *
25 * Notes:
26 *  The strings in this plugin use a templating engine to enable localization
27 *  and allow flexibility in the messages. Read the documentation for more details.
28 *
29 * Todo:
30 *  restore selected items on remote searchable multiselect upon page reload (same behavior as local mode)
31 *  (is it worth it??) add a public function to apply the nodeComparator to all items (when using nodeComparator setter)
32 *  support for option groups, disabled options, etc.
33 *  speed improvements
34 *  tests and optimizations
35 *    - test getters/setters (including options from the defaults)
36 */
37
38
39/********************************
40 *  Default callbacks
41 ********************************/
42
43// expect data to be "val1=text1[\nval2=text2[\n...]]"
44var defaultDataParser = function(data) {
45	if ( typeof data == 'string' ) {
46		var pattern = /^(\s\n\r\t)*\+?$/;
47		var selected, line, lines = data.split(/\n/);
48		data = {};
49		for (var i in lines) {
50			line = lines[i].split("=");
51			// make sure the key is not empty
52			if (!pattern.test(line[0])) {
53				selected = (line[0].lastIndexOf('+') == line.length - 1);
54				if (selected) line[0] = line.substr(0,line.length-1);
55				// if no value is specified, default to the key value
56				data[line[0]] = {
57					selected: false,
58					value: line[1] || line[0]
59				};
60			}
61		}
62	} else {
63		this._messages($.ui.multiselect.constante.MESSAGE_ERROR, $.ui.multiselect.locale.errorDataFormat);
64		data = false;
65	}
66	return data;
67};
68
69var defaultNodeComparator = function(node1,node2) {
70	var text1 = node1.text(),
71	    text2 = node2.text();
72	return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
73};
74
75(function($) {
76
77$.widget("ui.multiselect", {
78  options: {
79		// sortable and droppable
80		sortable: 'left',
81		droppable: 'both',
82		// searchable
83		searchable: true,
84		searchDelay: 400,
85		remoteUrl: null,
86		remoteParams: {},
87		// animated
88		animated: 'fast',
89		show: 'slideDown',
90		hide: 'slideUp',
91		// ui
92		dividerLocation: 0.6,
93		// callbacks
94		dataParser: defaultDataParser,
95		nodeComparator: defaultNodeComparator,
96		nodeInserted: null
97	},
98	_create: function() {
99		this.element.hide();
100		this.busy = false;  // busy state
101		this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
102		this.selectedContainer = $('<div class="ui-widget-content list-container selected"></div>').appendTo(this.container);
103		this.availableContainer = $('<div class="ui-widget-content list-container available"></div>').appendTo(this.container);
104		this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">'+$.tmpl($.ui.multiselect.locale.itemsCount,{count:0})+'</span><a href="#" class="remove-all">'+$.tmpl($.ui.multiselect.locale.removeAll)+'</a></div>').appendTo(this.selectedContainer);
105		this.availableActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="busy">'+$.tmpl($.ui.multiselect.locale.busy)+'</span><input type="text" class="search ui-widget-content ui-corner-all"/><a href="#" class="add-all">'+$.tmpl($.ui.multiselect.locale.addAll)+'</a></div>').appendTo(this.availableContainer);
106		this.selectedList = $('<ul class="list selected"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
107		this.availableList = $('<ul class="list available"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
108
109		var that = this;
110
111		// initialize data cache
112		this.availableList.data('multiselect.cache', {});
113		this.selectedList.data('multiselect.cache', {});
114
115		if ( !this.options.animated ) {
116			this.options.show = 'show';
117			this.options.hide = 'hide';
118		}
119
120		// sortable / droppable	/ draggable
121		var dragOptions = {
122			selected: {
123				sortable: ('both' == this.options.sortable || 'left' == this.options.sortable),
124				droppable: ('both' == this.options.droppable || 'left' == this.options.droppable)
125			},
126			available: {
127				sortable: ('both' == this.options.sortable || 'right' == this.options.sortable),
128				droppable: ('both' == this.options.droppable || 'right' == this.options.droppable)
129			}
130		};
131		this._prepareLists('selected', 'available', dragOptions);
132		this._prepareLists('available', 'selected', dragOptions);
133
134		// set up livesearch
135		this._registerSearchEvents(this.availableContainer.find('input.search'), true);
136		// make sure that we're not busy yet
137		this._setBusy(false);
138
139		// batch actions
140		this.container.find(".remove-all").bind('click.multiselect', function() { that.selectNone(); return false; });
141		this.container.find(".add-all").bind('click.multiselect', function() { that.selectAll(); return false; });
142
143		// set dimensions
144		this.container.width(this.element.width()+1);
145		this._refreshDividerLocation();
146		// set max width of search input dynamically
147		this.availableActions.find('input').width(Math.max(this.availableActions.width() - this.availableActions.find('a.add-all').width() - 30, 20));
148		// fix list height to match <option> depending on their individual header's heights
149		this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
150		this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
151
152		// init lists
153		this._populateLists(this.element.find('option'));
154	},
155
156	/**************************************
157    *  Public
158    **************************************/
159
160	destroy: function() {
161		this.container.remove();
162		this.element.show();
163
164		$.widget.prototype.destroy.apply(this, arguments);
165	},
166	isBusy: function() {
167		return !!this.busy;
168	},
169	isSelected: function(item) {
170		if (this.enabled()) {
171			return !!this._findItem(item, this.selectedList);
172		} else {
173			return null;
174		}
175	},
176	// get all selected values in an array
177	selectedValues: function() {
178		return $.map( this.element.find('option[selected]'), function(item,i) { return $(item).val(); });
179	},
180	// get/set enable state
181	enabled: function(state, msg) {
182		if (undefined !== state) {
183			if (state) {
184				this.container.unblock();
185				this.element.removeAttr('disabled');
186			} else {
187				this.container.block({message:msg||null,overlayCSS:{backgroundColor:'#fff',opacity:0.4,cursor:'default'}});
188				this.element.attr('disabled', true);
189			}
190		}
191		return !this.element.attr('disabled');
192	},
193	selectAll: function() {
194		if (this.enabled()) {
195			this._batchSelect(this.availableList.children('li.ui-element:visible'), true);
196		}
197	},
198	selectNone: function() {
199		if (this.enabled()) {
200			this._batchSelect(this.selectedList.children('li.ui-element:visible'), false);
201		}
202	},
203	select: function(text) {
204		if (this.enabled()) {
205			var available = this._findItem(text, this.availableList);
206			if ( available ) {
207				this._setSelected(available, true);
208			}
209		}
210	},
211	deselect: function(text) {
212		if (this.enabled()) {
213			var selected = this._findItem(text, this.selectedList);
214			if ( selected ) {
215				this._setSelected(selected, false);
216			}
217		}
218	},
219	search: function(query) {
220		if (!this.busy && this.enabled() && this.options.searchable) {
221			var input = this.availableActions.children('input:first');
222			input.val(query);
223			input.trigger('keydown.multiselect');
224		}
225	},
226	// insert new <option> and _populate
227	// @return int   the number of options added
228	addOptions: function(data) {
229		if (this.enabled()) {
230			this._setBusy(true);
231
232			// format data
233			var elements = [];
234			if (data = this.options.dataParser.call(this, data)) {
235				for (var key in data) {
236					// check if the option does not exist already
237					if (this.element.find('option[value="'+key+'"]').size()==0) {
238						elements.push( $('<option value="'+key+'"/>').text(data[key].value).appendTo(this.element)[0] );
239					}
240				}
241			}
242
243			if (elements.length>0) {
244				this._populateLists($(elements));
245			}
246
247			this._filter(this.availableList.children('li.ui-element'));
248
249			this._setBusy(false);
250			return elements.length;
251		} else {
252			return false;
253		}
254	},
255
256	/**************************************
257    *  Private
258    **************************************/
259
260	_setData: function(key, value) {
261		switch (key) {
262			// special treatement must be done for theses values when changed
263			case 'dividerLocation':
264				this.options.dividerLocation = value;
265				this._refreshDividerLocation();
266				break;
267			case 'searchable':
268				this.options.searchable = value;
269				this._registerSearchEvents(this.availableContainer.find('input.search'), false);
270				break;
271
272			case 'droppable':
273			case 'sortable':
274				// readonly options
275				this._messages(
276					$.ui.multiselect.constants.MESSAGE_WARNING,
277					$.ui.multiselect.locale.errorReadonly,
278					{option: key}
279				);
280			default:
281				// default behavior
282				this.options[key] = value;
283				break;
284		}
285	},
286	_ui: function(type) {
287		var uiObject = {sender: this.element};
288		switch (type) {
289			// events: messages
290			case 'message':
291				uiObject.type = arguments[1];
292				uiObject.message = arguments[2];
293				break;
294
295			// events: selected, deselected
296			case 'selection':
297				uiObject.option = arguments[1];
298				break;
299		}
300		return uiObject;
301	},
302	_messages: function(type, msg, params) {
303		this._trigger('messages', null, this._ui('message', type, $.tmpl(msg, params)));
304	},
305
306	_refreshDividerLocation: function() {
307		this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
308		this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
309	},
310	_prepareLists: function(side, otherSide, opts) {
311		var that = this;
312		var itemSelected = ('selected' == side);
313		var list = this[side+'List'];
314		var otherList = this[otherSide+'List'];
315		var listDragHelper = opts[otherSide].sortable ? _dragHelper : 'clone';
316
317		list
318			.data('multiselect.sortable', opts[side].sortable )
319			.data('multiselect.droppable', opts[side].droppable )
320			.data('multiselect.draggable', !opts[side].sortable && (opts[otherSide].sortable || opts[otherSide].droppable) );
321
322		if (opts[side].sortable) {
323			list.sortable({
324				appendTo: this.container,
325				connectWith: otherList,
326				containment: this.container,
327				helper: listDragHelper,
328				items: 'li.ui-element',
329				revert: !(opts[otherSide].sortable || opts[otherSide].droppable),
330				receive: function(event, ui) {
331					// DEBUG
332					//that._messages(0, "Receive : " + ui.item.data('multiselect.optionLink') + ":" + ui.item.parent()[0].className + " = " + itemSelected);
333
334					// we received an element from a sortable to another sortable...
335					if (opts[otherSide].sortable) {
336						var optionLink = ui.item.data('multiselect.optionLink');
337
338						that._applyItemState(ui.item.hide(), itemSelected);
339
340						// if the cache already contain an element, remove it
341						if (otherList.data('multiselect.cache')[optionLink.val()]) {
342							delete otherList.data('multiselect.cache')[optionLink.val()];
343						}
344
345						ui.item.hide();
346						that._setSelected(ui.item, itemSelected, true);
347					} else {
348						// the other is droppable only, so merely select the element...
349						setTimeout(function() {
350							that._setSelected(ui.item, itemSelected);
351						}, 10);
352					}
353				},
354				stop: function(event, ui) {
355					// DEBUG
356					//that._messages(0, "Stop : " + (ui.item.parent()[0] == otherList[0]));
357					that._moveOptionNode(ui.item);
358				}
359			});
360		}
361
362		// cannot be droppable if both lists are sortable, it breaks the receive function
363		if (!(opts[side].sortable && opts[otherSide].sortable)
364		 && (opts[side].droppable || opts[otherSide].sortable || opts[otherSide].droppable)) {
365			//alert( side + " is droppable ");
366			list.droppable({
367				accept: '.ui-multiselect li.ui-element',
368				hoverClass: 'ui-state-highlight',
369				revert: !(opts[otherSide].sortable || opts[otherSide].droppable),
370				greedy: true,
371				drop: function(event, ui) {
372					// DEBUG
373					//that._messages(0, "drop " + side + " = " + ui.draggable.data('multiselect.optionLink') + ":" + ui.draggable.parent()[0].className);
374
375					//alert( "drop " + itemSelected );
376					// if no optionLink is defined, it was dragged in
377					if (!ui.draggable.data('multiselect.optionLink')) {
378						var optionLink = ui.helper.data('multiselect.optionLink');
379						ui.draggable.data('multiselect.optionLink', optionLink);
380
381						// if the cache already contain an element, remove it
382						if (list.data('multiselect.cache')[optionLink.val()]) {
383							delete list.data('multiselect.cache')[optionLink.val()];
384						}
385						list.data('multiselect.cache')[optionLink.val()] = ui.draggable;
386
387						that._applyItemState(ui.draggable, itemSelected);
388
389					// received an item from a sortable to a droppable
390					} else if (!opts[side].sortable) {
391						setTimeout(function() {
392							ui.draggable.hide();
393							that._setSelected(ui.draggable, itemSelected);
394						}, 10);
395					}
396
397				}
398			});
399		}
400	},
401	_populateLists: function(options) {
402		this._setBusy(true);
403
404		var that = this;
405		// do this async so the browser actually display the waiting message
406		setTimeout(function() {
407			$(options.each(function(i) {
408				var list = (this.selected ? that.selectedList : that.availableList);
409		      var item = that._getOptionNode(this).show();
410				that._applyItemState(item, this.selected);
411				item.data('multiselect.idx', i);
412
413				// cache
414				list.data('multiselect.cache')[item.data('multiselect.optionLink').val()] = item;
415
416				that._insertToList(item, list);
417		    }));
418
419			// update count
420			that._setBusy(false);
421			that._updateCount();
422		}, 1);
423	},
424	_insertToList: function(node, list) {
425		var that = this;
426		this._setBusy(true);
427		// the browsers don't like batch node insertion...
428		var _addNodeRetry = 0;
429		var _addNode = function() {
430			var succ = (that.options.nodeComparator ? that._getSuccessorNode(node, list) : null);
431			try {
432				if (succ) {
433					node.insertBefore(succ);
434				} else {
435					list.append(node);
436				}
437				if (list === that.selectedList) that._moveOptionNode(node);
438
439				// callback after node insertion
440				if ('function' == typeof that.options.nodeInserted) that.options.nodeInserted(node);
441				that._setBusy(false);
442			} catch (e) {
443				// if this problem did not occur too many times already
444				if ( _addNodeRetry++ < 10 ) {
445					// try again later (let the browser cool down first)
446					setTimeout(function() { _addNode(); }, 1);
447				} else {
448					that._messages(
449						$.ui.multiselect.constants.MESSAGE_EXCEPTION,
450						$.ui.multiselect.locale.errorInsertNode,
451						{key:node.data('multiselect.optionLink').val(), value:node.text()}
452					);
453					that._setBusy(false);
454				}
455			}
456		};
457		_addNode();
458	},
459	_updateCount: function() {
460		var that = this;
461		// defer until system is not busy
462		if (this.busy) setTimeout(function() { that._updateCount(); }, 100);
463		// count only visible <li> (less .ui-helper-hidden*)
464		var count = this.selectedList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible').size();
465		var total = this.availableList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder,.shadowed)').size() + count;
466		this.selectedContainer.find('span.count')
467			.text($.tmpl($.ui.multiselect.locale.itemsCount, {count:count}))
468			.attr('title', $.tmpl($.ui.multiselect.locale.itemsTotal, {count:total}));
469	},
470	_getOptionNode: function(option) {
471		option = $(option);
472		var node = $('<li class="ui-state-default ui-element"><span class="ui-icon"/>'+option.text()+'<a href="#" class="ui-state-default action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
473		node.data('multiselect.optionLink', option);
474		return node;
475	},
476	_moveOptionNode: function(item) {
477		// call this async to let the item be placed correctly
478		setTimeout( function() {
479			var optionLink = item.data('multiselect.optionLink');
480			if (optionLink) {
481				var prevItem = item.prev('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible');
482				var prevOptionLink = prevItem.size() ? prevItem.data('multiselect.optionLink') : null;
483
484				if (prevOptionLink) {
485					optionLink.insertAfter(prevOptionLink);
486				} else {
487					optionLink.prependTo(optionLink.parent());
488				}
489			}
490		}, 100);
491	},
492	// used by select and deselect, etc.
493	_findItem: function(text, list) {
494		var found = null;
495		list.children('li.ui-element:visible').each(function(i,el) {
496			el = $(el);
497			if (el.text().toLowerCase() === text.toLowerCase()) {
498				found = el;
499			}
500		});
501		if (found && found.size()) {
502			return found;
503		} else {
504			return false;
505		}
506	},
507	// clones an item with
508	// didn't find a smarter away around this (michael)
509	// now using cache to speed up the process (yr)
510	_cloneWithData: function(clonee, cacheName, insertItem) {
511		var that = this;
512		var id = clonee.data('multiselect.optionLink').val();
513		var selected = ('selected' == cacheName);
514		var list = (selected ? this.selectedList : this.availableList);
515		var clone = list.data('multiselect.cache')[id];
516
517		if (!clone) {
518			clone = clonee.clone().hide();
519			this._applyItemState(clone, selected);
520			// update cache
521			list.data('multiselect.cache')[id] = clone;
522			// update <option> and idx
523			clone.data('multiselect.optionLink', clonee.data('multiselect.optionLink'));
524			// need this here because idx is needed in _getSuccessorNode
525			clone.data('multiselect.idx', clonee.data('multiselect.idx'));
526
527			// insert the node into it's list
528			if (insertItem) {
529				this._insertToList(clone, list);
530			}
531		} else {
532			// update idx
533			clone.data('multiselect.idx', clonee.data('multiselect.idx'));
534		}
535		return clone;
536	},
537	_batchSelect: function(elements, state) {
538		this._setBusy(true);
539
540		var that = this;
541		// do this async so the browser actually display the waiting message
542		setTimeout(function() {
543			var _backup = {
544				animated: that.options.animated,
545				hide: that.options.hide,
546				show: that.options.show
547			};
548
549			that.options.animated = null;
550			that.options.hide = 'hide';
551			that.options.show = 'show';
552
553			elements.each(function(i,element) {
554				that._setSelected($(element), state);
555			});
556
557			// filter available items
558			if (!state) that._filter(that.availableList.find('li.ui-element'));
559
560			// restore
561			$.extend(that.options, _backup);
562
563			that._updateCount();
564			that._setBusy(false);
565		}, 10);
566	},
567	// find the best successor the given item in the specified list
568	// TODO implement a faster sorting algorithm (and remove the idx depen
568dancy)
569	_getSuccessorNode: function(item, list) {
570		// look for successor based on initial option index
571		var items = list.find('li.ui-element'), comparator = this.options.nodeComparator;
572		var itemsSize = items.size();
573
574		// no successor, list is null
575		if (items.size() == 0) return null;
576
577		var succ, i = Math.min(item.data('multiselect.idx'),itemsSize-1), direction = comparator(item, $(items[i]));
578
579		if ( direction ) {
580			// quick checks
581			if (0>direction && 0>=i) {
582				succ = items[0];
583			} else if (0<direction && itemsSize-1<=i) {
584				i++;
585				succ = null;
586			} else {
587				while (i>=0 && i<items.length) {
588					direction > 0 ? i++ : i--;
589					if (i<0) {
590						succ = item[0]
591					}
592					if ( direction != comparator(item, $(items[i])) ) {
593						// going up, go back one item down, otherwise leave as is
594						succ = items[direction > 0 ? i : i+1];
595						break;
596					}
597				}
598			}
599		} else {
600			succ = items[i];
601		}
602		// update idx
603		item.data('multiselect.idx', i);
604
605		return succ;
606	},
607	// @param DOMElement item         is the item to set
608   // @param bool selected           true|false (state)
609   // @param bool noclone (optional) true only if item should not be cloned on the other list
610	_setSelected: function(item, selected, noclone) {
611		var that = this, otherItem;
612		var optionLink = item.data('multiselect.optionLink');
613
614		if (selected) {
615			// already selected
616			if (optionLink.attr('selected')) return;
617			optionLink.attr('selected','selected');
618
619			if (noclone) {
620				otherItem = item;
621			} else {
622				// retrieve associatd or cloned item
623				otherItem = this._cloneWithData(item, 'selected', true).hide();
624				item.addClass('shadowed')[this.options.hide](this.options.animated, function() { that._updateCount(); });
625			}
626			otherItem[this.options.show](this.options.animated);
627		} else {
628			// already deselected
629			if (!optionLink.attr('selected')) return;
630			optionLink.removeAttr('selected');
631
632			if (noclone) {
633				otherItem = item;
634			} else {
635				// retrieve associated or clone the item
636				otherItem = this._cloneWithData(item, 'available', true).hide().removeClass('shadowed');
637				item[this.options.hide](this.options.animated, function() { that._updateCount() });
638			}
639			if (!otherItem.is('.filtered')) otherItem[this.options.show](this.options.animated);
640		}
641
642		if (!this.busy) {
643			if (this.options.animated) {
644				// pulse
645				//otherItem.effect("pulsate", { times: 1, mode: 'show' }, 400);  // pulsate twice???
646				otherItem.fadeTo('fast', 0.3, function() { $(this).fadeTo('fast', 1); });
647			}
648		}
649
650		// fire selection event
651		this._trigger(selected ? 'selected' : 'deselected', null, this._ui('selection', optionLink));
652
653		return otherItem;
654	},
655	_setBusy: function(state) {
656		var input = this.availableActions.children('input.search');
657		var busy = this.availableActions.children('.busy');
658
659		this.busy = Math.max(state ? ++this.busy : --this.busy, 0);
660
661		this.container.find("a.remove-all, a.add-all")[this.busy ? 'hide' : 'show']();
662		if (state && (1 == this.busy)) {
663			if (this.options.searchable) {
664				// backup input state
665				input.data('multiselect.hadFocus', input.data('multiselect.hasFocus'));
666				// webkit needs to blur before hiding or it won't fire focus again in the else block
667				input.blur().hide();
668			}
669			busy.show();
670		} else if(!this.busy) {
671			if (this.options.searchable) {
672				input.show();
673				if (input.data('multiselect.hadFocus')) input.focus();
674			}
675			busy.hide();
676		}
677
678		// DEBUG
679		//this._messages(0, "Busy state changed to : " + this.busy);
680	},
681	_applyItemState: function(item, selected) {
682		if (selected) {
683			item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon');
684			item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
685			this._registerRemoveEvents(item.find('a.action'));
686
687		} else {
688			item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon');
689			item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
690			this._registerAddEvents(item.find('a.action'));
691		}
692
693		this._registerHoverEvents(item);
694
695		return item;
696	},
697	// apply filter and return elements
698	_filter: function(elements) {
699		var input = this.availableActions.children('input.search');
700		var term = $.trim( input.val().toLowerCase() );
701
702		if ( !term ) {
703			elements.removeClass('filtered');
704		} else {
705			elements.each(function(i,element) {
706				element = $(element);
707				element[(element.text().toLowerCase().indexOf(term)>=0 ? 'remove' : 'add')+'Class']('filtered');
708			});
709		}
710
711		return elements.not('.filtered, .shadowed').show().end().filter('.filtered, .shadowed').hide().end();
712	},
713	_registerHoverEvents: function(elements) {
714		elements
715			.unbind('mouseover.multiselect').bind('mouseover.multiselect', function() {
716				$(this).find('a').andSelf().addClass('ui-state-hover');
717			})
718			.unbind('mouseout.multiselect').bind('mouseout.multiselect', function() {
719				$(this).find('a').andSelf().removeClass('ui-state-hover');
720			})
721			.find('a').andSelf().removeClass('ui-state-hover')
722		;
723	},
724	_registerAddEvents: function(elements) {
725		var that = this;
726		elements.unbind('click.multiselect').bind('click.multiselect', function() {
727			// ignore if busy...
728			if (!this.busy) {
729				that._setSelected($(this).parent(), true);
730			}
731			return false;
732		});
733		if (this.availableList.data('multiselect.draggable')) {
734			// make draggable
735			elements.each(function() {
736				$(this).parent().draggable({
737					connectToSortable: that.selectedList,
738					helper: _dragHelper,
739					appendTo: that.container,
740					containment: that.container,
741					revert: 'invalid'
742				});
743			});
744			// refresh the selected list or the draggable will not connect to it first hand
745			if (this.selectedList.data('multiselect.sortable')) {
746				this.selectedList.sortable('refresh');
747			}
748		}
749	},
750	_registerRemoveEvents: function(elements) {
751		var that = this;
752		elements.unbind('click.multiselect').bind('click.multiselect', function() {
753			// ignore if busy...
754			if (!that.busy) {
755				that._setSelected($(this).parent(), false);
756			}
757			return false;
758		});
759		if (this.selectedList.data('multiselect.draggable')) {
760			// make draggable
761			elements.each(function() {
762				$(this).parent().draggable({
763					connectToSortable: that.availableList,
764					helper: _dragHelper,
765					appendTo: that.container,
766					containment: that.container,
767					revert: 'invalid'
768				});
769			});
770			// refresh the selected list or the draggable will not connect to it first hand
771			if (this.availableList.data('multiselect.sortable')) {
772				this.availableList.sortable('refresh');
773			}
774		}
775 	},
776	_registerSearchEvents: function(input, searchNow) {
777		var that = this;
778		var previousValue = input.val(), timer;
779
780		var _searchNow = function(forceUpdate) {
781			if (that.busy) return;
782
783			var value = input.val();
784			if ((value != previousValue) || (forceUpdate)) {
785				that._setBusy(true);
786
787				if (that.options.remoteUrl) {
788					var params = $.extend({}, that.options.remoteParams);
789					try {
790						$.ajax({
791							url: that.options.remoteUrl,
792							data: $.extend(params, {q:escape(value)}),
793							success: function(data) {
794								that.addOptions(data);
795								that._setBusy(false);
796							},
797							error: function(request,status,e) {
798								that._messages(
799									$.ui.multiselect.constants.MESSAGE_ERROR,
800									$.ui.multiselect.locale.errorRequest,
801									{status:status}
802								);
803								that._setBusy(false);
804							}
805						});
806					} catch (e) {
807						that._messages($.ui.multiselect.constants.MESSAGE_EXCEPTION, e.message);   // error message template ??
808						that._setBusy(false);
809					}
810				} else {
811					that._filter(that.availableList.children('li.ui-element'));
812					that._setBusy(false);
813				}
814
815				previousValue = value;
816			}
817		};
818
819		// reset any events... if any
820		input.unbind('focus.multiselect blur.multiselect keydown.multiselect keypress.multiselect');
821		if (this.options.searchable) {
822			input
823			.bind('focus.multiselect', function() {
824				$(this).addClass('ui-state-active').data('multiselect.hasFocus', true);
825			})
826			.bind('blur.multiselect', function() {
827				$(this).removeClass('ui-state-active').data('multiselect.hasFocus', false);
828			})
829			.bind('keydown.multiselect keypress.multiselect', function(e) {
830				if (timer) clearTimeout(timer);
831				switch (e.which) {
832					case 13:   // enter
833						_searchNow(true);
834						return false;
835
836					default:
837						timer = setTimeout(function() { _searchNow(); }, Math.max(that.options.searchDelay,1));
838				}
839			})
840			.show();
841		} else {
842			input.val('').hide();
843			this._filter(that.availableList.find('li.ui-element'))
844		}
845		// initiate search filter (delayed)
846		var _initSearch = function() {
847			if (that.busy) {
848				setTimeout(function() { _initSearch(); }, 100);
849			}
850			_searchNow(true);
851		};
852
853		if (searchNow) _initSearch();
854	}
855});
856// END ui.multiselect
857
858
859/********************************
860 *  Internal functions
861 ********************************/
862
863var _dragHelper = function(event, ui) {
864	var item = $(event.target);
865	var clone = item.clone().width(item.width());
866	clone
867		.data('multiselect.optionLink', item.data('multiselect.optionLink'))
868		.data('multiselect.list', item.parent() )
869		// node ui cleanup
870		.find('a').remove()
871	;
872	return clone;
873};
874
875
876
877/****************************
878 *  Settings
879 ****************************/
880
881$.extend($.ui.multiselect, {
882	getter: 'selectedValues enabled isBusy',
883	locale: {
884		addAll:'Add all',
885		removeAll:'Remove all',
886		itemsCount:'#{count} items selected',
887		itemsTotal:'#{count} items total',
888		busy:'please wait...',
889		errorDataFormat:"Cannot add options, unknown data format",
890		errorInsertNode:"There was a problem trying to add the item:\n\n\t[#{key}] => #{value}\n\nThe operation was aborted.",
891		errorReadonly:"The option #{option} is readonly",
892		errorRequest:"Sorry! There seemed to be a problem with the remote call. (Type: #{status})"
893	},
894	constants: {
895		MESSAGE_WARNING: 0,
896		MESSAGE_EXCEPTION: 1,
897		MESSAGE_ERROR: 2
898	}
899});
900
901})(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.