In the first part of this series, you learned how to create type ahead functionality in a textbox, which presents the user with a single suggestion for what they've already typed. This article builds upon that functionality by adding a dropdown list of multiple suggestions. To do so, you'll extend the autosuggest control and suggestion provider class definitions.
The dropdown suggestion list is nothing more than an absolutely-positioned
<div/>
containing several other <div/>
s.
It's placed directly under the textbox to create the appearance of a dropdown
menu (see the image below).
The HTML for the suggestion list in the image above looks like this:
<div class="suggestions">
<div class="current">Maine</div>
<div>Maryland</div>
<div>Massachusetts</div>
<div>Michigan</div>
<div>Minnesota</div>
<div>Mississippi</div>
<div>Missouri</div>
<div>Montana</div>
</div>
Of course, some CSS is needed to make the dropdown list function properly.
The outermost <div/>
has a class of "suggestions,"
which is defined as:
div.suggestions {
-moz-box-sizing: border-box;
box-sizing: border-box;
border: 1px solid black;
position: absolute;
}
The first two lines of this CSS class are for browsers that support two forms
of box sizing: content box and border box (for more information, read http://www.quirksmode.org/css/box.html).
In quirks mode, Internet Explorer defaults to border box; in standards mode,
IE defaults to content box. Most other DOM-compliant browsers (Mozilla, Opera,
and Safari) default to content box, meaning that there is a difference in how
the <div/>
will be rendered among browsers. To provide for
this, the first two lines of the CSS class set specific properties to border
box. The first line, -moz-box-sizing
, is Mozilla-specific and used
for older Mozilla browsers; the second line is for browsers that support the
official CSS3 box-sizing
property. Assuming that you use quirks
mode in your page, this class will work just fine (if you use standards mode,
simply remove these first two lines).
For the other two lines of the suggestions CSS class, simply add a border and
specify that the <div/>
is absolutely positioned.
Next, a little bit of formatting is need for the dropdown list items:
div.suggestions div {
cursor: default;
padding: 0px 3px;
}
The first line specifies the default cursor (the arrow) to be displayed when the mouse is over an item in the dropdown list. Without this, the cursor would display as the caret, which is the normal cursor for textboxes and Web pages in general. The user needs to realize that the dropdown item is not a part of the regular page flow and changing the cursor helps define that. The second line simply applies some padding to the item (which you can modify as you wish).
Lastly, some CSS is needed to format the currently selected item in the dropdown list. When an item is selected, the background will be changed to blue and the color will be changed to white. This provides a basic highlight that is typically used in dropdown menus. Here's the code:
div.suggestions div.current {
background-color: #3366cc;
color: white;
}
Now that you understand how the dropdown list will be built, it's time to begin scripting it.
Creating the DOM representation for the dropdown list is a multi-stage process.
First, a property is needed to store the <div/>
because various
methods of the AutoSuggestControl
need access to it. This property
is called layer and is initially set to null
:
function AutoSuggestControl(oTextbox, oProvider) {
this.layer = null;
this.provider = oProvider;
this.textbox = oTextbox;
this.init();
}
The layer
and the other parts of the dropdown list DOM will be
created after you define a few simple methods to help control its behavior.
The simplest method is hideSuggestions()
, which hides the dropdown
list after it has been shown:
AutoSuggestControl.prototype.hideSuggestions = function () {
this.layer.style.visibility = "hidden";
};
Next, a method is needed for highlighting the current suggestion in the dropdown
list. The highlightSuggestion()
method accepts a single argument,
which is the <div/>
element containing the current suggestion.
This method's purpose is to set the <div/>
's class
attribute to "current" on the current suggestion and clear the class
attribute on all others in the list. Doing so provides a highlighting effect
on the dropdown list similar to the regular form controls. The algorithm is
quite simple: iterate through the child nodes of the layer. If the child node
is equal to the node that was passed in, set the class
to "current,"
otherwise clear the class
attribute by setting it to an empty string:
AutoSuggestControl.prototype.highlightSuggestion = function (oSuggestionNode)
{
for (var i=0; i < this.layer.childNodes.length; i++)
{
var oNode = this.layer.childNodes[i];
if (oNode == oSuggestionNode)
{
oNode.className
= "current"
} else if (oNode.className ==
"current") {
oNode.className
= "";
}
}
};
With these two methods defined, it's time to create the dropdown list <div/>
.
The createDropDown()
method creates the outermost <div/>
and defines the event handlers for the dropdown list. To create the <div/>
,
use the createElement()
method and then assign the various styling
properties:
AutoSuggestControl.prototype.createDropDown = function () {
this.layer = document.createElement("div");
this.layer.className = "suggestions";
this.layer.style.visibility = "hidden";
this.layer.style.width = this.textbox.offsetWidth;
document.body.appendChild(this.layer);
//more code to come
};
The code above first creates the <div/>
and assigns it to
the layer
property. From there, the className
(equivalent
to the class
attribute) is set to "suggestions", as is
needed for the CSS to work properly. The next line hides the layer, since it
should be invisible initially. Then, the width of the layer is set equal to
the width of the textbox by using the textbox's offsetWidth
property
(this is optional dependent on your individual needs). The very last line adds
the layer to the document. The next step is to assign the event handlers.
There are three event handlers that need to be assigned: onmouseover
,
onmousedown
and onmouseup
. The onmouseover
event handler is simply used to highlight the current suggestion, the onmousedown
is used to select the given suggestion (place the suggestion in the textbox
and hide the dropdown list) and onmouseup
is used to just to set
focus back to the textbox after a selection has been made. Since all of these
event handlers occur on the textbox, it's best just to use a single function
for all of them, as seen below:
AutoSuggestControl.prototype.createDropDown = function () {
this.layer = document.createElement("div");
this.layer.className = "suggestions";
this.layer.style.visibility = "hidden";
this.layer.style.width = this.textbox.offsetWidth;
document.body.appendChild(this.layer);
var oThis = this;
this.layer.onmousedown = this.layer.onmouseup
=
this.layer.onmouseover = function (oEvent) {
oEvent = oEvent || window.event;
oTarget = oEvent.target || oEvent.srcElement;
if (oEvent.type ==
"mousedown") {
oThis.textbox.value
= oTarget.firstChild.nodeValue;
oThis.hideSuggestions();
} else if (oEvent.type == "mouseover")
{
oThis.highlightSuggestion(oTarget);
} else {
oThis.textbox.focus();
}
};
};
The first part of this section is the assignment of oThis
equal
to the this
object. This is necessary so that a reference to the
AutoSuggestControl
object is accessible from within the event handler.
Next, a compound assignment occurs, assigning the same function as an event
handler for onmousedown
, onmouseup
, and onmouseover
.
Inside of the function, the first two lines are used to account for the different
event models (DOM and IE). The event
object is stored in oEvent
and the target of the event is stored in oTarget
(the target will
always be a <div/>
containing a suggestion).
If the event being handled is mousedown
, then you set the value
of the textbox equal to the text inside of the event target. The text is a text
node, retrieved by the firstNode
property, and the actual text
is stored in the nodeValue
of the text node. After the suggestion
is placed into the textbox, the dropdown list is hidden.
When the event being handled is mouseover
, the event target is
passed into the highlightSuggestion()
method to provide the hover
effect. If the event is mouseup
, then the focus is set back to
the textbox (this fires immediately after mousedown
).
In order to get the full effect of the dropdown list it's imperative that it
appear immediately below the textbox. If the textbox were absolutely positioned,
this wouldn't be much of an issue. In actual practice, textboxes are rarely
absolutely positioned and more often are placed inline, which presents a problem
in aligning the dropdown list. To do so accurately, you must calculate the location
of the textbox using the offsetLeft
, offsetTop
, and
offsetParent
properties.
The offsetLeft
and offsetTop
properties tell you
how many pixels away from the left and top of the offsetParent
an element is placed. The offsetParent
is usually, but not always,
the parent node of the element, so to get the left position of the textbox,
you need to add up the offsetLeft
properties of the textbox and
all of its ancestor elements (stopping at <body/>
), as seen
below:
AutoSuggestControl.prototype.getLeft = function () {
var oNode = this.textbox;
var iLeft = 0;
while(oNode.tagName != "BODY") {
iLeft += oNode.offsetLeft;
oNode = oNode.offsetParent;
}
return iLeft;
};
The getLeft()
method begins by pointing oNode
at
the textbox and defining iLeft
with an initial value of 0. The
while loop will continue to add oNode.offsetLeft
to iLeft
as it traverses up the DOM structure to the <body/>
element.
The same algorithm can be used to get the top of the textbox:
AutoSuggestControl.prototype.getTop = function () {
var oNode = this.textbox;
var iTop = 0;
while(oNode.tagName != "BODY") {
iTop += oNode.offsetTop;
oNode = oNode.offsetParent;
}
return iTop;
};
These two methods will be used to place the dropdown list in the correct location.
The next step in this process is to create a method that adds the suggestions
into the dropdown list and then displays it. The showSuggestions()
method accepts an array of suggestions as an argument and then builds up the
DOM necessary to display them. From there, the method positions the dropdown
list and displays it to the user:
AutoSuggestControl.prototype.showSuggestions = function (aSuggestions)
{
var oDiv = null;
this.layer.innerHTML = "";
for (var i=0; i < aSuggestions.length; i++) {
oDiv = document.createElement("div");
oDiv.appendChild(document.createTextNode(aSuggestions[i]));
this.layer.appendChild(oDiv);
}
this.layer.style.left = this.getLeft() + "px";
this.layer.style.top = (this.getTop()+this.textbox.offsetHeight)
+ "px";
this.layer.style.visibility = "visible";
};
The first line simply defines the variable oDiv
for later use.
The second line clears the contents of the dropdown list by setting the innerHTML
property to an empty string. Then, the for
loop creates a <div/>
and a text node for each suggestion before adding it to the dropdown list layer.
The next section of code starts by setting the left position of the layer using
the getLeft()
method. To set the top position, you need to add
the value from getTop()
to the height of the textbox (retrieved
by using the offsetHeight
property). Lastly, the layer's visibility
is set to "visible" to show it.
Remember the autosuggest()
method from the last
article? To implement the dropdown list of suggestions it's necessary to
update this method.
The first update is the addition of a second argument which indicates whether
or not the type ahead functionality should be used (the reason why will be explained
shortly). Naturally, the typeAhead()
method should only be called
if this argument is true
. If there's at least one suggestion, type
ahead should be used and the dropdown list of suggestion should be displayed
by calling showSuggestions()
and passing in the array of suggestions;
if there's no suggestions, the dropdown list should be hidden by calling hideSuggestions()
:
AutoSuggestControl.prototype.autosuggest = function (aSuggestions,
bTypeAhead) {
if (aSuggestions.length > 0) {
if (bTypeAhead) {
this.typeAhead(aSuggestions[0]);
}
this.showSuggestions(aSuggestions);
} else {
this.hideSuggestions();
}
};
You will also remember that this method is called from the suggestion provider's
requestionSuggestions()
method, which means it too must be updated.
This is a fairly easy update; you need only add a second argument and then pass
it back into the autosuggest()
method when it's called:
StateSuggestions.prototype.requestSuggestions = function (oAutoSuggestControl,
bTypeAhead) {
var aSuggestions = [];
var sTextboxValue = oAutoSuggestControl.textbox.value;
if (sTextboxValue.length > 0){
for (var i=0; i < this.states.length;
i++) {
if (this.states[i].indexOf(sTextboxValue)
== 0) {
aSuggestions.push(this.states[i]);
}
}
}
oAutoSuggestControl.autosuggest(aSuggestions,
bTypeAhead);
};
With both of these methods updated, it's now necessary to update the handleKeyUp()
method. First, just add the second argument (true) when calling requestSuggestions()
:
AutoSuggestControl.prototype.handleKeyUp = function (oEvent) {
var iKeyCode = oEvent.keyCode;
if (iKeyCode < 32 || (iKeyCode >=
33 && iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode
<= 123)) {
//ignore
} else {
this.provider.requestSuggestions(this,
true);
}
};
This functionality now works exactly as it did previously, but there are a
couple of other keys that require special attention: Backspace and Delete. When
either of these keys are pressed, you don't want to activate the type ahead
functionality because it will disrupt the process of removing characters from
the textbox, but there's no reason not to show the dropdown list of suggestions.
For the Backspace (key code of 8) and Delete (key code of 46) keys, you can
also call requestSuggestions()
, but this time, pass in false
to indicate that type ahead should not occur:
AutoSuggestControl.prototype.handleKeyUp = function (oEvent) {
var iKeyCode = oEvent.keyCode;
if (iKeyCode == 8 || iKeyCode == 46) {
this.provider.requestSuggestions(this,
false);
} else if (iKeyCode < 32 || (iKeyCode >= 33
&& iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode <=
123)) {
//ignore
} else {
this.provider.requestSuggestions(this,
true);
}
};
Now when the user is removing characters, suggestions will still be provided and the user can click on one of them to select the value for the textbox. This is acceptable, but to really be usable the autosuggest control needs to respond to keyboard controls.
The desired keyboard functionality revolves around three keys: the up arrow, the down arrow and Enter (or Return). When the dropdown suggestion list is displayed, you should be able to press the down arrow to highlight to the first suggestion, then press it again to move to the second, and so on. The up arrow should then be used to move back up the list of suggestions. As each suggestion is highlighted, the value must be placed in the textbox. When the Enter key is pressed, the suggestions should be hidden, leaving the last suggestion to be highlighted in the textbox.
When the user scrolls through the suggestions in the list, you must know which
suggestion is current. To do this, a property must be added to the AutoSuggestControl
definition as follows:
function AutoSuggestControl(oTextbox, oProvider) {
this.cur = -1;
this.layer = null;
this.provider = oProvider;
this.textbox = oTextbox;
this.init();
}
The cur
property stores the index of the current suggestion in
the suggestions array. By default, this is set to -1 because this there are
no suggestions initially.
When the down arrow key is pressed, the next suggestion in the dropdown list
should be highlighted. To encapsulate this functionality, a method named nextSuggestion()
will be added. Here's the code:
AutoSuggestControl.prototype.nextSuggestion = function () {
var cSuggestionNodes = this.layer.childNodes;
if (cSuggestionNodes.length > 0 && this.cur
< cSuggestionNodes.length-1) {
var oNode = cSuggestionNodes[++this.cur];
this.highlightSuggestion(oNode);
this.textbox.value = oNode.firstChild.nodeValue;
}
};
This method obtains the collection of child nodes in the dropdown layer. Since
only the <div/>
elements containing the suggestions are child
nodes of the layer, the number of child nodes accurately matches the number
of suggestions. This number can be used to determine if there are any suggestions
(in which case it will be greater than 0) and also if there is a next suggestion
(which means that it's greater than cur
). To ensure that cur
never points to an empty node, it must never be allowed to be larger than the
number of child nodes minus 1 (because the last element in a collection with
n elements is n-1).
If it these two tests are passed, then cur
is incremented and
the child node in that position is retrieved and stored in oNode
.
Next, the node is passed in to highlightSuggestion()
, which highlights
it and unhighlights the previously highlighted suggestion. From there, the value
of the textbox is once again set to the text contained inside of the <div/>
.
As you may have suspected, another method to highlight the previous suggestion is also necessary. Here it is:
AutoSuggestControl.prototype.previousSuggestion = function () {
var cSuggestionNodes = this.layer.childNodes;
if (cSuggestionNodes.length > 0 && this.cur
> 0) {
var oNode = cSuggestionNodes[--this.cur];
this.highlightSuggestion(oNode);
this.textbox.value = oNode.firstChild.nodeValue;
}
};
The previousSuggestion()
method is similar to nextSuggestion()
.
The main differences are that you need to ensure cur
is greater
than 0 to proceed (you still must make sure that there are suggestions by checking
the number of child nodes in the dropdown layer) and that cur
must
be decremented instead of incremented. Other than these two changes, the algorithm
is the same. Now back to the three keys.
To handle the up arrow, down arrow, and Enter keys, a handleKeyDown()
method is necessary. Similar to handleKeyUp()
, this method also
requires the event
object to be passed in. Once again, you'll need
to rely on the key code to tell which key was pressed. The key codes for the
up arrow, down arrow, and Enter keys are 38, 40, and 13, respectively. The handleKeyDown()
method is defined as follows:
AutoSuggestControl.prototype.handleKeyDown = function (oEvent) {
switch(oEvent.keyCode) {
case 38: //up arrow
this.previousSuggestion();
break;
case 40: //down arrow
this.nextSuggestion();
break;
case 13: //enter
this.hideSuggestions();
break;
}
};
Remember, when the user presses the up or down arrows, the suggestion is automatically placed into the textbox. This means that when the Enter key is pressed, you need only hide the dropdown list of suggestions.
init()
Now that all of this new functionality has been added, it must be initialized.
Previously, the init()
method was used to set up the onkeyup
event handler, now it must be extended to also set up the onkeydown
and onblur
event handlers, as well as create the dropdown suggestion
list. The onkeydown
event handler is set up in a similar manner
as onkeyup
:
AutoSuggestControl.prototype.init = function () {
var oThis = this;
this.textbox.onkeyup = function (oEvent) {
if (!oEvent) {
oEvent
= window.event;
}
oThis.handleKeyUp(oEvent);
};
this.textbox.onkeydown = function (oEvent) {
if (!oEvent) {
oEvent
= window.event;
}
oThis.handleKeyDown(oEvent);
};
//more code to come
};
As you can see, the same algorithm is used with the onkeydown
event handler: first determine the location of the event
object,
then pass it into the handleKeyDown()
method.
Up to this point, the only way the dropdown list is hidden is when the user
hits the Enter key. But what if the user clicks elsewhere on the screen or uses
the Tab key to switch to a new form field? To prepare for this, you must set
up an onblur
event handler that hides the suggestions whenever
the textbox loses focus:
AutoSuggestControl.prototype.init = function () {
var oThis = this;
this.textbox.onkeyup = function (oEvent) {
if (!oEvent) {
oEvent
= window.event;
}
oThis.handleKeyUp(oEvent);
};
this.textbox.onkeydown = function (oEvent) {
if (!oEvent) {
oEvent
= window.event;
}
oThis.handleKeyDown(oEvent);
};
this.textbox.onblur = function () {
oThis.hideSuggestions();
};
this.createDropDown();
};
You'll also notice that the createDropDown()
method is called
to create the initial dropdown list structure. With the initializations complete,
it's time to test out your creation.
The example for the updates is set up in the same way as Part 1. The only difference is in the code that is used and the inclusion of the style sheet:
<html>
<head>
<title>Autosuggest Example
2</title>
<script type="text/javascript"
src="autosuggest2.js"></script>
<script type="text/javascript"
src="suggestions2.js"></script>
<link rel="stylesheet"
type="text/css" src="autosuggest.css" />
<script type="text/javascript">
window.onload
= function () {
var
oTextbox = new AutoSuggestControl(document.getElementById("txt1"),
new StateSuggestions());
}
</script>
</head>
<body>
<p><input type="text"
id="txt1" /></p>
</body>
</html>
As before, the autosuggest2.js
file contains the AutoSuggestControl
definition and suggestions2.js
contains the StateSuggestions
definition. The creation of the AutoSuggestControl
is handled in
exactly the same way.
You can view the example here (it should work in Internet Explorer 5.5+ and Mozilla 1.0+, including Firefox) or download it.
With the dropdown list and keyboard controls all working properly, there's only one part left to implement: going back to the server to get the suggestions. We'll examine this in the third and final part of this series.
Nicholas C. Zakas is a user interface designer for Web applications and is
the author of Professional
JavaScript for Web Developers (Wiley Press, ISBN 0764579088). Nicholas can
be contacted through his Web site, http://www.nczonline.net/, where he provides
open source JavaScript libraries and tools.