forked from GNUsocial/gnu-social
		
	
		
			
	
	
		
			759 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			759 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|   | /* | ||
|  |  * Autocomplete - jQuery plugin 1.0.2 | ||
|  |  * | ||
|  |  * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, 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 5747 2008-06-25 18:30:55Z 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) { | ||
|  | 		// 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 ) { | ||
|  | 				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v; | ||
|  | 			} | ||
|  | 			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 [""]; | ||
|  | 		} | ||
|  | 		var words = value.split( options.multipleSeparator ); | ||
|  | 		var result = []; | ||
|  | 		$.each(words, function(i, value) { | ||
|  | 			if ( $.trim(value) ) | ||
|  | 				result[i] = $.trim(value); | ||
|  | 		}); | ||
|  | 		return result; | ||
|  | 	} | ||
|  | 	 | ||
|  | 	function lastWord(value) { | ||
|  | 		if ( !options.multiple ) | ||
|  | 			return value; | ||
|  | 		var words = trimWords(value); | ||
|  | 		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)
 | ||
|  | 			$.Autocompleter.Selection(input, 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( "" ); | ||
|  | 					} | ||
|  | 				} | ||
|  | 			); | ||
|  | 		} | ||
|  | 		if (wasVisible) | ||
|  | 			// position cursor at end of input field
 | ||
|  | 			$.Autocompleter.Selection(input, input.value.length, input.value.length); | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	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 (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(); | ||
|  | 		} | ||
|  | 	}; | ||
|  | }; | ||
|  | 
 | ||
|  | $.Autocompleter.Selection = function(field, start, end) { | ||
|  | 	if( field.createTextRange ){ | ||
|  | 		var selRange = field.createTextRange(); | ||
|  | 		selRange.collapse(true); | ||
|  | 		selRange.moveStart("character", start); | ||
|  | 		selRange.moveEnd("character", end); | ||
|  | 		selRange.select(); | ||
|  | 	} else if( field.setSelectionRange ){ | ||
|  | 		field.setSelectionRange(start, end); | ||
|  | 	} else { | ||
|  | 		if( field.selectionStart ){ | ||
|  | 			field.selectionStart = start; | ||
|  | 			field.selectionEnd = end; | ||
|  | 		} | ||
|  | 	} | ||
|  | 	field.focus(); | ||
|  | }; | ||
|  | 
 | ||
|  | })(jQuery); |