jQuery Plugin: Auto Complete

June 11, 2009 | Demo | Docs | Download
Introduction

Auto-complete takes input from the user, and tries to form a list of words that match the users input. The function attaches itself to the input field selected, and also creates the UL drop down from within so all you need is have the styles for it ready.

Basic Usage

No parameters are required, but the path to your ajax script should be correct, and you will need styles set for the UL drop down.

$(function(){
	$('input[name=auto-complete]').autoComplete({ajax: '/path/to/ajax-script'});
});

And thanks to esldesks's most common mispelled words we have some test data to use like so:

Source
/*!
 * Auto Complete 5.0
 * November 22, 2009
 * Corey Hart @ http://www.codenothing.com
 */ 
(function($, undefined){
	// Expose autoComplete to the jQuery chain
	$.fn.autoComplete = function(){
		// Force array of arguments
		var args = Slice.call(arguments),
			self = this, 
			first = args.shift(),
			isMethod = (typeof first === 'string');

		// Deep namespacing is not supported in jQuery, a mistake I made in v4.1
		if (isMethod) first = first.replace('.', '-');
		
		// Allow for passing array of arguments, or multiple arguments
		// Eg: .autoComplete('trigger', [arg1, arg2, arg3...]) or .autoComplete('trigger', arg1, arg2, arg3...)
		// Mainly to allow for .autoComplete('trigger', arguments) to work
		// Note*: button.supply passes an array as the first param, so check against that first
		args = first === 'button-supply' || first === 'direct-supply' ? $.isArray(args[0]) && $.isArray(args[0][0]) ? args[0] : args :
			args[1] === undefined && $.isArray(args[0]) ? args[0] : args;

		// Autocomplete special triggers
		return isMethod ?
			// The only chain breaking operation is option, which gets passed back the
			// settings/value it requested, otherwise trigger the event and don't break the chain!
			$(self)[ first === 'option' && args.length < 2 ? 'triggerHandler' : 'trigger' ]('autoComplete.'+first, args) :

			// Allow passing a jquery event special object {from $.Event()}
			first && first[$.expando] ? $(self).trigger(first, args) :

			// Initiate the autocomplete (Only takes a single argument, the options object)
			AutoCompleteFunction.call(self, first);
	};

	// bgiframe is needed to fix z-index problem for IE6 users.
	$.fn.bgiframe = $.fn.bgiframe ? $.fn.bgiframe : $.fn.bgIframe ? $.fn.bgIframe : function(){
		// For applications that don't have bgiframe plugin installed, create a useless 
		// function that doesn't break the chain
		return this;
	};

	// The expando won't get attached to the jQuery object until 1.4 release(or so it seems in the nightlies)
	// To get the expando, we must create an event through jQuery, and filter it out.
	$.expando = $.expando !== undefined ? $.expando : (function(){
		var event = $.Event('keyup'), i;
		for (i in event)
			if (i.indexOf('jQuery') === 0)
				return i;
		// Use the event's timestamp on instances where 
		// expando isn't attached to the event object
		// (is it ever not?)
		return 'jQuery'+event.timeStamp;
	})();

	// Current timestamp
	function now(){
		return (new Date).getTime();
	}



// Internals
var
	// Munging
	TRUE = true,
	FALSE = false,

	// Copy of the slice prototype
	Slice = Array.prototype.slice,

	// Attach global aspects to jQuery itself
	AutoComplete = $.autoComplete = {
		// Index Counter
		counter: 0,

		// Attach length of stack to object
		length: 0,

		// Storage of elements
		stack: {},

		// Storage order of uid's
		order: [],

		// Global access to elements in use
		hasFocus: FALSE,

		// Callback methods for getting focus element
		getFocus: function(){
			return this.order[0] ? this.stack[ this.order[0] ] : undefined;
		},
		getPrevious: function(){
			// Removing elements cause some indexs on the order stack
			// to become undefined, so loop until one is found
			for ( var i=1, l=this.order.length; i < l; i++ )
				if (this.order[i])
					return this.stack[ this.order[i] ];
			// If none are found, return undefined
			return undefined;
		},

		// Attempts to remove element from the stack
		remove: function(i){
			for ( var k=0, l=this.order.length; k < l; k++ )
				if (this.order[k] === i)
					this.order[k] = undefined;
			this.stack[i] = undefined;
			this.length--;
			delete this.stack[i];
		},

		// Returns full stack in jQuery form
		getAll: function(){
			for ( var i = 0, l = this.counter, stack = []; i < l; i++ )
				if (this.stack[i])
					stack.push(this.stack[i]);
			return $(stack);
		},

		defaults: {
			// To smooth upgrade process to 5.0, set backwardsCompatible to true
			backwardsCompatible: FALSE,
			// Server Script Path
			ajax: 'ajax.php',
			ajaxCache: $.ajaxSettings.cache,
			// Data Configuration
			dataSupply: [],
			dataFn: undefined,
			dataName: 'ac-data',
			// Drop List CSS
			list: 'auto-complete-list',
			rollover: 'auto-complete-list-rollover',
			width: undefined, // Defined as inputs width when extended (can be overridden with this global/options/meta)
			striped: undefined,
			maxHeight: undefined,
			newList: FALSE,
			// Post Data
			postVar: 'value',
			postData: {},
			// Limitations
			minChars: 1,
			maxItems: -1,
			maxRequests: 0,
			requestType: 'POST',
			// Input
			inputControl: undefined,
			autoFill: FALSE,
			nonInput: undefined,
			multiple: FALSE,
			multipleSeparator: ' ',
			// Events
			onBlur: undefined,
			onFocus: undefined,
			onHide: undefined,
			onLoad: undefined,
			onMaxRequest: function(){},
			onRollover: undefined,
			onSelect: undefined,
			onShow: undefined,
			onSubmit: function(){return TRUE;},
			spinner: undefined,
			preventEnterSubmit: TRUE,
			delay: 0,
			// Caching Options
			useCache: TRUE,
			cacheLimit: 50
		}
	},

	// Autocomplete function
	AutoCompleteFunction = function(options){
		return this.each(function(){
		var
			// Cache a copy of the input element
			self = this,
			// Cache Input Object
			$input = $(self).attr('autocomplete', 'off'),
			// autoComplete enabled/disabled
			Active = TRUE,
			// Track every event triggered
			LastEvent = {},
			// String of current input value
			inputval = '',
			// Place holder for all list elements
			$elems = {length:0},
			// Place holder for the list element in focus
			$li,
			// View and heights for scrolling
			view, ulHeight, liHeight, liPerView,
			// Harcoded value for ul visiblity
			ulOpen = FALSE,
			// Timer for delay
			timeid,
			// Ajax requests holder
			xhr,
			// li element in focus during key up/down, and its data
			liFocus = -1, liData,
			// For multiple selections
			separator,
			// Index of current input
			inputIndex = (function(){ AutoComplete.length++; return ++AutoComplete.counter; })(),
			// Number of requests made
			requests = 0,
			// Internal Per Input Cache
			cache = {
				length: 0,
				val: undefined,
				list: {}
			},

			// Merge defaults with passed options and metadata options
			settings = $.extend(
				{ width: $input.outerWidth() },
				AutoComplete.defaults, 
				options||{},
				$.metadata ? $input.metadata() : {}
			),

			// Create the drop list (Use an existing one if possible)
			$ul = !settings.newList && $('ul.'+settings.list)[0] ?
				$('ul.'+settings.list).eq(0).bgiframe().data('autoComplete', TRUE) :
				$('<ul/>').appendTo('body').addClass(settings.list).bgiframe().hide().data('ac-selfmade', TRUE).data('autoComplete', TRUE),

			// Attach document click to force blur event
			$doc = $(document).bind('click.autoComplete-'+inputIndex, function(event){
				var $elem;
				// Make sure input is active and list is open
				if (Active && ulOpen &&
					// Double check the event timestamps to ensure there isn't
					// a delayed reaction from a button
					(!LastEvent || event.timeStamp - LastEvent.timeStamp > 200) && 
					// Check the target after all other checks are passed (less processing)
					( $elem = $(event.target) ).closest('ul').data('ac-input-index') !== inputIndex &&
					// Also ensure that the input it's being clicked on either
					$elem.data('ac-input-index') !== inputIndex){
						$ul.hide(event);
						// We want to trigger all blur events, so don't
						// pass special autoComplete flags here through the
						// trigger function
						$input.blur();
				}
				LastEvent = event;
			});

			// Attach special fn's to ul
			newUl();
			// Upper case requestType now instead of on every call
			settings.requestType = settings.requestType.toUpperCase();
			// Set separator to local variable for munging
			separator = settings.multiple ? settings.multipleSeparator : undefined;
			// Add input to stack
			AutoComplete.stack[inputIndex] = self;

			/**
			 * Input Central
			 */ 
			// Show autocomplete has been initialized on this element
			$input.data('autoComplete', TRUE)
			// Attach input index and initial settings
			.data('ac-input-index', inputIndex)
			// autoComplete Activity
			.data('ac-active', Active)
			// Attach settings to initail and current states
			.data('ac-initial-settings', $.extend(TRUE, {}, settings)).data('ac-settings', settings)
			// Central autoComplete specific function
			// Opera uses keypress as it has problems with keydown
			.bind(window.opera ? 'keypress.autoComplete' : 'keydown.autoComplete', function(event){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Track last event and store code for munging
				var key = (LastEvent = event).keyCode, enter = FALSE;

				// Tab Key
				if (key == 9 && ulOpen){
					select(event);
				}
				// Enter Key
				else if (key == 13 && $li){
					// IE needs keydown to return false on 'enter' so the element doesn't
					// lose focus. The problem with returning false is that it prevents bubbling,
					// and most importantly, form submission. To allow for most flexibility,
					// preventEnterSubmit is used along with activity of drop down UL list to
					// determine whether focus is on the drop list or is just on the input.
					//
					// Furthermore, preventEnterSubmit will now be defaulted to true, so as
					// to affect as few implementations as possible, and the ones that need
					// form submission on 'enter' can just set this flag to false for it to
					// work as needed.
					enter = settings.preventEnterSubmit && ulOpen ? FALSE : TRUE;
					select(event);
				}
				// Up Arrow
				else if (key == 38){
					if (liFocus > 0){
						liFocus--;
						up(event);
					}else{
						liFocus = -1;
						$input.val(inputval);
						$ul.hide(event);
					}
				}
				// Down Arrow
				else if (key == 40){
					if (liFocus < $elems.length-1){
						liFocus++;
						down(event);
					}
				}
				// Page Up
				else if (key == 33){
					if (liFocus > 0){
						liFocus -= liPerView;
						if (liFocus < 0) liFocus = 0;
						up(event);
					}
				}
				// Page Down
				else if (key == 34){
					if (liFocus < $elems.length-1){
						liFocus += liPerView;
						if (liFocus > $elems.length-1) liFocus = $elems.length-1;
						down(event);
					}
				}
				// Check for non input values defined by user
				else if (settings.nonInput && $.inArray(key, settings.nonInput)){
					$ul.html('').hide(event);
				}
				// Everything else is considered possible input, so
				// return before keyup prevention flag is set
				else{
					return TRUE;
				}

				// Prevent autoComplete keyup event's from triggering by
				// attaching a flag to the last event
				LastEvent[$.expando + '_autoComplete_keydown'] = TRUE;
				return enter;
			})
			// Run a keydown event to specifically catch the tab key
			.bind('keyup.autoComplete', function(event){
				// If autoComplete has been disabled or keyup prevention 
				// flag has be set, prevent input events
				if (!Active || LastEvent[$.expando + '_autoComplete_keydown']) return TRUE;

				/**
				 * If no special operations were run on keydown,
				 * allow for regular text searching
				 */
				inputval = $input.val();
				var key = (LastEvent = event).keyCode,
					val = separator ? inputval.split(separator).pop() : inputval;
				// Still check to make sure 'enter' wasn't pressed
				if (key != 13){
					// Caching key value
					cache.val = settings.inputControl === undefined ? val : 
						settings.inputControl.apply(self, settings.backwardsCompatible ? 
							[val, key, $ul, event] : [event, {val: val, key: key, ul: $ul}]);
					// Only send request if character length passes
					if (cache.val.length >= settings.minChars)
						sendRequest(event, settings, cache, (key==8||key==32));
					// Remove list on backspace of small string
					else if (key == 8)
						$ul.html('').hide(event);
				}
			})
			// Bind specific Blur Actions
			.bind('blur.autoComplete', function(event){
				// If autoComplete has been disabled or the drop list
				// is still open, prevent input events
				if (!Active || ulOpen) return TRUE;
				// Store event
				LastEvent = event;
				$input.data('ac-hasFocus', FALSE);
				liFocus = -1;
				// Only push undefined index onto order stack
				// if not already there (incase multiple blur events occur)
				if (AutoComplete.order[0] !== undefined)
					AutoComplete.order.unshift(undefined);
				// Expose focus
				AutoComplete.hasFocus = FALSE;
				$ul.hide(event);
				// Trigger blur callback last
				if (settings.onBlur) settings.onBlur.apply(self, settings.backwardsCompatible ?
					[inputval, $ul, event] : [event, {val: inputval, ul: $ul}]);
			})
			// Bind specific focus actions
			.bind('focus.autoComplete', function(event, flag){
				// If autoComplete has been disabled but not destoyed, just return true 
				if (!Active || 
					// Prevent inner focus events if caused by autoComplete inner functionality
					(AutoComplete.focus === inputIndex && flag === $.expando + '_autoComplete') || 
					// Because IE triggers focus AND closes the drop list before form submission,
					// prevent inner function focus functionality & pass on the select flag
					LastEvent[$.expando + '_autoComplete_enter'])
						return TRUE;
				// Store event
				LastEvent = event;
				// If ul is not associated with current input, clear it
				if (inputIndex != $ul.data('ac-input-index'))
					$ul.html('').hide(event);
				// Store focus into input
				$input.data('ac-hasFocus', TRUE);
				// Overwrite undefined index pushed on by the blur event
				if (AutoComplete.order[0] === undefined){
					if (AutoComplete.order[1] === inputIndex)
						AutoComplete.order.shift();
					else
						AutoComplete.order[0] = inputIndex;
				}
				// Only push another uid if it's not the current one
				else if (AutoComplete.order[0] != inputIndex && AutoComplete.order[1] != inputIndex)
					AutoComplete.order.unshift(inputIndex);
				// Keep the order array to within the global cacheLimit size
				if (AutoComplete.order.length > AutoComplete.defaults.cacheLimit)
					AutoComplete.order.pop();
				// Expose focus
				AutoComplete.hasFocus = TRUE;
				// Trigger focus callback last
				if (settings.onFocus) settings.onFocus.apply(self, settings.backwardsCompatible ? [$ul, event] : [event, {ul: $ul}]);
			})

			/**
			 * Autocomplete Methods
			 * -Extensions off autoComplete event
			 */ 
			// Allows for change of settings at any point
			.bind('autoComplete.settings', function(event, newSettings){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Give access to current settings and cache
				if ($.isFunction(newSettings)){
					var ret = newSettings.apply(self, settings.backwardsCompatible ? 
						[settings, cache, $ul, event] : [event, {settings: settings, cache: cache, ul: $ul}]);
					// Allow for extending of settings/cache based off function return values
					if ($.isArray(ret) && ret[0] !== undefined){
						settings = $.extend(TRUE, {}, settings, ret[0]||settings);
						cache = $.extend(TRUE, {}, cache, ret[1]||cache);
					}
				}else{
					// Extend deep so settings are kept
					settings = $.extend(TRUE, {}, settings, newSettings||{});
				}
				// Upper case requestType now instead of on every call
				settings.requestType = settings.requestType.toUpperCase();
				// Reassign local separator
				separator = settings.multiple ? settings.multipleSeparator : undefined;
				// Restablish current settings onto the inputs data
				$input.data('ac-settings', settings);
				// Change the drop down if user want's a differen't class attached
				$ul = !settings.newList && $ul.hasClass(settings.list) ? $ul : 
					!settings.newList && $('ul.'+settings.list)[0] ? $('ul.'+settings.list).bgiframe().data('autoComplete', TRUE) : 
					$('<ul/>').appendTo('body').addClass(settings.list).bgiframe().hide()
						.data('ac-selfmade', TRUE).data('autoComplete', TRUE);
				// Attach special ul fn's
				newUl();
				// Return & Store event
				return LastEvent = event;
			})
			// Clears the Cache & requests (requests can be blocked on request)
			.bind('autoComplete.flush', function(event, cacheOnly){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				cache = {length:0, val:undefined, list:{}};
				if (!cacheOnly) requests = 0;
				// Store & return event
				return LastEvent = event;
			})
			// External button trigger for ajax requests
			.bind('autoComplete.button-ajax', function(event, postData, cacheName){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Store event
				LastEvent = event;
				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger('focus', [$.expando + '_autoComplete']);
				// Allow for just passing the cache name
				if (typeof postData === 'string'){
					cacheName = postData;
					postData = {};
				}
				// If no cache name is given, supply a non-common word
				cache.val = cacheName||'NON_404_<>!@$^&';
				// Timer is done within sendRequest
				return sendRequest(
					event, 
					$.extend(TRUE, {}, settings, {maxItems: -1, postData: postData||{}}), 
					cache
				);
			})
			// External button trigger for supplied data
			.bind('autoComplete.button-supply', function(event, data, cacheName){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Store event
				LastEvent = event;
				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger('focus', [$.expando + '_autoComplete']);
				// Allow for just passing of cacheName
				if (typeof data === 'string'){
					cacheName = data;
					data = undefined;
				}
				// If no cache name is given, supply a non-common word
				cache.val = cacheName||'NON_404_SUPPLY_<>!@$^&';
				// If no data is supplied, use data in settings
				data = $.isArray(data) && data.length ? data : settings.dataSupply;
				// Timer done within sendRequest
				return sendRequest(
					event,
					$.extend(TRUE, {}, settings, {maxItems: -1, dataSupply: data, dataFn: function(){ return TRUE; } }), 
					cache
				);
			})
			// Supply list directly into the result function
			.bind('autoComplete.direct-supply', function(event, data, cacheName){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Store event
				LastEvent = event;
				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger('focus', [$.expando + '_autoComplete']);
				// Allow for just passing of cacheName
				if (typeof data === 'string'){
					cacheName = data;
					data = undefined;
				}
				// If no cache name is given, supply a non-common word
				cache.val = cacheName||'NON_404_SUPPLY_<>!@$^&';
				// If no data is supplied, use data in settings
				data = $.isArray(data) && data.length ? data : settings.dataSupply;
				// Load the results directly into the results function
				// bypassing error checks (Only do)
				return loadResults(
					event,
					data,
					$.extend(TRUE, {}, settings, {maxItems: -1, dataSupply: data, dataFn: function(){ return TRUE; } }), 
					cache
				);
			})
			// Triggering autocomplete programatically
			.bind('autoComplete.search', function(event, value){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				cache.val = value||'';
				// Timer done within sendRequest
				return sendRequest(LastEvent = event, settings, cache);
			})
			// Add jquery-ui like option access
			.bind('autoComplete.option', function(event){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Store event
				LastEvent = event;
				var args = Slice.call(arguments), length = args.length;
				return length == 3 ? (function(){settings[ args[1] ] = args[2]; $input.data('ac-settings', settings); return args[2];})() :
					length == 2 ? (function(){ 
						switch (args[1]){
							case 'ul': return $ul;
							case 'cache': return cache;
							case 'xhr': return xhr;
							case 'input': return $input;
							default: return settings[ args[1] ] || undefined;
						}
					})() :
					settings;
			})
			// Add enabling event (only applicable after disable)
			.bind('autoComplete.enable', function(event){
				$input.data('ac-active', Active = TRUE);
				// Store & return event
				return LastEvent = event;
			})
			// Add disable event
			.bind('autoComplete.disable', function(event){
				// Store event
				$input.data('ac-active', Active = FALSE);
				$ul.html('').hide(event);
				// Store & return event
				return LastEvent = event;
			})
			// Add a destruction function
			.bind('autoComplete.destroy', function(event){
				// Break down the input
				$input
					// Remove all autoComplete Specific Data
					.removeData('autoComplete')
					.removeData('ac-input-index')
					.removeData('ac-initial-settings')
					.removeData('ac-settings')
					.removeData('ac-active')
					// Remove all autoComplete specific events
					.unbind('.autoComplete')
					// jQuery requires every namespace attached to
					// a made up event to be removed separately
					.unbind( 'autoComplete.' + [
							'settings',
							'flush',
							'button-ajax',
							'button-supply',
							'direct-supply',
							'search',
							'option',
							'enable',
							'disable',
							'destroy'
						].join(' autoComplete.') )
					// Unbind the form submission event
					.parents('form').eq(0).unbind('submit.autoComplete-'+inputIndex);
				// Remove document click event
				$doc.unbind('click.autoComplete-'+inputIndex);
				// Remove from stack
				AutoComplete.remove(inputIndex);
				// Disable Activity
				Active = FALSE;
				// Clean the UL
				var list = $ul.html('').hide(event).data('ac-inputs'), i;
				list[inputIndex] = undefined;
				for (i in list)
					if (list[i] === TRUE)
						return LastEvent = event;
				// Remove the element from the DOM if self created no other input is using it
				if ($ul.data('ac-selfmade') === TRUE) $ul.remove();
				// Store & return event
				return LastEvent = event;
			})

			// Back to normal events
			// Prevent form submission if defined in settings
			.parents('form').eq(0).bind('submit.autoComplete-'+inputIndex, function(event){
				// If autoComplete has been disabled, prevent input events
				if (!Active) return TRUE;
				// Because IE triggers focus AND closes the drop list before form submission, store the flag if any
				var flag = LastEvent[$.expando + '_autoComplete_enter']||FALSE;
				// Store event
				LastEvent = event;
				return settings.preventEnterSubmit ?
					(ulOpen || flag) ? FALSE : settings.onSubmit.call(self, event, {form: this, ul: $ul}) :
					settings.onSubmit.call(self, event, {form: this, ul: $ul});
			});
	
			// Ajax/Cache Request
			function sendRequest(event, settings, cache, backSpace, timeout){
				// Pass spinner enabler
				if (settings.spinner) settings.spinner.call(self, event, {active: TRUE, ul: $ul});
				// Centralize the timer request
				if (timeid) timeid = clearTimeout(timeid);
				// Call send request again with timeout flag if on delay
				if (settings.delay > 0 && timeout === undefined) return timeid = setTimeout(function(){
						sendRequest(event, settings, cache, backSpace, TRUE);
						timeid = clearTimeout(timeid);
					}, settings.delay);

				// Abort previous request incase it's still running
				if (xhr) xhr.abort();

				// Load from cache if possible
				if (settings.useCache && cache.list[cache.val])
					return loadResults(event, cache.list[cache.val], settings, cache, backSpace);

				// Use user supplied data when defined
				if (settings.dataSupply.length)
					return userSuppliedData(event, settings, cache, backSpace);

				// Check Max requests first before sending request
				if (settings.maxRequests && ++requests >= settings.maxRequests){
					$ul.html('').hide(event);
					if (settings.spinner) settings.spinner.call(self, event, {active: FALSE, ul: $ul});
					return requests > settings.maxRequests ?
						FALSE : settings.onMaxRequest.apply(self, settings.backwardsCompatible ? 
								[cache.val, $ul, event, inputval] : [event, {search: cache.val, val: inputval, ul: $ul}]);
				}

				// Send request server side
				settings.postData[settings.postVar] = cache.val
				// Switched to base ajax request to remove list on errors
				return xhr = $.ajax({
					type: settings.requestType,
					url: settings.ajax,
					data: settings.postData,
					dataType: 'json',
					cache: settings.ajaxCache,
					success: function(list){
						loadResults(event, list, settings, cache, backSpace);
					},
					error: function(){
						$ul.html('').hide(event);
						if (settings.spinner) settings.spinner.call(self, event, {active: FALSE, ul: $ul});
					}
				});
			}

			// Parse User Supplied Data
			function userSuppliedData(event, settings, cache, backSpace){
				var list = [], // Result list
					args = [], // Backwards Compatibility
					fn = $.isFunction(settings.dataFn), // User supplied function
					regex = fn ? undefined : new RegExp('^'+cache.val, 'i'), // Only compile regex if needed
					k = 0, entry, i=0, l=settings.dataSupply.length; // Looping vars

				// Loop through each entry and find matches
				for ( ; i < l; i++ ){
					entry = settings.dataSupply[i];
					// Force object
					entry = typeof entry === 'object' && entry.value ? entry : {value: entry};
					// Setup arguments for dataFn in a backwards compatible way if needed
					args = settings.backwardsCompatible ? 
						[cache.val, entry.value, list, i, settings.dataSupply, $ul, event] :
						[event, {val: cache.val, entry: entry.value, list: list, i: i, supply: settings.dataSupply, ul: $ul}];
					// If user supplied function, use that, otherwise test with default regex
					if ((fn && settings.dataFn.apply(self, args)) || (!fn && entry.value.match(regex))){
						// Reduce browser load by breaking on limit if it exists
						if (settings.maxItems > -1 && ++k > settings.maxItems)
							break;
						list.push(entry);
					}
				}
				// Use normal load functionality
				return loadResults(event, list, settings, cache, backSpace);
			}

			// Key element Selection
			function select(event){
				// Ensure the select function only gets fired when list of open
				if (ulOpen){
					if (settings.onSelect) settings.onSelect.apply(self, settings.backwardsCompatible ? 
						[liData, $li, $ul, event] : [event, {data: liData, li: $li, ul: $ul}]);
					autoFill(undefined);
					inputval = $input.val();
					// Because IE triggers focus AND closes the drop list before form submission
					// attach a flag on 'enter' selection
					if (LastEvent.type=='keydown') LastEvent[$.expando + '_autoComplete_enter'] = TRUE;
				}
				$ul.hide(event);
				return $li;
			}

			// Key direction up
			function up(event){
				if ($li) $li.removeClass(settings.rollover);
				$ul.show(event);
				$li = $elems.eq(liFocus).addClass(settings.rollover);
				liData = $li.data(settings.dataName);
				if (!$li.length || !liData) return FALSE;
				autoFill( liData.value||'' );
				if (settings.onRollover) settings.onRollover.apply(self, settings.backwardsCompatible ? 
					[liData, $li, $ul, event] : [event, {data: liData, li: $li, ul: $ul}]);
				// Scrolling
				var v = liFocus*liHeight;
				if (v < view-ulHeight){
					view = v+ulHeight
					$ul.scrollTop( v );
				}
				return $li;
			}

			// Key direction down
			function down(event){
				if ($li) $li.removeClass(settings.rollover);
				$ul.show(event);
				$li = $elems.eq( liFocus ).addClass( settings.rollover );
				liData = $li.data( settings.dataName );
				if (!$li.length || !liData) return FALSE;
				autoFill( liData.value||'' );
				// Scrolling
				var v = (liFocus+1)*liHeight;
				if (v > view)
					$ul.scrollTop( (view = v) - ulHeight );
				// Callback
				if (settings.onRollover) settings.onRollover.apply(self, settings.backwardsCompatible ? 
					[liData, $li, $ul, event] : [event, {data: liData, li: $li, ul: $ul}]);
				return $li;
			}

			// Attach new show/hide functionality to only the
			// ul object (so not to infect all of jQuery)
			function newUl(){
				if (! $ul[$.expando + '_autoComplete']){
					// Make a copy of the old show/hide
					var hide = $ul.hide, show = $ul.show;
					$ul.hide = function(event, speed, callback){
						if (settings.onHide && ulOpen){
							settings.onHide.call(self, event, {ul: $ul});
							LastEvent[$.expando + '_autoComplete_hide'] = TRUE;
						}
						ulOpen = FALSE;
						return hide.call($ul, speed, callback);
					};
					$ul.show = function(event, speed, callback){
						if (settings.onShow && !ulOpen) settings.onShow.call(self, event, {ul: $ul});
						ulOpen = TRUE;
						return show.call($ul, speed, callback);
					};
					// A flag must be attached to the $ul cached object
					$ul[$.expando + '_autoComplete'] = TRUE;
				}
				var list = $ul.data('ac-inputs')||{};
				list[inputIndex] = TRUE;
				return $ul.data('ac-inputs', list);
			}

			// Auto-fill the input
			// Credit to Jörn Zaefferer @ http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
			// and http://www.pengoworks.com/workshop/jquery/autocomplete.htm for this functionality
			function autoFill(val){
				// Set starting and ending points based on values
				if (val === undefined){
					var start, end; start = end = $input.val().length;
				}else{
					if (separator) val = inputval.substr( 0, inputval.length-inputval.split(separator).pop().length ) + val + separator;
					var start = inputval.length, end = val.length;
					$input.val(val);
				}

				// Create selection if allowed
				if (! settings.autoFill || start > end){
					return FALSE;
				}
				else if (self.createTextRange){
					var range = self.createTextRange();
					if (val === undefined) {
						range.move('character', start);
						range.select();
					}else{
						range.collapse(TRUE);
						range.moveStart("character", start);
						range.moveEnd("character", end);
						range.select();
					}
				}
				else if (self.setSelectionRange){
					self.setSelectionRange(start, end);
				}
				else if (self.selectionStart){
					self.selectionStart = start;
					self.selectionEnd = end;
				}
				return TRUE;
			}

			// List Functionality
			function loadResults(event, list, settings, cache, backSpace){
				// Allow another level of result handling
				if (settings.onLoad) list = settings.onLoad.call(self, event, {list: list, settings: settings, cache: cache, ul: $ul});
				// Pass spinner killer as wait time is done in javascript processing
				if (settings.spinner) settings.spinner.call(self, event, {active: FALSE, ul: $ul});
				// Store results into the cache if allowed
				if (settings.useCache && cache.list[cache.val] === undefined){
					cache.length++;
					cache.list[cache.val] = list;
					// Clear cache if necessary
					if (cache.length > settings.cacheLimit){
						cache.list = {};
						cache.length = 0;
					}
				}

				// Ensure there is a list
				if (!list || list.length < 1)
					return $ul.html('').hide(event);

				// Refocus list element
				liFocus = -1;

				// Initialize Vars together (save bytes)
				var offset = $input.offset(), // Input position
				    container = [], // Container for list elements
				    aci=0,k=0,i=0,even=FALSE,length=list.length; // Loop Items

				// Push items onto container
				for (; i < length; i++){
					if (list[i].value){
						if (settings.maxItems > -1 && ++aci > settings.maxItems)
							break;
						container.push(
							settings.striped && even ? '<li class="'+settings.striped+'">' : '<li>',
							list[i].display||list[i].value,
							'</li>'
						);
						even = !even;
					}
				}

				// Load items into list
				$elems = $ul.html( container.join('') ).children('li');
				for ( length = $elems.length; k < length; k++ ){
					$.data( $elems[k], settings.dataName, list[k] );
					$.data( $elems[k], 'ac-index', k );
				}


				// Autofill input with first entry
				if (settings.autoFill && ! backSpace){
					liFocus = 0;
					liData = list[0];
					autoFill( liData.value||'' );
					$li = $elems.eq(0).addClass( settings.rollover );
				}

				// Clear off old events and attach new ones
				$ul.unbind('.autoComplete')
				// Attach input index in focus
				.data('ac-input-index', inputIndex)
				// Remove focus elements hover class
				.bind('mouseout.autoComplete', function(){
					$li.removeClass(settings.rollover);
				})
				// Mouseover using event delegation
				.bind('mouseover.autoComplete', function(event){
					$li = $(event.target).closest('li');
					// Ensure 'li' mouseover
					if ($li.length < 1) return FALSE;
					// Remove hover class from last rollover
					$elems.filter('.'+settings.rollover).removeClass(settings.rollover);
					liFocus = $li.addClass(settings.rollover).data('ac-index');
					liData = $li.data( settings.dataName );
					if (settings.onRollover) settings.onRollover.apply(self, settings.backwardsCompatible ? 
						[liData, $li, $ul, event] : [event, {data: liData, li: $li, ul: $ul}]);
				})
				// Click event using target from mouseover
				.bind('click.autoComplete', function(event){
					// Refocus the input box and pass flag to prevent inner focus events
					$input.trigger('focus', [$.expando + '_autoComplete']);
					liData = $li.data(settings.dataName);
					// Check against separator for input value
					$input.val( inputval = separator ? 
						inputval.substr( 0, inputval.length-inputval.split(separator).pop().length ) + liData.value + separator :
						liData.value 
					);
					$ul.hide(event);
					autoFill(undefined);
					if (settings.onSelect) settings.onSelect.apply(self, settings.backwardsCompatible ? 
						[liData, $li, $ul, event] : [event, {data: liData, li: $li, ul: $ul}]);
				})
				// Reposition list
				.css({
					top: offset.top + $input.outerHeight(),
					left: offset.left,
					width: settings.width
				})
				// Scroll to the top
				.scrollTop(0);

				// If Max Height specified, control it
				if (settings.maxHeight) $ul.css({
					height: liHeight*$elems.length > settings.maxHeight ? settings.maxHeight : 'auto', 
					overflow: 'auto'
				});

				// Apply list height to view for inital value,
				// and show the list now so no jerkiness from css
				// changes are shown to the user
				ulHeight = $ul.show(event).outerHeight();
				view = ulHeight;
				// Log li height for less computation
				liHeight = $elems.eq(0).outerHeight();
				// Number of elements per viewport
				liPerView = Math.floor(view/liHeight);

				// Include amount of time it took
				// to load the list
				LastEvent.timeStamp = now();

				// Every function needs to return something
				return $ul;
			}
		});
	};
})(jQuery);

Download

Latest: auto-complete-5.0.zip
Released: November 22, 2009
-Auto Fill of active list sugestions.
-Multiple word searches.
-Paging and Scrolling.
-Removed timer dependency. Now mainly event based.
-Event delegation used on mouse actions of the drop list.

Past Release's:
October 5, 2009: auto-complete-4.1.zip
September 28, 2009: auto-complete-4.0.zip
September 17, 2009: auto-complete-3.2.zip
August 22, 2009: auto-complete-3.1.zip
July 26, 2009: auto-complete-3.0.zip
June 11, 2009: auto-complete-2.1.zip
June 10, 2009: auto-complete-2.0.zip
June 5, 2009: auto-complete-1.0.zip