Chapter 1 The Client-Side Search
Engine
Application
Features
- Efficient Client-Side
Searching
- Multiple Search
Algorithms
- Sorted and Portioned
Search Results
- Scalable to Thousands
of Records
- Easily Coded for
JavaScript 1.0 Compatibility
JavaScript Techniques
- Using Delimited
Strings to Store Multiple Records
- Nested for Loops
- Wise Use of document.write()
- Using the Ternary
Operator for Iteration
|
Every site could use a search engine, but
why force your server to deal with all those queries? The
Client-Side Search Engine allows users to search through
your pages completely on the client side. Rather than sending
queries to a database or application server, each user downloads
the "database" within the requested web pages.
This makeshift database is simply a JavaScript array. Each
record is kept in one element of the array.
This approach has some significant benefits,
chiefly reducing the server's workload and improving response
time. As good as that sounds, keep in mind that this application
is restricted by the limitations of the user's resources,
especially processor speed and available memory. Nonetheless,
it can be a great utility for your web site. You can find
the code for this application in the ch01
folder of the zip file. Figure
1-1 shows the opening interface.
Figure 1-1.
The opening interface
|
|
This application comes equipped with two Boolean
search methods: AND and OR. You can search by document title
and description, or by document URL. User functionality
is pretty straightforward. It's as easy as entering the
terms you want to match, then pressing Enter. Here's the
search option breakdown:
- Entering terms separated by spaces
returns all records containing any
of the terms you included (Boolean OR).
- Placing a plus sign (+) before your
string of query term(s) matches only those records containing
all of the terms you enter (Boolean
AND).
- Entering url:
before a full or partial URL returns those records that
match any of the terms in the URL you enter.
TIP: Don't forget your zip file!
As noted in the preface, all the code used in this book
is available in a zip file on the O'Reilly site. To grab
the zip, go to http://www.oreilly.com/catalog/jscook/index.php.
Figure
1-2 shows the results page of a simple search. Notice
this particular query uses the default (no prefixes) search
method and "javascript" as the search term. Each
search generates on the fly a results page that displays
the fruits of the most recent search, followed by a link
back to the help page for quick reference.
Figure 1-2.
A typical search results page
|  |
It's also nice to be able to search by URL.
Figure
1-3 shows a site search using the url:
prefix to instruct the engine to search URLs only.
In this case the string html
is passed, so the engine returns all documents with html
in the URL. The document description is still displayed,
but the URL comes first. The URL search method is restricted
to single-match qualification, just like the default method.
That shouldn't be a problem, though. Not many people will
be eager to perform complex search algorithms on your URLs.
Figure 1-3.
Results page based on searching record
URLs
|  |
This application can limit the number of results
displayed per page and create buttons to view successive
or previous pages so that users aren't buried with mile-long
displays of record matches. The number displayed per page
is completely up to you, though the default is 10.
Execution Requirements
The version of the application discussed here
requires a browser that supports JavaScript 1.1. That's
good news for people using Netscape Navigator 3 and 4 and
Microsoft Internet Explorer 4 and 5, and bad news for IE
3 users. If you're intent on backwards compatibility, don't
fret. I'll show you how you can accommodate IE 3 users (at
the price of functionality) in the "Potential Extensions"
section of this chapter.
All client-side applications depend on the
resources of the client machine, a fact that's especially
true here. It's a safe bet the client will have the resources
to run the code, but if you pass the client a huge database
(more than about 6,000 or 7,000 records), your performance
will begin to degrade, and you'll eventually choke the machine.
I had no problem using a database of slightly
fewer than 10,000 records in MSIE 4 and Navigator 4. Incidentally,
the JavaScript source file holding the records was larger
than 1 MB. I had anywhere between 24 and 128 MB of RAM on
the machine. I tried the same setup with NN 3.0 Gold and
got a stack overflow error--just too many records in the
array.
On the low end, the JavaScript 1.0 version
viewed with MSIE 3.02 on an IBM ThinkPad didn't allow more
than 215 records. Don't let that low number scare you. The
laptop was so outdated you could hear the rat on the exercise
wheel powering the CPU. Most users will likely have a better
capacity.
The Syntax Breakdown
This application consists of three HTML files
(index.php, nav.php,
and main.php ) and a JavaScript
source file (records.js). The
three HTML files include a tiny frameset, a header page
where you enter the search terms, and a default page in
the display frame with the "how-to" instructions.
nav.php
The brains of the application lie in the header
file named nav.php. In fact,
the only other place you'll see JavaScript is in the results
pages manufactured on the fly. Let's have a glimpse at the
code. Example
1-1 leads the way.
Example 1-1: Source Code for nav.php
- <HTML>
- <HEAD>
- <TITLE>Search Nav Page</TITLE>
-
- <SCRIPT LANGUAGE="JavaScript1.1"
SRC="records.js"></SCRIPT>
- <SCRIPT LANGUAGE="JavaScript1.1">
- <!--
-
- var SEARCHANY = 1;
- var SEARCHALL = 2;
- var SEARCHURL = 4;
- var searchType = "";
- var showMatches = 10;
- var currentMatch = 0;
- var copyArray = new Array();
- var docObj = parent.frames[1].document;
-
- function validate(entry) {
- if (entry.charAt(0) == "+")
{
- entry = entry.substring(1,entry.length);
- searchType = SEARCHALL;
- }
- else if (entry.substring(0,4)
== "url:") {
- entry = entry.substring(5,entry.length);
- searchType = SEARCHURL;
- }
- else { searchType = SEARCHANY;
}
- while (entry.charAt(0) == "
") {
- entry = entry.substring(1,entry.length);
- document.forms[0].query.value
= entry;
- }
- while (entry.charAt(entry.length
- 1) == " ") {
- entry = entry.substring(0,entry.length
- 1);
- document.forms[0].query.value
= entry;
- }
- if (entry.length < 3) {
- alert("You cannot search
strings that small. Elaborate a little.");
- document.forms[0].query.focus();
- return;
- }
- convertString(entry);
- }
-
- function convertString(reentry)
{
- var searchArray = reentry.split("
");
- if (searchType == (SEARCHALL))
{ requireAll(searchArray); }
- else { allowAny(searchArray);
}
- }
-
- function allowAny(t) {
- var findings = new Array(0);
- for (i = 0; i < profiles.length;
i++) {
- var compareElement = profiles[i].toUpperCase();
- if(searchType == SEARCHANY) {
- var refineElement = compareElement.substring(0,
- compareElement.indexOf('|HTTP'));
- }
- else {
- var refineElement =
- compareElement.substring(compareElement.indexOf('|HTTP'),
- compareElement.length);
- }
- for (j = 0; j < t.length; j++)
{
- var compareString = t[j].toUpperCase();
- if (refineElement.indexOf(compareString)
!= -1) {
- findings[findings.length] = profiles[i];
- break;
- }
- }
- }
- verifyManage(findings);
- }
-
- function requireAll(t) {
- var findings = new Array();
- for (i = 0; i < profiles.length;
i++) {
- var allConfirmation = true;
- var allString = profiles[i].toUpperCase();
- var refineAllString = allString.substring(0,
- allString.indexOf('|HTTP'));
- for (j = 0; j < t.length; j++)
{
- var allElement = t[j].toUpperCase();
- if (refineAllString.indexOf(allElement)
== -1) {
- allConfirmation = false;
- continue;
- }
- }
- if (allConfirmation) {
- findings[findings.length] = profiles[i];
- }
- }
- verifyManage(findings);
- }
-
- function verifyManage(resultSet)
{
- if (resultSet.length == 0) { noMatch();
}
- else {
- copyArray = resultSet.sort();
- formatResults(copyArray, currentMatch,
showMatches);
- }
- }
-
- function noMatch() {
- docObj.open();
- docObj.writeln('<HTML><HEAD><TITLE>Search
Results</TITLE></HEAD>' +
- '<BODY BGCOLOR=WHITE TEXT=BLACK>'
+
- '<TABLE WIDTH=90% BORDER=0
ALIGN=CENTER><TR><TD VALIGN=TOP>' +
- '<FONT FACE=Arial><B><DL>'
+
- '<HR NOSHADE WIDTH=100%>"'
+ document.forms[0].query.value +
- '" returned no results.<HR
NOSHADE WIDTH=100%>' +
- '</TD></TR></TABLE></BODY></HTML>');
- docObj.close();
- document.forms[0].query.select();
- }
-
- function formatResults(results,
reference, offset) {
- var currentRecord = (results.length
< reference + offset ?
- results.length : reference + offset);
- docObj.open();
- docObj.writeln('<HTML><HEAD><TITLE>Search
Results</TITLE>\n</HEAD>' +
- '<BODY BGCOLOR=WHITE TEXT=BLACK>'
+
- '<TABLE WIDTH=90% BORDER=0
ALIGN=CENTER CELLPADDING=3><TR><TD>' +
- '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD
VALIGN=TOP>' +
- '<FONT FACE=Arial><B>Search
Query: <I>' +
- parent.frames[0].document.forms[0].query.value
+ '</I><BR>\n' +
- 'Search Results: <I>' +
(reference + 1) + ' - ' +
- currentRecord + ' of ' + results.length
+ '</I><BR><BR></FONT>' +
- '<FONT FACE=Arial SIZE=-1><B>'
+
- '\n\n<!-- Begin result set
//-->\n\n\t<DL>');
- if (searchType == SEARCHURL) {
- for (var i = reference; i <
currentRecord; i++) {
- var divide = results[i].split('|');
- docObj.writeln('\t<DT>'
+ '<A HREF="' + divide[2] + '">' +
- divide[2] + '</A>\t<DD><I>'
+ divide[1] + '</I><P>\n\n');
- }
- }
- else {
- for (var i = reference; i <
currentRecord; i++) {
- var divide = results[i].split('|');
- docObj.writeln('\n\n\t<DT>'
+ '<A HREF="' + divide[2] + '">' +
- divide[0] + '</A>' + '\t<DD>'
+ '<I>' + divide[1] + '</I><P>');
- }
- }
- docObj.writeln('\n\t</DL>\n\n<!--
End result set //-->\n\n');
- prevNextResults(results.length,
reference, offset);
- docObj.writeln('<HR NOSHADE
WIDTH=100%>' +
- '</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
- docObj.close();
- document.forms[0].query.select();
- }
-
- function prevNextResults(ceiling,
reference, offset) {
- docObj.writeln('<CENTER><FORM>');
- if(reference > 0) {
- docObj.writeln('<INPUT TYPE=BUTTON
VALUE="Prev ' + offset +
- ' Results" ' +
- 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray,
' +
- (reference - offset) + ', ' +
offset + ')">');
- }
- if(reference >= 0 &&
reference + offset < ceiling) {
- var trueTop = ((ceiling - (offset
+ reference) < offset) ?
- ceiling - (reference + offset)
: offset);
- var howMany = (trueTop > 1
? "s" : "");
- docObj.writeln('<INPUT TYPE=BUTTON
VALUE="Next ' + trueTop +
- ' Result' + howMany + '"
' +
- 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray,
' +
- (reference + offset) + ', ' +
offset + ')">');
- }
- docObj.writeln('</CENTER>');
- }
-
- //-->
- </SCRIPT>
- </HEAD>
- <BODY BGCOLOR="WHITE">
- <TABLE WIDTH="95%"
BORDER="0" ALIGN="CENTER">
- <TR>
- <TD VALIGN=MIDDLE>
- <FONT FACE="Arial">
- <B>Client-Side Search Engine</B>
- </TD>
-
- <TD VALIGN=ABSMIDDLE>
- <FORM NAME="search"
- onsubmit="validate(document.forms[0].query.value);
return false;">
- <INPUT TYPE=TEXT NAME="query"
SIZE="33">
- <INPUT TYPE=HIDDEN NAME="standin"
VALUE="">
- </FORM>
- </TD>
-
- <TD VALIGN=ABSMIDDLE>
- <FONT FACE="Arial">
- <B><A HREF="main.php"
TARGET="main">Help</A></B>
- </TD>
- </TR>
- </TABLE>
- </BODY>
- </HTML>
That's a lot of code. The easiest way to understand
what's going on here is simply to start at the top, and
work down. Fortunately, the code was written to proceed
from function to function in more or less the same order.
We'll examine this in the following order:
- The records.js
source file
- The global variables
- The functions
- The HTML
records.js
The first item worth examining is the JavaScript
source file records.js. You'll
find it in the <SCRIPT> tag
at line 5.
It contains a fairly lengthy array of elements
called profiles. The contents of
this file have been omitted from this book, as they would
have to be scrunched together. So after you've extracted
the files in the zip file, start up your text editor and
open ch01/records.js. Behold:
it's your database. Each element is a three-part string.
Here's one example:
"http://www.serve.com/hotsyte|HotSyte- The JavaScript Resource|The " +
"HotSyte home page featuring links, tutorials, free scripts, and more"
Record parts are separated by the pipe character
(|). These characters will
come in handy when matching database records are printed
to the screen. The second record part is the document title
(it has nothing to do with TITLE
tags); the third is the document description; and the first
is the document's URL.
By the way, there's no law against using character(s)
other than "|" to
separate your record parts. Just be sure it's something
the user isn't likely to enter as part of a query string
(perhaps &^ or ~[%).
Keep the backslash character (\)
out of the mix. JavaScript will interpret that as an escape
character and give you funky search results or choke the
app altogether.
Why is all this material included in a JavaScript
source file? Two reasons: modularity and cleanliness. If
your site has more than a few hundred web pages, you'll
probably want to have a server-side program generate the
code containing all the records. It's a bit more organized
to have this generated in a JavaScript source file.
You can also use this database in other search
applications simply by including records.js
in your code. In addition, I'd hate to have all that
code copied into an HTML file and displayed as source code.
|
This application relies on
searching pieces of information, much like a database.
To emulate searching a database, JavaScript can
parse (search) an array with similarly formatted
data.
It might seem like common sense
to set each array element equal to one piece of
data (such as a URL or the title of a web page).
That works, but you're setting yourself up for potential
grief.
You can significantly reduce
the number of global array elements if you concatenate
multiple substrings with a known delimiter (such
as |) into one array
element. When you parse each array element, JavaScript's
split() method of the
String object can create
an array of each of the elements. In other words,
why have a global array such as:
when you can have a local array
inside the function? For example:
Now you're probably thinking,
"Six of one and a half dozen of the other.
What's the difference?" The difference is that
the first version declares three global elements
that take up memory until you get rid of them. The
second declares only one global element. The three
elements created with split('|')
at search time are temporary because they are created
locally.
With the latter, JavaScript
disposes of the records
variable after the search function runs. That frees
memory. Plus that's less coding for you. For myself,
I'll take the second option. We'll hit this concept
again when we take a look at the code that does
the parsing. |
The Global Variables
Lines 9 through 16 of Example
1-1 declare and initialize the global variables.
var SEARCHANY = 1;
var SEARCHALL = 2;
var SEARCHURL = 4;
var searchType = '';
var showMatches = 10;
var currentMatch = 0;
var copyArray = new Array();
var docObj = parent.frames[1].document;
The following list explains the variable functions:
- SEARCHANY
- Indicates to search using any of
the entered terms.
- SEARCHALL
- Indicates to search using all of
the entered terms.
- SEARCHURL
- Indicates to search the URL only
(using any of the entered terms).
- searchType
- Indicates the type of search (set
to SEARCHANY, SEARCHALL,
or SEARCHURL).
- showMatches
- Determines the number of records
to display per results page.
- currentMatch
- Determines which record will first
be printed on the current results page.
- copyArray
- Copy of the temporary array of
matches used to display the next or previous set of results.
- docObj
- Variable referring to the document
object of the second frame. This isn't critical to the
application, but it helps manage your code because you'll
need to access the object (
parent.frames[1].document)
many times when you print the search results. docObj
refers to that object, reducing the amount of code
and serving as a centralized point for making changes.
The Functions
Next, let's look at the major functions:
validate( )
When the user hits the Enter button, the validate()
function at line 18 determines what the user wants to search
and how to search it. Recall the three options:
- Search the document title and description,
requiring only one term to match.
- Search the document title and description,
requiring all of the terms to match.
- Search the document URL or path,
requiring only one of the terms to match.
validate() determines
what and how to search by evaluating the first few characters
of the string it receives. How is the search method set?
Using the searchType
variable. If the user wants all terms to be included,
then searchType
is set to SEARCHALL. If the
user wants to search the title and description, validate()
sets all to false (that's the default, by the way). If the
user wants to search the URL, searchType
is set to null. Here's how it happens:
Line 19 shows the charAt()
method of the String object looking
for the + sign as the first character.
If found, the search method is set to option 2 (the Boolean
AND method).
if (entry.charAt(0) == "+") {
entry = entry.substring(1,entry.length);
searchType = SEARCHALL;
}
Line 23 shows the substring()
method of the String object looking
for "url:". If the
string is found, searchType
is set accordingly.
if (entry.substring(0,4) == "url:") {
entry = entry.substring(5,entry.length);
searchType = SEARCHURL;
}
What about the substring()
methods in lines 20 and 24? Well, after validate()
knows what and how to search, those character indicators
(+ and url:)
are no longer needed. Therefore, validate()
removes the required number of characters from the front
of the string and moves on.
If neither +
nor url: is found at the front
of the string, validate() sets
variable searchType
to SEARCHANY, and does a
little cleanup before calling convertString().
The while statements at lines 28 and 32 trim excess white
space from the beginning and end of the string.
After discovering the user preference and
trimming excess whitespace, validate()
has to make sure that there is something left to use in
a search. Line 36 verifies that the query string has at
least three characters. Searching fewer might not produce
useful results, but you can change this to your liking:
if (entry.length < 3) {
alert("You cannot search strings that small. Elaborate a little.");
document.forms[0].query.focus();
return;
}
If all goes well to this point, validate()
makes the call to convertString(),
passing a clean copy of the query string (entry).
convertString( )
convertString()
performs two related operations: it splits the string into
array elements, and calls the appropriate search function.
The split() method of the String
object divides the user-entered string by whitespace and
puts the outcome into the array searchArray.
This happens at line 45 as shown below:
var searchArray = reentry.split(" ");
For example, if the user enters the string
"client-side JavaScript development" in the search
field, searchArray
will contain the values client-side,
JavaScript, and development
for elements 0, 1, and 2, respectively. With that taken
care of, convertString() calls
the appropriate search function according to the value of
all. You can see this in lines
46 and 47:
if (searchType == (SEARCHALL)) { requireAll(searchArray); }
else { allowAny(searchArray); }
As you can see, one of two functions is called.
Both behave similarly, but they have their differences.
Here's a look at both functions: allowAny()
and requireAll().
allowAny( )
As the name implies, this function gets called
from the bench when the application has only a one-match
minimum. Here's what you'll see in lines 50-68:
function allowAny(t) {
var findings = new Array(0);
for (i = 0; i < profiles.length; i++) {
var compareElement = profiles[i].toUpperCase();
if(searchType == SEARCHANY) {
var refineElement =
compareElement.substring(0,compareElement.indexOf('|HTTP'));
}
else {
var refineElement =
compareElement.substring(compareElement.indexOf('|HTTP'),
compareElement.length);
}
for (j = 0; j < t.length; j++) {
var compareString = t[j].toUpperCase();
if (refineElement.indexOf(compareString) != -1) {
findings[findings.length] = profiles[i];
break;
}
The guts behind all three search functions
is comparing strings with nested for
loops. See the "JavaScript Technique: Nested for Loops"
sidebar for more information. The for
loops go to work at lines 52 and 63. The first for
loop has the task of iterating through each of the profiles
array elements (from the source file). For each profiles
element, the second for loop
iterates through each of the query terms passed to it from
convertString().
To ensure that users don't miss matching records
because they use uppercase or lowercase letters, lines 53
and 64 declare local variables compareElement
and compareString, respectively,
and then initialize each to an uppercase version of the
record and query term. Now it doesn't matter if users search
for "JavaScript," "javascript," or even
"jAvasCRIpt."
allowAny() still
needs to determine whether to search by document title and
description or by URL. So local variable refineElement,
the substring that will be compared to each of the query
terms, is set according to the value of searchType
at line 55 or 59. If searchType
equals SEARCHANY, refineElement
is set to the substring containing the record's document
title and description. Otherwise searchType
must be SEARCHURL, so refineElement
is set to the substring containing the document URL.
Remember the |
symbols? That's how JavaScript can distinguish the different
record parts. So the substring()
method returns a string starting from 0 and ending at the
character before the first instance of "|HTTP",
or returns a string starting at the first instance of "|HTTP"
until the end of the element. Now we have what we're about
to compare with what the user entered. Check it out at line
65:
if (refineElement.indexOf(compareString) != -1) {
findings[findings.length] = profiles[i];
break;
}
If compareString
is found within refineElement,
we have a match (it's about time). That original record
(not the URL-truncated version we searched) is added to
the findings array at line 66.
We can use findings.length as
an indexer to continually assign elements.
Once we've found a match, there is certainly
no reason to compare the record with other query strings.
Line 67 contains the break statement that stops the for
loop comparison for the current record. This isn't strictly
necessary, but it reduces excess processing.
After iterating through all records and search
terms, allowAny() passes any
matching records in the findings
array to function verifyManage()
at lines 95 through 101. If the search was successful, function
formatResults() gets the call
to print the results. Otherwise, function noMatch()
will let the user know that the search was unsuccessful.
Functions formatResults() and
noMatch() are discussed later
in the chapter. Let's finish examining the remaining search
methods with requireAll().
requireAll( )
Put a + in front
of your search terms, and requireAll()gets
the call. This function is nearly identical to allowAny(),
except that all terms the user enters must match the search.
With allowAny(), records were
added to the result set as soon as one term matched. In
this function, we have to wait until all terms have been
compared to each record before deciding to add anything
to the result set. Line 74 starts things off:
function requireAll(t) {
var findings = new Array();
for (i = 0; i < profiles.length; i++) {
var allConfirmation = true;
var allString = profiles[i].toUpperCase();
var refineAllString = allString.substring(0,
allString.indexOf('|HTTP'));
for (j = 0; j < t.length; j++) {
var allElement = t[j].toUpperCase();
if (refineAllString.indexOf(allElement) == -1) {
allConfirmation = false;
continue;
}
}
if (allConfirmation) {
findings[findings.length] = profiles[i];
}
}
verifyManage(findings);
}
|
Both the searching functions
allowAny() and requireAll()
use nested for loops.
This is a handy technique to iterate multidimensional
arrays as opposed to single-dimension arrays. (JavaScript
arrays are technically one-dimensional. However,
JavaScript can emulate multidimensional arrays as
described here.) Consider this five-element, single-dimension
array:
If you want to compare a string
to each of these, you simply run a for
(or while) loop, comparing
each array element to the string as you go. Like
this:
Not too demanding, so let's
up the ante. Multidimensional arrays are, well,
arrays of arrays. For example:
A single for
loop won't cut it. We'll need more fire power. The
first numbers array is
a single-dimension array (1 × 5). The new version
is a multidimensional array (3 × 5). Going through
all 15 elements (3 × 5) means we'll need an extra
loop:
That's the two-dimensional
answer to getting a shot at each element. Let's
take it a notch further. What if we build a color
palette in a table of all 216 web- safe colors--one
in each cell? Nested for
loops to the rescue. This time, however, we'll only
use a single dimension array.
Using hexadecimal numbers,
web-safe colors come in six-digit groups--two digits
for each color component--such as FFFFF, 336699,
and 99AACC. The two-digit pairs that make up all
web-safe colors are: 33, 66, 99, AA, CC, and FF.
Let's spark up an array:
--continued-- |
| "There's only one array
and one dimension. I want my money back."
Don't run to the bookstore
yet. There are three dimensions, but we'll use the
same array for each dimension. Here's how:
Drop this code in a web document
(it's in the zip file, at \Ch01\websafe.php
), and you'll get a 6 × 36 table with all 216
(that's 6 × 6 × 6) web-safe colors. Three for
loops and three dimensions. Of course, you could
modify the palette table in plenty of ways, but
this just shows you how nested for
loops can solve your coding woes. |
At first glance, things seem much as they
were with allowAny(). The nested
for loops, the uppercase conversion,
and the confirmation variable--they're all there. Things
change, however, at lines 79-80:
var refineAllString = allString.substring(0,allString.indexOf('|HTTP'));
Notice that variable searchType
was not checked to determine which part of the record to
keep for searching as it was in allowAny()
at line 50. There's no need. requireAll()
gets called only if searchType
equals SEARCHALL (see line 46).
URL searching doesn't include the Boolean AND method, so
it's a known fact that the document title and description
will be compared.
Function requireAll()
is a little tougher to please. Since all the terms a user
enters must be found in the compared string, so the searching
logic will be more restrictive than it is in allowAny().
See lines 83 through 86:
if (refineAllString.indexOf(allElement) == -1) {
allConfirmation = false;
continue;
}
It will be far easier to reject a record the
first time it doesn't match a term than it will be to compare
the number of terms with the number of matches. Therefore,
the first time a record does not contain a match, the continue
statement tells JavaScript to forget about it and move to
the next record.
If all terms have been compared to a record
and local variable allConfirmation
is still true, we have a match. allConfirmation
becomes false the moment a record fails to match its first
term. The current record is then added to the temporary
findings array at line 89. This
condition is harder to achieve, but the search results will
likely be more specific.
Once all records have been evaluated this
way, findings is passed to verifyManage()
to check for worthy results. If there are any matches at
all, formatResults() gets the
call. Otherwise, requireAll()
calls noMatch() to bring the
bad news to the user.
verifyManage( )
As you've probably realized, this function
determines whether the user's search produced any record
matches and calls one of two printout functions pending
the result. It all starts at line 95:
function verifyManage(resultSet) {
if (resultSet.length == 0) {
noMatch();
return;
}
copyArray = resultSet.sort();
formatResults(copyArray, currentMatch, showMatches);
}
Both allowAny()
and requireAll() call verifyManage()
after running the respective course and pass the findings
array as an argument. Line 96 shows that verifyManage()
calls function noMatch() if
array resultSet (a copy of findings)
contains nothing.
If resultSet contains
at least one matched record, however, global variable copyArray
is set to the lexically sorted version of all the elements
in resultSet. Sorting is not necessary,
but it's a great way to add order to your result set, and
you don't have to worry about the order in which you add
records to the profiles array.
You can keep adding them on the end, knowing that they'll
be sorted if a match occurs.
So why should we make an extra copy of a bunch
of records we already have? Remember that findings
is a local, and thus temporary, array. Once a search has
been performed (that is, the application executes one of
the search functions), findings
dies, and its allocated memory is freed for further use.
That's a good thing. There's no reason to hold onto memory
we could possibly use elsewhere, but we still need access
to those records.
Since the application displays, say, 10 records
per page, users potentially see only a subset of the matching
results. Variable copyArray is
global, so sorting the temporary result set and assigning
that to copyArray keeps all matching
records intact. Users can now view the results 10, 15, or
however many at a time. This global variable will keep the
matching results until the user submits a new query.
The last thing verifyManage()
does is call formatResults(),
passing an index number (currentMatch),
indicating which record to begin with and how many records
to display per page (showMatches).
Both currentMatch and showMatches
are global variables. They don't die after functions execute.
We need them for the life of the application.
noMatch( )
noMatch() does
what it implies. If your query produces no matches, this
function is the bearer of the bad news. It is rather short
and sweet, though it still generates a custom results (or
lack of results) page, stating that the query term(s) the
user entered didn't produce at least one match. Here it
is starting at line 103:
function noMatch() {
docObj.open();
docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B><DL>' +
'<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
'" returned no results.<HR NOSHADE WIDTH=100%>' +
'</TD></TR></TABLE></BODY></HTML>');
docObj.close();
document.forms[0].query.select();
}
formatResults( )
This function's job is to neatly display the
matching records for the user. Not terribly difficult, but
this function does cover a lot of ground. Here are the ingredients
for a successful results display:
- An HTML head, title, and body
- The document title, description,
and URL of each matching record with a link to the URL
of the each matching record
- "Previous" and "Next"
buttons to view earlier or later records, if applicable
The HTML head and title
The HTML head and title are straightforward.
Lines 116 through 129 print the head, title, and the beginning
of the body contents. Take a look:
function formatResults(results, reference, offset) {
var currentRecord = (results.length < reference + offset ?
results.length : reference + offset);
docObj.open();
docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE>\n</HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B>Search Query: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Search Results: <I>' + (reference + 1) + ' - ' + currentRecord +
' of ' + results.length + '</I><BR><BR></FONT>' +
'<FONT FACE=Arial SIZE=-1><B>' +
'\n\n<!- Begin result set //-->\n\n\t<DL>');
Before printing the heading and title, let's
find out which record we're going to start with. We know
the first record to print starts at results[reference].
And we should display offset records
unless reference + offset
is greater than the total number of records. To find out,
the ternary operator is again used to determine which is
larger. Variable currentRecord
is set to that number at line 117. We'll use that value
shortly.
Now, formatResults()
prints your run-of-the-Internet HTML heading and title.
The body starts with a centered table and a horizontal rule.
The application easily gives the user a reminder of the
search query (line 125), which came from the form field
value:
parent.frames[0].document.forms[0].query.value
Things get more involved at line 126, however.
This marks the beginning of the result set. The line of
printed text on the page displays the current subset of
matching records and the total number of matches, for instance:
Search Results: 1 - 10 of 38
We'll need three numbers to pull this off--the
first record of the subset to display, the number of records
to display, and the length of copyArray,
where the matching records are stored. Let's take a look
at this in terms of steps. Remember, this is not the logic
used to display the records. This logic lets the user know
how many records and with which
record to start. Here is how things happen:
- Assign the number of the current
record to variable reference,
then print it.
- Add another number called offset,
which is how many records to display per page (in this
case, 10).
- If the sum of reference
+ offset is greater than the total
number of matches, print the total number of matches.
Otherwise, print the sum of reference
+ offset. (This value has already
been determined and is reflected in currentRecord
).
- Print the total number of matches.
Steps 1 and 2 seem simple enough. Recall the
code in verifyManage(), particularly
line 99:
formatResult(copyArray, currentMatch, showMatches);
The local variable results
is a copy of copyArray. The variable
reference is set to currentMatch,
so the sum of reference +
offset is the sum of currentMatch
+ showResults.
In the first few lines of this code (13 and 14 to be exact),
showMatches was set to 10, and
currentMatch was set to 0. Therefore,
reference starts as 0, and reference
+ offset
equals 10. Step 1 is taken care of as soon as reference
is printed. The math we just did takes care of step
2.
In step 3, we use the ternary operator (at
lines 117-118) to decide whether the sum of reference
+ offset
is greater than the total number of matches. In other
words, will adding offset more
records to reference yield a number
higher than the total number of records? If reference
is 20, and there are 38 total records, adding 10 to
reference gives us 30. The display
would look like this:
Search Results: 20 - 30 of 38
If reference is
30, however, and there are 38 total records, adding 10 to
reference gives us 40. The display
would look like this:
Search Results: 30 - 40 of 38
Can't happen. The search engine cannot display
records 39 and 40 if it only found 38. This then indicates
that the end of the records has been reached. So the total
number of records will be displayed instead of the sum of
reference +
offset. That brings us to
step 4, and the end of the process:
Search Results: 30 - 38 of 38
TIP: Function formatResults()
is sprinkled with special characters such as \n
and \t. \n
represents a newline character, equivalent to pressing
Enter on your keyboard while writing code in your text
editor. \t is equivalent
to pressing the Tab key. All that these characters do
in this case is make the HTML of the search results look
neater if you view the source code. I included them here
to show you how they look. Keep in mind that they are
not necessary and don't affect your applications. If you
think they clutter your code, don't use them. I use them
sparingly in the rest of the book.
Displaying document titles, descriptions,
and linked URLs
Now that the subset of records has been indicated,
it's time to print that subset to the page. Enter lines
130 through 143:
if (searchType == SEARCHURL) {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split('|');
docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
}
}
else {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split('|');
docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
}
}
Lines 131 and 138 show both for
loops, which perform the same operation with currentRecord,
except that the order of the printed items is different.
Variable searchType comes up again.
If it equals SEARCHURL, the URL
will be displayed as the link text. Otherwise, searchType
equals SEARCHANY or SEARCHALL.
In either case the document title will be displayed as the
link text.
The type of search has been determined, but
how do you neatly display the records? We need only loop
through the record subset, and split the record parts accordingly
by title, description and URL, placing them however we so
desire along the way. Here is the for
loop used in either case (URL search or not):
for (var i = reference; i < lastRecord; i++) {
Now for the record parts. Think back to the
records.js file. Each element
of profiles is a string that identifies
the record "|" separating its parts. And that
is how we'll pull them apart:
var divide = results[i].split('|');
For each element, local variable divide
is set to an array of elements also separated by "|".
The first element (divide[0])
is the URL, the second element (divide[1])
is the document title, and the third (divide[2])
is the document description. Each of these elements is printed
to the page with accompanying HTML to suit (I chose <DL>,
<DT>, and <DD>
tags). If the user searched by URL, the URL would be shown
as the link text. Otherwise, the document title becomes
the link text.
Adding "Previous" and "Next"
buttons
The only thing left to do is add buttons so
that the user can view the previous or next subset(s) of
records. This actually happens in function prevNextResults(),
which we'll discuss shortly, but here are the last few lines
of formatResults():
docObj.writeln('\n\t</DL>\n\n<!- End result set //-->\n\n');
prevNextResults(results.length, reference, offset);
docObj.writeln('<HR NOSHADE WIDTH=100%>' +
'</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
docObj.close();
}
This part of the function calls prevNextResults(),
adds some final HTML, then sets the focus to the query string
text field. There isn't much to it, but we'll discuss it
shortly.
prevNextResults( )
If you've made it this far without screaming,
this function shouldn't be that much of a stretch. prevNextResults()
is as follows, starting with line 152.
function prevNextResults(ceiling, reference, offset) {
docObj.writeln('<CENTER><FORM>');
if(reference > 0) {
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset +
' Results" onClick="' +
parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference - offset) + ', ' + offset + ')">');
}
if(reference >= 0 && reference + offset < ceiling) {
var trueTop = ((ceiling - (offset + reference) < offset) ?
ceiling - (reference + offset) : offset);
var howMany = (trueTop > 1 ? "s" : "");
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop +
' Result' + howMany + '" onClick="' +
parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');
}
docObj.writeln('</CENTER>');
}
|
Take another look at formatResults().
You'll see that HTML written to the page with a
call to document.write()
or document.writeln().
The string passed to these methods is generally
long and spans multiple lines concatenated by +.
While you may argue that the code would be more
readable with a call to document.writeln()
on each line, there is a reason for doing otherwise.
Here's what I mean. The few lines of formatResults()
are as follows:
There is only one method call
to write the text to the page. Not too attractive.
One alternative would be to line things up neatly
with a method call on each line:
That might look more organized,
but each of those method calls means a little more
work for the JavaScript engine. Think about it.
What would you rather do: make five trips to and
from the store and buy things a little at a time,
or go to the store once and buy it all the first
time? Just pass a lengthy text string separated
with "+" signs,
and be done with it. |
This function prints a centered HTML form
at the bottom of the results page with one or two buttons.
Figure
1-3 shows a results page with both a "Prev"
and a "Next" button. There are three possible
combinations of buttons:
- A "Next" button onlyfor
the first results page displayed. There aren't any previous
records.
- A "Prev" button and a "Next"
buttonfor those results
pages that are between the first and last results pages.
There are records before and after those currently displayed.
- A "Prev" button only--for
the last results page. There are no more records ahead.
Three combinations. Two buttons. That means
this application must know when to print or not print a
button. The following list describes the circumstances under
which each combination will occur.
- "Next" Button Only
- Where should we include a Next
button? Answer: every results page except the last. In
other words, whenever the last record (reference
+ offset) of the results page is
less than the total number of records.
- Now, where do we exclude the "Prev"
button? Answer: on the first results page. In other words,
when reference equals 0 (which
we got from
currentMatch).
- "Prev" and the "Next"
Buttons
- When should both be displayed?
Given that a "Next" button should be included
on every results page except the last, and a "Prev"
button should be included on every results page except
the first, we'll need a "Prev" button as long
as reference is greater than 0,
and a "Next" button if reference
+ offset is less than the total
number of records.
- "Prev" Button Only
- Knowing when to include a "Prev"
button, under what circumstances should we exclude the
"Next" button? Answer: when the last results
page is displayed. In other words, when reference
+ offset is greater than or equal
to the total number of matching records.
Things might still be a little sketchy, but
at least we know when to include which button(s), and the
if statements in lines 154 and
160 do just that. These statements include one or both the
"Prev" and "Next" buttons depending
on the current subset and how many results remain.
Both buttons call function formatResults()
when the user clicks them. The only difference is the arguments
that they pass, representing different result subsets. Both
buttons are similar under the hood. They look different
because of the VALUE attribute.
Here is the beginning of the "Prev" button at
lines 155-156:
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset + ' Results" ' +
Now the "Next" button at lines 164-165:
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop + ' Result' +
howMany
Both lines contain the TYPE
and VALUE attributes of the
form button plus a number indicating how many previous or
next results. Since the number of previous results is always
the same (offset), the "Prev"
button value displays that number, for example, "Prev
10 Results." The number of next results can vary, however.
It is either offset or the number
remaining if the final subset is less than offset.
To address that, variable trueTop
is set to that value, whichever it is.
Notice how the value of the "Prev"
button always contains the word "Results." This
makes sense. The showMatches never
changes throughout the app. In this case it is and always
will be 10. So the user can always count on seeing 10 previous
results. However, that isn't always the case for the amount
of "Next" results. Suppose the last subset contains
only one record. The user shouldn't see a button labeled
"Next 1 Results." That's incorrect grammar. To
clean this up, prevNextResults()
contains a local variable named howMany
that uses the ternary operator once again. You'll find it
at line 163:
var howMany = (trueTop > 1 ? "s" : "");
If trueTop is greater
than 1, howMany is set to the
string "s". If trueTop
equals 1, howMany is set to the
empty string "". As you can see at line 165, howMany
is printed immediately after the word "Result."
If there is only one record in the subset, the word "Result"
appears unchanged. If there are more, however, the user
sees "Results."
The final step in both buttons is "telling"
them what to do when they are clicked. I mentioned earlier
that the onClick
events of both buttons call formatResults().
Lines 157-158 and 166-167 dynamically write the call to
formatResults() in the
onClick event handler of either button. Here is the
first set (the latter half of the document.writeln()
call):
'onClick="' + parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference - offset) + ', ' + offset + ')">');
The arguments are determined with the aid
of the ternary operator and written on the fly. Notice the
three arguments passed (once the JavaScript generates the
code) are copyArray, reference
- offset, and offset. The
"Prev" button will always get these three arguments.
By the way, notice how formatResults()
and copyArray
are written:
parent.frames[0].formatResults(...);
and:
parent.frames[0].copyArray
That may seem strange at first, but remember
that the call to formatResults()
does not happen from nav.php
(parent.frames[0]). It happens
from the results frame parent.frames[1],
which has no function named formatResults()
and no variable named copyArray.
Therefore, functions and variables need this reference.
The "Next" button gets a similar
call in the onClick event handler,
but wait a sec. Don't we have to deal with the possibility
of less than offset results in
the last results subset of copyArray
just as we did in formatResults()
when displaying the range of currently viewed results? Nope.
Function formatResults() takes
care of that decision process; all we do is add reference
to offset and pass it in. Take
a look at lines 166-167, again the latter half of the document.writeln()
method call:
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');
|
After that section, you must
have seen this one coming. The ternary operator
is pretty helpful, so here's my sermon. Ternary
operators require three operands, and they are used
throughout this app as a one-line if-else statement.
Here's the syntax straight from Netscape's JavaScript
Guide for Communicator 4.0, Chapter 9:
This conditional operator,
when properly populated, acts upon val1
if condition evaluates
to true, and val2 otherwise.
I'm making all the fuss about it because in many
cases I find it makes code easier to read and there
is usually less to write. This operator can be especially
helpful if you're coding within several nested statements.
The ternary operator is not
the cure for everything. If you have multiple things
that need to happen if condition
is true or false, take the if-else route. Otherwise,
give this a try in your code. |
The HTML
nav.php has very
little static HTML. Here it is again, starting with line
174:
</HEAD>
<BODY BGCOLOR="WHITE">
<TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER">
<TR>
<TD VALIGN=MIDDLE>
<FONT FACE="Arial">
<B>Client-Side Search Engine</B>
</TD>
<TD VALIGN=ABSMIDDLE>
<FORM NAME="search"
onsubmit="validate(document.forms[0].query.value); return false;">
<INPUT TYPE=TEXT NAME="query" SIZE="33">
<INPUT TYPE=HIDDEN NAME="standin" VALUE="">
</FORM>
</TD>
<TD VALIGN=ABSMIDDLE>
<FONT FACE="Arial">
<B><A HREF="main.php" TARGET="main">Help</A></B>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>
There aren't really any surprises. You have
a form embedded in a table. "Submitting" the form
executes the code we've been covering. The only question
you might have is: "How can the form be submitted without
a button?" As of the HTML 2.0 specification, most browsers
(including Navigator and MSIE) have enabled form submission
with a single text field form.
There's no law saying you have to do it this
way. Feel free to add a button or image to jazz it up.
Building Your Own JavaScript Database
Eventually you'll want to replace the records
I've provided with your own records. You can do this in
three easy steps.
- Open records.js
in your text editor.
- Remove the records already there
so that the file looks like this:
var
profiles = new Array( );
- For each record you want to add,
use the following syntax:
"Your_Page_Title|Your_Page_Description|http://your_page_url/file_name.php",
Add as many of these elements between the
parentheses as you want. Be sure to include the comma at
the end of each recordexcept
the last one. Notice also the page title, description,
and URL are each separated by "|"
(the pipe character). Don't use any of those in your titles,
descriptions, or URLs. That'll cause JavaScript errors.
Remember, too that if you include double quotes (")
other than the ones on the outside, be sure to escape them
with a backslash (e.g., use \" instead of just ").
Potential Extensions
The search engine is pretty useful the way
it is. What's even better is that you can make some significant
improvements or changes. Here are some possibilities:
- Make it JavaScript 1.0 compatible
- Make it harder to break
- Display banner ads
- Add refined search capabilities
- Develop cluster sets
JavaScript 1.0 Compatibility
You know it, and I know it. Both of the major
browsers are in the latter 4.x or early 5.x versions. Both
are free. But there are still people out there clunking
along with MSIE 3.02 or NN 2.x. I still get a surprising
hit count of visitors with those credentials to HotSyte--The
JavaScript Resource (http://www.serve.com/hotsyte/).
Since a search engine is pretty much a core
feature of a web site, you might consider converting this
app for JavaScript 1.0. Fortunately, all you have to do
is go through the code listed earlier, line by line, figure
out which features aren't supported in JavaScript 1.0, and
change all of them.
OK. I already did that, but admit it: I had
you going. Actually, you'll find the modified version in
/ch01/js1.0/.
Open index.php
in your browser just like you did with the original.
In this section, we'll take a quick look at what will make
the app work in JavaScript 1.0 browsers. There are three
changes:
- No JavaScript source file (a browser
issue really)
- No array sorting (with the
sort()
method)
- A workaround for the
split()
method
NN 2.x and MSIE 3.x do not support .js
source files.
The workaround for this is to embed the profiles array in