Previous Section Table of Contents Next Section

Hack 43 Autocomplete Text Fields

figs/moderate.gif figs/hack43.gif

Add autocomplete functionality to make Flash complete the data entry for your fill-in text forms.

Most of us don't want to type any more than we have to. For certain users-such as children, disabled users, or those using mobile devices with cumbersome keypads-saving keystrokes is a priority. One way to save time is to fill in commonly used words before the user finishes typing them (i.e., an autocomplete feature).

The 300 Most Common Words (http://www.zingman.com/commonWords.html) and alt-usage-english.org's Commonest Words (http://alt-usage-english.org/excerpts/fxcommon.html) purport to list the 300 most common words in written English. Other word lists include The American Heritage Word Frequency Book by John B. Carroll, Peter Davies, and Barry Richman (Houghton Mifflin).

When I first stumbled on those resources, I thought, "Hmm...that's a good start for creating an autocomplete text engine." After removing all words that are of two or fewer letters (and therefore don't save much time by being autocompleted) and sorting them alphabetically, Figure 6-2 shows the results.

Figure 6-2. The most common words in written English with at least three letters
figs/flhk_0602.gif


For those interested in stripping words from another word list they might encounter, here is the code I used:

function textLoader(data) {

        // Split the text file out based on any spaces

        dictionary = data.split(" ");

        // Sort the word list

        dictionary.sort( );

        // Display the sorted word list, separated by spaces

        trace(dictionary.join(" "));

}

var myText:LoadVars = new LoadVars( );

var dictionary:Array = new Array( );

myText.load("commonWords.txt");

myText.onData = textLoader;

This assumes the words are in a file called commonWords.txt and that they are separated by spaces. You would change the filename and delimiter (such as a Return character) in accordance with the name and format of your word list.

My stripped-down word list takes up only 1.5 KB, and you can reduce that to 990 bytes-a bargain!-by copying the output from the preceding script into the ActionScript listing itself, like this (although the full version looks a bit messier, as shown shortly):

var myText:String = "about above across ... years you young your";

var dictionary:Array = new Array( );

dictionary = myText.split(" ");

dictionary.sort( );

delete (myText);

When you publish the SWF, Flash compresses the dictionary definition text.

To use the dictionary, I found a rather ingenious hack for adding the autocomplete text at the Stickman site (http://www.the-stickman.com).

It uses two text fields, one on top of the other. The top one is an input field, and the lower one is a dynamic field that displays our autocomplete suggestions. To create this arrangement, assuming you already have a layer named actions (to the first frame of which is attached the previous code listing), add two new layers named entered text and completed text, as shown in Figure 6-3.

Figure 6-3. Setting up your layers for the autocomplete text hack
figs/flhk_0603.gif


In the entered text layer, create a multiline, borderless input text field, named myText_txt, as shown in Figure 6-4.

Figure 6-4. Setting up the input text field in the entered text layer
figs/flhk_0604.gif


Next, copy the text field, lock the entered text layer, and paste the text field in place into layer completed text (Ctrl-Shift-V on Windows or figs/command.gif-Shift-V on Mac). Using the Properties panel, change the text field to a bordered dynamic text field, and change the text color to something other than the original text field's color (this will be the color of your autocomplete text). Change the instance name to complete_txt in the Properties panel, as shown in Figure 6-5.

Figure 6-5. Changing the text properties in the Properties panel
figs/flhk_0605.gif


The Code

Finally, change the script attached to the actions layer to the following, which contains both the code and the preinitialized dictionary of words:

function autoComplete( ) {

  // Function to accept the currently suggested autocomplete

  // text when the CONTROL key is pressed.

  if (Key.isDown(Key.CONTROL)) {

    myText_txt.text = complete_txt.text + " ";

    Selection.setSelection(myText_txt.text.length, 

                           myText_txt.text.length);

  }

}

function fieldChange( ) {

  // Function to search for and display the 

  // autocomplete text string.

  match = "";

  // Extract the last word in progress in the text input field

  startOfWord = this.text.lastIndexOf(" ") + 1;

  lastWord = this.text.substring(startOfWord, this.text.length);

  // Search for any matches in the dictionary to the last

  // word in progress.

  if (lastWord.length > 1) {

    for (var i = 0; i < dictionary.length; i++) {

      if (lastWord == (dictionary[i].substr(0, lastWord.length))) {

        // If match found, store the match string in variable match

        match = dictionary[i];

        search = i;

        break;

      }

    }

  } else {

    search = 0;

  }

  // Display the current match in the autocomplete text field

  complete_txt.text = this.text.substr(0, startOfWord) + match;

}



// Initialize

var myText:String = "about above across after again against air all almost 

along also always and animals another answer any are around asked away 

back because been before began being below best better between big both 

boy boys but bye called came can change children city come cool could 

country day days did different does don't down during each earth email end 

enough even ever every example eyes far father feet few find first five 

following food for form found four from get give going good goodbye got 

great had hand hard has have head hear heard hello help her here high him 

himself his home house how however html important into it's its just keep 

kind knew know land large last learn left let life light like line little 

live long look looked made make man many may means men might miles more 

most mother much must name near need never new next night no not now 

number off often old once one only other others our out over own page 

paper part parts people picture place play point put read right room said 

same saw say school sea second see sentence set several she should show 

side since small some something sometimes soon sound still story study 

such sun sure take tell than that the their them then there these they 

thing things think this those thought three through time times today 

together told too took top toward try turned two under until use used 

using usually very want was water way ways web website well went were what 

when where which while white who whole why will with without word words 

work world would write year years you young your";



var dictionary:Array = new Array( );

var search:Number = 0;

var lastWord:String = "";

var startOfWord:String = "";

var control:Object = new Object( );

// Set up dictionary

dictionary = myText.split(" ");

dictionary.sort( );

// Set up events and listeners

myText_txt.onChanged = fieldChange;

control.onKeyDown = autoComplete;

Key.addListener(control);

The main part of this script is the fieldChange( ) function, which runs whenever the user enters something in the text field.

First, it clears a variable, match, which holds our autocomplete guess. Next, it extracts the last complete word typed by the user. We do this by looking for the last incidence of a space and counting from the next character to the end of the text.

For example, in the sentence, "The cat sat," the last space is the one before "sat" (and its zero-relative index is 7). Adding 1 to it gives us the position of the first character of the word "sat." Using this information (startOfWord), we can extract the substring "sat" from the sentence, remembering that the last letter of "sat" corresponds to the end of our sentence so far. Here we repeat the relevant code:

function fieldChange( ) {

  match = "";

  startOfWord = this.text.lastIndexOf(" ") + 1;

  lastWord = this.text.substring(startOfWord, this.text.length);

It's not worth autocompleting until the current word is at least two letters long, so we use an if statement in the next section of code to avoid performing the check when only one letter has been entered. If the user has entered at least two characters, the for loop compares our word with each entry in the dictionary until it finds a match.

The variable search is used to optimize future searches. Because we know that the dictionary is sorted alphabetically, we know that any future matches for the current word will be after the currently found word in the dictionary. Therefore, remembering the position of the current word allows us to continue searching from the same position later:

if (lastWord.length > 1) {

  for (var i = 0; i < dictionary.length; i++) {

    if (lastWord == (dictionary[i].substr(0, lastWord.length))) {

      // Match found

      match = dictionary[i];

      search = i;

      break;

    }

  }

Finally, if we haven't yet attempted a search, it is because the current word is too short, so we reset the search position, because when the word is long enough, we want to start searching the dictionary from the beginning.

} else {

  search = 0;

}

The code is set up to allow the user to accept the current autocomplete suggestion by pressing the Ctrl key (we discuss alternatives later). When the user presses Ctrl, the autoComplete( ) function is invoked via a listener event. The autoComplete( ) function transfers the current contents of the autocomplete text into the input field, effectively completing the word for the user.

function autoComplete( ) {

  if (Key.isDown(Key.CONTROL)) {

    myText_txt.text = complete_txt.text + " ";

    Selection.setSelection(myText_txt.text.length, 

      myText_txt.text.length);

  }

}

The last line of the preceding code moves the text cursor to the end of the current line. The trick is to set a zero-length selection to force the cursor to move to the end of the text field.

Figure 6-6 shows autocomplete in action. Entering "He sa" gives you an autocomplete suggestion of "He said." Pressing the Ctrl key adds the suggested text to the current input text. If desired, you can add static help text to inform the user that the Ctrl key initiates the autocomplete feature.

Figure 6-6. Autocomplete in action
figs/flhk_0606.gif


Final Thoughts

This hack shows the basic code needed to create an autocomplete feature. Almost everyone will want to change it, the most likely candidate being to accept the autocomplete text by hitting another key such as the spacebar. You need a bit more code to do that, because although you can check for a spacebar press simply by looking for a " " at the end of the current input text, you need to be sure that the last pressed key was not a backspace. Another potential drawback is if you press the spacebar to go to the next word, already having a complete word, then the autocomplete adds more unwanted text. For example, if you enter "table" and then press the spacebar, the autocomplete feature might change it to "tablecloth." But this is unlikely because the common words in the word list tend to be shorter words.

The other thing that will probably irk some folks is the way I have added a blank space on the end of every autocompleted word. These are both minor details though; the basic principle remains the same even if you change the implementation details.

Using the most common words as the core autocomplete dictionary should also start you well on your way to adding a more useful list. Although the addition of a language dictionary sounds as if it should add a massive download to your SWF, the most common 300 words add less than 1 KB. The top 3,000 words (which approaches the number of words used in normal conversation, ignoring proper nouns and specializations) should consume no more than 10-15 KB! Not as much as you might think, which makes it feasible to teach a speech synthesizer [Hack #52] a vocabulary so that it can attempt reading plain text. You could have it kick out words it doesn't understand and add them to the dictionary [Hack #44] as you see fit.

Here is a list of other enhancements you might make:

  • Trigger autocomplete on the Return or Enter key.

  • Prevent the user from tabbing to the hidden field by setting the tabIndex property for myText_txt but not for complete_txt.

  • Add logic so that autocomplete works even if you click in the middle of an existing text field.

  • Deal with wrapping and scrolling as required by longer text input.

  • Speed up the word search to accommodate longer dictionaries without degrading performance. For example, you can split the dictionary into multiple arrays stored in a hash table in which the key is the first two characters.

-Inspired by Stickman

    Previous Section Table of Contents Next Section