Make Life Easy With Autocomplete Textboxes
Let’s face it: people really don’t enjoy filling out forms, especially when values need to be typed in. That’s why applications like Microsoft Outlook incorporate autocomplete textboxes — textboxes that examine the first few characters a user has typed, and suggest a word from a given list. Web browsers also work this way when they automatically present a list of URLs as you begin to type a Web address into the address bar.
In this tutorial, with a little bit of JavaScript trickery, we’ll create the same type of behavior in both Internet Explorer (v5.5 and higher) and Mozilla (v1.0 and higher).
Simple Browser Detection
First, we’ll need to do a little browser detection, so here’s some simple code to accomplish that (though of course, you may use your own instead):
var isOpera = navigator.userAgent.indexOf("Opera") > -1;
var isIE = navigator.userAgent.indexOf("MSIE") > 1 && !isOpera;
var isMoz = navigator.userAgent.indexOf("Mozilla/5.") == 0 && !isOpera;
This code is obviously not very robust, but it’s sound enough for our purposes; let’s get to the guts of this project.
Selecting the Textbox
The first step in this process is to create a method that will select certain amounts of text in a textbox. We’ll call this method textboxSelect()
, and it will take three parameters. The first parameter is the textbox we want the method to act on; the second parameter, which is optional, is the position in which the selection should begin (if this parameter is omitted, then the entire textbox is selected); the third parameter, also optional, is the position at which the selection should end. If the second parameter is provided but there is no third parameter, the textbox is selected from the start position to the end of the text in the textbox.
We will address the easiest case first: if only one parameter is provided, then we should use the textbox’s native select()
method to select all the text in the textbox:
function textboxSelect(oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
...
}
}
Note that we use the switch statement to determine how many arguments have been filled in. If there’s only 1, that means only oTextbox
was provided. Next, we’re going to skip ahead to the case where there are three arguments (with both iStart
and iEnd
specified). Here, we’ll need to use a browser detect to determine what to do. For Internet Explorer, we’ll make use of a text range.
function textboxSelect (oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
case 3:
if (isIE) {
var oRange = oTextbox.createTextRange();
oRange.moveStart("character", iStart);
oRange.moveEnd("character", -oTextbox.value.length + iEnd);
oRange.select();
} else if (isMoz) {
...
}
}
}
In the bold code, we start by creating a text range for the textbox object. The range then has to be set to its starting and ending coordinates. To move the start of the range, we use the moveStart()
method. This method takes two parameters: the type of space to move (“character”), and how many of those spaces to move. The moveEnd()
method on the next line has the same parameters. The difference here is that moveEnd()
requires the second parameter to be a negative number (think of it as moving the end of the selection back one space, back two spaces, etc.).
In order to get this parameter, we take the negative value of the length of the text in the textbox and add to it the iEnd value. So, if iEnd
is 8 and the textbox contains 10 characters, the second parameter becomes –2, and the end of the range is moved back two characters. Lastly, we call the select()
method to highlight the range in the textbox.
Accomplishing the same thing for Mozilla is actually very easy. Textboxes have a setSelectionRange()
method that takes two parameters: the start and end of the selection. We can pass in iStart
and iEnd
directly:
function textboxSelect (oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
case 3:
if (isIE) {
var oRange = oTextbox.createTextRange();
oRange.moveStart("character", iStart);
oRange.moveEnd("character", -oTextbox.value.length + iEnd);
oRange.select();
} else if (isMoz) {
oTextbox.setSelectionRange(iStart, iEnd);
}
}
}
Now we’ll take a step back and look at the case when two parameters are given (iEnd
is not specified). Essentially, we want to do what we would if there were three arguments, the only difference being that iEnd
must be equal to the number of characters in the textbox. We can accomplish this like so:
function textboxSelect (oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
/3b#/case 2:
iEnd = oTextbox.value.length;
/* falls through */
case 3:
if (isIE) {
var oRange = oTextbox.createTextRange();
oRange.moveStart("character", iStart);
oRange.moveEnd("character", -oTextbox.value.length + iEnd);
oRange.select();
} else if (isMoz){
oTextbox.setSelectionRange(iStart, iEnd);
}
}
}
Note that in this case, we do not use the break statement. We want the execution to follow through into the next case, as we’ve set iEnd to an appropriate value. This will now work correctly in all three cases.
The very last part of this function is to set the focus to the textbox so that when a user types, it will replace the selected text:
function textboxSelect (oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
case 2:
iEnd = oTextbox.value.length;
/* falls through */
case 3:
if (isIE) {
var oRange = oTextbox.createTextRange();
oRange.moveStart("character", iStart);
oRange.moveEnd("character", -oTextbox.value.length + iEnd);
oRange.select();
} else if (isMoz){
oTextbox.setSelectionRange(iStart, iEnd);
}
}
oTextbox.focus();
}
Replacing the Textbox Selection
Another task we’ll need to be able to do is to replace the currently selected text with some other text. For this, we will create the textboxReplaceSelect()
method, which will take two parameters: the textbox to act on, and the text to insert. Because we will use ranges, we’ll need to create different code for IE and Mozilla. First, let’s take a look at the IE code:
function textboxReplaceSelect (oTextbox, sText) {
if (isIE) {
var oRange = document.selection.createRange();
oRange.text = sText;
oRange.collapse(true);
oRange.select();
} else if (isMoz) {
...
}
oTextbox.focus();
}
On line 4, we create a range from the document’s current selection (which we can safely assume is within the textbox, as this will be called on the keypress event of the textbox). On the following line, we replace the range’s text with the given string. On line 6, we call the range’s collapse()
method, which sets the length of the range to 0; the parameter, a Boolean, collapses to the end of the range if the value is true, and collapses to the beginning of the range if the value is false. Finally, we select the collapsed range, which places the cursor after the text we just inserted.
In Mozilla, we can achieve the same effect with some simple string manipulation. We can determine the start and end points of the selection by using the Mozilla selectionStart and selectionEnd attributes of a textbox:
function textboxReplaceSelect (oTextbox, sText) {
if (isIE) {
var oRange = document.selection.createRange();
oRange.text = sText;
oRange.collapse(true);
oRange.select();
} else if (isMoz) {
var iStart = oTextbox.selectionStart;
oTextbox.value = oTextbox.value.substring(0, iStart) + sText + oTextbox.value.substring(oTextbox.selectionEnd, oTextbox.value.length);
oTextbox.setSelectionRange(iStart + sText.length, iStart + sText.length);
}
oTextbox.focus();
}
On line 9, we are just saving the starting point of the textbox selection; we’ll need it later. Line 10 looks complicated, but in reality it just takes the string prior to the selection and the string after the selection, and adds sText between them. After that, we just need to replicate what we did in IE: set the cursor to appear after the text that was just inserted. So, on line 11, we set the selection range to be the old starting point plus the length of the text that was inserted. This effectively copies what we did for IE.
Matching
The next step in the process is to write a method that will search an array of strings and return the first value that begins with a given string (for example, we need to pass in “a” and have it return the first value in the array that begins with the letter “a”). This method will be called autocompleteMatch() and will take two parameters: the text to match and the array of values to match against.
The search is straightforward — we just iterate through the array and check each value. If the method returns true, then we return the value. If no matches are found, a null value is returned. Here’s the full code:
function autocompleteMatch (sText, arrValues) {
for (var i=0; i < arrValues.length; i++) {
if (arrValues[i].indexOf(sText) == 0) {
return arrValues[i];
}
}
return null;
}
Note that for this to work correctly, the strings in arrValues
must be in alphabetical order.
The Guts
With our other methods built, we can move into the guts of the autocomplete textbox: the autocomplete()
method. This method will take three parameters: the textbox to act on, the event object, and an array of possible values. Assuming that we have an array called arrValues
, the call would look like this:
<input type="text" onkeypress="return autocomplete(this, event, arrValues)" />
We will be looking at the keycode of the pressed key first:
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
...
}
}
Note that we still format the event object using our EventUtil.formatEvent()
method.
There are several keys that we don’t want to interfere with, such as the cursor keys; we’ll just return true in these special cases:
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
...
}
}
Next, we’ll take the default case: when the user types a character. In this case we need to complete a number of steps:
- We must replace the selected text with the character the user typed
- We must try to get a match to the text the user has typed in
- If found, we must set the textbox to the suggested text and set the selection to encompass only the letters that the user did not type
This may sound complicated, but the helper functions we’ve already written will make it much easier.
The important first step is to ascertain the character the user typed (using String.fromCharCode()
on the event object’s keyCode
attribute (in IE) and charCode
attribute (in Mozilla)). We use that character to replace the currently selected text. Then, we need to get the length of the text in the textbox.
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
default:
textboxReplaceSelect(oTextbox, String.fromCharCode(isIE ? oEvent.keyCode : oEvent.charCode);
var iLen = oTextbox.value.length;
...
}
}
Next, we need to search for a matching value in the array of values using our autocompleteMatch()
method.
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
default:
textboxReplaceSelect(oTextbox, String.fromCharCode(isIE ? oEvent.keyCode : oEvent.charCode);
var iLen = oTextbox.value.length;
var sMatch = autocompleteMatch(oTextbox.value, arrValues);
...
}
}
After we request a matching value, we need to determine if a match has indeed been found. To do this, we test sMatch
to see if it’s null. If it is not null, we need to replace the text in the textbox with sMatch. Then we’ll use iLen
(the length of the text actually typed by the user) to select only the text that the user hasn’t typed with the help of our textboxSelect()
method.
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
default:
textboxReplaceSelect(oTextbox, String.fromCharCode(isIE ? oEvent.keyCode : oEvent.charCode);
var iLen = oTextbox.value.length;
var sMatch = autocompleteMatch(oTextbox.value, arrValues);
if (sMatch != null) {
oTextbox.value = sMatch;
textboxSelect(oTextbox, iLen, oTextbox.value.length);
}
...
}
}
After all this, the last thing we need to do is return the value of false to the event handler. If we don’t do this, the typed character will appear twice.
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
default:
textboxReplaceSelect(oTextbox, String.fromCharCode(isIE ? oEvent.keyCode : oEvent.charCode);
var iLen = oTextbox.value.length;
var sMatch = autocompleteMatch(oTextbox.value, arrValues);
if (sMatch != null) {
oTextbox.value = sMatch;
textboxSelect(oTextbox, iLen, oTextbox.value.length);
}
return false;
}
}
Example
The final step, of course, is to make an example that tests the code. Without further ado, here it is:
<html>
<head>
<title>Autocomplete Textbox Example</title>
<script type="text/javascript">
var isOpera = navigator.userAgent.indexOf("Opera") > -1;
var isIE = navigator.userAgent.indexOf("MSIE") > 1 && !isOpera;
var isMoz = navigator.userAgent.indexOf("Mozilla/5.") == 0 && !isOpera;
function textboxSelect (oTextbox, iStart, iEnd) {
switch(arguments.length) {
case 1:
oTextbox.select();
break;
case 2:
iEnd = oTextbox.value.length;
/* falls through */
case 3:
if (isIE) {
var oRange = oTextbox.createTextRange();
oRange.moveStart("character", iStart);
oRange.moveEnd("character", -oTextbox.value.length + iEnd);
oRange.select();
} else if (isMoz){
oTextbox.setSelectionRange(iStart, iEnd);
}
}
oTextbox.focus();
}
function textboxReplaceSelect (oTextbox, sText) {
if (isIE) {
var oRange = document.selection.createRange();
oRange.text = sText;
oRange.collapse(true);
oRange.select();
} else if (isMoz) {
var iStart = oTextbox.selectionStart;
oTextbox.value = oTextbox.value.substring(0, iStart) + sText + oTextbox.value.substring(oTextbox.selectionEnd, oTextbox.value.length);
oTextbox.setSelectionRange(iStart + sText.length, iStart + sText.length);
}
oTextbox.focus();
}
function autocompleteMatch (sText, arrValues) {
for (var i=0; i < arrValues.length; i++) {
if (arrValues[i].indexOf(sText) == 0) {
return arrValues[i];
}
}
return null;
}
function autocomplete(oTextbox, oEvent, arrValues) {
switch (oEvent.keyCode) {
case 38: //up arrow
case 40: //down arrow
case 37: //left arrow
case 39: //right arrow
case 33: //page up
case 34: //page down
case 36: //home
case 35: //end
case 13: //enter
case 9: //tab
case 27: //esc
case 16: //shift
case 17: //ctrl
case 18: //alt
case 20: //caps lock
case 8: //backspace
case 46: //delete
return true;
break;
default:
textboxReplaceSelect(oTextbox, String.fromCharCode(isIE ? oEvent.keyCode : oEvent.charCode);
var iLen = oTextbox.value.length;
var sMatch = autocompleteMatch(oTextbox.value, arrValues);
if (sMatch != null) {
oTextbox.value = sMatch;
textboxSelect(oTextbox, iLen, oTextbox.value.length);
}
return false;
}
}
</script>
<script>
var arrValues = ["red", "orange", "yellow", "green", "blue", "indigo", "violet", "brown"];
</script>
</head>
<body>
<h2>Autocomplete Textbox Example</h2>
<p>Type in a color in lowercase:<br />
<input type="text" value="" id="txt1" onkeypress="return autocomplete(this, event, arrValues)" /></p>
</body>
</html>
Wrap-up
Users like to be able to type values in instead of using the mouse, and an autocomplete textbox will make them very happy.
The only limitation to this code is that it is case sensitive, though the autocompleteMatch()
function can easily be changed to make it case insensitive (I’ll leave you to figure out exactly how!). This code will not work in the Netscape Navigator 4.x family, or on Opera. Its functionality is unknown (but assumed not to work) in other browsers.