/* * jQuery Autocomplete plugin 1.1 * * Copyright (c) 2009 Jörn Zaefferer * * Dual licensed under the MIT and GPL licenses: *   http://www.opensource.org/licenses/mit-license.php *   http://www.gnu.org/licenses/gpl.html * * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ */;(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);
