Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

380CHAPTER 10

Type-ahead suggest

If no special key was pressed, then we check to see if we should hit the server to obtain the values or use the list that we already obtained from the server f. Here again we are using the caching mechanism of this script to limit the postbacks and reduce the load on the server. We can perform a couple of checks to see if we have to get new results. The first check is to determine whether or not the last active textbox is the textbox that currently has the focus. The next check is to make sure that the text the user typed into the textbox is the same as last time with only an addition at the end. If there are no results or our result set has 15 elements or less, then we need to check the server for data. The last check is to make sure that the current value’s length is greater than the last value. If any of these checks pass, then we need to obtain new data from the server. We set the objLastActive with the current textbox. We then set a boolean saying that a request has been sent so we do not perform multiple requests, and we call our function TypeAhead() to grab the values.

Then we set the current string in the textbox to the last-known string g. We’ll use that value again to see if we need to request data from the server on the next keystroke. This brings us to accessing the server to obtain the data.

10.3.3Accessing the server

The XMLHttpRequest object allows us to transfer the text from the textbox to the server and to receive the results from the server. In this case, we are posting the data to the server since the server-side page we created in listing 10.1 is referencing the elements submitted in a form. We must specify in our ContentLoader the location of the page on the server, the function to call when it is completed, and the form parameters to be submitted to the form, as shown in listing 10.11.

Listing 10.11 Ajax functionality to send data back to the server

function TypeAhead(xStrText){

var strParams = "q=" + xStrText +

"&where=" + theTextBox.obj.matchAnywhere; var loader1 = new net.ContentLoader(

theTextBox.obj.serverCode,

BuildChoices,null,"POST",strParams);

}

When we called the function TypeAhead() from the function GiveOptions(), we passed the current string value from the textbox to perform the search. We need to build the parameter string, strParams, that contains our textbox string value

The client-side framework

381

 

 

and also the matchAnywhere boolean value. Both of these were used in listing 10.1 to develop the results. Then we start to load the document by calling the ContentLoader. We are sending the URL of the server-side page and the JavaScript function to call when the results are returned as the first two parameters in the ContentLoader. The third parameter is null since we want to ignore any error messages. Ignoring the errors allows the type-ahead to act like a normal text field. The last two properties inform the ContentLoader to post the data to the server and send the form parameters contained in the string strParams.

When the results are returned from the server, the function BuildChoices() is called to allow us to finish the processing of the data on the client. When we developed the server-side code, we returned the results as a two-dimensional JavaScript array. This array contained the option’s text-value pairs for the choices. However, in the response, it is just a string of characters. We need to take this returned string and make it accessible as a JavaScript array. Listing 10.12 contains the functionality that executes the information returned from our ContentLoader using the eval() method.

Listing 10.12 Transforming the responseText property to a JavaScript array

function BuildChoices(){

var strText = this.req.responseText; eval(strText); BuildList(strLastValue); bMadeRequest = false;

}

The responseText property of the returned request object lets us obtain the text from the Ajax request. To allow this returned string to be used by our JavaScript code, we need to use the eval() method. The eval() method evaluates the string contained within its parentheses. In this case, it recognizes that the string is a variable declaration to make a new array. It processes the array so that we can access it. If we were just to write the string to the page, it would not be accessible to the JavaScript statement. Developers frown on using the eval() method since it is known to be slow. However, in this case we are eliminating the need to loop through an XML document on the client to obtain the values. Now we can call the function BuildList() to format and display the returned results. We also want to set our boolean bMadeRequest to false to inform the rest of the script that the request to the server is complete.

382CHAPTER 10

Type-ahead suggest

Building the results span

The use of JavaScript to manipulate the current document is normally considered to be DHTML. In this example, we are taking a two-dimensional array and turning it into lines of text on the screen. Looking back at figure 10.4, we see a list of words that have a portion of their text underlined. The underlined text is the text that matches what the user entered. We are going to display those words in the span element.

The BuildList() function that we create in listing 10.13 utilizes a series of three functions. The functions include finding the matched words, setting the position of the span, and formatting the results with the underline.

Listing 10.13 Formatting the results into a displayable format

function BuildList(theText){

Set element position

SetElementPosition(theTextBox);

 

 

 

var theMatches = MakeMatches(theText);

 

 

Format matches

 

 

theMatches = theMatches.join().replace(/\,/gi,"");

if(theMatches.length > 0){

 

 

document.getElementById("spanOutput")

 

 

.innerHTML = theMatches;

 

 

document.getElementById(

Show

"OptionsList_0").className =

results

"spanHighElement";

 

 

currentValueSelected = 0;

 

 

bNoResults = false;

 

 

}

 

 

else{

 

 

 

 

currentValueSelected = -1;

 

 

bNoResults = true;

 

 

if(theTextBox.obj.showNoMatchMessage)

 

 

document.getElementById(

 

 

"spanOutput").innerHTML =

Show no

"<span class='noMatchData'>" +

matches

theTextBox.obj

 

 

.noMatchingDataMessage +

 

 

"</span>";

 

 

else HideTheBox();

 

 

}

 

 

}

 

 

 

 

 

 

 

 

The function BuildList() in listing 10.13 takes the string the user entered and formats the results. The first thing we need to do is dynamically position the span element directly under the textbox from which the type-ahead is being implemented. To do this, we call the function SetElementPosition() (which we develop

The client-side framework

383

 

 

in the section “Dynamically setting an element’s position”). After we position the span element, we can start to manipulate the array to find the matches by using the MakeMatches() function (see the section “Using regular expressions”). This function returns an array that contains only the information that matches the user’s input. We are using JavaScript to limit the results on the client rather than requiring the processing to be done on the server like most of the type-ahead applications available online.

The MakeMatches() function formats the results and returns them as an array. We then turn this array into a string by using the join method. If the length of the string is greater than 0, then we can display the results in a span by setting its innerHTML property. Then we select the first element in the list and set its className so it is highlighted.

If the returned array contains no data, then we display our “no matches” message if the textbox allows it. We make sure that we set the currentSelectedValue to -1 so we know there are no matches. If no message is to be displayed, then we just hide the box.

We’ve finished the BuildList() function, so now we have to create all the functions that it calls. The first one we’ll tackle is SetElementPosition().

Dynamically setting an element’s position

The input textbox is positioned on the page by the browser's layout engine. When we construct the DHTML drop-down suggest, we want to place it exactly in line with the textbox. One of our most difficult tasks is finding the position of a nonpositioned element, in this case the textbox, so that we can compute the dropdown’s coordinates. A nonpositioned element is one that is relatively set on the page without specifying the absolute left and top positions. If we reference the left and top positions for our textbox, we’ll get an undefined string returned. Therefore, we need to use some JavaScript to determine the position of our element so that our box of choices lines up directly underneath it. In listing 10.14, we are dynamically positioning the span element to line up under our textbox.

Listing 10.14 Dynamically finding the position of a nonpositioned element

function SetElementPosition(theTextBoxInt){

var selectedPosX

=

0;

var selectedPosY

=

0;

var theElement =

theTextBoxInt;

if (!theElement)

return;

var theElemHeight = theElement.offsetHeight; var theElemWidth = theElement.offsetWidth;

384CHAPTER 10

Type-ahead suggest

while(theElement != null){

selectedPosX += theElement.offsetLeft; selectedPosY += theElement.offsetTop; theElement = theElement.offsetParent;

}

xPosElement = document.getElementById("spanOutput"); xPosElement.style.left = selectedPosX; if(theTextBoxInt.obj.matchTextBoxWidth)

xPosElement.style.width = theElemWidth; xPosElement.style.top = selectedPosY + theElemHeight xPosElement.style.display = "block"; if(theTextBoxInt.obj.useTimeout){

xPosElement.onmouseout = StartTimeout;

xPosElement.onmouseover = EraseTimeout;

}

else{

xPosElement.onmouseout = null; xPosElement.onmouseover = null;

}

}

In listing 10.14, we declare our function SetElementPosition(), which accepts one parameter: the textbox object reference. Two local variables, selectedPosX and selectedPosY, are set to 0. These two integers are used to calculate the position of the element. The textbox reference is set into another local variable. The textbox’s width and height are obtained by referencing the offsetHeight and offsetWidth properties.

A while loop is used to loop through the document tree. The document tree allows us to obtain the X and Y positions of the element relative to its parent. By looping through each positioned parent, we can find the exact position of the element by adding the offset position to the two local variables that we created.

Once we obtain the position of the textbox, we can retrieve the span’s object reference, which is used to set the left and top positions of the drop-down suggest element. We then look at the textbox’s obj object that we created to see if its width property is supposed to match the width of the textbox. If the boolean is true, then we set the width of the span. If the boolean is false, the width comes from the value specified in the stylesheet. The last step is to change the visibility of the span so it is not hidden from the user’s view any more. We do this by setting the display property to block.

Now that our span is adequately positioned and visible to the user, we can develop the code that fills in the selectable option’s content.

The client-side framework

385

 

 

Using regular expressions

Since we are going to be searching for string segments, regular expressions are one of the best ways to find matches with added flexibility. The MakeMatches() function that we create next allows us to find the words in the options list that match the user’s text in the textbox. This means we can avoid a trip to the server after every keystroke, since the function narrows the choices for us. The code in listing 10.15 lets us save bandwidth by limiting our result set.

Listing 10.15 Using regular expressions to limit results

var countForId = 0;

function MakeMatches(xCompareStr){

countForId = 0;

var matchArray = new Array();

var regExp = new RegExp(theTextBox.obj.regExAny + xCompareStr,theTextBox.obj.regExFlags);

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

var theMatch = arrOptions[i][0].match(regExp); if(theMatch){

matchArray[matchArray.length]=

CreateUnderline(arrOptions[i][0],

xCompareStr,i);

}

}

return matchArray;

}

We create the function MakeMatches(), which accepts one parameter: the string the user entered. We then reset the variable countForId to 0 and create a local array variable matchArray. (Note that countForId is a global variable. That keeps the example simple for now. We’ll do away with it in the refactoring section later!) The key to this function is creating a regular expression that finds the options that match the user’s input. Since we have already determined the parameters for the regular expression when we created the code in listing 10.6, we just need to reference our textbox’s object. We add the property reqExAny, which allows us to match at the beginning of or anywhere in the string. The regExFlags property lets us determine whether to ignore the case when performing the matches.

With the regular expression completed, we loop through the array arrOptions to verify that the options in the array match our regular expression. If they match, then we add the text to our array matchArray after we call the function

CreateUnderline(). CreateUnderline() formats the code to be displayed.

386CHAPTER 10

Type-ahead suggest

After we loop through all the elements in our array, we return the matched options to the main function BuildList(), where the matches are displayed to the user. MakeMatches() provides the caching mechanism that we talked about earlier. Instead of returning to the server to limit the search for every keystroke, regular expressions allow us to limit the available options to the user. The CreateUnderline() function is the last step in formatting the results.

Manipulating strings

The final step for formatting the strings so that the user can view and interact with them is to manipulate the string so that it is contained within a span tag, has an underline under the matching text, and has the onclick event handler attached to it so we can select it with the mouse. This section uses regular expressions again to build the formatted string, as you can see in listing 10.16.

Listing 10.16 Performing string manipulation with JavaScript

var undeStart = "<span class='spanMatchText'>"; var undeEnd = "</span>";

var selectSpanStart = "<span style='width:100%;display:block;' class='spanNormalElement' onmouseover='SetHighColor(this)' ";

var selectSpanEnd ="</span>";

function CreateUnderline(xStr,xTextMatch,xVal){ selectSpanMid = "onclick='SetText(" + xVal + ")'" +

"id='OptionsList_" +

countForId + "' theArrayNumber='"+ xVal +"'>"; var regExp = new RegExp(theTextBox.obj.regExAny +

xTextMatch,theTextBox.obj.regExFlags); var aStart = xStr.search(regExp);

var matchedText = xStr.substring(aStart,

aStart + xTextMatch.length);

countForId++;

return selectSpanStart + selectSpanMid + xStr.replace(regExp,undeStart + matchedText + undeEnd) + selectSpanEnd;

}

In listing 10.16, we define two variables to hold strings that are used to insert a CSS class around the portion of text that matches the string. This allows us to style the text easily. The first variable, undeStart, holds our start span tag, while the second variable, undeEnd, holds the closing span tag.

The client-side framework

387

 

 

The next two variables form the container for the entire string. This container lets us manipulate the background color and determine whether the cell is clicked on. You can see that in the variable selectSpanStart, we added a mouseover to highlight the cell. The selectSpanEnd variable is just the closing tag for the span.

Our function CreateUnderline() is called by the MakeMatches() function that we just coded. MakeMatches() passes in three parameters: the string the user entered, the option’s text, and the option’s value. With the passed-in data, we can develop the onclick handler and add an ID for the span. The onclick handler allows us to select the option, and the ID allows us to use DOM to select the option from the list.

We use a regular expression again to match the text typed by the user. This is so that we can insert the underline spans we created in the string. The search method is used to determine where the match is located in the string. After we find the location of the string, we can obtain the substring so that we can keep the original formatting. Our counter countForId is incremented, and we return our formatted string by joining together all the span elements that we created. The returned text is now formatted, but we still need to finish the CSS classes we added to the span elements.

The span elements were assigned CSS class names, so we do not have to manually go into the JavaScript code to change certain properties of the text. This allows us to fit the autocomplete textbox into any color scheme by simply changing these few CSS rules:

span.spanMatchText{ text-decoration: underline; font-weight: bold; }

span.spanNormalElement{ background: #C0C0C0; } span.spanHighElement{ background: #000040;

color: white; cursor: pointer; }

span.noMatchData{ font-weight: bold; color: #0000FF; }

Remember that in figure 10.4 the matching text was bold and underlined. You can see those two properties listed in the CSS rule span.spanMatchText. The span default style is represented with span.spanNormalElement, which contains a gray background color. The selected item is applied the CSS rule span.spanHighElement. By looking back at that figure you can see that the background color is dark gray and the text color is white. The cursor is also changed to a pointer, so the user knows she can select that option with the mouse. We can add more properties to any of the elements, such as fonts, sizes, borders, and so on. Now that we

388CHAPTER 10

Type-ahead suggest

have built the stylesheet rules, we have finished working with outputting the results. All that is left is handling the Enter and arrow keys and creating our timer (which hides the options in case of inactivity).

Highlighting the options

Earlier in the chapter, we captured the keypresses of the Up and Down Arrow keys so that the user could move the selectedIndex up or down without having to use her mouse. The arrow keys send us either 1 (to move down the selection) or –1 (to move up the selection). When we move a selection, we apply CSS classes to the span elements. We are also adjusting the global variable currentValueSelected so that it holds our current index. The MoveHighlight() function in listing 10.17 gives us a richer user interface since it interacts with both the mouse and the keyboard.

Listing 10.17 Changing the CSS class names of elements with JavaScript

function MoveHighlight(xDir){ if(currentValueSelected >= 0){

newValue = parseInt(currentValueSelected) + parseInt(xDir); if(newValue > -1 && newValue < countForId){

currentValueSelected = newValue; SetHighColor (null);

}

}

}

function SetHighColor(theTextBox){ if(theTextBox){

currentValueSelected = theTextBox.id.slice(theTextBox.id.indexOf("_")+1, theTextBox.id.length);

}

for(i = 0; i < countForId; i++){ document.getElementById('OptionsList_' + i).className = 'spanNormalElement';

}

document.getElementById('OptionsList_' + currentValueSelected).className = 'spanHighElement';

}

The MoveHighlight() function enables the user to use the Up and Down Arrow keys to make a selection. The function accepts one parameter, xDir, symbolizing the direction in which the highlight should be moved. The first check verifies that we have options to select. If there are options, we can obtain the new value. We

The client-side framework

389

 

 

verify that the new value is within the range of the selection. If it is, we set currentValueSelected and proceed to the next function, SetHighColor(), to highlight the new selection.

SetHighColor() is called from two different events: the arrow keys and the onmouseover event handler. This function is called to remove the highlight from the last selected option and add it to the new option that has been chosen. The onmouseover event in listing 10.16 passes in the object of the span; therefore, we need to obtain the index number of the span by ripping apart the ID. The arrow keys pass this value, so we are not required to perform this action since the moveHighlight() function already set currentValueSelected.

We loop through all of the span tags and set their CSS class to spanNormalElement. This resets their appearance to their nonselected state. After the looping is completed, we add the CSS class to the selected option. With the two functions that we just created, we have given the user the ability to select an option with either the mouse or the keyboard. All that is left is to take this selected value and add it to the textbox.

Setting the selected value

The purpose of the type-ahead suggest is to allow the users to select available options to limit the amount of effort required to fill in a form field. In order to do this, we need to take the index of the item that the user selected and set the text to the textbox and the value to the hidden text field. These three functions in listing 10.18 allow our span element to act like an HTML select element.

Listing 10.18 Handling the arrow keys and mouse click events

function SetText(xVal){

theTextBox.value = arrOptions[xVal][0]; //set text value theTextBox.obj.hidden.value = arrOptions[xVal][1]; document.getElementById("spanOutput").style.display = "none"; currentValueSelected = -1; //remove the selected index

}

function GrabHighlighted(){ if(currentValueSelected >= 0){

xVal = document.getElementById("OptionsList_" + currentValueSelected).getAttribute("theArrayNumber");

SetText(xVal);

HideTheBox();

}

}

390CHAPTER 10

Type-ahead suggest

function HideTheBox(){ document.getElementById("spanOutput").style.display = "none"; currentValueSelected = -1;

EraseTimeout();

}

The function that allows us to obtain the text and value of the selected item is GrabHighlighted(). First we need to see if the user has selected a value. If a value is selected, then we obtain the index number of the arrOptions array in which the text resides. To do this, we grab the value from the attribute, theArrayNumber, that we set earlier. Then, we call the function SetText() to set the selected option’s text and value into their respective form elements.

SetText() uses the index value passed in as a parameter to index the array arrOptions. The visible text the user sees is set by indexing the first index of the array. The hidden form element receives the second index value stored in our array. After we retrieve the values, we remove the option list from the screen by calling our function HideTheBox().

HideTheBox() allows us to remove the span, spanOutput, from the view. To do this, we reference the span and set its style.display property to none. We remove the selected index by setting the variable currentValueSelected to –1. Any timers that we may have set are removed by calling EraseTimeout(), which we develop next.

Using JavaScript timers

This is the final JavaScript section before the type-ahead project is complete, so your brain may be hurting from all of this client-side code. The JavaScript’s setTimeout() method executes a statement after an elapsed time has passed. The elapsed time is specified in milliseconds, which we added to the object we created back in listing 10.6. The reason for using a timer is to hide the selection span if there is an inactive timeout period. If we set the parameter in our object useTimeout to true, then this function will be called. The timer in listing 10.19 gives us one more feature for a rich user interface.

Listing 10.19 Attaching and removing timing events

function EraseTimeout(){ clearTimeout(isTiming); isTiming = false;

}

function StartTimeout(){

The client-side framework

391

 

 

isTiming = setTimeout("HideTheBox()", theTextBox.obj.theVisibleTime);

}

The function StartTimeout() sets the timer when the function is executed. We initialize the timer by setting the variable isTiming to the setTimeout method. The setTimeout method should call the function HideTheBox() after the set time span, indicated by theVisibleTime.

The only other thing we have to do is to remove the timeout. To cancel it, we create the EraseTimeout() function that uses JavaScript’s built-in clearTimeout() function for preventing HideTheBox() from firing. We set our boolean isTiming to false.

Upon finishing that last line of code, we can now run the type-ahead suggest project! Save the project, open it, and start typing in a word. Figure 10.5 shows the progression of the type-ahead suggest. The first letter, s, returned more than 15 options. The second letter, h, reduced the list to five options. The third letter, o, reduced the list to one, which we selected by pressing the Enter key. By adding this project to any form, you can increase the efficiency of your users so they do not have to type in entire words.

Figure 10.5 The progression of the type-ahead project