Hello! As I promised, I’ll be explaining all the JavaScript code of the auto-complete field in this post. If you haven’t read the previous post where I talk about the auto-complete field take a moment to read it first before continuing.

Now take some cookies because the post is quite long. Be comfortable and let’s start!

First lets define how our auto-complete field should work. We want an input field that will present suggestions obtained by Ajax in a box below it when we type some text. Then we should be able to navigate through the suggestions by using the UP and DOWN arrow keys and select one by pressing ENTER/RETURN. Additionally we should be able to select a suggestion by clicking on it with the mouse. After selecting a suggestion, the text of the input field should be updated and the suggestion box should disappear. But if we hit the ESC key instead, the suggestion box should disappear without updating the value of the input field.

Now we can start coding. But before we define some global variables. We’ll discuss about them at the appropriated time.

var acListTotal   =  0;
var acListCurrent = -1;
var acDelay       = 500;
var acURL         = null;
var acSearchId	  = null;
var acResultsId	  = null;
var acSearchField = null;
var acResultsDiv  = null;

Now we define the function that is responsible for configuring the auto-complete field, setAutoComplete. This function expects 3 parameters: the id of the input field, the id of the results div and the URL of the remote script.

function setAutoComplete(field_id, results_id, get_url){

Here we initialize some variables. We concatenate the ‘#’ char with the id of the field to build the id string.

	// initialize vars
	acSearchId  = "#" + field_id;
	acResultsId = "#" + results_id;
	acURL 		= get_url;

Now we automatically create the div that will hold the returned data (the suggestions). We use the jQuery append() method to append the HTML code to the body tag. If you are just learning jQuery $(”body”) is the way to select the body element (or any other element) and play with it.

	// create the results div
	$("body").append('<div id="' + results_id + '"></div>');

Now we use jQuery again to select the elements to use later in the script.

	// register mostly used vars (jQuery object)
	acSearchField	= $(acSearchId);
	acResultsDiv	= $(acResultsId);

After creating the results div we now reposition it.

	// reposition div
	repositionResultsDiv();

Time to define some listeners now. First we add a blur() listener to the input field. This event will be triggered when the field loses its focus. It loses its focus when we click anywhere outside the field. For the event we define an anonymous function that uses setTimeout to call clearAutoComplete with a delay of 200ms. The clearAutoComplete function, as we will see, hides the suggestion box. The purpose of the delay is because we want the user to be able to click on a suggestion and only then hide the suggestion box. Without the delay a click on a suggestion would trigger the blur event hiding the box before the click event. So we set a delay long enough for catching the click before hiding.

	// on blur listener
	acSearchField.blur(function(){ setTimeout("clearAutoComplete()", 200) });

Now we add the keyup() listener to the field. This event is triggered when some key is pressed and released. Here is the place to check which key was pressed and act accordingly.

	// on key up listener
	acSearchField.keyup(function (e) {

To get the code of the key we use the keyCode property of the event object passed as the argument or window.event for Internet Explorer. We also store the present value of the input field in the lastVal variable using the val() method. The reason why we store the present value will be explained ahead.

		// get keyCode (window.event is for IE)
		var keyCode = e.keyCode || window.event.keyCode;
		var lastVal = acSearchField.val();

Now we call a function to check if the user pressed the UP or the DOWN arrow key so we can create the selection functionality. We put the code in a function just to be more organized. The function returns TRUE if one of the keys was pressed and then we can return.

		// check an treat up and down arrows
		if(updownArrow(keyCode)){
			return;
		}

If the pressed key was neither an UP nor a DOWN we check if it was an ENTER or an ESC key. If true we call the clearAutoComplete function an return.

		// check for an ENTER or ESC
		if(keyCode == 13 || keyCode == 27){
			clearAutoComplete();
			return;
		}

Finally if no “action” key was pressed we call the autoComplete function with a delay passing the present value of the input as a parameter. Now you might ask why we don’t simply call autoComplete without delay. We could, but that would generate an unnecessary overhead in the server because we would call the remote script for each pressed key and it’s not optimal. If the user already have an idea of what he wants and types some text quickly we should only check the complete text and not each step until there. So we add a delay before calling the autoComplete function and inside the function we will check if the text is the same as when it was called acDelay ms ago. If they are the same the user stopped typing and we can call the remote script.

		// if is text, call with delay
		setTimeout(function () {autoComplete(lastVal)}, acDelay);
	});
}

Let’s go through the autoComplete function now. This function is responsible for calling the remote script, processing the returned data and adding it to the suggestion box.

// treat the auto-complete action
function autoComplete(lastValue)
{

We get the current value of the field and make some validation. If it’s empty we call clearAutoComplete and return because there is nothing to suggest.

	// get the field value
	var part = acSearchField.val();
 
	// if it's empty clear the resuts box and return
	if(part == ''){
		clearAutoComplete();
		return;
	}

As explained before, this function is called with a delay so it only gets activated when the user stops typing. To check that we compare the current value of the input field with the value it had when this function was called acDelay ms ago. If both are equal the user stopped typing and we continue, but if they are different we return stopping the request.

	// if it's equal the value from the time of the call, allow
	if(lastValue != part){
		return;
	}

Now we use the getJSON function to make a request to the remote script that will return data as JSON. This jQuery function then passes us the data as native JavaScript type. The getJSON function takes an URL and a function to process the response as its parameters.

	// get remote data as JSON
	$.getJSON(acURL + part, function(json){

Our remote script returns an array of data, so we check its length to discover how many results we have. If there are results we populate the suggestion box, if not, we call clearAutoComplete to hide the box. The total of results is stored in the global variable acListTotal to be used later.

		// get the total of results
		var ansLength = acListTotal = json.length;
 
		// if there are results populate the results div
		if(ansLength > 0){
 
			var newData = '';

For each result we create a div pair containing the suggestion value and the class unselected.

			// create a div for each result
			for(i=0; i < ansLength; i++) {
				newData += '<div class="unselected">' + json[i] + '</div>';
			}

Now we insert the generated HTML code inside the results div and display it by setting the CSS display property to block. It was previously set to none in our stylesheet file.

			// update the results div
			acResultsDiv.html(newData);
			acResultsDiv.css("display","block");

Here we register mouse events for each suggestion so we can style them on mouse over and select them by mouse clicking. First we get all suggestion divs by selecting the results div children $(acResultsId + ” > div”)

			// for all divs in results
			var divs = $(acResultsId + " > div");

Now we register an anonymous function for the the mouseover() event. Inside this function we set the className property of each div as unselected by running the jQuery each() method and then we set the className of the current div (the one with the mouse over it) as selected. This way the div under the mouse pointer gets a selected state and all others an unselected state.

			// on mouse over clean previous selected and set a new one
			divs.mouseover( function() {
				divs.each(function(){ this.className = "unselected"; });
				this.className = "selected";
			})

We also register an anonymous function for the click() event. In this function we set the value of the input field as the value of the first child node of the clicked element. The clicked element is a suggestion div and its child node is the suggestion text itself. It’s pure DOM.

			// on click copy the result text to the search field and hide
			divs.click( function() {
				acSearchField.val(this.childNodes[0].nodeValue);
				clearAutoComplete();
			});

And we call clearAutoComplete if there are no results.

		} else {
			clearAutoComplete();
		}
	});
}

The clearAutoComplete function is pretty simple because of jQuery. First we remove all the HTML code from inside the results div and then we change the CSS display property to none which hides the suggestion box.

// clear auto complete box
function clearAutoComplete()
{
	acResultsDiv.html('');
	acResultsDiv.css("display","none");
}

The function repositionResultsDiv is responsible for positioning the div that contains the results according to the position of the input field.

// reposition the results div accordingly to the search field
function repositionResultsDiv()
{

Here we get the field’s offset and then the top and left position. The offset() method is provided by the jQuery Dimensions plug-in.

	// get the field position
	var sf_pos    = acSearchField.offset();
	var sf_top    = sf_pos.top;
	var sf_left   = sf_pos.left;

This time we get the height and width of the input field using methods provided by the jQuery itself. The Dimensions plug-in extends these functions for the document and window element.

	// get the field size
	var sf_height = acSearchField.height();
	var sf_width  = acSearchField.width();

Now we define the CSS styles of the suggestion box in such a way it shows up right below the input field. You may need to tweak these values to get an optimal result. First we set its position to absolute. Then we set the left position the same as the input field minus 2px (tweak). The top position is the top of the input field plus its height plus a little gap of 5px. Finally we set the width the same as the input field minus 2px (tweak again).

	// apply the css styles - optimized for Firefox
	acResultsDiv.css("position","absolute");
	acResultsDiv.css("left", sf_left - 2);
	acResultsDiv.css("top", sf_top + sf_height + 5);
	acResultsDiv.css("width", sf_width - 2);
}

The last function updownArrow is the one that controls the behavior of the arrow keys. It receives the keyCode as an argument and check if it’s either the UP(38) or the DOWN(40) arrow key. It uses the global variable acListCurrent to track the index of the current selected suggestion in the results div. The index starts with 0. The acListCurrent is initialized with -1 in the beginning of the script.

// treat up and down key strokes defining the next selected element
function updownArrow(keyCode) {
	if(keyCode == 40 || keyCode == 38){

So if the UP key was pressed it first checks the current position. If it’s 0 (the first suggestion) or if it’s -1 (the initial value) it changes the index to the last element (acListTotal-1). For any other value it just decrements the value of acListCurrent.

		if(keyCode == 38){ // keyUp
			if(acListCurrent == 0 || acListCurrent == -1){
				acListCurrent = acListTotal-1;
			}else{
				acListCurrent--;
			}

If the DOWN key was pressed it first checks if the current index is the index of the last element. If so it changes the index to the first position. Otherwise it increments the current value, walking down the list.

		} else { // keyDown
			if(acListCurrent == acListTotal-1){
				acListCurrent = 0;
			}else {
				acListCurrent++;
			}
		}

After the index of the next element was defined, we need to set the styles accordingly. For each suggestion (each child of the results div) we call an anonymous function passing its index as the parameter.

		// loop through each result div applying the correct style
		acResultsDiv.children().each(function(i){

Then we check if the index of the element is the same as the acListCurrent value. If it is, we change the value of the input field to the value of the element and set the element class to selected. If it’s not, we set the element class to unselected.

			if(i == acListCurrent){
				acSearchField.val(this.childNodes[0].nodeValue);
				this.className = "selected";
			} else {
				this.className = "unselected";
			}
		});

Finally we return true to indicate that one of the arrows was pressed. If none of the arrows was pressed we reset the acListCurrent value to -1 and return false.

		return true;
	} else {
		// reset
		acListCurrent = -1;
		return false;
	}
}

Great! We finally reach the end! I hope it was useful somehow. This code can still be improved and I’ll improve it in the near future. In the mean time, if you have any doubt or any suggestion, please post a comment.

Files:
Auto-Complete (all files)
autocomplete.js