Chapter 9

Remember Where You've Been with Cookies


CONTENTS


One of the challenges of writing applications for the World Wide Web has been the inability of the Web to maintain state. That is, after a user sends a request to the server and a Web page is returned, the server forgets all about the user and the page she has just downloaded.

If the user clicks on a link, the server doesn't have background information about what page the user is coming from and, more importantly, if the user returns to the page at a later date, there is no information available to the server about the user's previous actions on the page.

Maintaining state can be important to developing complex interactive applications. Several sites work around this problem using complex server-end CGI scripts. However, Navigator 2.0 and 3.0 addresses the problem with cookies: a method of storing information locally in the browser and sending it to the server whenever the appropriate pages are requested by the user.

JavaScript provides the capability to work with client-side state information stored as cookies.

In addition to cookies, JavaScript offers the navigator object, which provides information about the version of the browser a user has and, in the future, will likely include methods to customize the browser.

The information available in the navigator object can be useful for a number of purposes, including ensuring that users are using a version of the browser that supports all the features of a script. The navigator object also includes properties for working with file types and installed plug-ins.

In this chapter, we take a detailed look at using cookies in the JavaScript applications as well as how to use the navigator object, including

What Are Cookies?

Cookies provide a method to store information at the client side and have the browser provide that information to the server along with a page request.

Note
The term cookies has no special significance. It is just a name in the same way Java is just a name for Sun's object-oriented programming language.

In order to understand how the mechanism works, it is important to have a basic understanding of how servers and clients communicate on the World Wide Web using the hypertext transfer protocol (HTTP).

HTTP and How It Works

The hypertext transfer protocol is fairly simple. When a user requests a page, an HTTP request is sent to the server. The request includes a header that defines several pieces of information, including the page being requested.

The server returns an HTTP response that also includes a header. The header contains information about the document being returned, including its MIME type (such as
text/html for a standard HTML page or image/gif for a GIF file).

These headers all contain one or more fields of information in a basic format:

Field-name: Information

Cookies and HTTP Headers

Cookie information is shared between the client browser and a server using fields in the HTTP headers. The way it works is fairly simple-in theory.

When the user requests a page for the first time, a cookie (or more than one cookie) can be stored in the browser by a Set-Cookie entry in the header of the response from the server. The Set-Cookie field includes the information to be stored in the cookie along with several optional pieces of information, including an expiry date, path, and server information, and if the cookie requires security.

Then, when the user requests a page in the future, if a matching cookie is found among all the stored cookies, the browser sends a Cookie field to the server in a request header. The header will contain the information stored in that cookie.

Cookie and Set-Cookie

The Set-Cookie and Cookie fields use a fairly simple syntax to transfer significant information between the client and server.

Set-Cookie takes the form:

Set-Cookie: name=VALUE; expires=DATE; path=PATH; domain=DOMAIN; secure

The name=VALUE entry is the only required piece of information that must be included in the Set-Cookie field. This is simply a string of characters defining information to be stored in the cookie for later transmission back to the server. The string cannot contain semicolons, commas, or spaces.

All the other entries in the Set-Cookie field are optional and are outlined in Table 9.1.

Table 9.1. Optional attributes for Set-Cookie.

NameDescription
expires=DATE Specifies the expiry date of a cookie. After this date the cookie will no longer be stored by the client or sent to the server (DATE takes the form Wdy, DD-Mon-YY HH:MM:SS GMT-dates are only stored in Greenwich Mean Time). By default, the value of expires is set to the end of the current Navigator session.
path=PATH Specifies the path portion of URLs for which the cookie is valid. If the URL matches both the path and domain, then the cookie is sent to the server in the request header. (If left unset, the value of path is the same as the document that set the cookie.)
domain=DOMAIN Specifies the domain portion of URLs for which the cookie is valid. The default value for this attribute is the domain of the current document setting the cookie.
secure Specifies that the cookie should only be transmitted over a secure link (i.e. to HTTP servers using the SSL protocol-known as HTTPS servers).

By comparison, the Cookie field in a request header contains only a set of name-value pairs for the requested URL:

Cookie: name1=VALUE1; name=VALUE2 ...

It is important to realize that multiple Set-Cookie fields can be sent in a single response header from the server.

Note
A cookie that has the same path and name as an existing cookie will overwrite the old one-this can be used as a way of erasing cookies-by writing a new one with an expiry date that has already passed.

There are some limitations on the use of cookies. Navigator will store only 300 cookies in total. Within that 300, each cookie is limited to four kilobytes in length, including all the optional attributes, and only 20 cookies will be stored for each domain. When the number of cookies is exceeded, the browser will delete the least recently used and when the length of a cookie is too long, the cookie is trimmed to fit.

Examples of How Cookies Are Used

There are several ways that cookies can be used to enhance interactive applications.

For instance, there are sites using cookies to implement shopping carts. That is, a user traverses multiple pages at a site and selects items he wants to buy. The selections are stored in cookies until a JavaScript script or CGI script is executed to total up the purchases.

Other applications that could use cookies include

Cookies and CGI Scripts

In order for cookies to be useful, it is necessary for the server to be able to take advantage of the cookie information it receives and for the server to be able to generate cookie headers if they are needed.

This is primarily done by using CGI scripts.

For instance, if you want to provide a custom search tool that would search World Wide Web indexes selected by the user, you would need to develop a system that follows this basic pattern:

  1. User calls the site by using an URL that requests a CGI script.
  2. The script checks whether it is the user's first time at the site by checking whether there is a Cookie field in the HTTP request header.
  3. If there is no cookie, the script sends back a new search page with all choices unselected and an empty search field.
  4. If there is a Cookie field, the script interprets the cookie and returns a page with all the user's previous choices selected.
  5. When the user conducts a search, the script returns the search results along with a Set-Cookie field in the header to reset the cookie to the newly selected values that the user used for the search.

This type of application could produce results similar to Figures 9.1 and 9.2.

Figure 9.1 : If the user has never visited the URL, a page with a new form is sent to the user.

Figure 9.2 : On subsequent visits, the user receivers a page in the same state as he or she last left it.

To implement this type of server-side processing for cookies may require significant increases in the load on a Web server. With this model, most pages are being built dynamically based on receiving cookie information in the header.

This is in contrast to typical Web pages, which are static, and all the server needs to do is send the correct file to the client without any additional processing.

Using Cookies in JavaScript

In JavaScript, however, cookies become available for processing by the client.

JavaScript makes the cookie property of the document object available for processing. The cookie property exposes all the attributes of cookies for the page to the script and enables the script to set new cookies. In this way much, if not all, of the server-end processing that would be done to take advantage of cookies can now be done by the client in a JavaScript script.

The cookie property simply contains a string with the value that would be sent out in a Cookie field for that page.

As a string, it can be manipulated like any other string literal or variable using the methods and properties of the string object.

Note
In Chapter 10, "Strings, Math, and the History List," we will take a detailed look at the string object and all of its methods and properties.

By assigning values to document.cookie, it is possible to create new cookies. The value of the string assigned to the cookie property should be the same as what would be sent by the server in the Set-Cookie header field.

For instance, if you create two cookies named cookie1 and cookie2 as follows:

document.cookie = "cookie1=First_cookie";
document.cookie = "cookie2=Second_cookie";

then document.write(document.cookie) would produce output that looks like this:

cookie1=First_cookie; cookie2=Second_cookie

If you want to set optional properties such as the expiry date or path, you can use a command like:

document.cookie = 'cookie1=First_cookie;
expires=Mon, 01-Jul-95 12:00:00 GMT; path="/"';

This would create a cookie named cookie1 that expires at noon on 1 July 1995 and is valid for all documents in the default domain because the path is set to the top-level directory for the domain.

Of course, times are likely to be set using offsets from the current time. For instance, you may want an expiry date one day or one year from the current date. You can use the methods of the Date object to achieve this:

expires = new Date();
expires.setTime (expires.getTime() + 24 * 60 * 60 * 365 * 1000);
document.cookie = "cookie2=Second_cookie; expires=" + expires.toGMTString();

These commands use the Date object to set a time one year after today by adding 24¥60¥60¥365¥1000 (the number of milliseconds in one year) to the current date and time. You can then use expires.toGMTString() to return the date string in GMT time as required by the cookie.

Note
setTime(), getTime(), and toGMTString() are methods of the Date() object, which is discussed in more detail later in this chapter.

Storing User Choices in Cookies

In this example, you are going to use cookies to expand the functionality of the script you created in Exercise 8.4 in the previous chapter.

In Exercise 8.4, you extended the simple color testing application to include the capability for the user to select a URL to test the colors.

In this example you further extend the script so that if a user has entered a URL, it is stored in a cookie, as are the colors. The next time the user returns to the page, the URL is recalled, loaded, and displayed with the stored colors. The expiry date for a cookie should be 30 days from the current date.

In order to achieve this, you need to do several things. You need to save the colors and URLs as cookies whenever they are changed. You also need a function that can decode the cookie when the page is loaded for the first time.

Listing 9.1 includes these additions.


Listing 9.1. Keeping track of the user's color choices.
<HTML>

<HEAD>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FORM OTHER BROWSERS

var expires = new Date();
expires.setTime (expires.getTime() + 24 * 60 * 60 * 30 * 1000);
var expiryDate = expires.toGMTString();

function display(form) {
  parent.output.document.bgColor = form.bg.value;
  parent.output.document.fgColor = form.fg.value;
  parent.output.document.linkClor = form.link.value;
  parent.output.document.alinkColor = form.alink.value;
  parent.output.document.vlinkColor = form.vlink.value;
}

function loadPage(url) {
  var toLoad = url.value;
  if (url.value == "")
    toLoad = "sample.htm";
  open (toLoad,"output");
}

function newCookie(name,value) {
  document.cookie = name + "=" + value + "; expires=" + expiryDate;
}

function getCookie(name) {
  var cookieFound = false;
  var start = 0;
  var end = 0;
  var cookieString = document.cookie;

  var i = 0;

  // SCAN THE COOKIE FOR name
  while (i <= cookieString.length) {
    start = i;
    end = start + name.length;
    if (cookieString.substring(start,end) == name) {
      cookieFound = true;
      break;
    }
    i++;
  }

  // IS name FOUND?
  if (cookieFound) {
    start = end + 1;
    end = document.cookie.indexOf(";",start);
    if (end < start)
      end = document.cookie.length;
    return document.cookie.substring(start,end);
  }
  return "";
}

// STOP HIDING SCRIPT -->
</SCRIPT>

</HEAD>

<BODY onLoad="loadPage(document.forms[0].url); display(document.forms[0]);">

<CENTER>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS

document.write('<H1>The Color Picker</H1>');
document.write('<FORM METHOD=POST>');
document.write('Enter Colors:<BR>');

var thisCookie = ((document.cookie != "") && (document.cookie != null));

var bg = (thisCookie) ? getCookie("bg") : document.bgColor;
var fg = (thisCookie) ? getCookie("fg") : document.fgColor;
var link = (thisCookie) ? getCookie("link") : document.linkColor;
var alink = (thisCookie) ? getCookie("alink") : document.alinkColor;
var vlink = (thisCookie) ? getCookie("vlink") : document.vlinkColor;
var url = (thisCookie) ? getCookie("url") : "sample.htm";

document.write('Background: <INPUT TYPE=text NAME="bg" VALUE="' + bg + '"
onChange="newCookie(this.name,this.value);"> ... ');
document.write('Text: <INPUT TYPE=text NAME="fg" VALUE="' + fg + '"
onChange="newCookie(this.name,this.value);"><BR>');
document.write('Link: <INPUT TYPE=text NAME="link" VALUE ="' + link + '"
onChange="newCookie(this.name,this.value);"> ...');
document.write('Active Link: <INPUT TYPE=text NAME="alink" VALUE="' + alink + '"
onChange="newCookie(this.name,this.value);"><BR>');
document.write('Followed Link: <INPUT TYPE="text" NAME="vlink" VALUE ="' + vlink
+ '" onChange="newCookie(this.name,this.value);"><BR>');
document.write('Test URL: <INPUT TYPE="text" SIZE=40 NAME="url" VALUE="' + url +
'" onChange="newCookie(this.name,this.value); loadPage(this);"><BR>');
document.write('<INPUT TYPE=button VALUE="TEST"
onClick="display(this.form);">');

document.write('</FORM>');

// STOP HIDING FROM OTHER BROWSERS -->
</SCRIPT>

</CENTER>

</BODY>

</HTML>

In order to use cookies to store the current state for the color tester application, you have to add two new functions (newCookie() and getCookie()) to the header, as well as alter the script that dynamically generates the HTML form in the upper frame. The other two functions remain unchanged.

The newCookie() Function

The newCookie() function is the simpler of the two new functions. It stores a cookie given a name and value as arguments. The expiryDate variable is a global variable created by taking the current date, adding 30 days, and then converting it to a string in Greenwich Mean Time using toGMTString():

var expires = new Date();
expires.setTime (expires.getTime() + 24 * 60 * 60 * 30 * 1000);
var expiryDate = expires.toGMTString();

The getCookie() Function

The getCookie() function is designed to return a particular cookie value. What makes this somewhat complicated is that document.cookie contains a string of name-value pairs separated by a semicolon followed by a space.

In order to find the particular value you want and return it, you need to do some relatively sophisticated processing on the document.cookie string.

function getCookie(name) {
  var cookieFound = false;
  var start = 0;
  var end = 0;
  var cookieString = document.cookie;

You start by declaring the variables. cookieFound is a Boolean variable, which you use to keep track of whether a name-value pair matching the argument has been found. start and end are used to hold indexes for the substring() function and cookieString holds the value of document.cookie simply because it is a little easier to read-it's my personal preference.

  var i = 0;

  // SCAN THE COOKIE FOR name
  while (i <= cookieString.length) {
    start = i;
    end = start + name.length;
    if (cookieString.substring(start,end) == name) {
      cookieFound = true;
      break;
    }
    i++;
  }

This loop is fairly simple. You use i as the counter and simply loop through cookieString character by character and check the substring starting at i that is the length of the name you're looking for. If there is a match, you set cookieFound to true and break out of the loop.

// IS name FOUND?
  if (cookieFound) {
    start = end + 1;
    end = document.cookie.indexOf(";",start);
    if (end < start)
      end = document.cookie.length;
    return document.cookie.substring(start,end);
  }

If you've found a cookie that matches the name you are looking for, then you set start to the value of end + 1-this means you are starting after the equal sign that follows the name. Next you use indexOf() to look for the semicolon that may be ending the cookie (unless it is the last cookie in the string). You store the value in end.

We will see more of the string.indexOf() method in Chapter 10 when we discuss the string object in more detail. The method takes two arguments: indexOf(string,startIndex) and starts searching for string from the index startIndex. The value returned is the index where string first occurs. If indexOf() doesn't find the character it is looking for, it returns a value of zero.

Once you have a value for end, you check if a semicolon was found. If not, you know the name-value pair is the last in the list and you can set end to the last character in cookieString, which is the value of cookieString.length.

Finally, you return the substring indicated by start and end.

  return "";
}

If you haven't found a cookie, you simply return an empty string.

The Body of the Document

All of the HTML output is done from a JavaScript script in the body of the document.

<BODY onLoad="loadPage(document.forms[0].url); display(document.forms[0]);">

<CENTER>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS

document.write('<H1>The Color Picker</H1>');
document.write('<FORM METHOD=POST>');
document.write('Enter Colors:<BR>');

Here you output the title and set up the form.

var thisCookie = ((document.cookie != "") && (document.cookie != null));

var bg = (thisCookie) ? getCookie("bg") : document.bgColor;
var fg = (thisCookie) ? getCookie("fg") : document.fgColor;
var link = (thisCookie) ? getCookie("link") : document.linkColor;
var alink = (thisCookie) ? getCookie("alink") : document.alinkColor;
var vlink = (thisCookie) ? getCookie("vlink") : document.vlinkColor;
var url = (thisCookie) ? getCookie("url") : "sample.htm";

The form elements are built dynamically so that the contents of each field match any existing cookies. If there is no cookie, then the contents of the color fields will match the defaults for the browser.

You do this by checking whether the cookie exists and then using conditional expressions to assign values to several variables, that will be used later to build the actual form elements. If cookies exist, you get the values by calling getCookie(). If not, you use the appropriate color properties of the document object, except in the case of url which you assign to a default URL.

document.write('Background: <INPUT TYPE=text NAME="bg" VALUE="' + bg + '"
onChange="newCookie(this.name,this.value);"> ... ')
document.write('Text: <INPUT TYPE=text NAME="fg" VALUE="' + fg + '"
onChange="newCookie(this.name,this.value);"><BR>');
document.write('Link: <INPUT TYPE=text NAME="link" VALUE ="' + link + '"
onChange="newCookie(this.name,this.value);"> ...');
document.write('Active Link: <INPUT TYPE=text NAME="alink" VALUE="' + alink + '"
onChange="newCookie(this.name,this.value);"><BR>');
document.write('Followed Link: <INPUT TYPE="text" NAME="vlink" VALUE ="' + vlink
+ '" onChange="newCookie(this.name,this.value);"><BR>');
document.write('Test URL: <INPUT TYPE="text" SIZE=40 NAME="url" VALUE="' + url +
'" onChange="newCookie(this.name,this.value); loadPage(this);"><BR>');
document.write('<INPUT TYPE=button VALUE="TEST"
onClick="display(this.form);">');

Once you have calculated the initial values for the form fields, you use document.write() to output the HTML for each field. In each text entry field you use onChange to store a new cookie when the user changes the value of the field. The button calls display() on a click event.

Encoding Cookies

As I mentioned earlier, the information stored in the name-value pair of a cookie cannot contain any spaces. This poses something of a limitation because many applications will need to store complete phrases or strings containing spaces in cookies.

The solution to this lies in encoding the illegal characters in a cookie. Netscape suggests using an encoding scheme such as that used in URL strings.

However, any coding scheme will work. For instance, alternative characters such as % or + could be used for spaces and a similar approach could be taken for other illegal characters, such as semicolons.

Any script that is going to build cookies using white spaces and other illegal characters, or that is going to read similar cookies, will need to include methods for dealing with these characters.

JavaScript provides the escape() and unescape() methods, which take a string as an argument. escape() returns the string encoded like an URL and unescape() translates it back from this encoding.

An easier recipe for cookies.
It should be clear now that some type of standardized method for creating new cookies, reading existing cookies, and encoding cookies would make writing scripts much easier.
Just as he wrote the hIdaho Frameset, Bill Dortch has developed a set of freely available functions to perform all these tasks. The functions are available at
http://www.hidaho.com/cookies/cookie.txt.
The source code is reproduced on the CD-ROM:
<script language="javascript">
<!-- begin script
//
//  Cookie Functions - Second Helping  (21-Jan-96)
//  Written by:  Bill Dortch, hIdaho Design <bdortch@netw.com>
//  The following functions are released to the public domain.
//
//  The Second Helping version of the cookie functions dispenses with
//  my encode and decode functions, in
favor of JavaScript's new built-in
//  escape and unescape functions,
which do more complete encoding, and
//  which are probably much faster.
//
//  The new version also extends the SetCookie function, though in
//  a backward-compatible manner, so if you used the First Helping of
//  cookie functions as they were written,
you will not need to change any
//  code, unless you want to take advantage of the new capabilities.
//
//  The following changes were made to SetCookie:
//
//  1.  The expires parameter is now optional - that is, you can omit
//      it instead of passing it null to expire the cookie at the end
//      of the current session.
//
//  2.  An optional path parameter has been added.
//
//  3.  An optional domain parameter has been added.
//
//  4.  An optional secure parameter has been added.
//
//  For information on the significance of these parameters, and
//  and on cookies in general, please refer to the official cookie
//  spec, at:
//
//      http://www.netscape.com/newsref/std/cookie_spec.html
//
//
// "Internal" function to return the decoded value of a cookie
//
function getCookieVal (offset) {
  var endstr = document.cookie.indexOf (";", offset);
  if (endstr == -1)
    endstr = document.cookie.length;
  return unescape(document.cookie.substring(offset, endstr));
}

//
//  Function to return the value of the cookie specified by "name".
//    name - String object containing the cookie name.
//    returns - String object containing the cookie value, or null if
//      the cookie does not exist.
//
function GetCookie (name) {
  var arg = name + "=";
  var alen = arg.length;
  var clen = document.cookie.length;
  var i = 0;
  while (i < clen) {
    var j = i + alen;
    if (document.cookie.substring(i, j) == arg)
      return getCookieVal (j);
    i = document.cookie.indexOf(" ", i) + 1;
    if (i == 0) break;
  }
  return null;
}

//
//  Function to create or update a cookie.
//    name - String object containing the cookie name.
//    value - String object containing the cookie value.  May contain
//      any valid string characters.
//    [expires] - Date object containing the
expiration data of the cookie.  If
//      omitted or null, expires the cookie
at the end of the current session.
//    [path] - String object indicating the path
for which the cookie is valid.
//      If omitted or null, uses the path of the calling document.
//    [domain] - String object indicating
the domain for which the cookie is
//      valid.  If omitted or null,
uses the domain of the calling document.
//    [secure] - Boolean (true/false) value
Indicating whether cookie transmission
//      requires a secure channel (HTTPS).
//
//  The first two parameters are required.  
The others, if supplied, must
//  be passed in the order listed above.  
To omit an unused optional field,
//  use null as a place holder.  
For example, to call SetCookie using name,
//  value and path, you would code:
//
//      SetCookie ("myCookieName", "myCookieValue", null, "/");
//
//  Note that trailing omitted parameters
do not require a placeholder.
//
//  To set a secure cookie for path "/myPath", that expires after the
//  current session, you might code:
//
//      SetCookie (myCookieVar, cookieValueVar, null,
"/myPath", null, true);
//
function SetCookie (name, value) {
  var argv = SetCookie.arguments;
  var argc = SetCookie.arguments.length;
  var expires = (argc > 2) ? argv[2] : null;
  var path = (argc > 3) ? argv[3] : null;
  var domain = (argc > 4) ? argv[4] : null;
  var secure = (argc > 5) ? argv[5] : false;
  document.cookie = name + "=" + escape (value) +
    ((expires == null) ? "" :
("; expires=" + expires.toGMTString())) +
((path == null) ? "" : ("; path=" + path)) +
    ((domain == null) ? "" : ("; domain=" + domain)) +
    ((secure == true) ? "; secure" : "");
}

//  Function to delete a cookie.
(Sets expiration date to current date/time)
//    name - String object containing the cookie name
//
function DeleteCookie (name) {
  var exp = new Date();
  exp.setTime (exp.getTime() - 1);  // This cookie is history
  var cval = GetCookie (name);
  document.cookie = name + "=" + cval + ";
expires=" + exp.toGMTString();
}

// end script -->
</script>
The source code should be included in the header of any document that includes scripts that work with cookies.
Although Dortch has done a good job of documenting each of the functions in the comments of the source code, we will run through them all in the next few sections.
The getCookieVal() function.
This function is an internal function called by GetCookie(). Given the index of the first character of the value of a name-value pair in a cookie, it returns the value as an unencoded string.
The function uses the unescape method to decode the value.
The GetCookie() function.
The getCookie() function is used to retrieve the value of a particular cookie. It takes the name of the cookie as an argument and returns the value. If the cookie doesn't exist, the function returns a null value.
The SetCookie() function.
This function can be used to create a new cookie or to update an existing cookie. The function requires two arguments and can take several optional arguments:
setCookie(name,value,expires,path,domain,secure)
where expires, path, domain, and secure are optional parameters, and name and value are required. Name, value, path, and domain should be strings. expires should be passed as a Date object and secure should be a Boolean value.
The order of the arguments is important, so if you want to leave out a particular value in the middle of the order, you should pass the null value as a placeholder.
The DeleteCookie() function.
This function does just what the name suggests: deletes the cookies specified by a name argument. The cookie is deleted by updating it with an expiry date equal to the current date and time.

Building a News Search Page

You are now going to use cookies to develop a more sophisticated application.

Most users of the World Wide Web are well aware that the Web can be a great source of the latest news. However, finding just the right news can be a little daunting. The process can take loading a variety of news providers' Web pages to get all the information you want.

Using a combination of cookies and frames, you are going to build an application that provides news from multiple sources in one browser window.

The concept is simple: The screen is divided into two main sections-the left side contains a form for manipulating the application, and the right side contains three frames displaying the news sources selected by the user.

In the control frame, users should be provided with three drop-down selection lists to enable them to select the news sources for each of the three frames on the right. In addition, users should be able to add news sources to the list, as well as delete sources from the list.

Cookies are used to store the list of news sources, as well as the currently selected sources for each frame.

The application is created using scripts in two files: the top-level frameset called news.htm (see Listing 9.2) and the main control file that you will call control.htm (see Listing 9.3). The file wait.htm is a placeholder that is displayed when the three news source frames on the right side are first created (see Listing 9.4).


Listing 9.2. The parent frameset (news.htm).
<!-- SOURCE CODE FOR TOP-LEVEL FRAMESET -->
<HTML>

<HEAD>
<TITLE>Example 9.2</TITLE>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS
//
//  WE NEED TO IncLUDE THE COOKIE FUncTIONS
//
//
//  Cookie Functions - Second Helping  (21-Jan-96)
//  Written by:  Bill Dortch, hIdaho Design <bdortch@netw.com>
//  The following functions are released to the public domain.
//

// "Internal" function to return the decoded value of a cookie
//
function getCookieVal (offset) {
  var endstr = document.cookie.indexOf (";", offset);
  if (endstr == -1)
    endstr = document.cookie.length;
  return unescape(document.cookie.substring(offset, endstr));
}

//
//  Function to return the value of the cookie specified by "name".
//
function GetCookie (name) {
  var arg = name + "=";
  var alen = arg.length;
  var clen = document.cookie.length;
  var i = 0;
  while (i < clen) {
    var j = i + alen;
    if (document.cookie.substring(i, j) == arg)
      return getCookieVal (j);
    i = document.cookie.indexOf(" ", i) + 1;
    if (i == 0) break;
  }
  return null;
}

//
//  Function to create or update a cookie.
//
function SetCookie (name, value) {
  var argv = SetCookie.arguments;
  var argc = SetCookie.arguments.length;
  var expires = (argc > 2) ? argv[2] : null;
  var path = (argc > 3) ? argv[3] : null;
  var domain = (argc > 4) ? argv[4] : null;
  var secure = (argc > 5) ? argv[5] : false;
  document.cookie = name + "=" + escape (value) +
    ((expires == null) ? "" : ("; expires=" + expires.toGMTString())) +
    ((path == null) ? "" : ("; path=" + path)) +
    ((domain == null) ? "" : ("; domain=" + domain)) +
    ((secure == true) ? "; secure" : "");
}

//  Function to delete a cookie. (Sets expiration date to current date/time)
//
function DeleteCookie (name) {
  var exp = new Date();
  exp.setTime (exp.getTime() - 1);  // This cookie is history
  var cval = GetCookie (name);
  document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString();
}

//
//  END OF THE COOKIE FUncTIONS. OUR SCRIPT STARTS HERE.
//

function getURL(frame) {
  var name = GetCookie(frame);
  return GetCookie(name);
}

function initialize() {
  if (GetCookie("sites") == null) {
    var expiryDate = new Date();
    expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));
    SetCookie("sites","CNN,USA-Today,Yahoo",expiryDate,"/");
    SetCookie("CNN","http://www.cnn.com/",expiryDate,"/");
    SetCookie("USA-Today","http://www.usatoday.com/",expiryDate,"/");

    SetCookie("Yahoo","http://www.yahoo.com/headlines/news/",expiryDate,"/");
    SetCookie("frameOne","CNN",expiryDate,"/");
    SetCookie("frameTwo","USA-Today",expiryDate,"/");
    SetCookie("frameThree","Yahoo",expiryDate,"/");
    SetCookie("number","3",expiryDate,"/");
  }
}



initialize();

var frameOne = getURL("frameOne");
var frameTwo = getURL("frameTwo");
var frameThree = getURL("frameThree");

// STOP HIDING HERE -->
</script>

</HEAD>

<FRAMESET COLS="35%,*" onLoad="parent.frames['frameOne'].location=frameOne;
parent.frames['frameTwo'].location=frameTwo;
parent.frames['frameThree'].location=frameThree;">
  <FRAME SRC="control.htm" NAME="control">
  <FRAMESET ROWS="33%,33%,*">
    <FRAME SRC="wait.htm" NAME="frameOne">
    <FRAME SRC="wait.htm" NAME="frameTwo">
    <FRAME SRC="wait.htm" NAME="frameThree">
  </FRAMESET>
</FRAMESET>



</HTML>


Listing 9.3. The source code for control.htm.
<!-- SOURCE CODE FOR control.htm -->
<HTML>

<HEAD>
<TITLE>Example 9.3</TITLE>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS
//
//  WE NEED TO IncLUDE THE COOKIE FUncTIONS
//
//
//  Cookie Functions - Second Helping  (21-Jan-96)
//  Written by:  Bill Dortch, hIdaho Design <bdortch@netw.com>
//  The following functions are released to the public domain.
//

// "Internal" function to return the decoded value of a cookie
//
function getCookieVal (offset) {
  var endstr = document.cookie.indexOf (";", offset);
  if (endstr == -1)
    endstr = document.cookie.length;
  return unescape(document.cookie.substring(offset, endstr));
}

//
//  Function to return the value of the cookie specified by "name".
//
function GetCookie (name) {
  var arg = name + "=";
  var alen = arg.length;
  var clen = document.cookie.length;
  var i = 0;
  while (i < clen) {
    var j = i + alen;
    if (document.cookie.substring(i, j) == arg)
      return getCookieVal (j);
    i = document.cookie.indexOf(" ", i) + 1;
    if (i == 0) break;
  }
  return null;
}

//
//  Function to create or update a cookie.
//
function SetCookie (name, value) {
  var argv = SetCookie.arguments;
  var argc = SetCookie.arguments.length;
  var expires = (argc > 2) ? argv[2] : null;
  var path = (argc > 3) ? argv[3] : null;
  var domain = (argc > 4) ? argv[4] : null;
  var secure = (argc > 5) ? argv[5] : false;
  document.cookie = name + "=" + escape (value) +
    ((expires == null) ? "" : ("; expires=" + expires.toGMTString())) +
    ((path == null) ? "" : ("; path=" + path)) +
    ((domain == null) ? "" : ("; domain=" + domain)) +
    ((secure == true) ? "; secure" : "");
}

//  Function to delete a cookie. (Sets expiration date to current date/time)
//
function DeleteCookie (name) {
  var exp = new Date();
  exp.setTime (exp.getTime() - 1);  // This cookie is history
  var cval = GetCookie (name);
  document.cookie = name + "=" + cval + "; expires=" + exp.toGMTString();
}

//
//  END OF THE COOKIE FUncTIONS. OUR SCRIPT STARTS HERE.
//

function getURL(frame) {
  var name = GetCookie(frame);
  return GetCookie(name);
}

var expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));

var number = parseInt(GetCookie("number"));
var siteList = GetCookie("sites");
var sites = new createArray(number);
sites = extractSites(siteList,number);

function createArray(num) {
  for (var i=1; i <= num; i++)
    this[i] = "";
  this.length = num;
}

function extractSites(list,num) {
  var results = new createArray(num);
  var first = 0;
  var last = 0;
  for (var i = 1; i <= num; i ++) {
    first = (i == 1) ? 0 : last+1;
    last = (i == num) ? list.length : list.indexOf(",",first+1);
    results[i] = list.substring(first,last);
  }
  return results;
}

function makeList() {
  var result = "";
  for (var i = 1; i <= number; i++) {
    result += sites[i];
    result += (i == number) ? "" : ",";
  }
  return result;
}

function getList(frame) {
  var result = '<SELECT NAME="' + frame + '" onChange="loadURL(this);">';
  for (var i = 1; i<=number; i++) {
    result += '<OPTION';
    result += (GetCookie(frame) == sites[i]) ? " SELECTED" : "";
    result += ">" + sites[i] + "\n";
  }
  result += "</SELECT>";
  return result;
}

function addURL(form) {
  if ((form.name.value == "") || (form.name.value == null)) {
    returnl
  }
  var name = form.name.value;
  var url = form.url.value;
  SetCookie(name,url,expiryDate,"/");
  sites[++number] = name;
  SetCookie("sites",makeList(),expiryDate,"/");
  SetCookie("number",number,expiryDate,"/");
  window.open("control.htm","control");
}

function deleteURL(form) {
  var name = form.name.value;
  var gone = false;
  for (var i=1; i<=number; i++) {
    if (sites[i] == name) {
      gone = true;
      number--;
      for (var j=i; j<=number; j++) {
        sites[j] = sites[j+1];
      }
      sites[number+1] = null;
      break;
    }
  }
  if (gone) {
    SetCookie("number",number,expiryDate,"/");
    SetCookie("sites",makeList(),expiryDate,"/");
    var today = new Date();
    SetCookie(name,GetCookie(name),today,"/");
  }
  window.open("control.htm","control");
}

function loadURL(field) {
  var frame = field.name;
  var index = field.selectedIndex;
  var name = field.options[index].text;
  var url = GetCookie(name);
  window.open(url,frame);
  SetCookie(frame,name,expiryDate,"/");
}

// Set things up before building forms
var oneList = "";
var twoList = "";
var threeList = "";

oneList = getList("frameOne");
twoList = getList("frameTwo");
threeList = getList("frameThree");

// STOP HIDING HERE -->
</script>

</HEAD>

<BODY>

<H1>The<BR>News<BR>Source</H1>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS

document.write("<FORM METHOD=POST>");
document.write("Source One:");
document.write(oneList);
document.write("</FORM>");
document.write("<FORM METHOD=POST>");
document.write("<BR>");
document.write("Source Two:")
document.write(twoList);
document.write("</FORM>");
document.write("<FORM METHOD=POST>");
document.write("<BR>");
document.write("Source Three:");
document.write(threeList);
document.write("</FORM>");

// STOP HIDING -->
</SCRIPT>

<BR>
<FORM METHOD=POST>

Name:
<INPUT TYPE="text" NAME="name">
<BR>
URL:
<INPUT TYPE="text" NAME="url">
<BR>
<INPUT TYPE="button" VALUE="Add URL" onClick="addURL(this.form);">
<BR>
<INPUT TYPE="button" VALUE="Delete URL" onClick="deleteURL(this.form);">

</FORM>

</BODY>

</HTML>


Listing 9.4. Creating a Wait message.
<!-- SOURCE CODE FOR wait.htm -->
<HTML>

<BODY>
<H1>Please Wait ...</H1>
</BODY>

</HTML>

The results should look like Figures 9.3 and 9.4.

Figure 9.3 : Using cookies to create a useroriented custom news sources Web page.

Figure 9.4 : Users can add a new URL and automatically update selection lists.

This program is somewhat complex in that it uses dynamically generated HTML, both in the parent frameset and the main HTML file in the control frame. Both files include Bill Dortch's cookie functions because the scripts in both files need to access the cookies.

In order to understand how the application works, you need to understand how you are using cookies to store all the information.

You need to keep track of the following information:

All this information is stored in cookies. You keep an optionName=url cookie for each option. In addition, you have three cookies of the form frameName=optionName for each of the frames. The list of all names is stored in the form sites=optionName1,optionName2,optionName3, and so on, where the list is comma-separated and encoded using Bill Dortch's functions. The number of options is stored in the cookie number=numberOfOptions.

In the parent frameset, you have written two functions of your own, as well as building the HTML for the entire frameset using the document.write() method.

function getURL(frame) {
  var name = GetCookie(frame);
  return GetCookie(name);
}

The getURL() function accepts a frame name as a parameter. It first gets the name of the option for the frame from the appropriate cookie and then gets the URL for that option name with another call to GetCookie(). The URL is returned.

function initialize() {
  if (GetCookie("sites") == null) {
    var expiryDate = new Date();
    expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));
    SetCookie("sites","CNN,USA-Today,Yahoo",expiryDate,"/");
    SetCookie("CNN","http://www.cnn.com/",expiryDate,"/");
    SetCookie("USA-Today","http://www.usatoday.com/",expiryDate,"/");


    SetCookie("Yahoo","http://www.yahoo.com/headlines/news/",expiryDate,"/");
    SetCookie("frameOne","CNN",expiryDate,"/");
    SetCookie("frameTwo","USA-Today",expiryDate,"/");


    SetCookie("frameThree","Yahoo",expiryDate,"/");
    SetCookie("number","3",expiryDate,"/");
  }
}

The initialize() function is the first function called in the script. It simply checks whether any sites are currently stored as cookies using if (GetCookies("sites") == null). If there are no sites stored, the function defines an initial list of three options and stores all the relevant information in the appropriate cookies.

After calling initialize() to ensure you have sites stored in cookies, you use getURL() to extract the URLs for the three news frames from the cookies.

Finally, the three URLs are loaded into their respective frames by using an onLoad event handler in the FRAMESET tag. This ensures that all the frames are loaded and ready when you attempt to open the URLs in them. The parent frameset itself divides the right-hand column into three frames where you load wait.htm as a placeholder until the various news sources begin to load. In the left-column frame you load control.htm, which is the main application:

<FRAMESET COLS="35%,*" onLoad="parent.frames['frameOne'].location=frameOne;
parent.frames['frameTwo'].location=frameTwo;
parent.frames['frameThree'].location=frameThree;">
  <FRAME SRC="control.htm" NAME="control">
  <FRAMESET ROWS="33%,33%,*">
    <FRAME SRC="wait.htm" NAME="frameOne">
    <FRAME SRC="wait.htm" NAME="frameTwo">
    <FRAME SRC="wait.htm" NAME="frameThree">
  </FRAMESET>
</FRAMESET>

Once the frameset is built, you use getURL() to extract the URLs for the three news frames from the cookies. Then you use window.open() to open the appropriate URL in each frame.

The file control.htm (refer back to Listing 9.3) is where all the interactive work of the application takes place. The file includes several HTML forms that provide the three drop-down selection lists-one for each of the three news frames-and a simple form to add and remove URLs from the list.

In order to implement all of the functionality you need, control.htm includes several additional functions in the header of the file.

var expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));

var number = parseInt(GetCookie("number"));
var siteList = GetCookie("sites");
var sites = new createArray(number);
sites = extractSites(siteList,number);

function createArray(num) {
  for (var i=1; i <= num; i++)
    this[i] = "";
  this.length = num;
}

You start by setting up all the global variables you are going to need throughout the script. expiryDate is set to one year after the current date and is used whenever you create new cookies or update existing ones.

The variable number contains the number of options in the selection lists and is initially extracted from the appropriate cookie using GetCookie(). Also, siteList is the string of option names from the sites cookie. It is passed to extractSites to fill up the array sites, which is used throughout the script to reference option names.

You have used a standard createArray() type function to build the array sites.

function extractSites(list,num) {
  var results = new createArray(num);
  var first = 0;
  var last = 0;
  for (var i = 1; i <= num; i ++) {
    first = (i == 1) ? 0 : last+1;
    last = (i == num) ? list.length : list.indexOf(",",first+1);
    results[i] = list.substring(first,last);
  }
  return results;
}

The extractSites() function accepts two arguments: the string of comma-separated option names and the number of options in the list. It returns an array of option names.

The real work of the function all takes place in the for loop. The loop is repeated once for each of the options in the list. The first step is to figure out the index of the first character of the next option name in the list. The command first = (i == 1) ? 0 : last+1; does this by checking whether the loop counter is still at the start (equal to 1). If it is, the value of first is zero; otherwise, it is one character past the last character of the previous name, which was stored in last.

The index of the last character is calculated in a similar way using the command:

last = (i == num) ? list.length : list.indexOf(",",first+1);

This command checks whether the counter is at its final value. If it is, then last should be set to the last character in the string using list.length. Otherwise, the indexOf() method is used to find the next comma in the string.

These two lines are a good example of using conditional expressions to write what would otherwise be a set of bulkier if-else statements.

Once first and last are calculated, then the next entry in the array is set using list.substring() to extract the option name from the string.

Finally, the array results is returned as the result of the function.

function makeList() {
  var result = "";
  for (var i = 1; i <= number; i++) {
    result += sites[i];
    result += (i == number) ? "" : ",";
  }
  return result;
}

makeList() is used to perform exactly the opposite function of extractSites(). Given the array of option names stored in the global array sites, it returns a single string containing a comma-separated list of options.

This is done, again, in a single for loop that loops through each entry in the array and adds it to the result variable using the += concatenation operator. The command

result += (i == number) ? "" : ",";

uses a conditional expression to make sure that a comma is added only after an entry from the array, if it is not the last entry in the array.

function getList(frame) {
  var result = '<SELECT NAME="' + frame + '" onChange="loadURL(this);">';
  for (var i = 1; i<=number; i++) {
    result += '<OPTION';
    result += (GetCookie(frame) == sites[i]) ? " SELECTED" : "";
    result += ">" + sites[i] + "\n";
  }
  result += "</SELECT>";
  return result;
}

The getList() function is used to build the drop-down selection menus for each of the three frames based on the current cookie settings. The function accepts a frame name as an argument and returns, as a single string, the SELECT HTML container, complete with all options set and the appropriate one preselected.

The function uses the frame argument to correctly set the NAME attribute of the SELECT tag. The onChange event handler contains the function call to load a new URL when the user chooses a new option.

The for loop in the function builds the list of options. It does this by looping through each option and adding an OPTION tag to the string. It then uses sites[i] to add the name to be displayed for each option. The command

result += (GetCookie(frame) == sites[i]) ? " SELECTED" : "";

checks if the current option is stored in the cookie for the frame. If it is, then the SELECTED attribute is added to the OPTION tag so that that option will appear selected initially.

function addURL(form) {
  if ((form.name.value == "") || (form.name.value == null)) {
   returnl
  }
  var name = form.name.value;
  var url = form.url.value;
  SetCookie(name,url,expiryDate,"/");
  sites[++number] = name;
  SetCookie("sites",makeList(),expiryDate,"/");
  SetCookie("number",number,expiryDate,"/");
  window.open("control.htm","control");
}

addURL() is invoked when the user clicks on the Add URL button near the bottom of the frame. It adds a new entry to the selection lists for each frame.

Given the form object as an argument, the addURL() function extracts the name and URL from the fields in the form and then sets the cookie for that name, as well as updates the sites array and stores new sites and number cookies.

Notice the use of the unary increment operator in sites[++number] = name; to increment the value of the number variable before using it as an index to sites. This effectively adds a new entry to sites before the next line:

SetCookie("sites",makeList(),expiryDate,"/");

This line updates the sites cookie by calling makeList() to build a comma-separated string out of the updated array.

Finally, the function reloads control.htm into control to rebuild the selection menus.

function deleteURL(form) {
  var name = form.name.value;
  var gone = false;
  for (var i=1; i<=number; i++) {
    if (sites[i] == name) {
      gone = true;
      number--;
      for (var j=i; j<=number; j++) {
        sites[j] = sites[j+1];
      }
      sites[number+1] = null;
      break;
    }
  }
  if (gone) {
    SetCookie("number",number,expiryDate,"/");
    SetCookie("sites",makeList(),expiryDate,"/");
    var today = new Date();
    SetCookie(name,GetCookie(name),today,"/");
  }
  window.open("control.htm","control");
}

In this excerpt, the deleteURL() function removes an entry from the list of options.

This process is actually a bit more complicated than adding a new URL to the list because a little bit of work needs to be done to remove an entry from the middle of the array and then close up the hole that this creates in the array.

The function uses a for loop to move through the array. The if statement checks whether the current entry matches the one you want to delete.

If there is a match, then the work begins. gone is set to true so that later in the function you know a match was found-after all, the user could incorrectly type the name of the entry to delete. Next, number is decreased to reflect the fact that the number of entries in the list will decrease by one. Another for loop is used to count from the current index of the entry you are deleting to the new value of number (that is, one before the current last entry in the array).

The command sites[j] = sites[j+1]; copies the array entry immediately following the current entry into the current entry. In this way, you fill in the hole created by removing an entry.

Finally, after the for loop finishes, you set the previous last entry to the null value with sites[number+1] = null;.

Once you finish the for loop, if you have found an entry to delete, you update the sites and number cookies just as you did in addURL() and then you remove the cookie for the deleted entry by updating it with an expiry date equal to the current date and time.

function loadURL(field) {
   var frame = field.name;
   var url = GetURL(frame);
   open(url,frame);
   SetCookie(frame,name,expiryDate,"/");
}

The loadURL() function is invoked when the user changes the value of one of the selection lists. It receives the field object for the selection list as an argument.

Based on this information, it can extract the frame name, which is actually the NAME value of the selection element. Using this information, you can call getURL() to get the URL for that frame.

Once you have this information, you can open the URL in the frame and then update the cookie for that frame to reflect the new selection by the user.

// Set things up before building forms
var oneList = "";
var twoList = "";
var threeList = "";

oneList = getList("frameOne");
twoList = getList("frameTwo");
threeList = getList("frameThree");

The last thing you do in the header of the document is set up three variables containing the selection lists for the three different news source frames by calling getList().

document.write("<FORM METHOD=POST>");
document.write("Source One:");
document.write(oneList);
document.write("</FORM>");
document.write("<FORM METHOD=POST>");
document.write("<BR>");
document.write("Source Two:")
document.write(twoList);
document.write("</FORM>");
document.write("<FORM METHOD=POST>");
document.write("<BR>");
document.write("Source Three:");
document.write(threeList);
document.write("</FORM>");

In the body of the document, you use scripts to build each of the three drop-down selection lists. Each list is a separate form, but could just as easily have been a single form.

<FORM METHOD=POST>

Name:
<INPUT TYPE="text" NAME="name">
<BR>
URL:
<INPUT TYPE="text" NAME="url">
<BR>
<INPUT TYPE="button" VALUE="Add URL" onClick="addURL(this.form);">
<BR>
<INPUT TYPE="button" VALUE="Delete URL" onClick="deleteURL(this.form);">

</FORM>

The last element in the document is the form used to add and delete entries from the list of options. The form contains two text entry fields and two buttons that invoke either addURL() or deleteURL() in their onClick event handlers.

The navigator Object

As mentioned at the beginning of this chapter, the navigator object makes information about the current version of Navigator available to scripts.

The properties of the navigator object are outlined in Table 9.2.

Table 9.2. Properties of the navigator object.

NameDescription
appName The name of the application in which the page is loaded represented as a string (i.e. "Netscape").
AppVersion The version information of the current browser as a string in the form "2.0 (Win16; I)" where 2.0 is the version number, Win16 is the platform, and I indicates the international version (as opposed to U for the domestic version).
appcodeName The code name of the current browser (i.e. "Mozilla").
MimeTypes An array of objects reflecting the MIME types supported by the browser. The mimeTypes property is discussed in more detail in Chapter 14.
Plugins An array of objects reflecting the plug-ins installed in the browser. The plugins property is discussed in more detail in Chapter 14.
UserAgent The user agent for the current browser as a string in the form "Mozilla/ 2.0 (Win16; I)".

In order to understand the significance of this information, it is important to understand the concept of the user agent. In the initial communication between the client and the server during an HTTP request, the browser sends the user agent string to the server. That information becomes available on the server for a number of uses, including processing by CGI scripts or for delivering specific versions of the pages based on the nature of the client browser.

The user agent information has two parts separated by a slash: a code name for the browser and the version information for the browser. This is the information stored in the appcodeName and appVersion properties.

Using the navigator Properties

At first, it may seem as though this information serves little practical use-but it can be very useful.

For instance, during the beta development of the Navigator 2.0 and 3.0 browser, releases of new versions of the betas were quite frequent. Each version supported new features of JavaScript and fixed problems with earlier implementations, which sometimes created incompatibilities.

Many page authors are now using the properties of the navigator object to check whether the browsers being used will support the features used in the script. If not, users are alerted so that they don't try to run the script and get errors-or even find Netscape, or their pcs, crashing.

For example, if a page should only be run with Navigator 2.0 beta 6a on any platform and beta 6a is the latest version, the HTML file should look like this:

<HTML>

<HEAD>
<TITLE>navigator Example</TITLE>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS

function checkBrowser() {
  if ((navigator.appVersion.substring(0,6) != "2.0b6a") &&
(navigator.appName != "Netscape))
alert("Please use version 2.0b6a of the Netscape Navigator
web browser with this page.");
}


Rest of script

// STOP HIDING FROM OTHER BROWSERS -->
</SCRIPT>

</HEAD>

<BODY onLoad="checkBrowser();">

HTML code

</BODY>

</HTML>

Similarly, in the future, if different browsers support JavaScript, they may offer different features or additional objects, and the use of the navigator object can help script authors ensure that their scripts run on the largest number of browsers while also taking advantage of the unique features of each.

Summary

In this chapter, you learned about an extremely useful feature of JavaScript: the cookie property.

Using the cookie property, scripts can set and read cookies that store state information in the client browser. Using cookies, you can retain information between sessions to produce applications that outlive the currently loaded document and even the current browser session.

Once again, Bill Dortch has provided the JavaScript community with a set of freely available functions that make setting and retrieving cookies easier.

In addition to cookies, you took a look at the navigator object and how it can be used to ensure that users are using the appropriate browser for your scripts.

In the next chapter, we take a look at a variety of objects and features of JavaScript that we haven't covered in detail yet, including the string object, the Math object, and the history object.

Commands and Extensions Review

Command/ExtensionType Description
Set-Cookie HTTP headerSets cookies in the client browser-part of an HTTP response header
Cookie HTTP headerReturns cookies to the server-part of an HTTP request header
expires Set-Cookie attribute Indicates the expiry date for a cookie (in GMT)
path Set-Cookie attribute Used to set the path for files applicable to a cookie
domain Set-Cookie attribute Used to set the domain for files applicable to a cookie
secure Set-Cookie attribute Specifies that a cookie should be transmitted only on secure links
cookie JavaScript propertyString containing the value of cookies for the current document
indexOf() JavaScript methodA method of the string object that returns the index of the next occurrence of a substring in a string
escape() JavaScript methodEncodes a string using URL encoding
unescape() JavaScript methodDecodes a string encoded using URL encoding
appName JavaScript propertyThe name of the browser application as a string
appVersion JavaScript propertyThe version number and platform of the browser as a string
appcodeName JavaScript propertyThe code name of the browser as a string
mimeTypes JavaScript propertyAn array of objects reflecting the MIME types supported by the browser
plugins JavaScript propertyAn array of objects reflecting the plug-ins installed in the browser
userAgent JavaScript propertyThe user agent string for the browser
link() JavaScript methodMethod of the string object that encloses the string in an <A> HTML tag
fontcolor() JavaScript methodMethod of the string object that sets the HTML font color for the string

Q&A

QHow can I be sure that other people's applications and scripts won't overwrite the cookies that I have created?
AIf you are setting cookies with the specific path of your document, then it is possible for other documents to create cookies with the same name, domain, and path. However, these cookies do not overwrite your cookies. Instead, document.cookies will contain two entries with the same name, but different values. Only a script in the file in the same original location can overwrite cookies created by that file.
QCan I rely on cookies to store information vital to my application between sessions?
ANot really. While most users will not delete their browsers or change browsers between sessions-which would mean the loss of cookie information-there are too many variables to be sure that your cookies still will be present when the user next returns to your page. For instance, cookies could have been added exceeding the limits, and your cookies could be the ones that get deleted. It is generally good to write your scripts in such a way that if the cookies you are looking for no longer exist, you can perform alternate actions and recreate the cookies.
QWhy does the navigator object have both the appName and appcodeName properties? They seem to be pretty much the same thing in different forms.
AWhile it's true that generally you can glean the same information from both properties (such as Mozilla for Netscape, and so on), it is not always the case that you will know that the code name for Lynx or the application name for a less well-known browser. Having both properties, you have the maximum information available to determine the client browser.

Exercises

  1. A user loads the page http://sample.page/sample/file.html on 1 January 1996 at noon (GMT), and a script in it contains the following lines:

    document.cookie = "user=joe; expires=Wed,
    31-Jan-96 00:00:00 GMT; path=/";
    document.cookie = "message=hello";
    document.cookie = "food=lasagna; expires=Wed, 31-Jan-96 00:00:00 GMT;
    domain=another.site; path=/anotherpath/";
    document.cookie = "color=blue; expires=Tue, 02-Jan-96 12:00:00 GMT;
    domain=sample.page; path=/sample/";

    If the user accesses the following pages at the following times, what will be displayed by the command document.write(document.cookie):

    a.   http://sample.page/otherfile.html on 3 January
    b.   http://sample.page/sample/file.html on 31 January at noon (GMT)
    c.   http://another.site/anotherfile.html on 31 January at 11:59 a.m.
  2. Extend your script in Listings 9.2 through 9.4 (the news sources example) so that some sort of error checking takes place. If the user tries to add an entry with no URL, the user should get an error. In addition, if the user tries to add more than 10 entries to the list, prompt the user for the name of an entry to replace and check the name to make sure it is valid before proceeding.
  3. Design a page that asks questions of first-time visitors (or those who haven't visited for more than 30 days). Ask for their favorite color and their favorite food (from a list of options) and then customize the Web page to include the specified background color and a picture of the food indicated.
    Every time users come to the page within 30 days of their last visit, build the specified page without asking for the information.

Answers

  1. The following strings would be displayed:

    a.   user=joe.
    b.   There are no cookies-user expired at midnight, 12 hours earlier.
    c.   There are no cookies-The only cookie set for this site is at a lower level path than the user is accessing.
  2. All the changes are needed in the addURL() function:

    function addURL(form) {
      var name = form.name.value;
      var url = form.url.value;
      if ((name == "") || (name == null) || (url == "") ||
    (url == null)) {
    alert ("Please Enter both a name and URL.");
        form.name.focus();
        return;
      }
      if (number == 10) {
        var delete = prompt("Cannot enter more than 10 items.\n
    Enter the name of an entry to replace or enter nothing
    to stop adding" + name + ".","");
    if ((delete == "") || (delete == null)) {
          form.name.focus();
          return;
        }
        var i=1;
        while (i <= number) {
          if (sites[i] == delete) {
            form.name.value = delete;
            deleteURL(form);
            form.name.value = name;
            break;
          }
          if (i == number) {
            delete = prompt("No such entry to delete. Cannot enter more
    than 10 items.\nEnter the name of an entry to replace or
    enter nothing to stop adding" + name + ".","");

    i = 0;
          }
          i++;
        }
      }
      SetCookie(name,url,expiryDate,"/");
      sites[++number] = name;
      SetCookie("sites",makeList(),expiryDate,"/");
      SetCookie("number",number,expiryDate,"/");
      window.open("control.htm","control");
    }

    You can add the following elements to the function to perform the necessary error checking and to make sure the user doesn't enter too many elements.

    if ((name == "") || (name == null) || (url == "") || (url == null)) {
        alert ("Please Enter both a name and URL.");
        form.name.focus();
        return;
      }

    This if statement checks whether either field is the empty string or the null value. If the result is true, then an alert message is displayed, focus is returned to the form, and the function ends with the return statement.

    The next if statement handles the 10-item limitation on the list. The work that takes place if there are already 10 entries in the list is a bit more complex than the previous if statement.

        var delete = prompt("Cannot enter more than 10 items.\n
    Enter the name of an entry to replace or enter nothing
    to stop adding" + name + ".","");

    The first step is to ask users what to do because there are too many entries already. They can either provide an item to replace or they can cancel the addition. The user's selection is stored in the delete variable.

    if ((delete == "") || (delete == null)) {
          form.name.focus();
          return;
        }

    Once the user responds, the script immediately checks whether the user has decided to cancel the addition of the new entry by entering nothing in the prompt dialog box. If the user is canceling, then focus is returned to the form, and the function exits with the return statement.

    var i=1;
        while (i <= number) {
          if (sites[i] == delete) {
            form.name.value = delete;
            deleteURL(form);
            form.name.value = name;
            break;
          }

    The while loop is used to check whether the entry the user wants to replace actually exists. This is done by looping through each entry in the array. The first step, then, is to check whether the current entry matches the user's selection. If it does, then the value of delete is temporarily stored in the name field of the form, deleteURL() is called, and then the name field is returned to its previous value. The break statement ends the while loop.

          if (i == number) {
            delete = prompt("No such entry to delete. Cannot enter more
    than 10 items.\nEnter the name of an entry to replace or
    enter nothing to stop adding" + name + ".","");
    i = 0;
          }

    The next step in the loop is to see if you have exhausted all the entries in the list. You check this with the condition if (i == number). If the last entry has been reached without a match in the previous if statement, then the user is again prompted to enter an item to replace or to enter an empty string. Then the counter is reset to start checking again.
  3. The following script achieves the desired effect. The output looks like Figures 9.5 and 9.6.

Figure 9.5 : The script prompts the first-time visitor for the color and food information.

Figure 9.6 : On subsequent visits, information stored in the cookies automatically formats the page.

<HTML>

<HEAD>
<TITLE>Exercise 9.3</TITLE>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS
//
//  WE NEED TO IncLUDE THE COOKIE FUncTIONS
//
//
//  Cookie Functions - Second Helping  (21-Jan-96)
//  Written by:  Bill Dortch, hIdaho Design <bdortch@netw.com>
//  The following functions are released to the public domain.
//
// "Internal" function to return the decoded value of a cookie
//
function getCookieVal (offset) {
  var endstr = document.cookie.indexOf (";", offset);
  if (endstr == -1)
    endstr = document.cookie.length;
  return unescape(document.cookie.substring(offset, endstr));
}

//
//  Function to return the value of the cookie specified by "name".
//
function GetCookie (name) {
  var arg = name + "=";
  var alen = arg.length;
  var clen = document.cookie.length;
  var i = 0;
  while (i < clen) {
    var j = i + alen;
    if (document.cookie.substring(i, j) == arg)
      return getCookieVal (j);
    i = document.cookie.indexOf(" ", i) + 1;
    if (i == 0) break;
  }
  return null;
}

//
//  Function to create or update a cookie.
//
function SetCookie (name, value) {
  var argv = SetCookie.arguments;
  var argc = SetCookie.arguments.length;
  var expires = (argc > 2) ? argv[2] : null;
  var path = (argc > 3) ? argv[3] : null;
  var domain = (argc > 4) ? argv[4] : null;
  var secure = (argc > 5) ? argv[5] : false;
  document.cookie = name + "=" + escape (value) +
    ((expires == null) ? "" : ("; expires=" +
Âexpires.toGMTString())) +
((path == null) ? "" : ("; path=" + path)) +
    ((domain == null) ? "" : ("; domain=" + domain)) +
    ((secure == true) ? "; secure" : "");
}

//  Function to delete a cookie.
Â(Sets expiration date to current date/time)
//
function DeleteCookie (name) {
  var exp = new Date();
  exp.setTime (exp.getTime() - 1);  // This cookie is history
  var cval = GetCookie (name);
  document.cookie = name + "=" + cval + ";
Âexpires=" + exp.toGMTString();
}

//
//  END OF THE COOKIE FUncTIONS. OUR SCRIPT STARTS HERE.
//

var expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (30 * 24 * 60 * 60 * 1000));

var food = new createArray(3);
food[1] = "Beets";
food[2] = "Jello-Pudding";
food[3] = "Cockroaches";

function createArray(num) {
  this.length = num;
  for (var i = 1; i <= num; i++)
    this[i] = "";
}

function listArray(stuff) {
  var result = "";
  for (var i = 1; i <= stuff.length; i++)
    result += i + ". " + stuff[i] + "/";
  return result;
}

var color = "";
var favFood = "";

function initialize() {
  if (GetCookie("color") == null) {
    color = prompt("Enter your favorite Netscape color.","A Color");
    SetCookie("color",color,expiryDate);
    var foodNum = prompt("Food: - " + listArray(food),"0");
    favFood = food[foodNum];
    SetCookie("food",favFood,expiryDate);
  } else {
    color = GetCookie("color");
    SetCookie("color",color,expiryDate);
    favFood = GetCookie("food");
    SetCookie("food",favFood,expiryDate);
  }
}

// STOP HIDING HERE -->
</SCRIPT>

</HEAD>

<SCRIPT LANGUAGE="JavaScript">
<!-- HIDE FROM OTHER BROWSERS

initialize();

document.write('<BODY BGCOLOR="' + color + '">');

document.write("<CENTER>")

document.write('<IMG SRC="' + favFood + '.gif">');

// STOP HIDING -->
</SCRIPT>

<H1>Welcome to the favorite food and color page</H1>

</CENTER>

</BODY>

</HTML>

The logic behind this script is rather simple, but it provides an example of how to rebuild cookies after they expire and how to keep cookies current when it is relevant to do so.

The createArray() and listArray() functions should be obvious. listArray() returns a string containing all the items in an array numerically listed by their index numbers in the array. This is used to prompt users for their food choices.

The initialize() function is where all the work takes place. The function checks for the existence of the color cookie. If the cookie doesn't exist, the program prompts users for the food and color information and stores it in the appropriate cookies, as well as setting the color and favFood global variables for use in the body of the document.

If the cookies exist, the values are loaded into color and favFood and then the cookies are updated so that their expiry dates get reset to 30 days into the future.

In the body of the document, a script is used to set the BGCOLOR attribute of the BODY tag and the SRC attribute of the IMG tag based on the color and favFood variables.