jQuery Plugin: Auto Complete

June 11, 2009 | Demo | Docs | Download | Github
Updated: 2010-04-16
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.1
 * April 13, 2010
 * Corey Hart @ http://www.codenothing.com
 */ 
(function( $, window, 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',
			handler, el;

		// Deep namespacing is not supported in jQuery, a mistake I made in v4.1
		if ( isMethod ) {
			first = first.replace( rdot, '-' );
		}
		
		// 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*: Some triggers pass an array as the first param, so check against that first
		args = ( AutoComplete.arrayMethods[ first ] === TRUE && $.isArray( args[0] ) && $.isArray( args[0][0] ) ) || 
			( args.length === 1 && $.isArray( args[0] ) ) ? 
				args[0] : args;

		// Check method against handlers that need to use triggerHandler 
		handler = isMethod && ( AutoComplete.handlerMethods[ first ] === -1 || args.length < ( AutoComplete.handlerMethods[ first ] || 0 ) ) ? 
			'triggerHandler' : 'trigger';

		return isMethod ?
			self[ handler ]( 'autoComplete.' + first, args ) :

			// Allow passing a jquery event special object {from $.Event()}
			first && first.preventDefault !== undefined ? self.trigger( first, args ) :

			// Initiate the autocomplete on each element (Only takes a single argument, the options object)
			self.each(function(){
				if ( $( el = this ).data( 'autoComplete' ) !== TRUE ) {
					AutoCompleteFunction( el, 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;
	};

	// Allows for single event binding to document and forms associated with the autoComplete inputs
	// by deferring the event to the input in focus
	function setup( $input, inputIndex ) {
		if ( setup.flag !== TRUE ) {
			setup.flag = TRUE;
			rootjQuery.bind( 'click.autoComplete', function( event ) {
				AutoComplete.getFocus( TRUE ).trigger( 'autoComplete.document-click', [ event ] );
			});
		}

		var $form = $input.closest( 'form' ), formList = $form.data( 'ac-inputs' ) || {}, $el;

		formList[ inputIndex ] = TRUE;
		$form.data( 'ac-inputs', formList );

		if ( $form.data( 'autoComplete' ) !== TRUE ) {
			$form.data( 'autoComplete', TRUE ).bind( 'submit.autoComplete', function( event ) {
				return ( $el = AutoComplete.getFocus( TRUE ) ).length ?
					$el.triggerHandler( 'autoComplete.form-submit', [ event, this ] ) :
					TRUE;
			});
		}
	}

	// Removes the single events attached to the document and respective input form
	function teardown( $input, inputIndex ) {
		AutoComplete.remove( inputIndex );

		if ( setup.flag === TRUE && AutoComplete.length === 0 ) {
			setup.flag = FALSE;
			rootjQuery.unbind( 'click.autoComplete' );
		}

		var $form = $input.closest( 'form' ), formList = $form.data( 'ac-inputs' ) || {}, i;

		formList[ inputIndex ] = FALSE;
		for ( i in formList ) {
			if ( formList.hasOwnProperty( i ) && formList[ i ] === TRUE ) {
				return;
			}
		}

		$form.unbind( 'submit.autoComplete' );
	}

	// Default function for adding all supply items to the list
	function allSupply( event, ui ) {
		if ( ! $.isArray( ui.supply ) ) {
			return [];
		}

		for ( var i = -1, l = ui.supply.length, ret = [], entry; ++i < l; ) {
			entry = ui.supply[ i ];
			entry = entry && entry.value ? entry : { value: entry };
			ret.push( entry );
		}

		return ret;
	}



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

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

	// Make a copy of the document element for caching
	rootjQuery = $( window.document ),

	// Also make a copy of an empty jQuery set for fast referencing
	emptyjQuery = $( ),

	// regex's
	rdot = /\./,

	// Opera and Firefox on Mac need to use the keypress event to track holding of
	// a key down and not releasing
	keypress = window.opera || ( /macintosh/i.test( window.navigator.userAgent ) && $.browser.mozilla ),

	// Event flag that gets passed around
	ExpandoFlag = 'autoComplete_' + $.expando,

	// Make a local copy of the key codes used throughout the plugin
	KEY = {
		backspace: 8,
		tab: 9,
		enter: 13,
		shift: 16,
		space: 32,
		pageup: 33,
		pagedown: 34,
		left: 37,
		up: 38,
		right: 39,
		down: 40
	},

	// Attach global aspects to jQuery itself
	AutoComplete = $.autoComplete = {
		// Autocomplete Version
		version: '5.1',

		// Index Counter
		counter: 0,

		// Length of stack
		length: 0,

		// Storage of elements
		stack: {},

		// jQuery object versions of the storage elements
		jqStack: {},

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

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

		// Expose the used keycodes
		keys: KEY,

		// Methods whose first argument may contain an array
		arrayMethods: {
			'button-supply': TRUE,
			'direct-supply': TRUE
		},

		// Defines the maximum number of arguments that can be passed for using
		// triggerHandler method instead of trigger. Passing -1 forces triggerHandler
		// no matter the number of arguments
		handlerMethods: {
			'option': 2
		},

		// Events triggered whenever one of the autoComplete
		// input's come into focus or blur out.
		focus: undefined,
		blur: undefined,

		// Allow access to jquery cached object versions of the elements
		getFocus: function( jqStack ) {
			return ! AutoComplete.order[0] ? jqStack ? emptyjQuery : undefined :
				jqStack ? AutoComplete.jqStack[ AutoComplete.order[0] ] :
				AutoComplete.stack[ AutoComplete.order[0] ];
		},

		getPrevious: function( jqStack ) {
			// Removing elements cause some indexs on the order stack
			// to become undefined, so loop until one is found
			for ( var i = 0, l = AutoComplete.order.length; ++i < l; ) {
				if ( AutoComplete.order[i] ) {
					return jqStack ?
						AutoComplete.jqStack[ AutoComplete.order[i] ] :
						AutoComplete.stack[ AutoComplete.order[i] ];
				}
			}

			return jqStack ? emptyjQuery : undefined;
		},

		remove: function( n ) {
			for ( var i = -1, l = AutoComplete.order.length; ++i < l; ) {
				if ( AutoComplete.order[i] === n ) {
					AutoComplete.order[i] = undefined;
				}
			}

			AutoComplete.length--;
			delete AutoComplete.stack[n];
		},

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

		defaults: {
			// To smooth upgrade process to 5.x, set backwardsCompatible to true
			backwardsCompatible: FALSE,
			// Server Script Path
			ajax: 'ajax.php',
			ajaxCache: $.ajaxSettings.cache,
			// Data Configuration
			dataSupply: [],
			dataFn: undefined,
			formatSupply: undefined,
			// 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,
			bgiframe: undefined,
			newList: FALSE,
			// Post Data
			postVar: 'value',
			postData: {},
			postFormat: undefined,
			// Limitations
			minChars: 1,
			maxItems: -1,
			maxRequests: 0,
			maxRequestsDeep: FALSE,
			requestType: 'POST',
			// Input
			inputControl: undefined,
			autoFill: FALSE,
			nonInput: [ KEY.shift, KEY.left, KEY.right ],
			multiple: FALSE,
			multipleSeparator: ' ',
			// Events
			onBlur: undefined,
			onFocus: undefined,
			onHide: undefined,
			onLoad: undefined,
			onMaxRequest: undefined,
			onRollover: undefined,
			onSelect: undefined,
			onShow: undefined,
			onListFormat: undefined,
			onSubmit: undefined,
			spinner: undefined,
			preventEnterSubmit: TRUE,
			delay: 0,
			// Caching Options
			useCache: TRUE,
			cacheLimit: 50
		}
	},

	// Autocomplete function
	AutoCompleteFunction = function( self, options ) {
		// Start with counters as they are used within declarations
		AutoComplete.length++;
		AutoComplete.counter++;

		// Input specific vars
		var $input = $( self ).attr( 'autocomplete', 'off' ),
			// Data object stored on 'autoComplete' data namespace of input
			ACData = {},
			// Track every event triggered
			LastEvent = {},
			// String of current input value
			inputval = '',
			// Holds the current list
			currentList = [],
			// 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,
			// Hardcoded value for ul visiblity
			ulOpen = FALSE,
			// Timer for delay
			timeid,
			// Ajax requests holder
			xhr,
			// li element in focus, and its data
			liFocus = -1, liData,
			// Fast referencing for multiple selections
			separator,
			// Index of current input
			inputIndex = 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 && rootjQuery.find( 'ul.' + settings.list )[ 0 ] ?
				rootjQuery.find( 'ul.' + settings.list ).eq( 0 ).bgiframe( settings.bgiframe ) :
				$('<ul/>').appendTo('body').addClass( settings.list ).bgiframe( settings.bgiframe ).hide().data( 'ac-selfmade', TRUE );


		// Start Binding
		$input.data( 'autoComplete', ACData = {
			index: inputIndex,
			hasFocus: FALSE,
			active: TRUE,
			settings: settings,
			initialSettings: $.extend( TRUE, {}, settings )
		});

		// IE catches the enter key only on keypress/keyup, so add a helper
		// to track that event if needed
		if ( $.browser.msie ) {
			$input.bind( 'keypress.autoComplete', function( event ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				if ( event.keyCode === KEY.enter ) {
					var enter = TRUE;

					// See entertracking on main key(press/down) event for explanation
					if ( $li && $li.hasClass( settings.rollover ) ) {
						enter = settings.preventEnterSubmit && ulOpen ? FALSE : TRUE;
						select( event );
					}
					else if ( ulOpen ) { 
						$ul.hide( event );
					}

					return enter;
				}
			});
		}


		// Opera && firefox on Mac use keypress to track holding down of key, 
		// while everybody else uses keydown for same functionality
		$input.bind( keypress ? 'keypress.autoComplete' : 'keydown.autoComplete' , function( event ) {
			// If autoComplete has been disabled, prevent input events
			if ( ! ACData.active ) {
				return TRUE;
			}

			// Track last event and store code for munging
			var key = ( LastEvent = event ).keyCode, enter = FALSE;


			// Tab Key
			if ( key === KEY.tab && ulOpen ) {
				select( event );
			}
			// Enter Key
			else if ( key === KEY.enter ) {
				// When tracking whether to submit the form or not, we have
				// to ensure that the user is actually selecting an element from the drop
				// down list. It no element is selected, then hide the list and track form
				// submission. If an element is selected, then track for submission first, 
				// then hide the list.
				enter = TRUE;
				if ( $li && $li.hasClass( settings.rollover ) ) {
					enter = settings.preventEnterSubmit && ulOpen ? FALSE : TRUE;
					select( event );
				}
				else if ( ulOpen ) { 
					$ul.hide( event );
				}
			}
			// Up Arrow
			else if ( key === KEY.up && ulOpen ) {
				if ( liFocus > 0 ) {
					liFocus--;
					up( event );
				} else {
					liFocus = -1;
					$input.val( inputval );
					$ul.hide( event );
				}
			}
			// Down Arrow
			else if ( key === KEY.down && ulOpen ) {
				if ( liFocus < $elems.length - 1 ) {
					liFocus++;
					down( event );
				}
			}
			// Page Up
			else if ( key === KEY.pageup && ulOpen ) {
				if ( liFocus > 0 ) {
					liFocus -= liPerView;

					if ( liFocus < 0 ) {
						liFocus = 0;
					}

					up( event );
				}
			}
			// Page Down
			else if ( key === KEY.pagedown && ulOpen ) {
				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 ) > -1 ) {
				$ul.html('').hide( event );
				enter = TRUE;
			}
			// 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[ 'keydown_' + ExpandoFlag ] = TRUE;
			return enter;
		})
		.bind({
			'keyup.autoComplete': function( event ) {
				// If autoComplete has been disabled or keyup prevention 
				// flag has be set, prevent input events
				if ( ! ACData.active || LastEvent[ 'keydown_' + ExpandoFlag ] ) {
					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 != KEY.enter ) {

					// Caching key value
					cache.val = settings.inputControl === undefined ? val : 
						settings.inputControl.apply( self, settings.backwardsCompatible ? 
							[ val, key, $ul, event, settings, cache ] :
							[ event, {
								val: val,
								key: key,
								settings: settings,
								cache: cache,
								ul: $ul
							}]
						);

					// Only send request if character length passes
					if ( cache.val.length >= settings.minChars ) {
						sendRequest( event, settings, cache, ( key === KEY.backspace || key === KEY.space ) );
					}
					// Remove list on backspace of small string
					else if ( key == KEY.backspace ) {
						$ul.html('').hide( event );
					}
				}
			},

			'blur.autoComplete': function( event ) {
				// If autoComplete has been disabled or the drop list
				// is still open, prevent input events
				if ( ! ACData.active || ulOpen ) {
					return TRUE;
				}

				// Only push undefined index onto order stack
				// if not already there (in-case multiple blur events occur)
				if ( AutoComplete.order[0] !== undefined ) {
					AutoComplete.order.unshift( undefined );
				}

				// Expose focus
				AutoComplete.hasFocus = FALSE;
				ACData.hasFocus = FALSE;
				liFocus = -1;
				$ul.hide( LastEvent = event );

				// Trigger both the global and element specific blur events
				if ( AutoComplete.blur ) {
					AutoComplete.blur.call( self, event, { settings: settings, cache: cache, ul: $ul } );
				}

				if ( settings.onBlur ) {
					settings.onBlur.apply( self, settings.backwardsCompatible ?
						[ inputval, $ul, event, settings, cache ] : [ event, {
							val: inputval,
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}
			},

			'focus.autoComplete': function( event, flag ) {
				// Prevent inner focus events if caused by autoComplete inner functionality
				// Also, because IE triggers focus AND closes the drop list before form submission,
				// keep the select flag by not reseting the last event
				if ( ! ACData.active || ( ACData.hasFocus && flag === ExpandoFlag ) || LastEvent[ 'enter_' + ExpandoFlag ] ) {
					return TRUE;
				}

				if ( inputIndex !== $ul.data( 'ac-input-index' ) ) {
					$ul.html('').hide( event );
				}

				// 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;
					}
				}
				else if ( AutoComplete.order[0] != inputIndex && AutoComplete.order[1] != inputIndex ) {
					AutoComplete.order.unshift( inputIndex );
				}

				if ( AutoComplete.defaults.cacheLimit !== -1 && AutoComplete.order.length > AutoComplete.defaults.cacheLimit ) {
					AutoComplete.order.pop();
				}

				// Expose focus
				AutoComplete.hasFocus = TRUE;
				ACData.hasFocus = TRUE;
				LastEvent = event;

				// Trigger both the global and element specific focus events
				if ( AutoComplete.focus ) {
					AutoComplete.focus.call( self, event, { settings: settings, cache: cache, ul: $ul } );
				}

				if ( settings.onFocus ) {
					settings.onFocus.apply( self, 
						settings.backwardsCompatible ? [ $ul, event, settings, cache ] : [ event, {
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}
			},

			/**
			 * Autocomplete Custom Methods (Extensions off autoComplete event)
			 */ 
			// Catches document click events from the global scope
			'autoComplete.document-click': function( e, event ) {
				if ( ACData.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)
					$( event.target ).closest( 'ul' ).data( 'ac-input-index' ) !== inputIndex ) {
						$ul.hide( LastEvent = event );
						$input.blur();
				}
			},

			// Catches form submission ( so only one event is attached to the form )
			'autoComplete.form-submit': function( e, event, form ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				LastEvent = event;

				// Because IE triggers focus AND closes the drop list before form submission,
				// tracking enter is set on the keydown event
				return settings.preventEnterSubmit && ( ulOpen || LastEvent[ 'enter_' + ExpandoFlag ] ) ? FALSE : 
					settings.onSubmit === undefined ? TRUE : 
					settings.onSubmit.call( self, event, { form: form, settings: settings, cache: cache, ul: $ul } );
			},

			// Catch mouseovers on the drop down element
			'autoComplete.ul-mouseenter': function( e, event, li ) {
				if ( $li ) {
					$li.removeClass( settings.rollover );
				}

				$li = $( li ).addClass( settings.rollover );
				liFocus = $elems.index( li );
				liData = currentList[ liFocus ];
				view = $ul.scrollTop() + ulHeight;
				LastEvent = event;

				if ( settings.onRollover ) {
					settings.onRollover.apply( self, settings.backwardsCompatible ? 
						[ liData, $li, $ul, event, settings, cache ] : 
						[ event, {
							data: liData,
							li: $li,
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}
			},

			// Catch click events on the drop down
			'autoComplete.ul-click': function( e, event ) {
				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger( 'focus', [ ExpandoFlag ] );

				// 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( LastEvent = event );
				autoFill();

				if ( settings.onSelect ) {
					settings.onSelect.apply( self, settings.backwardsCompatible ? 
						[ liData, $li, $ul, event, settings, cache ] :
						[ event, {
							data: liData,
							li: $li,
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}
			},

			// Allow for change of settings at any point
			'autoComplete.settings': function( event, newSettings ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				var ret, $el;
				LastEvent = event;

				// Give access to current settings and cache
				if ( $.isFunction( newSettings ) ) {
					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 ) {
						$.extend( TRUE, settings, ret[0] || settings );
						$.extend( TRUE, cache, ret[1] || cache );
					}
				} else {
					$.extend( TRUE, settings, newSettings || {} );
				}

				// Change the drop down if dev want's a differen't class attached
				$ul = ! settings.newList && $ul.hasClass( settings.list ) ? $ul : 
					! settings.newList && ( $el = rootjQuery.find( 'ul.' + settings.list ).eq( 0 ) ).length ? 
						$el.bgiframe( settings.bgiframe ) :
						$('<ul/>').appendTo('body').addClass( settings.list )
							.bgiframe( settings.bgiframe ).hide().data( 'ac-selfmade', TRUE );

				// Custom drop list modifications
				newUl();

				// Change case here so it doesn't have to be done on every request
				settings.requestType = settings.requestType.toUpperCase();

				// Local copy of the seperator for faster referencing
				separator = settings.multiple ? settings.multipleSeparator : undefined;

				// Just to be sure, reset the settings object into the data storage
				ACData.settings = settings;
			},

			// Clears the Cache & requests (requests can be blocked from clearing)
			'autoComplete.flush': function( event, cacheOnly ) {
				if ( ! ACData.active ) {
					return TRUE;
				}
				
				if ( ! cacheOnly ) {
					requests = 0;
				}

				cache = { length: 0, val: undefined, list: {} };
				LastEvent = event;
			},

			// External button trigger for ajax requests
			'autoComplete.button-ajax': function( event, postData, cacheName ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				if ( typeof postData === 'string' ) {
					cacheName = postData;
					postData = {};
				}

				// Save off the last event before triggering focus on the off-chance
				// it is needed by a secondary focus event
				LastEvent = event;

				// Refocus the input box, but pass flag to prevent inner focus events
				$input.trigger( 'focus', [ ExpandoFlag ] );

				// If no cache name is given, supply a non-common word
				cache.val = cacheName || 'button-ajax_' + ExpandoFlag;

				return sendRequest(
					event, 
					$.extend( TRUE, {}, settings, { maxItems: -1, postData: postData || {} } ),
					cache
				);
			},

			// External button trigger for supplied data
			'autoComplete.button-supply': function( event, data, cacheName ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				if ( typeof data === 'string' ) {
					cacheName = data;
					data = undefined;
				}

				// Again, save off event before triggering focus
				LastEvent = event;

				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger( 'focus', [ ExpandoFlag ] );

				// If no cache name is given, supply a non-common word
				cache.val = cacheName || 'button-supply_' + ExpandoFlag;

				// If no data is supplied, use data in settings
				data = $.isArray( data ) ? data : settings.dataSupply;

				return sendRequest(
					event,
					$.extend( TRUE, {}, settings, { maxItems: -1, dataSupply: data, formatSupply: allSupply } ),
					cache
				);
			},

			// Supply list directly into the result function
			'autoComplete.direct-supply': function( event, data, cacheName ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				if ( typeof data === 'string' ) {
					cacheName = data;
					data = undefined;
				}

				// Again, save off event before triggering focus
				LastEvent = event;

				// Refocus the input box and pass flag to prevent inner focus events
				$input.trigger( 'focus', [ ExpandoFlag ] );

				// If no cache name is given, supply a non-common word
				cache.val = cacheName || 'direct-supply_' + ExpandoFlag;

				// 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 request holdups
				return loadResults(
					event,
					data,
					$.extend( TRUE, {}, settings, { maxItems: -1, dataSupply: data, formatSupply: allSupply } ),
					cache
				);
			},

			// Triggering autocomplete programatically
			'autoComplete.search': function( event, value ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				cache.val = value || '';
				return sendRequest( LastEvent = event, settings, cache );
			},

			// Add jquery-ui like option access
			'autoComplete.option': function( event, name, value ) {
				if ( ! ACData.active ) {
					return TRUE;
				}

				LastEvent = event;
				switch ( Slice.call( arguments ).length ) {
					case 3: 
						settings[ name ] = value;
						return value;
					case 2:
						return name === 'ul' ? $ul :
							name === 'cache' ? cache :
							name === 'xhr' ? xhr :
							name === 'input' ? $input :
							settings[ name ] || undefined;
					default:
						return settings;
				}
			},

			// Add enabling event (only applicable after disable)
			'autoComplete.enable': function( event ) {
				ACData.active = TRUE;
				LastEvent = event;
			},

			// Add disable event
			'autoComplete.disable': function( event ) {
				ACData.active = FALSE;
				$ul.html('').hide( LastEvent = event );
			},

			// Add a destruction function
			'autoComplete.destroy': function( event ) {
				var list = $ul.html('').hide( LastEvent = event ).data( 'ac-inputs' ) || {}, i;

				// Remove all autoComplete specific data and events
				$input.removeData( 'autoComplete' ).unbind( '.autoComplete autoComplete' );

				// Remove form/drop list/document event catchers if possible
				teardown( $input, inputIndex );

				// Remove input from the drop down element of inputs
				list[ inputIndex ] = undefined;

				// Go through the drop down element and see if any other inputs are attached to it
				for ( i in list ) {
					if ( list.hasOwnProperty( i ) && list[ i ] === TRUE ) {
						return LastEvent;
					}
				}

				// Remove the element from the DOM if self created
				if ( $ul.data( 'ac-selfmade' ) === TRUE ) {
					$ul.remove();
				}
				// Kill all data associated with autoComplete for a cleaned drop down element
				else {
					$ul.removeData( 'autoComplete' ).removeData( 'ac-input-index' ).removeData( 'ac-inputs' );
				}
			}
		});

		// Ajax/Cache Request
		function sendRequest( event, settings, cache, backSpace, timeout ) {
			// Merely setting max requests still allows usage of cache and supplied data,
			// this 'Deep' option prevents those scenarios if needed
			if ( settings.maxRequestsDeep === true && requests >= settings.maxRequests ) {
				return FALSE;
			}

			if ( settings.spinner ) {
				settings.spinner.call( self, event, { active: TRUE, settings: settings, cache: cache, ul: $ul } );
			}

			if ( timeid ) {
				timeid = clearTimeout( timeid );
			}

			// Call send request again with timeout flag if on delay
			if ( settings.delay > 0 && timeout === undefined ) {
				timeid = window.setTimeout(function(){
					sendRequest( event, settings, cache, backSpace, TRUE );
				}, settings.delay );
				return timeid;
			}

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

			// Load from cache if possible
			if ( settings.useCache && $.isArray( 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, settings: settings, cache: cache, ul: $ul } );
				}

				if ( settings.onMaxRequest && requests === settings.maxRequests ) {
					return settings.onMaxRequest.apply( self, settings.backwardsCompatible ? 
						[ cache.val, $ul, event, inputval, settings, cache ] : 
						[ event, {
							search: cache.val,
							val: inputval,
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}
				
				return FALSE;
			}

			settings.postData[ settings.postVar ] = cache.val;
			xhr = $.ajax({
				type: settings.requestType,
				url: settings.ajax,
				cache: settings.ajaxCache,
				dataType: 'json',

				// Send personalised data
				data: settings.postFormat ?
					settings.postFormat.call( self, event, {
						data: settings.postData,
						search: cache.val,
						val: inputval,
						settings: settings,
						cache: cache,
						ul: $ul
					}) :
					settings.postData,

				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, settings: settings, cache: cache, ul: $ul } );
					}
				}
			});

			return xhr;
		}

		// Parse User Supplied Data
		function userSuppliedData( event, settings, cache, backSpace ) {
			var list = [], args = [],
				fn = $.isFunction( settings.dataFn ),
				regex = fn ? undefined : new RegExp( '^'+cache.val, 'i' ),
				items = 0, entry, i = -1, l = settings.dataSupply.length;

			if ( settings.formatSupply ) {
				list = settings.formatSupply.call( self, event, {
					search: cache.val,
					supply: settings.dataSupply,
					settings: settings,
					cache: cache,
					ul: $ul
				});
			} else {
				for ( ; ++i < l ; ) {
					// Force object wrapper for entry
					entry = settings.dataSupply[i];
					entry = entry && typeof entry.value === 'string' ? 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, settings, cache ] :
						[ event, {
							search: cache.val,
							entry: entry.value,
							list: list,
							i: i,
							supply: settings.dataSupply,
							settings: settings,
							cache: cache,
							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 && ++items > 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, settings, cache ] :
						[ event, {
							data: liData,
							li: $li,
							settings: settings,
							cache: cache,
							ul: $ul
						}]
					);
				}

				autoFill();
				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[ 'enter_' + ExpandoFlag ] = TRUE;
				}

				$ul.hide( event );
			}

			$li = undefined;
		}

		// Key direction up
		function up( event ) {
			if ( $li ) {
				$li.removeClass( settings.rollover );
			}

			$ul.show( event );
			$li = $elems.eq( liFocus ).addClass( settings.rollover );
			liData = currentList[ liFocus ];

			if ( ! $li.length || ! liData ) {
				return FALSE;
			}

			autoFill( liData.value );
			if ( settings.onRollover ) {
				settings.onRollover.apply( self, settings.backwardsCompatible ? 
					[ liData, $li, $ul, event, settings, cache ] :
					[ event, {
						data: liData,
						li: $li,
						settings: settings,
						cache: cache,
						ul: $ul
					}]
				);
			}

			// Scrolling
			var scroll = liFocus * liHeight;
			if ( scroll < view - ulHeight ) {
				view = scroll + ulHeight;
				$ul.scrollTop( scroll );
			}
		}

		// Key direction down
		function down( event ) {
			if ( $li ) {
				$li.removeClass( settings.rollover );
			}

			$ul.show( event );
			$li = $elems.eq( liFocus ).addClass( settings.rollover );
			liData = currentList[ liFocus ];

			if ( ! $li.length || ! liData ) {
				return FALSE;
			}

			autoFill( liData.value );

			// Scrolling
			var scroll = ( liFocus + 1 ) * liHeight;
			if ( scroll > view ) {
				$ul.scrollTop( ( view = scroll ) - ulHeight );
			}

			if ( settings.onRollover ) {
				settings.onRollover.apply( self, settings.backwardsCompatible ? 
					[ liData, $li, $ul, event, settings, cache ] : [ event, {
						data: liData,
						li: $li,
						settings: settings,
						cache: cache,
						ul: $ul
					}]
				);
			}
		}

		// Attach new show/hide functionality to only the ul object (so not to infect all of jQuery),
		// And also attach event handlers if not already done so
		function newUl() {
			var hide = $ul.hide, show = $ul.show, list = $ul.data( 'ac-inputs' ) || {};

			if ( ! $ul[ ExpandoFlag ] ) {
				$ul.hide = function( event, speed, callback ) {
					if ( settings.onHide && ulOpen ) {
						settings.onHide.call( self, event, { ul: $ul, settings: settings, cache: cache } );
					}

					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, settings: settings, cache: cache } );
					}

					ulOpen = TRUE;
					return show.call( $ul, speed, callback );
				};

				// A flag must be attached to the $ul cached object
				$ul[ ExpandoFlag ] = TRUE;
			}

			// Attach global handlers for event delegation (So there is no more loss time in unbinding and rebinding)
			if ( $ul.data( 'autoComplete' ) !== TRUE ) {
				$ul.data( 'autoComplete', TRUE )
				.delegate( 'li', 'mouseenter.autoComplete', function( event ) {
					AutoComplete.getFocus( TRUE ).trigger( 'autoComplete.ul-mouseenter', [ event, this ] );
				})
				.bind( 'click.autoComplete', function( event ) {
					AutoComplete.getFocus( TRUE ).trigger( 'autoComplete.ul-click', [ event ] );
					return FALSE;
				});
			}

			list[ inputIndex ] = TRUE;
			$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 ) {
			var start, end, range;

			// Set starting and ending points based on values
			if ( val === undefined || val === '' ) {
				start = end = $input.val().length;
			} else {
				if ( separator ) {
					val = inputval.substr( 0, inputval.length - inputval.split( separator ).pop().length ) + val + separator;
				}

				start = inputval.length;
				end = val.length;
				$input.val( val );
			}

			// Create selection if allowed
			if ( ! settings.autoFill || start > end ) {
				return FALSE;
			}
			else if ( self.createTextRange ) {
				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;
			}
		}

		// List Functionality
		function loadResults( event, list, settings, cache, backSpace ) {
			// Allow another level of result handling
			currentList = settings.onLoad ?
				settings.onLoad.call( self, event, { list: list, settings: settings, cache: cache, ul: $ul } ) : list;

			// Tell spinner function to stop if set
			if ( settings.spinner ) {
				settings.spinner.call( self, event, { active: FALSE, settings: settings, cache: cache, ul: $ul } );
			}

			// Store results into the cache if allowed
			if ( settings.useCache && ! $.isArray( cache.list[ cache.val ] ) ) {
				cache.length++;
				cache.list[ cache.val ] = list;

				// Clear cache if necessary
				if ( settings.cacheLimit !== -1 && cache.length > settings.cacheLimit ) {
					cache.list = {};
					cache.length = 0;
				}
			}

			// Ensure there is a list
			if ( ! currentList || currentList.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
				items = 0, i = -1, striped = FALSE, length = currentList.length; // Loop Items

			if ( settings.onListFormat ) {
				settings.onListFormat.call( self, event, { list: currentList, settings: settings, cache: cache, ul: $ul } );
			}
			else {
				// Push items onto container
				for ( ; ++i < length; ) {
					if ( currentList[i].value ) {
						if ( settings.maxItems > -1 && ++items > settings.maxItems ) {
							break;
						}

						container.push(
							settings.striped && striped ? '<li class="' + settings.striped + '">' : '<li>',
							currentList[i].display || currentList[i].value,
							'</li>'
						);

						striped = ! striped;
					}
				}
				$ul.html( container.join('') );
			}

			// Cache the list items
			$elems = $ul.children( 'li' );

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

			// Align the drop down element
			$ul.data( 'ac-input-index', inputIndex ).scrollTop( 0 ).css({
				top: offset.top + $input.outerHeight(),
				left: offset.left,
				width: settings.width
			})
			// The drop list has to be shown before maxHeight can be configured
			.show( event );

			// Log li height for less computation
			liHeight = $elems.eq( 0 ).outerHeight();

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

			// ulHeight gets manipulated, so assign to viewport seperately 
			// so referencing conflicts don't override viewport
			ulHeight = $ul.outerHeight();
			view = ulHeight;

			// Number of elements per viewport
			liPerView = liHeight === 0 ? 0 : Math.floor( view / liHeight );

			// Include amount of time it took to load the list
			// and run modifications
			LastEvent.timeStamp = ( new Date() ).getTime();
		}

		// Custom modifications to the drop down element
		newUl();

		// Do case change on initialization so it's not run on every request
		settings.requestType = settings.requestType.toUpperCase();

		// Local quick copy of the seperator (so we don't have to run this check every time)
		separator = settings.multiple ? settings.multipleSeparator : undefined;

		// Expose copies of both the input element and the cached jQuery version
		AutoComplete.stack[ inputIndex ] = self;
		AutoComplete.jqStack[ inputIndex ] = $input;

		// Form and Document event attachment
		setup( $input, inputIndex );
	};

})( jQuery, window || this );

Download

Latest: auto-complete-5.1.zip
Released: April 16, 2010
-Full cross browser support implemented (includes IE 6-7, FF, Chrome, Opera, & Safari)
-Single events are now attached to the document, drop-list, and forms(when found)
-Better formatting callbacks for more control over functionality
-Speed improvements and many new options and callbacks

Past Release's:
November 22, 2009: auto-complete-5.0.zip
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

Have a question or comment? ask@codenothing.com.