The brains of the application lie in the header file named
nav.html
. 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.html
1 <HTML> 2 <HEAD> 3 <TITLE>Search Nav Page</TITLE> 4 5 <SCRIPT LANGUAGE="JavaScript1.1" SRC="records.js"></SCRIPT> 6 <SCRIPT LANGUAGE="JavaScript1.1"> 7 <!-- 8 9 var SEARCHANY = 1; 10 var SEARCHALL = 2; 11 var SEARCHURL = 4; 12 var searchType = ""; 13 var showMatches = 10; 14 var currentMatch = 0; 15 var copyArray = new Array(); 16 var docObj = parent.frames[1].document; 17 18 function validate(entry) { 19 if (entry.charAt(0) == "+") { 20 entry = entry.substring(1,entry.length); 21 searchType = SEARCHALL; 22 } 23 else if (entry.substring(0,4) == "url:") { 24 entry = entry.substring(5,entry.length); 25 searchType = SEARCHURL; 26 } 27 else { searchType = SEARCHANY; } 28 while (entry.charAt(0) == " ") { 29 entry = entry.substring(1,entry.length); 30 document.forms[0].query.value = entry; 31 } 32 while (entry.charAt(entry.length - 1) == " ") { 33 entry = entry.substring(0,entry.length - 1); 34 document.forms[0].query.value = entry; 35 } 36 if (entry.length < 3) { 37 alert("You cannot search strings that small. Elaborate a little."); 38 document.forms[0].query.focus(); 39 return; 40 } 41 convertString(entry); 42 } 43 44 function convertString(reentry) { 45 var searchArray = reentry.split(" "); 46 if (searchType == (SEARCHALL)) { requireAll(searchArray); } 47 else { allowAny(searchArray); } 48 } 49 50 function allowAny(t) { 51 var findings = new Array(0); 52 for (i = 0; i < profiles.length; i++) { 53 var compareElement = profiles[i].toUpperCase(); 54 if(searchType == SEARCHANY) { 55 var refineElement = compareElement.substring(0, 56 compareElement.indexOf('|HTTP')); 57 } 58 else { 59 var refineElement = 60 compareElement.substring(compareElement.indexOf('|HTTP'), 61 compareElement.length); 62 } 63 for (j = 0; j < t.length; j++) { 64 var compareString = t[j].toUpperCase(); 65 if (refineElement.indexOf(compareString) != -1) { 66 findings[findings.length] = profiles[i]; 67 break; 68 } 69 } 70 } 71 verifyManage(findings); 72 } 73 74 function requireAll(t) { 75 var findings = new Array(); 76 for (i = 0; i < profiles.length; i++) { 77 var allConfirmation = true; 78 var allString = profiles[i].toUpperCase(); 79 var refineAllString = allString.substring(0, 80 allString.indexOf('|HTTP')); 81 for (j = 0; j < t.length; j++) { 82 var allElement = t[j].toUpperCase(); 83 if (refineAllString.indexOf(allElement) == -1) { 84 allConfirmation = false; 85 continue; 86 } 87 } 88 if (allConfirmation) { 89 findings[findings.length] = profiles[i]; 90 } 91 } 92 verifyManage(findings); 93 } 94 95 function verifyManage(resultSet) { 96 if (resultSet.length == 0) { noMatch(); } 97 else { 98 copyArray = resultSet.sort(); 99 formatResults(copyArray, currentMatch, showMatches); 100 } 101 } 102 103 function noMatch() { 104 docObj.open(); 105 docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>' + 106 '<BODY BGCOLOR=WHITE TEXT=BLACK>' + 107 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' + 108 '<FONT FACE=Arial><B><DL>' + 109 '<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value + 110 '" returned no results.<HR NOSHADE WIDTH=100%>' + 111 '</TD></TR></TABLE></BODY></HTML>'); 112 docObj.close(); 113 document.forms[0].query.select(); 114 } 115 116 function formatResults(results, reference, offset) { 117 var currentRecord = (results.length < reference + offset ? 118 results.length : reference + offset); 119 docObj.open(); 120 docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE>\n</HEAD>' + 121 '<BODY BGCOLOR=WHITE TEXT=BLACK>' + 122 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' + 123 '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' + 124 '<FONT FACE=Arial><B>Search Query: <I>' + 125 parent.frames[0].document.forms[0].query.value + '</I><BR>\n' + 126 'Search Results: <I>' + (reference + 1) + ' - ' + 127 currentRecord + ' of ' + results.length + '</I><BR><BR></FONT>' + 128 '<FONT FACE=Arial SIZE=-1><B>' + 129 '\n\n<!-- Begin result set //-->\n\n\t<DL>'); 130 if (searchType == SEARCHURL) { 131 for (var i = reference; i < currentRecord; i++) { 132 var divide = results[i].split('|'); 133 docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' + 134 divide[2] + '</A>\t<DD><I>' + divide[1] + '</I><P>\n\n'); 135 } 136 } 137 else { 138 for (var i = reference; i < currentRecord; i++) { 139 var divide = results[i].split('|'); 140 docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' + 141 divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>'); 142 } 143 } 144 docObj.writeln('\n\t</DL>\n\n<!-- End result set //-->\n\n'); 145 prevNextResults(results.length, reference, offset); 146 docObj.writeln('<HR NOSHADE WIDTH=100%>' + 147 '</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>'); 148 docObj.close(); 149 document.forms[0].query.select(); 150 } 151 152 function prevNextResults(ceiling, reference, offset) { 153 docObj.writeln('<CENTER><FORM>'); 154 if(reference > 0) { 155 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset + 156 ' Results" ' + 157 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 158 (reference - offset) + ', ' + offset + ')">'); 159 } 160 if(reference >= 0 && reference + offset < ceiling) { 161 var trueTop = ((ceiling - (offset + reference) < offset) ? 162 ceiling - (reference + offset) : offset); 163 var howMany = (trueTop > 1 ? "s" : ""); 164 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop + 165 ' Result' + howMany + '" ' + 166 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 167 (reference + offset) + ', ' + offset + ')">'); 168 } 169 docObj.writeln('</CENTER>'); 170 } 171 172 //--> 173 </SCRIPT> 174 </HEAD> 175 <BODY BGCOLOR="WHITE"> 176 <TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER"> 177 <TR> 178 <TD VALIGN=MIDDLE> 179 <FONT FACE="Arial"> 180 <B>Client-Side Search Engine</B> 181 </TD> 182 183 <TD VALIGN=ABSMIDDLE> 184 <FORM NAME="search" 185 onsubmit="validate(document.forms[0].query.value); return false;"> 186 <INPUT TYPE=TEXT NAME="query" SIZE="33"> 187 <INPUT TYPE=HIDDEN NAME="standin" VALUE=""> 188 </FORM> 189 </TD> 190 191 <TD VALIGN=ABSMIDDLE> 192 <FONT FACE="Arial"> 193 <B><A HREF="main.html" TARGET="main">Help</A></B> 194 </TD> 195 </TR> 196 </TABLE> 197 </BODY> 198 </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 fileThe global variables
The functions
The HTML
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.
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.
Next, let’s look at the major functions:
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
searchType
to SEARCHALL
(that’s the default, by the way). If the user wants to search
the URL, searchType
is set
to SEARCHURL. 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()
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 searchType
.
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()
.
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 both search functions is comparing strings with
nested for loops. See the sidebar JavaScript Technique: Nested for Loops 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
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()
.
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); }
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, verifyManage()
calls
noMatch()
to bring the bad news to the user.
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()
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(); }
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 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 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
Note
Function formatResults()
is sprinkled with special
characters such as \n
and \t
.
\n
represents a newline character, which is
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.
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.
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.
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>'); }
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 only—for the first results page displayed. There aren’t any previous records.
A “Prev” button and a “Next” button—for 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 (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 an 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.html
(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 + ')">');
nav.html
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.html" 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.
Get JavaScript Application Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.