Web DevCenter    
 Published on Web DevCenter (http://www.oreillynet.com/javascript/)
 See this if you're having trouble printing code examples


JavaScript & DHTML Cookbook

Cooking with JavaScript & DHTML

by Danny Goodman, author of JavaScript & DHTML Cookbook
09/03/2003

Editor's note: Here on the Web DevCenter we've published a number of recipes excerpted from Danny JavaScript & DHTML Cookbook. This week Danny is back with another bonus recipe you won't find in his book. Find out what you need to do to let users of IE for Windows type in their select element choices.

Introduction

How many times have you been filling out a web form, gleefully typing away to enter text into text fields, and then you tab to a select element, where you risk tennis elbow once again by sliding your arm over to the mouse to make your selection?

Users of minority browsers, including older Netscape browsers, recent Mozilla-based browsers, and even Internet Explorer for the Macintosh have had it easy: once the select element has focus, they can type in the first few characters of the desired item (like a U.S. state or a country name), and the item matching those characters is immediately highlighted and selected. For whatever reason, this feature has been missing from Internet Explorer for Windows. The first typed character works (and you can continue typing the first character to cycle through all items beginning with that letter), but you can't, say type NO to zip past Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, and New York to reach North Carolina. This recipe fills that gap to give your form fillers a more uniform experience and handy creature comforts across browsers.

Bonus Recipe: Typing select Element Choices in IE for Windows

NN n/a, IE 5

Problem

You want users of Internet Explorer for Windows to be able to type their choices in select elements.

Solution

Implementing the solution starts by adding an onkeydown event handler to each select element you wish to empower with the type-ahead feature. Use the event binding syntax of your choice, either as an attribute of the <select> tag:

<select name="state" id="state" onkeydown="typeAhead()">

or as a property of the element assigned by script:

document.getElementById("state").onkeydown = typeAhead;

The script function is written generically, so that any number of select elements in a form can utilize the same typeAhead() function.

Related Articles:

Super-Efficient Image Rollovers -- Problem: You want to reduce the number of individual image files downloaded to the browser to accomplish three-state image rollovers. Answer: Well, Danny Goodman has it in this bonus recipe you won't even find in his latest book, JavaScript & DHTML Cookbook. Read on for all the skinny.

To operate most effectively, the text of the option elements nested inside the select element should be listed in alphabetical order. Matches will be done on the displayed text of each option, irrespective of the string assigned to the option's value attribute.

Discussion

Simulating the type-ahead feature of select elements found in other browsers requires more than just reading the user's keystrokes. You must also include a timeout so that if no additional keystrokes occur after a heartbeat (or thereabouts), the accumulated string of typed characters is cleared, allowing for a new string to be gathered and compared against nested option element items.

While devising this recipe, I also added a feature available for pop-up lists in some other programs and operating systems: if there is no direct match for the key(s) typed by the user, the next lowest item is selected. For example, if the list is for the states of the United States, no state begins with the letter J. But if you type J, the event handler function selects the item that comes closest to, but is "less than," J in the list -- Iowa in this case.

The JavaScript code entails two pieces: one global object and one function. All code for this recipe is shown below:

// global storage object for type-ahead info, including reset() method
var typeAheadInfo = {last:0, 
                     accumString:"", 
                     delay:500,
                     timeout:null, 
                     reset:function() {this.last=0; this.accumString=""}
                    };

// function invoked by select element's onkeydown event handler
function typeAhead() {
   // limit processing to IE event model supporter; don't trap Ctrl+keys
   if (window.event && !window.event.ctrlKey) {
      // timer for current event
      var now = new Date();
      // process for an empty accumString or an event within [delay] ms of last
      if (typeAheadInfo.accumString == "" || now - typeAheadInfo.last < typeAheadInfo.delay) {
         // make shortcut event object reference
         var evt = window.event;
         // get reference to the select element
         var selectElem = evt.srcElement;
         // get typed character ASCII value
         var charCode = evt.keyCode;
         // get the actual character, converted to uppercase
         var newChar =  String.fromCharCode(charCode).toUpperCase();
         // append new character to accumString storage
         typeAheadInfo.accumString += newChar;
         // grab all select element option objects as an array
         var selectOptions = selectElem.options;
         // prepare local variables for use inside loop
         var txt, nearest;
         // look through all options for a match starting with accumString
         for (var i = 0; i < selectOptions.length; i++) {
            // convert each item's text to uppercase to facilitate comparison
            // (use value property if you want match to be for hidden option value)
            txt = selectOptions[i].text.toUpperCase();
            // record nearest lowest index, if applicable
            nearest = (typeAheadInfo.accumString > 
                       txt.substr(0, typeAheadInfo.accumString.length)) ? i : nearest;
            // process if accumString is at start of option text
            if (txt.indexOf(typeAheadInfo.accumString) == 0) {
               // stop any previous timeout timer
               clearTimeout(typeAheadInfo.timeout);
               // store current event's time in object 
               typeAheadInfo.last = now;
               // reset typeAhead properties in [delay] ms unless cleared beforehand
               typeAheadInfo.timeout = setTimeout("typeAheadInfo.reset()", typeAheadInfo.delay);
               // visibly select the matching item
               selectElem.selectedIndex = i;
               // prevent default event actions and propagation
               evt.cancelBubble = true;
               evt.returnValue = false;
               // exit function
               return false;   
            }            
         }
         // if a next lowest match exists, select it
         if (nearest != null) {
            selectElem.selectedIndex = nearest;
         }
      } else {
         // not a desired event, so clear timeout
         clearTimeout(typeAheadInfo.timeout);
      }
      // reset global object
      typeAheadInfo.reset();
   }
   return true;
}

The global object, named typeAheadInfo, preserves relevant information between keystrokes, such as the index of the next lowest option element (the last property) and the string accumulated from the relatively rapid keystroke sequence (accumString).

Two properties, delay and timeout, concern themselves with the timeout mechanism that determines how long the object should maintain a string of characters as being a contiguous sequence. The delay property is the number of milliseconds that the eventual setTimeout() method uses to trigger an erasure of the accumulated string because too much time has elapsed between keystrokes. You can adjust the amount as needed by your users. The timeout property is simply where the reference to the setTimeout() timer is held for possible cancellation when needed.

A function definition in the object resets two of the object's properties. I've used the anonymous function syntax of JavaScript for the sake of keeping all code associated with this object in one place (rather than defining a separate function).

Related Reading

JavaScript & DHTML Cookbook
Solutions and Example for Web Programmers
By Danny Goodman

Each time a key is pressed in the select element, the typeAhead() function executes once. Using object detection to limit execution to browsers that support IE's proprietary window.event object, the function first timestamps the event (the W3C DOM Event object has a property for this, but the IE event model doesn't), getting the current time from a Date object constructor function. An important if condition determines if the function should process the event under two conditions:

Setting the stage for more detailed processing, the function pulls important information from the event object, namely a reference to the targeted select element and the character code of the key that triggered the event. Because string comparisons later in the script need the actual typed character, the static String object's fromCharCode() method returns the character; but the character is also converted to uppercase because all string comparisons below will be in uppercase (they could also be in all lowercase if you prefer). The character is appended to the accumString property of the global typeAheadInfo object. After a couple more local variable initializations (including one shortcut reference to the array of option elements nested inside the select element), it's time to start looking for matches inside a for loop.

The control factor for this loop is the array of option elements. Each time through the loop, the function grabs a copy of the text property value from one option under test, and converts it to uppercase for the upcoming comparison. Before getting there, however, a statement assigns a value to the nearest variable, depending on the sorting relationship between the accumulated string and the same number of characters at the start of the current option's text. As long as the option's text sorts after the accumulated string, the nearest variable is assigned the value of the i loop counting variable. Once the accumulated string no longer sorts before the option's text, the nearest value stays the same in subsequent trips through the loop. Later, if necessary, this value is used to select the closest match to the accumulated string if there is no complete match.

One final test in the function looks to see if the accumulated string is at the start of the option's text. If it is, any previous timeout timer is cancelled. The time of the current event is stowed in the typeAheadInfo.last property, and a new timer is started. Using the loop counter variable, the matching option's item is selected, and the function exits. The select element may now process another keydown event.

If the accumulated string doesn't match any of the options list, the for loop continues to the end of the options, at which time it selects the nearest match. The typeAhead object is reset, ready for the next series of typed characters from the user.

Open the example page to play with two versions of the same select element (one set as a pop-up menu, the other as a pick list) wired with the typeAhead() function. Click on, or tab to, one of the select elements, and begin typing the first few characters of your favorite U.S. state. This code does not conflict or interfere with the native type-ahead behavior of other browsers.

The code shown above works with the text property of an option element object. For most lists, this is the desired way to go because typical users will be thinking about the text they see in the lists, rather than the perhaps cryptic codes assigned to the option values. But if your form is for specially trained users comfortable with the codes associated with plain-language listings, you could also allow type-ahead to work on the value properties, rather than the text properties. For example, order takers may be very familiar with stock-number codes associated with product names that appear in the option list, and prefer to use those as their type-ahead values. If you can assemble the option elements such that the values are in alphabetical order, you can wire the code to work with the hidden values instead.

At this point in IE's evolution, we don't know if the browser engine predicted for the Microsoft Longhorn operating system will have native type-ahead built into the browser. But in the meantime, you can implement it for your IE5, IE5.5 and IE6 users.

See Also

Recipe 8.13 of JavaScript & DHTML Cookbook about using scripts to change select element content dynamically.


O'Reilly & Associates recently released (April 2003) JavaScript & DHTML Cookbook.

Copyright © 2009 O'Reilly Media, Inc.