nav.html

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.jssource 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.jsin your code. In addition, I’d hate to have all that code copied into an HTML file and displayed as source code.

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 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, searchTypeis 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 searchTypeto 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 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().

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 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().

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.

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 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:

  1. Assign the number of the current record to variable reference, then print it.

  2. Add another number called offset, which is how many records to display per page (in this case, 10).

  3. 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 ).

  4. 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.

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.

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>');  }

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 onClickevents 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 + ')">');

The HTML

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.