Advertisement

Asynchronous Search With PHP and jQuery

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In this tutorial we’re going to be creating a widget for our web pages which allows visitors to search the contents of our site. We’ll be using jQuery to pass the search term to the back-end, and to receive and display the results. We’ll be using PHP to search for the term in a local site and then return any matching URLs back to the page as a JSON object. Part 2 shows how to search a database of a content management system (CMS).

The PHP back-end used in this tutorial is just an example and is not the only way that we could perform the search. The back-end that the widget is connected to will differ depending on the structure of the website that it’s used on. The code used in this example would work well on a small to medium site with lots of static content. A data-driven or product-heavy site would probably make better use of a database-searching back-end, which would be equally as easy to code.


The PHP

We’ll look at the PHP first and then build around that. There’ll be a simple little script which begins at a specified directory and then spiders down through any subdirectories, collecting the URLs of all of the pages within the tree.

We’ll then need to search though each page to see if it contains the term that the visitor has searched for, and make a note of its URL if it does. Finally we can convert the information to JSON format for easy processing in the browser. Let’s make a start; in a new page in your text editor add the following code:

<?php 
 
//function to get all files in a tree 
function searchFiles($startDir, $urls = array()) { 
 
} 
?>

Most of the functionality in the PHP will be within the function we define here; the searchFiles function accepts two arguments, the first is the directory to start searching in, the second is an array. Next we need to add the spidering and searching logic, all of which can go into this function; within the function, add the following code:

//get search term from POST 
$term = $_GET["term"]; 
 
//scan starting dir 
$contents = scandir($startDir);

First we get the search term, which will be passed to the file as part of the GET request. We aren’t using a database so in this example, I haven’t focused on any security measures.

We then use the native PHP function scandir which will read the contents ofsc a specified directory. The directory to scan is obtained from the first parameter passed to the function. The return value of scandir is stored in the $contents variable and will be an array.

Directly after the code we just looked at, add the following code:

//loop through each item 
for ($x = 0; $x < count($contents); $x++) { 
 
//build path to each item 
$path = $startDir . "/" . $contents[$x]; 
					 
if(is_dir($path)) { //if item is a dir 
			 
//skip these 
if($contents[$x] !== "." && $contents[$x] !== "..") { 
				 
  //recursively call function to open sub dirs 
  $urls = searchFiles($path, $urls); 
} 
} elseif(is_file($path)) { //if item is a file 
			 
//only get HTML files 
$chunks = explode(".", $contents[$x]); 
if ($chunks[count($chunks) -1] == "html") { 
			 
  //open file 
  $handle = fopen($path, "r"); 
  $fsize = filesize($path);  
  $fileContents = fread($handle, $fsize); 
				 
  //get text from page 
  $pageChunks = split("<title>", $fileContents); 
  $title = split("</title>", $pageChunks[1]); 
				 
  //strip tags from each file 
  $cleanContents = strip_tags($fileContents); 
				 
  if(stristr($cleanContents, $term) === FALSE) { 
    continue; 
  } else { 
						 
    //trim start of string 
    $trimmedPath = substr($path, 2); 
					 
    //add to matching URLs array 
    $urls[] = array("url" => $trimmedPath, "title" => $title[0]); 
  }		 
} 
} 
}

The for loop, which encapsulates quite a bit of functionality, cycles through each item in the array returned by scandir. Remember that at this stage, it’s just the contents of the starting directory that we’re working with.

We first build the path for each item by concatenating the starting directory with a forward slash and the current filename. This is needed so that we can access files that are within any subdirectories inside the starting directory.

Next we check whether the current item is a directory using the native PHP is_dir function. If it is a directory, we ignore the current (.) and parent (..) directories, so that we only get subdirectories, and then recursively call the searchFiles function again, passing in the $path variable as the directory to search and the $urls array if it exists. It will only exist if the function has already been called, and if it does exist, will contain the URLs of any pages that have already been searched and found to contain the search term.

If the current item is a file, which we confirm with the is_file function, we then check the extension of the file to see whether it’s a file type that we want to search. We obviously don’t want to search script files or CSS files, or any other resource that doesn’t contain content. In this example we’re just searching HTML files. We can check the extension by exploding the string using the period as the separator, and then looking at the last item in the resulting array.

If the current item does have a HTML extension, we open the file in read-only mode and store the entire contents of the file, tags and all, in the $fileContents variable. We use the filesize PHP function to ensure that we read the entire file into the variable. The contents of the file will be stored as one long string.

Next we want to get the title of the page so that we can use this if the file does contain the search term. We can do this easily by first exploding our giant string on the

<title>

string. We then explode the remaining string on the

</title>

string, which will give us the title of the page, which we store in the $title array.

After this we can further prep the string of the file’s content for processing by removing all of the HTML tags from it. This means that only the content of the page will be searched. Once we have a clean string, we can then see if the search term is within the string using the case-insensitive stristr function.

If the page does contain the search term we then tidy up the path to the file by removing the first two characters (the ./) as we won’t need these to link to the file. Finally we add the URL of the file and the title of the page as a new item in the $urls associative array.

Once the function has finished executing we can return the associative array:

//return array of filenames 
return $urls;

We still have a couple of tasks to complete with PHP; directly after the searchFiles function add the following code:

//set starting dir as current dir 
$startDir = "."; 
 
//call function initially 
$urls = searchFiles($startDir); 
 
//delay response 
sleep(1); 
 
//convert to JSON obejct and echo to page 
$response = $_GET["jsoncallback"] . "(" . json_encode($urls) . ")"; 
echo $response;

We first set the current directory as the starting directory; the $startDir variable is passed to the searchFiles function the first time it is called, which we do next, storing the return value (our associative array) in the $urls variable. If no matches to the search term are found the array will still be created, but it will be empty, which we can test for in our JavaScript a little later on.

We also use the PHP sleep function to delay the response by a single second; we probably wouldn’t need to do this in a real implementation as the delay between the browser and server would be likely to be more than this anyway, but for the purpose of this example delaying the response allows us to see the loading spinner that we’ll be using and just seems to make the example work better.

Finally, we convert the $urls array into a properly formatted JSON object using PHP’s native json_encode function, and wrap the object in braces and a Get request, which we then echo back to the page. Save this file as search.php. It will need to go into the root directory of the site that it is to be used on.


JSON

JSON is a lightweight and efficient mechanism for transporting data across a network. It’s generally quicker and easier to work with than XML, but has yet to achieve the same level of adoption as XML. A technique known as JSONP, which jQuery natively supports, allows us to process the data completely within the browser and completely free of the standard cross-domain exclusion policy. This makes accessing and reusing content from remote domains much simpler.

Our PHP file will convert a standard PHP associative array into a literal JSON object containing an array. Within each item in this array will be another object, and the data returned from our function will appear as property values within this nested object. We haven’t written the jQuery which will process the object yet, but the following screenshot shows how the response object will appear in Firebug so that you can visualize the structure of the JSON:



Some jQuery and HTML

Now we can create the page that will call the server-side script that we just created when a search is performed; in a new file in your text editor, add the following code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
<html> 
<head> 
<link rel="stylesheet" type="text/css" href="search.css"> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
<title>jQuery Search Engine</title> 
</head> 
<body> 
<div id="container"> 
<h2 class="text">Search My Site:</h2> 
<input type="text" id="query"><button id="search" class="text">Search</button> 
</div> 
<script type="text/javascript" src="jquery-1.3.2.min.js"></script> 
<script type="text/javascript"> 
 
</script> 
</body> 
</html>

We link to a stylesheet, which we’ll crate shortly, and jQuery. On the page we just have the search UI which consists of a container, a label, an input and a button, nothing else. The rest of the elements that the search widget uses we’ll create as and when we need them. We also left an empty script element at the bottom of the page; within these tags add the following code:

$(function() { 
 
});

This is the jQuery short-hand way of specifying a function to execute on page load. All of the logic to pass the search term to the server and process the response will lie within this anonymous function. Now add the following code to the function:

//hide noResults message if present 
($("#noResults").length > 0) ? $("#noResults").fadeOut("fast", function() { 
$(this).remove(); 
}) : null ; 
				 
//hide error if present 
($("#error").length > 0) ? $("#error").fadeOut("fast", function() { 
$(this).remove(); 
}) : null ; 
					 
//hide results if present 
($("#results").length > 0) ? $("#results").fadeOut("fast", function() { 
$(this).remove(); 
}) : null ; 
				 
//hide success icon if present 
($("#success").length > 0) ? $("#success").fadeOut("fast", function() { 
$(this).remove(); 
}) : null ;

We first need to check whether the no results or error messages, or any previous results, are showing in case the visitor has already interacted with the UI. We can also check for the success icon that may have been appended to the widget.

If any of these (which we’ll add the code for imminently), exist, which we test for by seeing whether jQuery returns anything when we check for it using an id selector, we simply hide them using the fadeOut animation and then remove them from the page using a callback which is executed once the animation finishes.

Next we check that the text input for the search term isn’t empty:

//check input not empty 
if($("#query").val()) { 
 
} else { 
 
//show error message		 
$("<p>").addClass("text").attr("id", "noResults").text("Sorry, no results found").appendTo("#container").slideDown("fast");					 
}

If the input is empty, we can create and display a simple error message that prevents empty submissions and alerts the visitor to the error. We can show the error using the slideDown animation. The following screenshot shows how the error will appear:


The first branch of the conditional however is where the majority of the processing takes place; add the following code within the first part of the if statement:

//disable button and input 
$(this).attr("disabled", "disabled"); 
$("#query").attr("disabled", "disabled"); 
					 
//add spinner 
$("<img>").attr({ src: "spinner.gif", id: "spinner" }).css({ position: "absolute", top: 27, right: 80 }).insertAfter("#search"); 
					 
//get search term and build querystring 
var term = $("#query").val(), 
query = "term=" + term;

First we disable the button to prevent multiple submissions of the same search term. We also disable the input as a visual cue to the search term. Next we can add a little loading spinner so that it appears as if the page is doing something while it waits for the response. Loading spinners are cool and can be downloaded in a ready-to-use format from a variety of sites. We add it to the page and simply position it so that it appears to be inside the input. We then get the value entered into the input, and build the query-string that will be passed to the server. The following image shows how the spinner will appear:


Next we can make the request using jQuery’s getJSON method:

//request JSON object of matching urls 
$.getJSON("search.php?jsoncallback=?", query, function(data) { 
 
});

We provide three arguments to the method; the first is the URL of our PHP file with the JSONP query-string attached. The name that we specify after the first question mark should match the name we assigned to the superglobal passed back in the GET request from the server. Adding =? after this enables JSONP. jQuery will automatically pass the response object to the anonymous function that we pass to the getJSON method as the third argument. The second argument is the data we want to send to the server initially, which of course is the search term.

The anonymous function will be executed when the response from the server is received, let’s add the code for it next; within the curly braces add the following code:

if (data.length > 0) { 
 
//add success icon 
$("<div>").attr("id", "success").insertAfter("#container > h2").fadeIn("fast"); 
 
//create container for results 
$("<div>").attr("id", "results").css("display", "none").appendTo("#container"); 
							 
//add message 
$("<p>").addClass("text").text("The following pages contain the search term:").appendTo("#results"); 
							 
//create list 
$("<ul>").attr("id", "resultList").appendTo("#results"); 
 
//process response 
for (var x = 0; x < data.length; x++) { 
								 
//create list item 
var li = $("<li>"); 
								 
//create result 
$("<a />").addClass("result text").attr("href", data[x].url).text(data[x].title).appendTo(li); 
								 
li.appendTo("#resultList"); 
} 
														 
//show results 
$("#results").slideDown("fast");

We first check that the length of the response object is greater than zero; if it is we create a success icon, which will be added directly after the h2 element. We’ll be adding some other icons later on in the CSS, but this one needs to be created here. The icon will appear as in the following screenshot:


We also create a container element for the results and append it to the main container of the widget, although we don’t show it straight away. We then add a success message to the results container. The final pre-processing DOM insertion we do is to create a new unordered list element.

Next we use a simple for loop to cycle through each item in the array within our object. Each item will represent one page which contains the search term, so all we do on each iteration is create a new list item, and create a link to the page, using the URL for the href of the link, and the page title as the text of the link (we could also use it for the title of the link). The link is then appended to the list item, and the list item to the list.

Once we’ve been through each item in the array we then show the results using a nice slideDown animation. Now we need to cater for if the response object is just an empty array. This will mean that no pages contain the search term:

} else { 
							 
//show error message		 
$("<p>").addClass("text").attr("id", "noResults").text("Sorry, no results found").appendTo("#container").slideDown("fast");								 
}

All we need to do is show an error message indicating that no matching pages were found. We append it to the page and again show it with an animation. The final bit of jQuery will be executed regardless of whether results are returned or not:

//remove spinner 
$("#spinner").remove(); 
						 
//enable button and input 
$("#search").attr("disabled", ""); 
$("#query").attr("disabled", "");

We simply remove the loading spinner and re-enable the button and input so that further searches can be carried out. At this point either the list of results or the failure message should be visible. This brings us to the end of the jQuery code; we just need to add some CSS now and our widget should be complete. Save the file as search.html.


A Little Styling

We now just need to add a little CSS to complete the widget; a lot of the styles are purely for decoration, but some of them contribute to how the widget behaves. The list of the search results will appear to drop down from the bottom of the search container like a menu and will overlay any other content that may be on the page, so some of the styles are required.

In a new page your text editor, add the following selectors and rules:

.text {  
font-family:verdana; font-size:70%; font-weight:bold; color:#ffffff; margin-top:0; 
} 
#container { 
width:234px; position:relative; background-color:#4a4747;  
border:2px solid #373434; padding:10px 0 30px 20px; 
} 
#container h2 { margin:4px 0; font-size:80%; float:left; } 
#query {  
margin-right:4px; padding:0; position:relative; top:1px;  
font-size:80%; width:145px; 
} 
#search { color:#000000; position:relative; top:1px; } 
#error { 
margin:8px 0 0; display:none; background:url(warn.gif) no-repeat 6px 0; 
padding-left:28px; position:absolute; left:20px; top:55px; 
} 
#success { 
background:url(tick.gif) no-repeat; width:14px; height:11px; 
margin:7px 0 0 5px; float:left; display:none; 
} 
#results { 
position:absolute; width:218px; padding:0 20px 14px; 
background-color:#4a4747; left:-2px; top:80%; display:none; 
} 
#resultList { margin:0 0 10px; padding:0; } 
#resultList li { list-style-type:none; } 
#resultList li a { text-decoration:none; } 
#resultList li a:hover { text-decoration:underline; } 
#noResults { 
position:absolute; margin:8px 0 0; display:none; left:20px; top:55px; background:url(cross.gif) no-repeat 0 1px; padding-left:18px; 
}

Save this file as search.css. The first selector is a class for all of the text in the widget and is just more convenient than setting all of the same rules on each of the text elements individually. Other than that we set some sizes and some positioning. Most of the elements that we create dynamically are initially hidden so that we can animate them in instead of just showing them instantly.

The widget has enough space in it for the no results and error messages, so these are positioned absolutely within the outer widget container. The drop-down results menu is also positioned absolutely, so that it doesn’t push other page content around, and is aligned to the left and bottom edges of the widget container. We also add the icon images here too.


Summary

This is now all of the code we need to create the fully functional widget. To give the example more impact, the code download contains a fake site with plenty of content that we can search for. All you need to do is drop the main folder into a content-serving directory of a web server that has PHP installed and configured.

We should find that if we hit the button without entering a word in the text field we see the error message, and if we search for something that isn’t found we see the no results message. When we search for a term that is found, we get to see the list of results drop down and can navigate to any page that is listed. The next screenshot shows how the results menu will appear (I’ve also added some dummy content to the search page so that we can verify that the menu will overlay other content without messing with the page flow):




Screencast: Part 1



Part 2

A little while ago I wrote a tutorial focused on creating a static search engine that spidered down through a site hierarchy and searched each web page that it found for a given text string. This type of search was aimed at owners of web sites consisting of static HTML pages. In this tutorial we’re going to do the same thing, but this time instead of navigating folders and subfolders looking for pages, we’re going to search a database instead. This type of search is aimed at sites which are created dynamically from database content, such as many popular blogging platforms.

Overall we’re going to be doing the same kind of thing; we’ll capture the search word from an input, request a PHP file, passing in the search word. We’ll then look for the search term and pass back a list of URLs of pages that contain the search term as a JSON object. We can then process the response and build a list of results to display (or provide a message if the term was not found).

Believe it or not, searching a database for content is actually easier and requires less code than recursively and exhaustively searching through directories and subdirectories, so for roughly the same amount of code, we can do the same thing as before, but add some new features such as sorting the results, and making the list keyboard-navigable. For reference, we’ll be using MySQL version 5.0 and PHP version 5.2.

The topics we’ll be covering are summarised below:

  • Creating and populating a database with the MySQL CLI (Command Line Interface)
  • Reading a database with PHP
  • Searching through text with PHP
  • Working with PHP arrays – multidimensional and associative
  • Creating JSON and passing it to the browser in a way so that JSONP can be leveraged
  • Processing JSON with jQuery
  • Working with Keyboard events in jQuery

Getting Started

You may not have read the previous tutorial, or have the source files from that example to hand. Don’t worry; you don’t need them. We’ll look at every line of code that needs to be written as if this were a standalone article. Having read the previous tutorial is not a requirement for reading this one, although, it will give you a better grounding.

The first thing we should do is create a working environment; we’ll need a full web server installed and configured, with PHP and MySQL also available. Within the content-serving directory of the web server create a new folder called dbSearch. This is the project folder and is where all of the different resources we create will be stored. At this point, all that needs to go into this folder is the latest version of jQuery (1.3.2 at the time of writing).


Creating the Database

When deploying this widget on a blog or data-driven site, the database containing the content will already exist. For the purpose of this tutorial however, we’ll need to set one up. We can do this quickly and easily using the MySQL CLI. In the CLI (having opened it and entered the password) type the following command:

CREATE DATABASE dbSearch;

Note that the CLI is not case-sensitive, but a lot of people, especially those new to MySQL find that capitalising the commands helps distinguish commands from values and other identifiers. This command will create a new database called dbSearch. Once created, we should select the database for use with the following command:

USE dbSearch

The USE command is one of the few, possibly only, commands that don’t need to end in a semi-colon. Next we should create a table to store the data in. We can do this with the CREATE command:

CREATE TABLE pages(url TINYTEXT, title TINYTEXT, content MEDIUMTEXT);

This command will create a new table called pages and add three columns to it; a url column which we’ll use to store the relative filename for each page in, a title column which we can store the page titles in, and a content column which we’ll store the page content in. We use variants of the TEXT data type for the data that we’ll add to the table; the first variant TINYTEXT accepts up to a maximum of 255 characters, not a lot in the grand scheme of things, but easily enough for any of the URLs or page titles we’ll be using in this example.

The content column is set to the MEDIUMTEXT data type, which allows for up to 16777215 characters (around 15 MB). In a proper implementation we’d probably want to use LONGTEXT, which allows for up to roughly 4 GB of text. Remember that depending on the platform your blog is built on, the database and tables will probably already exist and be configured. At this point, the CLI window should appear something like the following screenshot:



Loading Data Into the Table

This is another step that will not need to be completed when deploying to a live site as the database will already contain this kind of information. For the purposes of this tutorial however, we need some fake data to search through.

There are a range of different methods for entering data into a database table; we could manually enter the data one record at a time, which is ok for small datasets but probably a little monotonous for the amount of data we’ll be entering in this example. Instead we’ll feed a text file to our database to populate it with some content. The text file is included in the code download for this example.

When loading data into a database using a text file, the contents of the file needs to be structured in a specific way. Each line corresponds to a single record, and the data for each column should be separated by tab spaces, just like the following example:

afilename.html	The Title	Some content, lorem ipsum etc, etc.

Having created the text file (or having extracted it from the code download), we need to tell MySQL to consume it and use the data within it to populate our table. The following command will achieve this:

LOAD DATA LOCAL INFILE ‘c:/apache site/dbSearch/tableData.txt’ INTO TABLE pages LINES TERMINATED BY ‘\r\n’;

We simply tell the server which file contains the data, the table it is to be loaded into and how each line in the file is terminated. The string specified as the line terminator will vary depending on the platform that is used to create the text file.

To ensure that the data is loaded into the table correctly, we can check it using the SELECT command:

SELECT * FROM pages;

This should simply select all of the data in the table and output it to the CLI, as shown in the following screenshot:


Each column and record is separated by the pipe character |.

The Server-Side PHP

Now that we have our data source we can work on the file that will interact with it – the server side PHP file. Coding this file next means that when we come to do the jQuery that will bring everything together, the data will be available for us to use. In a new file in your text editor add the following code:

<?php 
 
  //db connection detils 
  $host = "localhost"; 
  $user = "root"; 
  $password = "your_password_here!"; 
  $database = "dbSearch"; 
	 
  //get search term from GET 
  $term = " " . $_GET["term"] . " "; 
	 
  //make connection 
  $server = mysql_connect($host, $user, $password); 
  $connection = mysql_select_db($database, $server); 
	 
  //query the database 
  $query = mysql_query("SELECT * FROM pages"); 
	 
  //loop through and return results 
  for ($x = 0, $numrows = mysql_num_rows($query); $x < $numrows; $x++) { 
 
    $row = mysql_fetch_assoc($query); 
		 
    if(substr_count(strtolower($row["content"]), strtolower($term)) === 0) { 
      continue; 
    } else { 
      $urls[] = array("url" => $row["url"], "title" => $row["title"], "occurs" => substr_count(strtolower($row["content"]), strtolower($term)));  
    } 
  } 
	 
  // Comparison function 
  function cmp($a, $b) { 
    return ($a["occurs"] > $b["occurs"]) ? -1 : 1; 
  } 
	 
  // Sort the array 
  uasort($urls, "cmp"); 
   
  //set GET response and convert data to JSON			 
  $response = $_GET["jsoncallback"] . "(" . json_encode($urls) . ")"; 
	 
  //delay response 
  sleep(1); 
 
  //echo JSON to page 
  echo $response; 
 
?>

Save this file as search.php in the dbSearch folder. This is much less server-side code than we needed in part one - using the database really helps to streamline our code here. Let’s walk through the file and see what we do.

The first four variables are used to store the connection information that we’ll need to supply in order to interact with the database. Don’t forget to change the password variable to the one you use to sign in to the MySQL CLI. Next we obtain the search term that will be passed to the script by the page. We enclose the search term within blank spaces so that only the word by itself is matched, and not words within other words. For example, if we didn’t do this a search for hole would also match whole.

Next we connect to the MySQL server and select the database using the variables we just defined. We also query the database, selecting all of the rows of data in the table. We use the same command for selecting the data as we did when using the CLI earlier.

We then loop through each row of data returned by the query using the PHP for loop. The loop accepts four arguments; a counter variable $x, the total number of rows from the table, the condition for the execution of another iteration of the loop (while the counter variable is less than the total number of rows), and the counter increment which will increase the value of the counter variable by 1 on each iteration of the loop.


Searching the Data

The first thing we do within the loop is store the current row of data in the $row variable using the mysql_fetch_assoc PHP function which returns an associative array where each column in the table row appears as an item in the array. The column name is the label used to access each item in the array.

We then search the content item in the array using an if statement and the substr_count PHP function to see if the search term occurs 0 times. If it does occur 0 times we simply continue to the next iteration of the loop. If the string occurs more than 0 times we then create a new multidimensional array called $urls and add to it the url and title items of the array and a new item called occurs, which is the integer returned by the substr_count function. Each item in this array is itself an associative array. We make use of the strtolower function to make the search case insensitive.

The result of this code is a multidimensional array where each item is an associative array containing the URLs, page titles and the number of times the search term was found of pages whose content contains the search term. One thing we can do next to really add value to the search is to sort the array so that the first item in the array contains the highest number of occurrences of the search term, giving each result a ‘rank’. We can do this very easily using the uasort PHP function.

The uasort function expects 2 arguments; the array to sort and a custom function where the items in the array are compared. The cmp function is a custom comparison function which accepts 2 of the items from the array passed to uasort. The function will then return false or true if the value of the first occurs item is greater than the value of the second. The uasort function will automatically convert the outer array into an associative array and each item will be given its original pre-sort index number as a label.

We then wrap this array in parenthesis and convert it to a JSON object. Each property of this object is a nested JavaScript object that we’ll be able to process quickly and efficiently within the browser. Just before we echo back the JSON object we use the PHP sleep function to delay the response by 1 second; this part of the script shouldn’t be used if and when this widget is deployed. I’ve just found when running this example locally that it works better with this delay. After the delay we echo back the JSON object. We haven’t created the page that will interact with this file yet, but the following screenshot shows the structure that the JSON object will take:



The JSON Data Structure

JSON is a subset of JavaScript that allows us to define simple or complex objects and arrays. The values of these data structures can be any of a number of different data types including strings, numbers, Boolean or null values and can even be other objects, as in this example. PHP’s native json_encode function works by preserving standard arrays so that they remain as arrays, but converting associative arrays to objects. When this occurs, the label of the associative array item is used as the name of the property.

The structure of the JSON object that we’re using in this example is different than the structure we used in part one of this tutorial. The reason for this, as I explained earlier, is because of the additional data supplied by the uasort function. Previously, our JSON object was an array, which we were able to iterate through rapidly and easily. The fact that our JSON object’s structure has changed doesn’t make it any harder to get at our data, as we’ll see shortly.

For reference, we can easily return our JSON object to an array by wrapping the array within the array_values PHP function inside the json_encode function, like this:

 
//set GET response and convert data to JSON 
$response = $_GET["jsoncallback"] . "(" . json_encode(array_values($urls)) . ")";

The code that we’ll be using to process our JSON object in this example however is extremely flexible because it can be used to access the data in our new JSON format, but the exact same code can be used to access the array format that the JSON took in part 1.


Styling the Search Widget

Next we can define the stylesheet for our widget; in a new file add the following selectors and rules:

 
.text { 
  font-family:verdana; font-size:70%; font-weight:bold; color:#ffffff; 
  margin-top:0; 
} 
#container { 
  width:234px; position:relative; background-color:#4a4747; 
  border:2px solid #373434; padding:10px 0 30px 20px; 
} 
#container h2 { margin:4px 0; font-size:80%; float:left; } 
#query { 
  margin-right:4px; padding:0; position:relative; top:1px; 
  font-size:80%; width:145px; 
} 
#search { color:#000000; position:relative; top:1px; } 
#error { 
  margin:8px 0 0; display:none; background:url(warn.gif) no-repeat 6px 0; 
  padding-left:28px; position:absolute; left:20px; top:55px; 
} 
#success { 
  background:url(tick.gif) no-repeat; width:14px; height:11px; 
  margin:7px 0 0 5px; float:left; display:none; 
} 
#noResults { 
  position:absolute; margin:8px 0 0; display:none; left:20px; top:55px; 
  background:url(cross.gif) no-repeat 0 1px; padding-left:18px; 
} 
#results { 
  position:absolute; min-width:254px; background-color:#4a4747; 
  border:2px solid #373434; border-top:0; left:-2px; top:80%; 
  display:none; 
} 
#results p, #resultList li { padding-left:20px; } 
#resultList { margin:0 0 10px; padding:0; } 
#resultList li { list-style-type:none; white-space:nowrap; } 
#resultList li a { text-decoration:none; } 
#resultList li a:focus { outline-color:#079d67; } 
.selected { background-color:#079d67; }

Save this file as search.css in the dbSearch folder. Let’s look at the styles that we define; first we create a class for text elements so that the various bits of typography are consistently styled. The next four id selectors target the default elements that appear in the search widget when the page initially loads. Pretty much all of the styles set by these rules are arbitrary and have been decided by me for the purposes of this example. This is how it’ll look when the page loads:


The #error and #success selectors are for the different types of feedback that the user may receive, such as the message that is shown when no search term is entered, the message that is shown when the search term hasn’t been found, or the icon that is shown when results are found. Again most of the styles used for these elements can be changed according to your preference. The most important rule in each of these is display:none; which of course hides them until they are ready to be shown. The following screenshot shows the error message:


The remaining rules are all used to style the list of results that is produced when the search term is found in the data store. We don’t know beforehand how long each result is going to be, so we use the min-width rule to allow the width of the result list to grow. The white-space:nowrap rule also prevents the width of each of the results from being restricted.

Again, a lot of the rules here are used to set this particular skin, nothing more, so you can change the appearance of the widget easily without preventing it from working. The menu is positioned absolutely so that it does not interfere with other elements on the page. When the results are presented the first result is focused and has the selected class applied to it. We set the focus outline of the link to the same color as the selected class instead of removing the focus outline for accessibility reasons. The following screenshot shows how the results list will appear:



Creating the Page Shell

We’ll look at the underlying page first; in a new file in your text editor create the following page:

 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
<html> 
  <head> 
    <link rel="stylesheet" type="text/css" href="search.css"> 
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
    <title>jQuery Search Engine</title> 
  </head> 
  <body> 
    <div id="container"> 
      <h2 class="text">Search My Site:</h2> 
      <input type="text" id="query" tabindex=1><button id="search" class="text">Search</button> 
    </div> 
    <p>Lorem ipsum dolor...</p> 
    <script type="text/javascript" src="jquery-1.3.2.min.js"></script> 
    <script type="text/javascript"> 
      $(function() { 
 
      }); 
    </script> 
  </body> 
</html>

The page is as simple as possible, having just the search widget and some layout text present. The text is there to show how the result list overlays any existing page content (although there is no provision for overlaying flash content or select boxes). The widget itself is also very minimal, containing a simple heading, the search input, and a button. The rest of the elements that we’ll need we can create as and when necessary.

We link to our stylesheet in the head of the page, as well as a local version of jQuery at the end of the body. After this, we leave a script element containing the standard jQuery document.ready function which is where the bulk of our code will reside. We can add this next; there’s a lot, so after the following code sample we’ll look at what each bit does individually:

 
//click handler for button 
$("#search").click(function() { 
						 
  //hide noResults message if present 
  ($("#noResults")) ? $("#noResults").fadeOut("fast", function() { 
    $(this).remove();				 
  }) : null ; 
							 
  //hide error if present 
  ($("#error")) ? $("#error").fadeOut("fast", function() { 
    $(this).remove(); 
  }) : null ; 
								 
  //hide results if present 
  ($("#results")) ? $("#results").fadeOut("fast", function() { 
    $(this).remove(); 
  }) : null ; 
							 
  //hide success icon if present 
  ($("#success")) ? $("#success").fadeOut("fast", function() { 
    $(this).remove(); 
  }) : null ; 
		 
  //check input not empty 
  if($("#query").val()) { 
		 
    //disable button and input 
    $(this).attr("disabled", "disabled"); 
	$("#query").attr("disabled", "disabled"); 
									 
	//add spinner 
	$("<img>").attr({ src: "spinner.gif", id: "spinner" }).css({     
        position: "absolute", 
        top: 38, 
        right: 90 
      }).insertAfter("#search"); 
									 
	//get search term and build querystring 
	var term = $("#query").val(), 
	query = "term=" + term; 
									 
	//request JSON object of matching urls 
	$.getJSON("search.php?jsoncallback=?", query, function(data) { 
										 
	  if (!data) { 
								 
	  //show error message		 
	  $("<p>").addClass("text").attr("id", "noResults").text("Sorry, no results found").appendTo("#container").slideDown("fast");								 
	  } else { 
								 
	  //add success icon 
	  $("<div>").attr("id", "success").insertAfter("#container > h2").fadeIn("fast"); 
												 
	  //create container for results 
	  $("<div>").attr("id", "results").appendTo("#container"); 
										 
	  //add message 
	  $("<p>").addClass("text").text("The following pages contain the search term:").appendTo("#results"); 
												 
	  //create list 
	  $("<ol>").attr("id", "resultList").appendTo("#results"); 
					       
        //process response 
	  for (prop in data) { 
								 
	    //create list item 
	    var li = $("<li>"); 
 
	    //create result 
	    $("<a />").addClass("result text").attr({ href: data[prop].url, title: data[prop].title + " (" + data[prop].occurs + " occurences)" }).text(data[prop].title).appendTo(li); 
								   
          li.appendTo("#resultList"); 
	  } 
								 
	  //add some extra class names 
      $("#resultList").children(":first").addClass("first").parent().children(":last").addClass("last"); 
																			 
        //show results 
	  $("#results").slideDown("fast", function() { 
								   
	    //add class to first item and focus link 
	    $(this).find("ol").children(":first").addClass("selected").find("a").focus();									 
        });													 
	} 
											 
	  //remove spinner 
	  $("#spinner").remove(); 
											 
	  //enable button and input 
	  $("#search").attr("disabled", ""); 
	  $("#query").attr("disabled", ""); 
      });				 
    } else { 
		 
      //display error 
      ($("#error").length > 0) ? null : $("<p>").attr("id", "error").addClass("text").text("Please enter a search term!").appendTo("#container").slideDown("fast");			 
    } 
  });

All of this code is encapsulated within a click handler for the search button; within the anonymous function we pass to the click helper method there are several distinct sections. Let’s look at each of them in turn.


Housekeeping

Our first task is to tidy up, as this may not be the first time the button has been clicked and there may be elements left over from previous interactions. There are four things we need to look for and remove if they are present:

 
//hide noResults message if present 
($("#noResults")) ? $("#noResults").fadeOut("fast", function() { 
  $(this).remove();				 
}) : null ; 
							 
//hide error if present 
($("#error")) ? $("#error").fadeOut("fast", function() { 
  $(this).remove(); 
}) : null ; 
								 
//hide results if present 
($("#results")) ? $("#results").fadeOut("fast", function() { 
  $(this).remove(); 
}) : null ; 
							 
//hide success icon if present 
($("#success")) ? $("#success").fadeOut("fast", function() { 
  $(this).remove(); 
}) : null ;

We can test whether each of these elements exist using the JavaScript ternary construct; if they do exist we fade them out, if they don’t exist we do nothing.


Pre-Search Processing

Next there are a few things we need to do before we perform the actual search; we first check that the input field in the widget does have a value and if it doesn’t, we show the error message:

 
//check input not empty 
if($("#query").val()) { 
		 
  //disable button and input 
  $(this).attr("disabled", "disabled"); 
  $("#query").attr("disabled", "disabled"); 
									 
  //add spinner 
  $("<img>").attr({ src: "spinner.gif", id: "spinner" }).css({     
    position: "absolute", 
    top: 38, 
    right: 90 
  }).insertAfter("#search"); 
									 
  //get search term and build querystring 
  var term = $("#query").val(), 
  query = "term=" + term; 
									 
} else { 
		 
  //display error 
  ($("#error").length > 0) ? null : $("<p>").attr("id", "error").addClass("text").text("Please enter a search term!").appendTo("#container").slideDown("fast");			 
}

There is actually a lot more code inside the first part of the conditional, but this is everything we do before we make the request; we first disable both the button and the input element, to prevent multiple submissions while a request is in progress. We also add an AJAX spinner so that the visitor knows that something is happening behind the scenes.

The last thing we do before actually making the request is to prepare the data that is to be sent to the server, which is the search term entered into the input field.


Requesting and Handling the Response

We’re now ready to request and process the response; making the request is easy with jQuery’s getJSON method:

 
//request JSON object of matching urls 
$.getJSON("search.php?jsoncallback=?", query, function(data) { 
 
});

The method takes three arguments; the URL of the server-side resource that will receive the query and return the data. The second argument is the data to send to the server, and the last one is an anonymous callback function which will be executed if the request is successful. The code that goes into this anonymous function is used to process the response and display the results:

 
if (!data) { 
								 
  //show error message		 
  $("<p>").addClass("text").attr("id", "noResults").text("Sorry, no results found").appendTo("#container").slideDown("fast");								 
  } else { 
 
						 
} 
											 
//remove spinner 
$("#spinner").remove(); 
											 
//enable button and input 
$("#search").attr("disabled", ""); 
$("#query").attr("disabled", "");

In this section we first check that there is data in the response; if the search term isn’t found in the database null will be returned and if this is the case we can create and show a message. The message will appear like this:


After we’ve processed the object and displayed either the ‘no results’ message or the results, we can then remove the AJAX spinner, and enable the button and input to allow for additional searches to be performed.


Displaying the Results

If the search term is found, we’ll have a JSON object to process and results to create and display:

 
//add success icon 
$("<div>").attr("id", "success").insertAfter("#container > h2").fadeIn("fast"); 
												 
//create container for results 
$("<div>").attr("id", "results").appendTo("#container"); 
										 
//add message 
$("<p>").addClass("text").text("The following pages contain the search term:").appendTo("#results"); 
												 
//create list 
$("<ol>").attr("id", "resultList").appendTo("#results"); 
					       
//process response 
for (prop in data) { 
								 
  //create list item 
  var li = $("<li>"); 
 
  //create result 
  $("<a />").addClass("result text").attr({ href: data[prop].url, title: data[prop].title + " (" + data[prop].occurs + " occurences)" }).text(data[prop].title).appendTo(li); 
								   
  li.appendTo("#resultList"); 
} 
								 
//add some extra class names      $("#resultList").children(":first").addClass("first").parent().children(":last").addClass("last"); 
																			 
//show results 
$("#results").slideDown("fast", function() { 
								   
  //add class to first item and focus link    $(this).find("ol").children(":first").addClass("selected").find("a").focus();									 
});

First we need to create a few new elements; we create a success icon, which is inserted into the widget and positioned so that it appears next to the widget’s title (it just looks out of place anywhere else), and a container element for the results. We also create a message stating that the following list contains the search results.

The order of the items in the list is important in this context, so we create an ordered list element which we’ll populate in just a moment. The search results are in descending order within the JSON object, with the highest ranking (i.e. the page that contains the highest number of occurrences of the search term) item at the top.

We then use a for in loop to iterate over each property within the JSON object; on each iteration we create a new list item, then a new anchor element. We give the link some class names, so that they pick up the appropriate styles. We set some of the attributes of the link element using various values of the properties of the inner objects within the JSON object.

This is achieved using a combination of bracket and dot notation. The object consists of a series of properties and the value of each property is another object. Within each inner object there are another series of properties whose values contain our data. To access each property within the outer object we use the variable prop, which is defined in the loop, using square bracket syntax: data[prop] and to access our data, we simply append the name of the property who’s value we’d like to obtain: .url for example.

We also add the text content of the anchor element using data from our object before finally appending it to the list item. Following this the list item is appended to the list. One the loop has ended and all of the list items have been created and appended, we then need to give the first and last list items specific class names so that we can easily reference them later on in the script.

In the final part of this section of code we slide the results list into view and then select the first item in the list, giving it a class name and focusing it. This is pretty much where we finished off in part one of this tutorial, but now we’re going to add keyboard navigability to the results.


Enabling Keyboard Navigation

We attach our event listener to the anchor within the list item that has the class name selected, which we applied to the first item in the list; this way the widget will only be listening for events when it is relevant to do so. We attach the listener using jQuery’s live method so that we don’t have to keep rebinding to the event whenever we show the results:

 
//listen for keyboard events 
$(".selected").find("a").live("keydown", function(e) { 
 
});

Whenever the keydown event is detected the anonymous function is executed, and is automatically passed the event object. Before we get on with moving the selection to the next item in the list, there are a couple of things we need to do:

 
//prevent default browser behaviour for tab key 
(e.keyCode == 9) ? e.preventDefault() : null ; 
					 
//close results if escape key clicked 
(e.keyCode == 27) ? $("#results").fadeOut("fast", function() { 
  $(this).remove(); 
  $("#query").val(""); 
}) : null ;

First we need to check whether the key that was pressed was the tab key, as this has its own default behavior that needs to be disabled. We do this by using the JavaScript ternary to see whether the keyCode property of the event object is equal to 9. If it does we prevent the browser’s default behavior using the preventDefault method.

We can also check whether the escape key was pressed and if it was we can close the results list and reset the value of the input field.

Next we need to do different things depending on whether the currently selected list item is the first or last item in the list, which we can test using the class names we added earlier:

 
if($(this).parent().hasClass("first")) { 
					 
  //program up and down arrow keys and tab to move highlight 
  (e.keyCode == 40 || e.keyCode == 9) ? $(this).parent().removeClass("selected").next().addClass("selected").children(":first").focus() : (e.keyCode == 38) ? $(this).parent().removeClass("selected").parent().children(":last").addClass("selected").children(":first").focus() : null ; 
					 
} else if($(this).parent().hasClass("last")) { 
					 
  //program up and down arrow keys and tab to move highlight 
  (e.keyCode == 40 || e.keyCode == 9) ? $(this).parent().removeClass("selected").parent().children(":first").addClass("selected").children(":first").focus() : (e.keyCode == 38) ? $(this).parent().removeClass("selected").prev().addClass("selected").children(":first").focus() : null ; 
					 
} else { 
										 
  //program up and down arrow keys and tab to move highlight 
  (e.keyCode == 40 || e.keyCode == 9) ? $(this).parent().removeClass("selected").next().addClass("selected").children(":first").focus() : (e.keyCode == 38) ? $(this).parent().removeClass("selected").prev().addClass("selected").children(":first").focus() : null ; 
 
}

Within each branch of the conditional we also need to react differently depending on which key was pressed; we’re targeting the up and down arrow keys, which move the selection up or down the list respectively, and also the tab key, which will move the selection down the list. We use a nested ternary conditional for its compact syntax, which is equivalent to an if else statement.

Each branch of the outer conditional contains very similar expressions; let’s walk through the first one to see what’s going on. The first part of the ternary checks for the down arrow key or the tab key, if either of these is detected we navigate up from the anchor, which is in the context of $(this), to the parent list item and remove the selected class name. Then we navigate to the next sibling list item and give it the class name selected. We then navigate down to its first child, which will be the anchor element, and focus it.

If neither of these keys is detected we then check for the up arrow key, represented by 38. We want the selection to cycle through the list as if it were a menu, so if the up key is pressed while the first item is selected, we should move to the last item in the list and apply the selected class and focus. If none of these keys are detected we do nothing.

The ternary within the next branch of the outer conditional is very similar but this time we are looking at the last list item, but this time if the down or tab key is pressed, we move the selection to the first item in the list. The final condition again is very similar, but this time we just move the selection up or down depending on which key was pressed.


Catering for Mouse Interactions

Just because we’ve built keyboard navigation into our widget, it doesn’t necessarily mean that every visitor is going to make use of it, so for consistency we should move the selection around if the mouse pointer hovers over any of the list items. The code for this is very simple indeed:

 
//listen for mouseover 
$("#resultList").find("li").live("mouseover", function() {			 
 
  $(this).parent().children().removeClass("selected"); 
  $(this).addClass("selected").children(":first").focus(); 
				 
});

This is a simpler version of what we did with the keyboard event handlers; when the pointer hovers over a list item, we remove the selected class from all of list items and then add it back to whichever item was hovered, focusing the anchor element as we go.

Finally we can add a function that will close the result menu if any element outside of the menu is clicked. We do this by attaching a click handler to the body tag, and checking that the element that was clicked does not have a parent higher up in the DOM which is an ordered list with an id of resultList:

 
//add click handler to body 
$("body").click(function(e) { 
					 
  //close results if anything outside results is clicked 
  if($(e.target).closest("ol").attr("id") != "resultList") { 
						 
    //remove menu 
    ($("#resultList")) ? $("#results").fadeOut("fast", function() { 
	$(this).remove(); 
	$("#query").val(""); 
    }) : null ; 
  } 
});

Attaching the event listener to the body in this situation is useful because a click on absolutely any element on the page will bubble up to the body where we can capture and examine it.


Summary

We should now have a fully working widget which will allow us to search all of the content from a site that is contained within a database. The list of results will be both keyboard and mouse navigable and should appear as in the following screenshot:

Let’s recap what we’ve covered in this tutorial:

  • Many web sites and blogs are powered by a database in which all of the page content is stored. We looked at an example MySQL data source and saw how we can easily populate a test database using a simple text file.
  • We looked at how we can use PHP to retrieve the information from the database and search through it to look for occurrences of the search term. We looked at constructing a data structure, sorting the data, and converting it to an easily consumed JSON object.
  • We then looked at how to process this object in the browser and update the DOM of the widget to reflect whether the search was successful or not. We at looked at some of the considerations required to enable keyboard navigation of the results, turning it into a menu.



Screencast: Part 2


Advertisement