Tuesday, July 14, 2009

AJAX and PHP Building Responsive Web Applications by Cristian Darie, Bogdan Brinzarea Chapter 6

AJAX Suggest and
Autocomplete

Suggest and Autocomplete are popular features implemented in most modern browsers, email clients, source-code editors, word processors, and operating systems. Suggest and Autocomplete are the two sides of the same coin—they go hand in hand. Usually, there is no distinction made between the two of them, but "autocomplete" is used more frequently.

Autocomplete refers to the application's ability to predict the word or phrase the user wants to type. This feature is very useful because it speeds up the interaction making the user interface friendlier, it helps in using the right vocabulary, and it helps avoiding typing errors.

In browsers, you can see autocomplete in action when you type a new address in the address bar or when you fill in some form, and the autocomplete engine of that particular browser is triggered. In email programs, it is very useful be able to choose the recipient by typing only a few letters.

In source-code text editors, I'm sure you appreciate the code completion feature. Long variable names make the code easier to understand, but harder to type, unless your editor supports code completion. In some editors, after typing an object's name followed by a period, you get a
scrolling list of the object's public members. It is like having the documentation at your fingertips. Microsoft has implemented it in the Visual Studio Integrated Development Environment, and has patented it under the name of IntelliSense. The GNU Emacs editor was supporting the
autocomplete feature long before Microsoft introduced it.

In operating systems' shells such as Unix's bash, sh, or the Windows command prompt, autocomplete for command names, filenames, and paths is usually done by pressing the Tab key after typing the first few letters of the word. I'm sure you find this feature very useful when you have a very long path to type!

Introducing AJAX Suggest and Autocomplete Autocomplete is yet another good example of a feature that was traditionally used only in desktop applications. Popular implementations of this feature in web applications are very recent. (Note
that the typical form autocompletion in web browsers, or the remember-password feature, is
implemented locally by the web browsers, it's not a feature of the site.)

It's all about enriching web applications' user interfaces with features that have already been integrated into desktop applications. See a nice autocomplete example that implements this feature at http://demo.script.aculo.us/ajax/autocompleter.

The most popular example of this feature is Google Suggest.

Google Suggest
Why Google Suggest? Because it is the most popular web implementation of suggest and autocomplete using AJAX. Believe it or not, Google was not the first to implement this technology. Christian Stocker used it in his Bitflux Blog http://blog.bitflux.ch/archive/
2004/07/13/livesearch_roundup.html in April 2004, seven months prior to Google's release. One article that describes exactly how autocomplete textboxes can be implemented in a web page using JavaScript goes as back as September 2003, http://www.sitepoint.com/article/life- autocomplete-textboxes. XMLHttpRequest is known to have been in use for a couple of years now. Therefore, Google didn't invent anything; it just put together a perfect example.

The web address where Google Suggest can be accessed is http://www.google.com/
webhp?complete=1&hl=en

Figure 6.1: Google Suggest in Beta

The clever part of the JavaScript script in an application like Google Suggest is that it caches a table of previous suggestions received for a certain keyword. Therefore, if you type a keyword and then erase back a few characters, the old suggestions received from the request will have been cached and hence there will be no need to fetch them again.

The same technique has also been implemented in Gmail (www.gmail.com) and Google Maps
(http://maps.google.com). .

Implementing AJAX Suggest and Autocomplete
In this chapter we'll develop a suggest and autocomplete feature that helps the user to find PHP functions and their official help page from http://www.php.net. The PHP functions database required for this chapter includes all the PHP functions from http://www.php.net/quickref.php.

We will implement the following features in our application:

• The matching functions are retrieved as you type and displayed in a scrollable drop- down list.
• The current keyword is autocompleted with the missing letters from the first suggestion returned as result. The added letters are highlighted.
• The initial letters matching the search keyword are bolded in the drop-down list.
• The drop-down list is scrollable, but the scroll bar appears only if the list of results exceeds a predefined number of suggestions.

Figure 6.2: Many Interesting Functions

Time for Action—AJAX Suggest and Autocomplete
1. As always, we start by creating the necessary database structures. Create a new table named suggest in the ajax database that contains a single field (name), which is also
the primary key:
CREATE TABLE suggest
(
name VARCHAR(100) NOT NULL DEFAULT '',
PRIMARY KEY (name)
);

2. The suggest table will be populated with the complete list of PHP functions that we took from http://www.php.net/quickref.php; because the table contains over
4,000 records, we are listing only the first ten here. Please use the script from the code download for the complete list:
INSERT INTO suggest (name) VALUES ('abs'),
('acos'), ('acosh'), ('addcslashes'), ('addslashes'), ('aggregate'), ('aggregate_info'), ('aggregate_methods'),
('aggregate_methods_by_list'), ('aggregate_methods_by_regexp');

3. Create a new folder named suggest, under the ajax folder.
4. We will start by creating the code for the server side. In the suggest folder, create a file named config.php, and add the database configuration code to it (change these values to match your configuration):
<?php
// defines database connection data
define('DB_HOST', 'localhost'); define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax');
?>

5. Then add the standard error-handling file error_handler.php:
<?php
// set the user error handler method to be error_handler
set_error_handler('error_handler', E_ALL);
// error handler function
function error_handler($errNo, $errStr, $errFile, $errLine)
{
// clear any output that has already been generated if(ob_get_length()) ob_clean();
// output the error message
$error_message = 'ERRNO: ' . $errNo . chr(10) .
'TEXT: ' . $errStr . chr(10) .
'LOCATION: ' . $errFile .
', line ' . $errLine;
echo $error_message;
// prevent processing any more PHP scripts exit;
}
?>

6. Create another file named suggest.php, and add this code to it:
<?php
// reference the file containing the Suggest class require_once('suggest.class.php');
// create a new Suggest instance
$suggest = new Suggest();
// retrieve the keyword passed as parameter
$keyword = $_GET['keyword'];
// clear the output if(ob_get_length()) ob_clean();
// headers are sent to prevent browsers from caching
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT' ); header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache');
header('Content-Type: text/xml');
// send the results to the client
echo $suggest->getSuggestions($keyword);
?>

7. Create another file named suggest.class.php, and add this code to it:
<?php
// load error handling module
require_once('error_handler.php');
// load configuration file require_once('config.php');

// class supports server-side suggest & autocomplete functionality class Suggest
{
// database handler private $mMysqli;

// constructor opens database connection function __construct()
{
// connect to the database
$this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD,
DB_DATABASE);
}

// destructor, closes database connection function __destruct()
{
$this->mMysqli->close();
}

// returns all PHP functions that start with $keyword public function getSuggestions($keyword)
{
// escape the keyword string
$patterns = array('/\s+/', '/"+/', '/%+/');
$replace = array('');
$keyword = preg_replace($patterns, $replace, $keyword);
// build the SQL query that gets the matching functions from the database if($keyword != '')
$query = 'SELECT name ' .
'FROM suggest ' .
'WHERE name LIKE "' . $keyword . '%"';
// if the keyword is empty build a SQL query that will return no results else
$query = 'SELECT name ' .

'FROM suggest ' .
'WHERE name=""';
// execute the SQL query
$result = $this->mMysqli->query($query);
// build the XML response
$output = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
$output .= '<response>';
// if we have results, loop through them and add them to the output
if($result->num_rows)
while ($row = $result->fetch_array(MYSQLI_ASSOC))
$output .= '<name>' . $row['name'] . '</name>';
// close the result stream
$result->close();
// add the final closing tag
$output .= '</response>';
// return the results return $output;
}
//end class Suggest
}
?>

8. Create a new file named index.html, and add this code to it:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>AJAX Suggest and Autocomplete</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="suggest.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="suggest.js"></script>
</head>
<body>
<noscript>
Your browser does not support JavaScript!!
</noscript>
<div id="content" onclick="hideSuggestions();">
<div id="message">Enter the first letters of your function:</div>
<input type="text" name="keyword" id="keyword" maxlength="70"
size="69" onkeyup = "handleKeyUp(event)" value="" />
<div id="scroll">
<div id="suggest">
</div>
</div>
</div>
</body>
</html>

9. Create another file named suggest.css, and add this code to it:
body
{
font-family: helvetica, sans-serif;
margin: 0px;
padding: 0px;
font-size: 12px
}

#content
{
height: 100%;
width: 100%;
text-align:center
}

#message

{
font-weight: bold;
text-align: center; margin-left: 10px; margin-bottom: 10px; margin-top: 10px
}

a
{
text-decoration: none;
margin: 0px;
color: #173f5f
}

input
{
border: #999 1px solid;
font-family: helvetica, sans-serif;
font-weight: normal;
font-size: 10px
}

#scroll
{
position: relative;
margin: 0 auto; visibility: hidden; background-color: white; z-index: 1;
width: 300px;
height: 180px;
border-top-style: solid; border-right-style: solid; border-left-style: solid; border-collapse: collapse; border-bottom-style: solid; border-color: #000000; border-width: 1px;
overflow: auto
}

#scroll div
{
margin: 0 auto;
text-align:left
}

#suggest table
{
width: 270px;
font-size: 11px;
font-weight: normal;
color: #676767;
text-decoration: none;
border: 0px;
padding: 0px; text-align:left; margin: 0px
}

.highlightrow
{
background-color: #999999;
cursor: pointer
}

10. Create another file named suggest.js, and add this code to it:
/* URL to the PHP page called for receiving suggestions for a keyword*/
var getFunctionsUrl = "suggest.php?keyword=";
/* URL for seeing the results for the selected suggestion */
var phpHelpUrl="http://www.php.net/manual/en/function.";
/* the keyword for which an HTTP request has been initiated */
var httpRequestKeyword = "";
/* the last keyword for which suggests have been requested */
var userKeyword = "";
/* number of suggestions received as results for the keyword */
var suggestions = 0;
/* the maximum number of characters to be displayed for a suggestion */
var suggestionMaxLength = 30;
/* flag that indicates if the up or down arrow keys were pressed
the last time a keyup event occurred */
var isKeyUpDownPressed = false;
/* the last suggestion that has been used for autocompleting the keyword
*/
var autocompletedKeyword = "";
/* flag that indicates if there are results for the current requested
keyword*/
var hasResults = false;
/* the identifier used to cancel the evaluation with the clearTimeout
method. */
var timeoutId = -1;
/* the currently selected suggestion (by arrow keys or mouse)*/
var position = -1;
/* cache object containing the retrieved suggestions for different keywords */
var oCache = new Object();
/* the minimum and maximum position of the visible suggestions */
var minVisiblePosition = 0;
var maxVisiblePosition = 9;
// when set to true, display detailed error messages var debugMode = true;
/* the XMLHttp object for communicating with the server */
var xmlHttpGetSuggestions = createXmlHttpRequestObject();
/* the onload event is handled by our init function */
window.onload = init;

// creates an XMLHttpRequest instance function createXmlHttpRequestObject()
{
// will store the reference to the XMLHttpRequest object
var xmlHttp;
// this should work for all browsers except IE6 and older try
{
// try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest();
}
catch(e)
{
// assume IE6 or older
var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0",
"MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP");
// try every prog id until one works
for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++)
{
try

{
// try to create XMLHttpRequest object
xmlHttp = new ActiveXObject(XmlHttpVersions[i]);
}
catch (e) {}
}
}
// return the created object or display an error message
if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}

/* function that initializes the page */
function init()
{
// retrieve the input control for the keyword
var oKeyword = document.getElementById("keyword");
// prevent browser from starting the autofill function
oKeyword.setAttribute("autocomplete", "off");
// reset the content of the keyword and set the focus on it oKeyword.value = "";
oKeyword.focus();
// set the timeout for checking updates in the keyword's value setTimeout("checkForChanges()", 500);
}

/* function that adds to a keyword an array of values */
function addToCache(keyword, values)
{
// create a new array entry in the cache
oCache[keyword] = new Array();
// add all the values to the keyword's entry in the cache for(i=0; i<values.length; i++)
oCache[keyword][i] = values[i];
}

/*
function that checks to see if the keyword specified as parameter is in the cache or tries to find the longest matching prefixes in the cache
and adds them in the cache for the current keyword parameter
*/
function checkCache(keyword)
{
// check to see if the keyword is already in the cache if(oCache[keyword])
return true;
// try to find the biggest prefixes for(i=keyword.length-2; i>=0; i--)
{
// compute the current prefix keyword
var currentKeyword = keyword.substring(0, i+1);
// check to see if we have the current prefix keyword in the cache if(oCache[currentKeyword])
{
// the current keyword's results already in the cache var cacheResults = oCache[currentKeyword];
// the results matching the keyword in the current cache results
var keywordResults = new Array();
var keywordResultsSize = 0;
// try to find all matching results starting with the current prefix
for(j=0;j<cacheResults.length;j++)
{

if(cacheResults[j].indexOf(keyword) == 0)
keywordResults[keywordResultsSize++] = cacheResults[j];
}
// add all the keyword's prefix results to the cache addToCache(keyword, keywordResults);
return true;
}
}
// no match found return false;
}

/* initiate HTTP request to retrieve suggestions for the current keyword
*/
function getSuggestions(keyword)
{
/* continue if keyword isn't null and the last pressed key wasn't up or
down */
if(keyword != "" && !isKeyUpDownPressed)
{
// check to see if the keyword is in the cache isInCache = checkCache(keyword);
// if keyword is in cache...
if(isInCache == true)
{
// retrieve the results from the cache
httpRequestKeyword=keyword;
userKeyword=keyword;
// display the results in the cache
displayResults(keyword, oCache[keyword]);
}
// if the keyword isn't in cache, make an HTTP request
else
{
if(xmlHttpGetSuggestions)
{
try
{
/* if the XMLHttpRequest object isn't busy with a previous request... */
if (xmlHttpGetSuggestions.readyState == 4 ||
xmlHttpGetSuggestions.readyState == 0)
{
httpRequestKeyword = keyword;
userKeyword = keyword;
xmlHttpGetSuggestions.open("GET",
getFunctionsUrl + encode(keyword), true);
xmlHttpGetSuggestions.onreadystatechange =
handleGettingSuggestions;
xmlHttpGetSuggestions.send(null);
}
// if the XMLHttpRequest object is busy... else
{
// retain the keyword the user wanted userKeyword = keyword;
// clear any previous timeouts already set if(timeoutId != -1)
clearTimeout(timeoutId);
// try again in 0.5 seconds
timeoutId = setTimeout("getSuggestions(userKeyword);", 500);
}
}
catch(e)

{
displayError("Can't connect to server:\n" + e.toString());
}
}
}
}
}

/* transforms all the children of an xml node into an array */
function xmlToArray(resultsXml)
{
// initiate the resultsArray var resultsArray= new Array();
// loop through all the xml nodes retrieving the content
for(i=0;i<resultsXml.length;i++)
resultsArray[i]=resultsXml.item(i).firstChild.data;
// return the node's content as an array
return resultsArray;
}

/* handles the server's response containing the suggestions for the requested keyword */
function handleGettingSuggestions()
{
//if the process is completed, decide what to do with the returned data if (xmlHttpGetSuggestions.readyState == 4)
{
// only if HTTP status is "OK"
if (xmlHttpGetSuggestions.status == 200)
{
try
{
// process the server's response updateSuggestions();
}
catch(e)
{
// display the error message
displayError(e.toString());
}
}
else
{
displayError("There was a problem retrieving the data:\n" +
xmlHttpGetSuggestions.statusText);
}
}
}

/* function that processes the server's response */
function updateSuggestions()
{
// retrieve the server's response
var response = xmlHttpGetSuggestions.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Void server response." : response);
// retrieve the document element
response = xmlHttpGetSuggestions.responseXML.documentElement;
// initialize the new array of functions' names
nameArray = new Array();
// check to see if we have any results for the searched keyword

if(response.childNodes.length)
{
/* we retrieve the new functions' names from the document element as an array */
nameArray= xmlToArray(response.getElementsByTagName("name"));
}
// check to see if other keywords are already being searched for if(httpRequestKeyword == userKeyword)
{
// display the results array displayResults(httpRequestKeyword, nameArray);
}
else
{
// add the results to the cache
// we don't need to display the results since they are no longer useful addToCache(httpRequestKeyword, nameArray);
}
}

/* populates the list with the current suggestions */
function displayResults(keyword, results_array)
{
// start building the HTML table containing the results var div = "<table>";
// if the searched for keyword is not in the cache then add it to the cache
if(!oCache[keyword] && keyword)
addToCache(keyword, results_array);
// if the array of results is empty display a message
if(results_array.length == 0)
{
div += "<tr><td>No results found for <strong>" + keyword +
"</strong></td></tr>";
// set the flag indicating that no results have been found
// and reset the counter for results
hasResults = false;
suggestions = 0;
}
// display the results else
{
// resets the index of the currently selected suggestion position = -1;
// resets the flag indicating whether the up or down key has been pressed
isKeyUpDownPressed = false;
/* sets the flag indicating that there are results for the searched for keyword */
hasResults = true;
// get the number of results from the cache suggestions = oCache[keyword].length;
// loop through all the results and generate the HTML list of results for (var i=0; i<oCache[keyword].length; i++)
{
// retrieve the current function crtFunction = oCache[keyword][i];
// set the string link for the for the current function
// to the name of the function crtFunctionLink = crtFunction;
// replace the _ with - in the string link
while(crtFunctionLink.indexOf("_") !=-1)
crtFunctionLink = crtFunctionLink.replace("_","-");
// start building the HTML row that contains the link to the
// PHP help page of the current function

div += "<tr id='tr" + i +
"' onclick='location.href=document.getElementById(\"a" + i +
"\").href;' onmouseover='handleOnMouseOver(this);' " + "onmouseout='handleOnMouseOut(this);'>" +
"<td align='left'><a id='a" + i +
"' href='" + phpHelpUrl + crtFunctionLink + ".php";
// check to see if the current function name length exceeds the maximum
// number of characters that can be displayed for a function name
if(crtFunction.length <= suggestionMaxLength)
{
// bold the matching prefix of the function name and of the keyword
div += "'><b>" +
crtFunction.substring(0, httpRequestKeyword.length) + "</b>"
div += crtFunction.substring(httpRequestKeyword.length, crtFunction.length) +

}
else
{

"</a></td></tr>";

// check to see if the length of the current keyword exceeds
// the maximum number of characters that can be displayed if(httpRequestKeyword.length < suggestionMaxLength)
{
/* bold the matching prefix of the function name and that of the keyword */
div += "'><b>" +
crtFunction.substring(0, httpRequestKeyword.length) + "</b>"
div += crtFunction.substring(httpRequestKeyword.length, suggestionMaxLength) +

}
else
{

"</a></td></tr>";

// bold the entire function name div += "'><b>" +
crtFunction.substring(0,suggestionMaxLength) +
"</b></td></tr>"
}
}
}
}
// end building the HTML table
div += "</table>";
// retrieve the suggest and scroll object
var oSuggest = document.getElementById("suggest");
var oScroll = document.getElementById("scroll");
// scroll to the top of the list oScroll.scrollTop = 0;
// update the suggestions list and make it visible oSuggest.innerHTML = div;
oScroll.style.visibility = "visible";
// if we had results we apply the type ahead for the current keyword if(results_array.length > 0)
autocompleteKeyword();
}

/* function that periodically checks to see if the typed keyword has changed */
function checkForChanges()
{
// retrieve the keyword object

var keyword = document.getElementById("keyword").value;
// check to see if the keyword is empty
if(keyword == "")
{
// hide the suggestions
hideSuggestions();
// reset the keywords userKeyword="";
httpRequestKeyword="";
}
// set the timer for a new check
setTimeout("checkForChanges()", 500);
// check to see if there are any changes if((userKeyword != keyword) &&
(autocompletedKeyword != keyword) && (!isKeyUpDownPressed))
// update the suggestions
getSuggestions(keyword);
}

/* function that handles the keys that are pressed */
function handleKeyUp(e)
{
// get the event
e = (!e) ? window.event : e;
// get the event's target
target = (!e.target) ? e.srcElement : e.target;
if (target.nodeType == 3)
target = target.parentNode;
// get the character code of the pressed button code = (e.charCode) ? e.charCode :
((e.keyCode) ? e.keyCode :
((e.which) ? e.which : 0));
// check to see if the event was keyup if (e.type == "keyup")
{
isKeyUpDownPressed =false;
// check to see we if are interested in the current character
if ((code < 13 && code != 8) || (code >=14 && code < 32) ||
(code >= 33 && code <= 46 && code != 38 && code != 40) ||
(code >= 112 && code <= 123))
{
// simply ignore non-interesting characters
}
else
/* if Enter is pressed we jump to the PHP help page of the current
function */
if(code == 13)
{
// check to see if any function is currently selected if(position>=0)
{
location.href = document.getElementById("a" + position).href;
}
}
else
// if the down arrow is pressed we go to the next suggestion if(code == 40)
{
newTR=document.getElementById("tr"+(++position));
oldTR=document.getElementById("tr"+(--position));
// deselect the old selected suggestion if(position>=0 && position<suggestions-1)
oldTR.className = "";

// select the new suggestion and update the keyword if(position < suggestions - 1)
{
newTR.className = "highlightrow";
updateKeywordValue(newTR);
position++;
}
e.cancelBubble = true;
e.returnValue = false;
isKeyUpDownPressed = true;
// scroll down if the current window is no longer valid
if(position > maxVisiblePosition)
{
oScroll = document.getElementById("scroll");
oScroll.scrollTop += 18; maxVisiblePosition += 1; minVisiblePosition += 1;
}
}
else
// if the up arrow is pressed we go to the previous suggestion if(code == 38)
{
newTR=document.getElementById("tr"+(--position));
oldTR=document.getElementById("tr"+(++position));
// deselect the old selected position
if(position>=0 && position <= suggestions - 1)
{
oldTR.className = "";
}
// select the new suggestion and update the keyword if(position > 0)
{
newTR.className = "highlightrow";
updateKeywordValue(newTR);
position--;
// scroll up if the current window is no longer valid if(position<minVisiblePosition)
{
oScroll = document.getElementById("scroll");
oScroll.scrollTop -= 18;
maxVisiblePosition -= 1;
minVisiblePosition -= 1;
}
}
else
if(position == 0)
position--; e.cancelBubble = true; e.returnValue = false; isKeyUpDownPressed = true;
}
}
}

/* function that updates the keyword value with the value of the currently selected suggestion */
function updateKeywordValue(oTr)
{
// retrieve the keyword object
var oKeyword = document.getElementById("keyword");
// retrieve the link for the current function
var crtLink = document.getElementById("a" +
oTr.id.substring(2,oTr.id.length)).toString();
// replace - with _ and leave out the .php extension

crtLink = crtLink.replace("-", "_");
crtLink = crtLink.substring(0, crtLink.length - 4);
// update the keyword's value
oKeyword.value = unescape(crtLink.substring(phpHelpUrl.length, crtLink.length));
}

/* function that removes the style from all suggestions*/
function deselectAll()
{
for(i=0; i<suggestions; i++)
{
var oCrtTr = document.getElementById("tr" + i);
oCrtTr.className = "";
}
}

/* function that handles the mouse entering over a suggestion's area event */
function handleOnMouseOver(oTr)
{
deselectAll();
oTr.className = "highlightrow";
position = oTr.id.substring(2, oTr.id.length);
}

/* function that handles the mouse exiting a suggestion's area event */
function handleOnMouseOut(oTr)
{
oTr.className = "";
position = -1;
}

/* function that escapes a string */
function encode(uri)
{
if (encodeURIComponent)
{
return encodeURIComponent(uri);
}

if (escape)
{
return escape(uri);
}
}

/* function that hides the layer containing the suggestions */
function hideSuggestions()
{
var oScroll = document.getElementById("scroll");
oScroll.style.visibility = "hidden";
}

/* function that selects a range in the text object passed as parameter */
function selectRange(oText, start, length)
{
// check to see if in IE or FF
if (oText.createTextRange)
{
//IE
var oRange = oText.createTextRange();
oRange.moveStart("character", start); oRange.moveEnd("character", length - oText.value.length); oRange.select();

}
else
// FF
if (oText.setSelectionRange)
{
oText.setSelectionRange(start, length);
}
oText.focus();
}

/* function that autocompletes the typed keyword*/
function autocompleteKeyword()
{
//retrieve the keyword object
var oKeyword = document.getElementById("keyword");
// reset the position of the selected suggestion position=0;
// deselect all suggestions deselectAll();
// highlight the selected suggestion
document.getElementById("tr0").className="highlightrow";
// update the keyword's value with the suggestion updateKeywordValue(document.getElementById("tr0"));
// apply the type-ahead style selectRange(oKeyword,httpRequestKeyword.length,oKeyword.value.length);
// set the autocompleted word to the keyword's value
autocompletedKeyword=oKeyword.value;
}

/* function that displays an error message */
function displayError(message)
{
// display error message, with more technical details if debugMode is true alert("Error accessing the server! "+
(debugMode ? "\n" + message : ""));
}

11. The code is ready for testing now. Load the address http://localhost/ajax/ suggest/ with a web browser. Let's say, you're looking for the help page of strstr. After typing s, you're shown a list of functions that start with this letter:

Figure 6.3: PHP Knows Many Functions That Start with "s"

12. OK, PHP has many functions that start with letter s. Observe that the first matching function is autocompleted in the search box and that you have a long list of functions to scroll through. Let's type the second letter of the word strstr: t.
13. The list of functions has diminished as expected. Find the function you are interested in by continuing to type its name, or by using the keyboard's up and down arrows, or using the mouse. When you have found it, press Enter or click it using the mouse.

Figure 6.4: PHP Documentation for strstr

What Just Happened?
Let us start with the index.html file.

The interesting part in this script is that a scroll region can be implemented in DHTML. A little piece of heaven regarding scrolling can be found at http://www.dyn-web.com/dhtml/scroll/. The idea for having a part of the page with a scrollbar next to it is to have two layers one inside another. In our example the div scroll and the div suggest do the trick.

The outer layer is scroll. It has a fixed width and height and its most useful property is
overflow. Generally, the content of a block box is confined to just the content edges of the box.
In certain cases, a box may overflow, meaning that part of its content lies outside the box. In CSS, the overflow property specifies what happens when an element overflows its area. You can find the possible values of overflow at http://www.w3.org/TR/REC-CSS2/visufx.html.

Another thing that can be interesting is how we can center an object horizontally. The classic align
= center attribute is not valid in XHTML 1.0 and therefore a workaround needs to be found. The solution is to use the margin attribute set to auto for the element you want centered. If you have a valid doctype, Internet Explorer 6 will render an element having auto margins centered; otherwise, as is the case with the earlier versions, the attribute will be ignored. For earlier versions of Internet Explorer, you need to have the text-align attribute set to center for the parent of the element you

want centered. This is because Internet Explorer incorrectly applies the text-align attribute to all block elements instead of only inline elements making things work.

The input control handles the keyup event, which in fact triggers the process of fetching and displaying the suggestions for the current keyword. The content div handles the click event so that when the user clicks outside the suggestions area, the suggestions won't be displayed until the user modifies the current keyword.

For this application, almost everything is about JavaScript, DOM, and CSS. The server side is
very simple and it does not imply any significant effort, but the client-side code in suggest.js is a bit more complex. Let's enumerate the client features we implemented:

1. When a user starts typing, a drop-down list with suggestions appears; the list is updated as the user types new characters or erases some of them.

2. The first matching characters are in "Bold" in the list of suggestions.
3. The first matching suggestion is autocompleted in the keyword box.
4. By moving through the suggestions with the up and down arrow keys the keyword box is completed with the current selected suggestion.
5. By moving with the mouse over the suggestions nothing happens.
6. By pressing Enter or by clicking the mouse on a suggestion the page is redirected to the PHP help page on the php.net site.
7. The page is also redirected to php.net when the user presses Enter in the keyword box.
8. When the mouse is clicked outside the suggestions' list or the keyword box the list of suggestions is hidden.
9. The suggestions are cached on the client side.

We have a function that periodically checks to see if the keyword has changed. If so, an HTTP request to the server page containing the current keyword is initiated. In response, the server page returns the matching PHP functions as suggestions for that keyword. The client browser displays the suggestions in a drop-down list. The user can navigate through the suggestions using the up and down arrow keys or the mouse. On typing a new letter or on erasing one, the list of suggestions is updated. After seeing the images in the previous section and after a short overview of the process, it is time for us to see exactly how all these can be implemented.
The createXmlHttpRequestObject is the function that we use for creating our XMLHttpRequest object. The init function does nothing more than setting off the autocomplete attribute for the keyword
box. This is done in order to prevent browsers initiating their own autocomplete engine. Because setting "autocomplete"="off" is not a valid attribute according to XHTML specifications, the HTML is invalidated. This attribute was introduced by Microsoft and has been adopted by the majority of browsers.

The function that checks to see if the keyword has changed is checkForUpdates. If so, it starts the process of updating the suggestions list. For navigating through the list of suggestions, the
function handleKeyUp is used. We will see more about this function later in this chapter.

We have talked about caching the results. Yes, this is a very good optimization technique for this kind of application. Therefore, we have two functions that deal with the cache object— checkCache and addToCache.

The checkCache function checks to see if a given keyword is in the cache. If it's not in the cache, it tries to find the longest matching prefixes for our keyword in the list of cached values. Those matching prefixes are added to the cache by calling the addToCache function.

The addToCache function inserts in the cache for a given keyword a list of values that represent the suggestions for the keyword.

The getSuggestions function is called for fetching new suggestions. If the current keyword is already in the cache (checkCache function), we populate the suggestions list directly with those suggestions that have been cached. If a request is already in progress, the keyword that we would have wanted to use for a new call is saved and a timeout for this function is set. This way, we make sure that we save the last keyword for which we could not make a server call and as soon as the current request completes a new server call is initiated with the last keyword.

The handleGettingSuggestions function checks to see when the request to the server is completed and if there are no errors, the updateSuggestions function is called.

The updateSuggestions function checks to see if it is necessary to update the suggestion list. We check to see if during the server call there was another attempt to initiate a server call. By this we know if the user modified the current keyword and if so we don't need to display the retrieved suggestions since they are no longer interesting for the user. Nevertheless, the client caches all the suggestions from the server.

The xmlToArray function is the one that converts a collection of XML nodes into an array.

The function that actually builds the list of suggestions is displayResults. It receives as parameters the keyword and the list of available functions as an array. The first thing to do is to cache the current results, so that if we want to search again the same keyword, we don't have to make another call to
the web server. We go through all the suggestions in the array and we dynamically build a table containing the suggestions. If no suggestions are available, a message is displayed.

The updateKeywordValue function is responsible for updating the current keyword with the value contained in the suggestion currently selected given as a tr object.

The hideSuggestions function hides the div element containing all suggestions for the current keyword.

The deselectAll function deselects the currently selected suggestions.

The handleOnMouseOver and handleOnMouseOut functions handle the events that occur when the mouse cursor enters or exits the tr area of a suggestion. These functions update the style of the suggestion where the event takes place accordingly.

The encode function escapes the string passed as a parameter and it is used by the
getSuggestions function when calling the server page.

Next, we will talk about the handleKeyUp function. This is the function used for navigation through the results and submission. Since we are interested only in few keys, the others are ignored. Before getting there we need to make sure the code works on every browser. In order for this to happen, we need to write a few lines as we can see for ourselves.

In order to know which characters to consider, we need to know the codes of the keys. The event object received as parameter has a property keyCode that has the code of the pressed key. In the following table, you can find a list of most of the special keys:

Table 1: Key codes

Key Code Key Code
Backspace 8

Tab 9

Enter 13

Shift 16

Ctrl 17

Alt 18

Pause/Break 19

Caps Lock 20

Esc 27

Page Up 33

Page Down 34

End 35

Home 36

Left Arrow 37

Up Arrow 38

Right Arrow 39

Down Arrow 40 Print Screen 44

Delete 46

F1 112

F2 113

F3 114

F4 115

F5 116

F6 117

F7 118

F8 119

F9 120

F10 121

F11 122

F12 123

On pressing Enter (code 13), the page submits to the php.net help with the specification for the currently selected suggestion if any is selected. On pressing the up or down arrow keys the currently selected suggestion moves one position up or down if possible. The current keyword is also updated with the value of the current selected suggestion. We do not handle any other pressed keys since they modify the keyword and we have already presented the checkForChanges function that handles this part.

Another problem that arises when having more than ten suggestions available is that we have a scrollable div region. As we stated before, we want the user to be able to navigate through the results by using the up and down arrow keys. If the user reaches a result that is not currently

visible, we need to scroll in the region in order to make that result visible. In order to implement this, we keep minimum and maximum positions of the results that are currently visible. It's as if we had a window that moves through the results according to the arrows' movements and the current selected result.

The selectRange and autocompleteKeyword functions do the trick for the type-ahead look by autocompleting the current keyword with the rest of the missing letters up to the first suggestion. The part that is missing is added as highlighted text to the current keyword. The select() method selects all the text, and hence selecting only a part of a text is not possible. In order to do this, Internet Explorer offers one solution while Mozilla / Firefox offers another one. It is not for the first time that issues are not the same in all browsers, so we have to take each case and solve it separately. In Firefox, issues are simple because there is just one function that does all the work
for us—setSelectionRange. This function takes two parameters—the start position of the selection and the length of the selection. In Internet Explorer, we have to use the TextRange object in order to achieve the same goal. Let us take a closer look at it because it might be useful for us in the future and for this, we need to know what it can do.

The TextRange object can carry out tasks such as searching or selecting text. Text ranges let you pick out characters, words, and sentences from the text. Each of these three is a logical unit of the object. In order to use such an object you have to follow these steps:

• Create the text range
• Apply a method to the selected text

You can copy the text, search in the text, and select a part of the text, as in our case.

To create such an object you can call the createTextRange method on a body, textarea, or
button element.

Each object has a start and an end position defining the scope of the text. When you create a new text range, the start and end positions contain the entire content by default. To modify the scope of the text range we can use the move, moveStart, and moveEnd functions. Each of them takes two parameters—the first parameter specifies the logical unit and the second one the number of units
to move. The result contains the numbers of units moved. The select method makes the selection equal to the current object. In order to have a complete view of its capabilities check the following link on MSDN: http://msdn.microsoft.com/library/default.asp?url=/workshop/
author/dyncontent/textrange.asp.

After receiving the suggestions and inserting them into the page, we need to autocomplete the keyword with the value of the first suggestion. This is accomplished by using the selectRange function described above.

For the error-handling part, we use the displayError function that displays an alert with the error message as parameter.
OK, now we have seen how it goes for the client side of the application. Let's check the server side. For the server side, things are very simple. The suggest.php file retrieves the parameter passed by
the client and that represents the searched for keyword. Then it calls a method of the Suggest class

in suggest.class.php to find the matching suggestions for our keyword. The web server returns an XML response containing the PHP functions that match the current keyword. As we can see for ourselves, the effort resides on the client side and almost nothing on the server side.

The PHP help implemented as an AJAX suggest and autocomplete solution has proven to be far more challenging than we would have thought at the beginning. As mentioned above, we had many things to deal with. Hopefully, these problems also brought useful solutions for our application and can be used as a learning base for other applications.

Summary
In the beginning of the chapter, we gave a definition of autocomplete and suggest. We have seen how popular these notions are in domains from code editors to operating systems' consoles.

The application developed throughout this chapter offers an online PHP help with links to the official help on www.php.net.

The functionality offered here resembles to that offered by Google Suggest from many points of view, but it also has some additional features.

0 comments: