text: ;(function($) {
  	
  $.fn.extend({
  	autocomplete: function(urlOrData, options) {
  		var isUrl = typeof urlOrData == "string";
  		options = $.extend({}, $.Autocompleter.defaults, {
  			url: isUrl ? urlOrData : null,
  			data: isUrl ? null : urlOrData,
  			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
  			max: options && !options.scroll ? 10 : 150
  		}, options);
  		
  		// if highlight is set to false, replace it with a do-nothing function
  		options.highlight = options.highlight || function(value) { return value; };
  		
  		// if the formatMatch option is not specified, then use formatItem for backwards compatibility
  		options.formatMatch = options.formatMatch || options.formatItem;
  		
  		return this.each(function() {
  			new $.Autocompleter(this, options);
  		});
  	},
  	result: function(handler) {
  		return this.bind("result", handler);
  	},
  	search: function(handler) {
  		return this.trigger("search", [handler]);
  	},
  	flushCache: function() {
  		return this.trigger("flushCache");
  	},
  	setOptions: function(options){
  		return this.trigger("setOptions", [options]);
  	},
  	unautocomplete: function() {
  		return this.trigger("unautocomplete");
  	}
  });
  
  $.Autocompleter = function(input, options) {
  
  	var KEY = {
  		UP: 38,
  		DOWN: 40,
  		DEL: 46,
  		TAB: 9,
  		RETURN: 13,
  		ESC: 27,
  		COMMA: 188,
  		PAGEUP: 33,
  		PAGEDOWN: 34,
  		BACKSPACE: 8
  	};
  
  	// Create $ object for input element
  	var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  
  	var timeout;
  	var previousValue = "";
  	var cache = $.Autocompleter.Cache(options);
  	var hasFocus = 0;
  	var lastKeyPressCode;
  	var config = {
  		mouseDownOnSelect: false
  	};
  	var select = $.Autocompleter.Select(options, input, selectCurrent, config);
  	
  	var blockSubmit;
  	
  	// prevent form submit in opera when selecting with return key
  	$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
  		if (blockSubmit) {
  			blockSubmit = false;
  			return false;
  		}
  	});
  	
  	// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
  	$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
  		// a keypress means the input has focus
  		// avoids issue where input had focus before the autocomplete was applied
  		hasFocus = 1;
  		// track last key pressed
  		lastKeyPressCode = event.keyCode;
  		switch(event.keyCode) {
  		
  			case KEY.UP:
  				event.preventDefault();
  				if ( select.visible() ) {
  					select.prev();
  				} else {
  					onChange(0, true);
  				}
  				break;
  				
  			case KEY.DOWN:
  				event.preventDefault();
  				if ( select.visible() ) {
  					select.next();
  				} else {
  					onChange(0, true);
  				}
  				break;
  				
  			case KEY.PAGEUP:
  				event.preventDefault();
  				if ( select.visible() ) {
  					select.pageUp();
  				} else {
  					onChange(0, true);
  				}
  				break;
  				
  			case KEY.PAGEDOWN:
  				event.preventDefault();
  				if ( select.visible() ) {
  					select.pageDown();
  				} else {
  					onChange(0, true);
  				}
  				break;
  			
  			// matches also semicolon
  			case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
  			case KEY.TAB:
  			case KEY.RETURN:
  				if( selectCurrent() ) {
  					// stop default to prevent a form submit, Opera needs special handling
  					event.preventDefault();
  					blockSubmit = true;
  					return false;
  				}
  				break;
  				
  			case KEY.ESC:
  				select.hide();
  				break;
  				
  			default:
  				clearTimeout(timeout);
  				timeout = setTimeout(onChange, options.delay);
  				break;
  		}
  	}).focus(function(){
  		// track whether the field has focus, we shouldn't process any
  		// results if the field no longer has focus
  		hasFocus++;
  	}).blur(function() {
  		hasFocus = 0;
  		if (!config.mouseDownOnSelect) {
  			hideResults();
  		}
  	}).click(function() {
  		// show select when clicking in a focused field
  		if ( hasFocus++ > 1 && !select.visible() ) {
  			onChange(0, true);
  		}
  	}).bind("search", function() {
  		// TODO why not just specifying both arguments?
  		var fn = (arguments.length > 1) ? arguments[1] : null;
  		function findValueCallback(q, data) {
  			var result;
  			if( data && data.length ) {
  				for (var i=0; i < data.length; i++) {
  					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
  						result = data[i];
  						break;
  					}
  				}
  			}
  			if( typeof fn == "function" ) fn(result);
  			else $input.trigger("result", result && [result.data, result.value]);
  		}
  		$.each(trimWords($input.val()), function(i, value) {
  			request(value, findValueCallback, findValueCallback);
  		});
  	}).bind("flushCache", function() {
  		cache.flush();
  	}).bind("setOptions", function() {
  		$.extend(options, arguments[1]);
  		// if we've updated the data, repopulate
  		if ( "data" in arguments[1] )
  			cache.populate();
  	}).bind("unautocomplete", function() {
  		select.unbind();
  		$input.unbind();
  		$(input.form).unbind(".autocomplete");
  	});
  	
  	
  	function selectCurrent() {
  		var selected = select.selected();
  		if( !selected )
  			return false;
  		
  		var v = selected.result;
  		previousValue = v;
  		
  		if ( options.multiple ) {
  			var words = trimWords($input.val());
  			if ( words.length > 1 ) {
  				var seperator = options.multipleSeparator.length;
  				var cursorAt = $(input).selection().start;
  				var wordAt, progress = 0;
  				$.each(words, function(i, word) {
  					progress += word.length;
  					if (cursorAt <= progress) {
  						wordAt = i;
  						return false;
  					}
  					progress += seperator;
  				});
  				words[wordAt] = v;
  				// TODO this should set the cursor to the right position, but it gets overriden somewhere
  				//$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
  				v = words.join( options.multipleSeparator );
  			}
  			v += options.multipleSeparator;
  		}
  		
  		$input.val(v);
  		hideResultsNow();
  		$input.trigger("result", [selected.data, selected.value]);
  		return true;
  	}
  	
  	function onChange(crap, skipPrevCheck) {
  		if( lastKeyPressCode == KEY.DEL ) {
  			select.hide();
  			return;
  		}
  		
  		var currentValue = $input.val();
  		
  		if ( !skipPrevCheck && currentValue == previousValue )
  			return;
  		
  		previousValue = currentValue;
  		
  		currentValue = lastWord(currentValue);
  		if ( currentValue.length >= options.minChars) {
  			$input.addClass(options.loadingClass);
  			if (!options.matchCase)
  				currentValue = currentValue.toLowerCase();
  			request(currentValue, receiveData, hideResultsNow);
  		} else {
  			stopLoading();
  			select.hide();
  		}
  	};
  	
  	function trimWords(value) {
  		if (!value)
  			return [""];
  		if (!options.multiple)
  			return [$.trim(value)];
  		return $.map(value.split(options.multipleSeparator), function(word) {
  			return $.trim(value).length ? $.trim(word) : null;
  		});
  	}
  	
  	function lastWord(value) {
  		if ( !options.multiple )
  			return value;
  		var words = trimWords(value);
  		if (words.length == 1) 
  			return words[0];
  		var cursorAt = $(input).selection().start;
  		if (cursorAt == value.length) {
  			words = trimWords(value)
  		} else {
  			words = trimWords(value.replace(value.substring(cursorAt), ""));
  		}
  		return words[words.length - 1];
  	}
  	
  	// fills in the input box w/the first match (assumed to be the best match)
  	// q: the term entered
  	// sValue: the first matching result
  	function autoFill(q, sValue){
  		// autofill in the complete box w/the first match as long as the user hasn't entered in more data
  		// if the last user key pressed was backspace, don't autofill
  		if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
  			// fill in the value (keep the case the user has typed)
  			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
  			// select the portion of the value not typed by the user (so the next character will erase)
  			$(input).selection(previousValue.length, previousValue.length + sValue.length);
  		}
  	};
  
  	function hideResults() {
  		clearTimeout(timeout);
  		timeout = setTimeout(hideResultsNow, 200);
  	};
  
  	function hideResultsNow() {
  		var wasVisible = select.visible();
  		select.hide();
  		clearTimeout(timeout);
  		stopLoading();
  		if (options.mustMatch) {
  			// call search and run callback
  			$input.search(
  				function (result){
  					// if no value found, clear the input box
  					if( !result ) {
  						if (options.multiple) {
  							var words = trimWords($input.val()).slice(0, -1);
  							$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
  						}
  						else {
  							$input.val( "" );
  							$input.trigger("result", null);
  						}
  					}
  				}
  			);
  		}
  	};
  
  	function receiveData(q, data) {
  		if ( data && data.length && hasFocus ) {
  			stopLoading();
  			select.display(data, q);
  			autoFill(q, data[0].value);
  			select.show();
  		} else {
  			hideResultsNow();
  		}
  	};
  
  	function request(term, success, failure) {
  		if (!options.matchCase)
  			term = term.toLowerCase();
  		var data = cache.load(term);
  		// recieve the cached data
  		if (data && data.length) {
  			success(term, data);
  		// if an AJAX url has been supplied, try loading the data now
  		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
  			
  			var extraParams = {
  				timestamp: +new Date()
  			};
  			$.each(options.extraParams, function(key, param) {
  				extraParams[key] = typeof param == "function" ? param() : param;
  			});
  			
  			$.ajax({
  				// try to leverage ajaxQueue plugin to abort previous requests
  				mode: "abort",
  				// limit abortion to this input
  				port: "autocomplete" + input.name,
  				dataType: options.dataType,
  				url: options.url,
  				data: $.extend({
  					q: lastWord(term),
  					limit: options.max
  				}, extraParams),
  				success: function(data) {
  					var parsed = options.parse && options.parse(data) || parse(data);
  					cache.add(term, parsed);
  					success(term, parsed);
  				}
  			});
  		} else {
  			// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
  			select.emptyList();
  			failure(term);
  		}
  	};
  	
  	function parse(data) {
  		var parsed = [];
  		var rows = data.split("\n");
  		for (var i=0; i < rows.length; i++) {
  			var row = $.trim(rows[i]);
  			if (row) {
  				row = row.split("|");
  				parsed[parsed.length] = {
  					data: row,
  					value: row[0],
  					result: options.formatResult && options.formatResult(row, row[0]) || row[0]
  				};
  			}
  		}
  		return parsed;
  	};
  
  	function stopLoading() {
  		$input.removeClass(options.loadingClass);
  	};
  
  };
  
  $.Autocompleter.defaults = {
  	inputClass: "ac_input",
  	resultsClass: "ac_results",
  	loadingClass: "ac_loading",
  	minChars: 1,
  	delay: 400,
  	matchCase: false,
  	matchSubset: true,
  	matchContains: false,
  	cacheLength: 10,
  	max: 100,
  	mustMatch: false,
  	extraParams: {},
  	selectFirst: true,
  	formatItem: function(row) { return row[0]; },
  	formatMatch: null,
  	autoFill: false,
  	width: 0,
  	multiple: false,
  	multipleSeparator: ", ",
  	highlight: function(value, term) {
  		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  	},
      scroll: true,
      scrollHeight: 180
  };
  
  $.Autocompleter.Cache = function(options) {
  
  	var data = {};
  	var length = 0;
  	
  	function matchSubset(s, sub) {
  		if (!options.matchCase) 
  			s = s.toLowerCase();
  		var i = s.indexOf(sub);
  		if (options.matchContains == "word"){
  			i = s.toLowerCase().search("\\b" + sub.toLowerCase());
  		}
  		if (i == -1) return false;
  		return i == 0 || options.matchContains;
  	};
  	
  	function add(q, value) {
  		if (length > options.cacheLength){
  			flush();
  		}
  		if (!data[q]){ 
  			length++;
  		}
  		data[q] = value;
  	}
  	
  	function populate(){
  		if( !options.data ) return false;
  		// track the matches
  		var stMatchSets = {},
  			nullData = 0;
  
  		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
  		if( !options.url ) options.cacheLength = 1;
  		
  		// track all options for minChars = 0
  		stMatchSets[""] = [];
  		
  		// loop through the array and create a lookup structure
  		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
  			var rawValue = options.data[i];
  			// if rawValue is a string, make an array otherwise just reference the array
  			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
  			
  			var value = options.formatMatch(rawValue, i+1, options.data.length);
  			if ( value === false )
  				continue;
  				
  			var firstChar = value.charAt(0).toLowerCase();
  			// if no lookup array for this character exists, look it up now
  			if( !stMatchSets[firstChar] ) 
  				stMatchSets[firstChar] = [];
  
  			// if the match is a string
  			var row = {
  				value: value,
  				data: rawValue,
  				result: options.formatResult && options.formatResult(rawValue) || value
  			};
  			
  			// push the current match into the set list
  			stMatchSets[firstChar].push(row);
  
  			// keep track of minChars zero items
  			if ( nullData++ < options.max ) {
  				stMatchSets[""].push(row);
  			}
  		};
  
  		// add the data items to the cache
  		$.each(stMatchSets, function(i, value) {
  			// increase the cache size
  			options.cacheLength++;
  			// add to the cache
  			add(i, value);
  		});
  	}
  	
  	// populate any existing data
  	setTimeout(populate, 25);
  	
  	function flush(){
  		data = {};
  		length = 0;
  	}
  	
  	return {
  		flush: flush,
  		add: add,
  		populate: populate,
  		load: function(q) {
  			if (!options.cacheLength || !length)
  				return null;
  			/* 
  			 * if dealing w/local data and matchContains than we must make sure
  			 * to loop through all the data collections looking for matches
  			 */
  			if( !options.url && options.matchContains ){
  				// track all matches
  				var csub = [];
  				// loop through all the data grids for matches
  				for( var k in data ){
  					// don't search through the stMatchSets[""] (minChars: 0) cache
  					// this prevents duplicates
  					if( k.length > 0 ){
  						var c = data[k];
  						$.each(c, function(i, x) {
  							// if we've got a match, add it to the array
  							if (matchSubset(x.value, q)) {
  								csub.push(x);
  							}
  						});
  					}
  				}				
  				return csub;
  			} else 
  			// if the exact item exists, use it
  			if (data[q]){
  				return data[q];
  			} else
  			if (options.matchSubset) {
  				for (var i = q.length - 1; i >= options.minChars; i--) {
  					var c = data[q.substr(0, i)];
  					if (c) {
  						var csub = [];
  						$.each(c, function(i, x) {
  							if (matchSubset(x.value, q)) {
  								csub[csub.length] = x;
  							}
  						});
  						return csub;
  					}
  				}
  			}
  			return null;
  		}
  	};
  };
  
  $.Autocompleter.Select = function (options, input, select, config) {
  	var CLASSES = {
  		ACTIVE: "ac_over"
  	};
  	
  	var listItems,
  		active = -1,
  		data,
  		term = "",
  		needsInit = true,
  		element,
  		list;
  	
  	// Create results
  	function init() {
  		if (!needsInit)
  			return;
  		element = $("<div/>")
  		.hide()
  		.addClass(options.resultsClass)
  		.css("position", "absolute")
  		.appendTo(document.body);
  	
  		list = $("<ul/>").appendTo(element).mouseover( function(event) {
  			if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
  	            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
  			    $(target(event)).addClass(CLASSES.ACTIVE);            
  	        }
  		}).click(function(event) {
  			$(target(event)).addClass(CLASSES.ACTIVE);
  			select();
  			// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
  			input.focus();
  			return false;
  		}).mousedown(function() {
  			config.mouseDownOnSelect = true;
  		}).mouseup(function() {
  			config.mouseDownOnSelect = false;
  		});
  		
  		if( options.width > 0 )
  			element.css("width", options.width);
  			
  		needsInit = false;
  	} 
  	
  	function target(event) {
  		var element = event.target;
  		while(element && element.tagName != "LI")
  			element = element.parentNode;
  		// more fun with IE, sometimes event.target is empty, just ignore it then
  		if(!element)
  			return [];
  		return element;
  	}
  
  	function moveSelect(step) {
  		listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
  		movePosition(step);
          var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
          if(options.scroll) {
              var offset = 0;
              listItems.slice(0, active).each(function() {
  				offset += this.offsetHeight;
  			});
              if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
                  list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
              } else if(offset < list.scrollTop()) {
                  list.scrollTop(offset);
              }
          }
  	};
  	
  	function movePosition(step) {
  		active += step;
  		if (active < 0) {
  			active = listItems.size() - 1;
  		} else if (active >= listItems.size()) {
  			active = 0;
  		}
  	}
  	
  	function limitNumberOfItems(available) {
  		return options.max && options.max < available
  			? options.max
  			: available;
  	}
  	
  	function fillList() {
  		list.empty();
  		var max = limitNumberOfItems(data.length);
  		for (var i=0; i < max; i++) {
  			if (!data[i])
  				continue;
  			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
  			if ( formatted === false )
  				continue;
  			var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
  			$.data(li, "ac_data", data[i]);
  		}
  		listItems = list.find("li");
  		if ( options.selectFirst ) {
  			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
  			active = 0;
  		}
  		// apply bgiframe if available
  		if ( $.fn.bgiframe )
  			list.bgiframe();
  	}
  	
  	return {
  		display: function(d, q) {
  			init();
  			data = d;
  			term = q;
  			fillList();
  		},
  		next: function() {
  			moveSelect(1);
  		},
  		prev: function() {
  			moveSelect(-1);
  		},
  		pageUp: function() {
  			if (active != 0 && active - 8 < 0) {
  				moveSelect( -active );
  			} else {
  				moveSelect(-8);
  			}
  		},
  		pageDown: function() {
  			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
  				moveSelect( listItems.size() - 1 - active );
  			} else {
  				moveSelect(8);
  			}
  		},
  		hide: function() {
  			element && element.hide();
  			listItems && listItems.removeClass(CLASSES.ACTIVE);
  			active = -1;
  		},
  		visible : function() {
  			return element && element.is(":visible");
  		},
  		current: function() {
  			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
  		},
  		show: function() {
  			var offset = $(input).offset();
  			element.css({
  				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
  				top: offset.top + input.offsetHeight,
  				left: offset.left
  			}).show();
              if(options.scroll) {
                  list.scrollTop(0);
                  list.css({
  					maxHeight: options.scrollHeight,
  					overflow: 'auto'
  				});
  				
                  if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
  					var listHeight = 0;
  					listItems.each(function() {
  						listHeight += this.offsetHeight;
  					});
  					var scrollbarsVisible = listHeight > options.scrollHeight;
                      list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
  					if (!scrollbarsVisible) {
  						// IE doesn't recalculate width when scrollbar disappears
  						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
  					}
                  }
                  
              }
  		},
  		selected: function() {
  			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
  			return selected && selected.length && $.data(selected[0], "ac_data");
  		},
  		emptyList: function (){
  			list && list.empty();
  		},
  		unbind: function() {
  			element && element.remove();
  		}
  	};
  };
  
  $.fn.selection = function(start, end) {
  	if (start !== undefined) {
  		return this.each(function() {
  			if( this.createTextRange ){
  				var selRange = this.createTextRange();
  				if (end === undefined || start == end) {
  					selRange.move("character", start);
  					selRange.select();
  				} else {
  					selRange.collapse(true);
  					selRange.moveStart("character", start);
  					selRange.moveEnd("character", end);
  					selRange.select();
  				}
  			} else if( this.setSelectionRange ){
  				this.setSelectionRange(start, end);
  			} else if( this.selectionStart ){
  				this.selectionStart = start;
  				this.selectionEnd = end;
  			}
  		});
  	}
  	var field = this[0];
  	if ( field.createTextRange ) {
  		var range = document.selection.createRange(),
  			orig = field.value,
  			teststring = "<->",
  			textLength = range.text.length;
  		range.text = teststring;
  		var caretAt = field.value.indexOf(teststring);
  		field.value = orig;
  		this.selection(caretAt, caretAt + textLength);
  		return {
  			start: caretAt,
  			end: caretAt + textLength
  		}
  	} else if( field.selectionStart !== undefined ){
  		return {
  			start: field.selectionStart,
  			end: field.selectionEnd
  		}
  	}
  };
  
  })(jQuery);