Previous Page TOC Next Page See Page



- 10 -
Applying Cookies and Frames


Cookies and frames are two very powerful features of the Netscape Navigator which, when used effectively in JavaScript, can greatly enhance Web interfaces and enable the deployment of applications using a Web browser as a delivery agent.

In this chapter you will see the development of an application that applies the information learned in the previous chapters about Cookies and frames.

The application is a collapsible menu manager. Increasingly, Web sites provide hierarchical menus of their sites that can be expanded and collapsed in much the same way as Windows 95's Explorer file lists or Windows 3.1's File Manager.

These menus can be implemented in two ways: at the server-end, using CGI-BIN scripts to dynamically build each new page as the user collapses or expands sub-menus, or at the client-end, using JavaScript to refresh the page each time the user selects a new entry.

In building this application, you will see examples of the following:


Creating a Menu Manager


The menu manager developed in this chapter is designed to enable Web authors to implement collapsible hierarchical menus similar to the system used to display directory structures in Windows 95's Explorer or Windows 3.1's File Manager.

For example, in Windows 95, a system of plus and minus signs next to each directory name allows it to be collapsed or expanded. When collapsed, no sub-directories under the higher-level directory are displayed. When expanded, the first level of sub-directories under a directory are displayed. Figures 10.1 and 10.2 illustrate how this is implemented in Windows 95's Explorer.

Figure 10.1. When directories are collapsed in Explorer, no sub-directories are displayed.

Figure 10.2. If a directory is expanded, the first level of sub-directories are displayed and the plus sign next to the parent directory changes to a minus sign.

For the menu manager, you want to implement a simpler graphical interface with either an "expand" or "collapse" link next to menu entries as appropriate. Figure 10.3 shows how this appears in Netscape Navigator.

Figure 10.3. The menu manager uses links for the expand and collapse controls (expand controls shown here).

Each entry on the menu is a link—after all, that's what a menu is all about—and can have a sub-menu entry, or many entries listed under it. There should be no theoretical limit to the number of menu entries and sub-menu entries, although memory and other system limitations may impose practical limitations on different computers.

Planning the Program


Some consideration needs to be given to the basic structure of the program.

You need to be able to track information about the menu as a whole as well as tracking each entry on the menu, displaying the menu in its current status, expanding and collapsing menu entries, and creating the menu.

These goals are best done by defining two objects—sectionObj, used for entries in the menu, and menuObj, used to define the complete menu.

The sectionObj Object Definition

Certain basic information and functionality needs to be stored, and provided, for each menu entry, regardless of its level on the list. In particular, you need to know the following about each menu entry:

In addition, you need to be able to provide several methods:

This combination of information functionality is a logical candidate for an object that contains properties and methods as appropriate. This is especially true because the program needs to provide this combination for each entry in the menu. The object definition for each menu entry follows:

function sectionObj() {
   this.section = new Array();
   this.title = "";
   this.link = "";
   this.display = displaySection;
   this.open = doOpen;
   this.close = doClose;
   pointers[pointers.length] = this;
   this.number = pointers.length - 1;
   if (this.number >= menu.statusString.length) { menu.statusString +=closed; }
   this.status = menu.statusString.charAt(this.number);
}

The sub-menu entries for the current menu entry, if there are any, are stored in the property section, which is initialized as an empty array. As the section array is populated, each entry is another instance of the sectionObj object. If the array isn't populated, it returns a length of zero that provides a means of checking if there are any sub-menu entries under the current menu entry. This is important when it comes time to display the men (discussed later in this chapter).

You should notice that in addition to the properties and methods listed above, the preceding object definition also contains two lines of code dealing with pointers:

pointers[pointers.length] = this;
this.number = pointers.length - 1;

These lines of code deal with the need to be able to directly refer to any particular object directly by a reference number when you build the expand and collapse links later in this chapter. Here, a global array called pointers is populated with objects for each menu entry as they are created. This is done by assigning a pointer to the current object to pointers[pointers.length] because pointers.length should return a value one greater than the index of the last entry in the array. By assigning to pointers[pointers.length] you are effectively increasing the length of the array by one. Once this is done, the index number for the current object in the pointer array is stored in the number property.

In addition, the technique used to set this.status requires some explanation:

if (this.number >= menu.statusString.length) { menu.statusString +=closed; }
this.status = menu.statusString.charAt(this.number);

As you will see later, whenever a menu entry is expanded or collapsed, the new arrangement is saved in a Cookie, which is read when the document is reloaded so that the menu can be displayed with the new status. The current status of the menu is stored as a string of characters in the statusString property of the menu object, which is an instance of the menuObj object definition discussed later.

Thus, each character of the string statusString corresponds to the object at the same position in the pointers array. statusString.charAt(this.number) should match the value of the status property of the object indicated by pointers[this.number]. Therefore, in the sectionObj object definition, this fact can be used to ensure that you're adding a menu entry that exists in the current statusString. It also can be used to ensure that if the menu entry doesn't already exist, it is added. this.status can be set to the value of the character at the corresponding place in statusString.

Methods of the sectionObj Object

The object definition for sectionObj refers to four methods:

The open() method is invoked when the user tries to expand a closed menu entry. As the following code shows, this method simply changes the value of the status property of the current sectionObj object, rebuilds the statusString property of the menu object, then stores this string in a Cookie with the same name as the name property of the menu object:

function doOpen() {
   this.status = open;
   menu.statusString = "";
   for (k = 0; k < pointers.length; k ++) {
      menu.statusString += pointers[k].status;
   }
   setCookie(menu.name,menu.statusString,toExpire,null,null,false);
   self.location = self.location;
}

Attention should be given to the way in which statusString is rebuilt. First statusString is cleared to an empty string and then a for loop is used to step through all the objects in the pointers array, concatenating the value of the status property to the end statusString with each iteration of the loop. This effectively rebuilds statusString with the newly changed status information.

The close() method works in a similar way, except that it sets the status property to a closed state rather than an open state:

function doClose() {
this.status = closed;
menu.statusString = "";
for (k = 0; k < pointers.length; k ++) {
menu.statusString += pointers[k].status;
   }
setCookie(menu.name,menu.statusString,toExpire,null,null,false);
self.location = self.location;
}

In both doOpen() and doClose() (as well as in sectionObj() above) you will notice the use of the open and closed variables, which are defined as constants elsewhere in the script. Their values are "o" and "c" respectively and they're used as the two entries in the statusString string to represent open and closed menu entries.

The last method is display() as defined by displaySection(). This is the most complex method of the sectionObj object:

function displaySection() {
   if (this.status == open) {
      document.write("<LI>", this.title.bold().link(this.link), " ");
      toprint = (this.section.length > 0) ? '[ <A HREF="javascript:pointers[' + this.number + '].close()" TARGET=_self">Collapse</A> ]' : '';
      document.write(toprint.fontsize(2));
      document.write("<UL>");
      var j = 0;
      for (j = 0; j < this.section.length; j ++) {
         this.section[j].display();
      }
      document.write("</UL>");
   } else {
      document.write("<LI>", this.title.bold().link(this.link), " ");
      toprint = (this.section.length > 0) ? '[ <A HREF="javascript:pointers[' + this.number + '].open()" TARGET="_self">Expand</A> ]' : '';
      document.write(toprint.fontsize(2));
   }
}

This method first checks to see if the menu is expanded or collapsed by checking the status property.

If the entry is expanded, the method displays the current entry's title as an entry in an unnumbered list with the <LI> tag. The link() method of the string object is used to add the hyperlink stored in the link property of the menu entry object.

Next, this.section.length is tested to see if it's greater than zero. If it is, then a link is built for collapsing the menu entry base. This is done by using the pointers array, with the number property of the current object as an index to refer to the current object in the hypertext link assigned to toprint. If this.section.length isn't greater than zero, the conditional statement assigns an empty string to toprint.

After toprint is displayed, a for loop is used to run through the section property (which is an array of objects for each sub entry) and calls those objects' display() methods in turn. Notice that the for loop is surrounded by two document.write() calls that output <UL> and </UL>. This makes the sub menu entries into nested unnumbered lists, which Netscape indents further than the current level, as shown in Figure 10.4.

Figure 10.4. Nested unnumbered lists produce indented output with each subsequent nested list.

If the status property wasn't set to open, then the method prints out the title and link in the same way, creates an "expand" link, and displays it if needed.

The menuObj Object

The menu itself has several pieces of data and functionality that need to be associated with it, making it a good candidate for being an object. The properties which the menuObj need to have include:

The associated method of the menuObj object is a method to initiate display of the whole menu tree. The menuObj object definition function looks like the following:

function menuObj(menuName) {
   this.statusString = getCookie(menuName);
   this.name = menuName;
   this.section = new Array();
   this.title = "";
   this.display = displayMenu;
   this.init = initMenu;
}

Other than the specific properties and methods of sectionObj, the main difference between the two objects is that menuObj takes an argument for the menu name which can then be used in the status Cookie associated with the menu. This name-specific Cookie allows multiple menu documents to be created without having Cookies for different menus overwriting each other.

The display() method is slightly different than the one that is part of the sectionObj object:

function displayMenu() {
   document.write(this.title.fontsize(5).bold());
   document.write("<HR>");
   document.write("<UL>");
   for (i = 0; i < this.section.length; i ++) {
      this.section[i].display();
   }
   document.write("</UL>");
}

Here, the title is displayed using the fontsize() and bold() methods of the string object to define the appearance of the text. Next, the top-level unnumbered list is opened by writing out a <UL> tag. Then the function uses a for loop to call the display() method of each top-level menu object.

Global Variables and Other Components

In addition to defining the two objects and their associated properties and methods, the menu manager requires two other components: global variables and a Cookie manager.

All work with Cookies is done by calling functions such as setCookie() and getCookie(). These are the same functions that were created in the Cookie manager at the end of Chapter 9, "Using Cookies." The menu manager needs to include the complete code of the Cookie manager in order to work:

//SET A NEW COOKIE
//Arguments:
//  name: cookie name (string)
//  value: cookie value (unencoded string)
//  expiry: expiry date (date)
//  path: document path (string)
//  domain: document domain (string)
//  secure: secure required? (boolean)
function setCookie(name,value,expiry,path,domain,secure) {
   var nameString = name + "=" + value;
   var expiryString = (expiry == null) ? "" : "; expires=" + expires.toGMTString();
   var pathString = (path == null) ? "" : "; path=" + path;
   var domainString = (path == null) ? "" : "; domain=" + domain;
   var secureSring = (secure) ? "; secure" : "";
   document.cookie = nameString + expiryString + pathString + domainString + secureString;
}
//GET A NEW COOKIE
//Arguments:
//  name: cookie name (string)
function getCookie(name) {
   var cookieFound = false;
   var start = 0;
   var end = 0;
   var cookieString = document.cookie;
   var i = 0;
   //LOOK FOR name IN cookieString
   while (i <= cookieString.length) {
      start = i;
      end = start + name.length;
      if (cookieString.substring(start,end) == name {
         cookieFound = true;
         break;
      }
      i++;
   }
   //CHECK IF NAME WAS FOUND
   if (cookieFound) {
      start = end + 1;
      end = cookieString.indexOf(";",start);
      if (end < start)
         end = cookieString.length;
      return unescape(cookieString.substring(start,end));
   }
   //NAME WAS NOT FOUND
   return "";
}
//DELETE A COOKIE
//Arguments:
//  name: cookie name (string);
function deleteCookie(name) {
   var expires = new Date();
   expires.setTime (expires.getTime() - 1);
   setCookie(name,"Delete Cookie",expires,null,null,false);
}

In addition, there are several global variables used throughout the object definitions that need to be defined as part of the menu manager: open, closed, pointers, and toExpire. Their definitions should be as follows:

var open = "o";
var closed = "c";
var pointers = new Array();
var toExpire = new Date(); toExpire.setTime(toExpire.getTime() + 1000*60*60*24);

open, closed, and pointers are discussed earlier in this chapter. toExpire is used whenever you set Cookies to set the expiry date 24 hours into the future. There is no need to make the Cookie persist too long, but it is useful to have them persist for long enough that if a user is using the menu, quits from the browser and returns a few minutes later it comes up in the same configuration in which the menu was last viewed.

Working with the Menu Manager


In order to use the menu manager in an application, it's necessary to define the menu and then call the display() method of the menu object.

The first step in defining a menu is to create an instance of the menuObj object. This instance should be named menu. For example, to create a menu with three top-level menu entries, you could use

var menu = new menuObj("JSDmenu");
menu.title = "An Example Menu";

This creates an instance of menuObj called menu, with the name "JSDmenu", and assigns the title "An Example Menu" to it.

Once the menu object is instantiated, it's possible to define its menu entries. To do this, a new entry in the section array needs to be created as an instance of sectionObj and then its title and link properties can be set. For example, in a menu with three top-level menu entries, the following code would define the top-level entries:

menu.section[0] = new sectionObj();
menu.section[0].title = "First Menu Entry";
menu.section[0].link = "http://first.menu.link/";
menu.section[1] = new sectionObj();
menu.section[1].title = "Second Menu Entry";
menu.section[1].link = "http://second.menu.link/";
menu.section[2] = new sectionObj();
menu.section[2].title = "Third Menu Entry";
menu.section[2].link = "http://third.menu.link/";

In order to add two second-level entries under the second menu entry, it's necessary to add entries to the section array of the menu.section[1] object and then assign titles and links to them. The new entries in the array should also be instances of the sectionObj object:

menu.section[1].section[0] = new sectionObj();
menu.section[1].section[0].setTitle("Sub-menu Entry One");
menu.section[1].section[0].link = "http://fourth.menu.link/";
menu.section[1].section[1] = new sectionObj();
menu.section[1].section[1].setTitle("Sub-menu Entry Two");
menu.section[1].section[1].link = "http://fifth.menu.link";

Similarly, to add four second-level entries to the third menu entry, you would use the following:

menu.section[2].section[0] = new sectionObj();
menu.section[2].section[0].setTitle("Sub-menu Entry Three");
menu.section[2].section[0].link = "http://sixth.menu.link/";
menu.section[2].section[1] = new sectionOj();
menu.section[2].section[1].setTitle("Sub-menu Entry Four");
menu.section[2].section[1].link = "http://seventh.menu.link";
menu.section[2].section[2] = new sectionObj();
menu.section[2].section[2].setTitle("Sub-menu Entry Five");
menu.section[2].section[2].link = "http://eighth.menu.link/";
menu.section[2].section[3] = new sectionOj();
menu.section[2].section[3].setTitle("Sub-menu Entry Six");
menu.section[2].section[3].link = "http://ninth.menu.link";

To add a third-level entry under "Sub-menu Entry Four", you need to create an instance of sectionObj as an element of the menu.section[2].section[1].section and then assign it a title and a link:

menu.section[2].section[1].section[0] = new sectionObj();
menu.section[2].section[1].section[0].setTitle("Sub-menu Entry Seven");
menu.section[2].section[1].section[0].link = "http://tenth.menu.link";

Finally, to display the menu, it's necessary to call menu.displayMenu() from inside the body of the HTML document. The complete menu manager, along with the menu just defined, would look like the following:

//SET A NEW COOKIE
//Arguments:
//  name: cookie name (string)
//  value: cookie value (unencoded string)
//  expiry: expiry date (date)
//  path: document path (string)
//  domain: document domain (string)
//  secure: secure required? (boolean)
function setCookie(name,value,expiry,path,domain,secure) {
   var nameString = name + "=" + value;
   var expiryString = (expiry == null) ? "" : "; expires=" + expires.toGMTString();
   var pathString = (path == null) ? "" : "; path=" + path;
   var domainString = (path == null) ? "" : "; domain=" + domain;
   var secureSring = (secure) ? "; secure" : "";
   document.cookie = nameString + expiryString + pathString + domainString + secureString;
}
//GET A NEW COOKIE
//Arguments:
//  name: cookie name (string)
function getCookie(name) {
   var cookieFound = false;
   var start = 0;
   var end = 0;
   var cookieString = document.cookie;
   var i = 0;
   //LOOK FOR name IN cookieString
   while (i <= cookieString.length) {
      start = i;
      end = start + name.length;
      if (cookieString.substring(start,end) == name {
         cookieFound = true;
         break;
      }
      i++;
   }
   //CHECK IF NAME WAS FOUND
   if (cookieFound) {
      start = end + 1;
      end = cookieString.indexOf(";",start);
      if (end < start)
         end = cookieString.length;
      return unescape(cookieString.substring(start,end));
   }
   //NAME WAS NOT FOUND
   return "";
}
//DELETE A COOKIE
//Arguments:
//  name: cookie name (string);
function deleteCookie(name) {
   var expires = new Date();
   expires.setTime (expires.getTime() - 1);
   setCookie(name,"Delete Cookie",expires,null,null,false);
}
var toExpire = new Date(); toExpire.setTime(toExpire.getTime() + 1000*60*60*24);
function sectionObj() {
   this.section = new Array();
   this.title = "";
   this.link = "";
   this.display = displaySection;
   this.open = doOpen;
   this.close = doClose;
   pointers[pointers.length] = this;
   this.number = pointers.length - 1;
   if (this.number >= menu.statusString.length) { menu.statusString +=closed; }
   this.status = menu.statusString.charAt(this.number);
}
function doOpen() {
   this.status = open;
   menu.statusString = "";
   for (k = 0; k < pointers.length; k ++) {
      menu.statusString += pointers[k].status;
   }
   setCookie(menu.name,menu.statusString,toExpire,null,null,false);
   self.location = self.location;
}
function doClose() {
   this.status = closed;
   menu.statusString = "";
   for (k = 0; k < pointers.length; k ++) {
      menu.statusString += pointers[k].status;
   }
   setCookie(menu.name,menu.statusString,toExpire,null,null,false);
   self.location = self.location;
}
function displaySection() {
if (this.status == open) {
      document.write("<LI>", this.title.bold().link(this.link), " ");
      toprint = (this.section.length > 0) ? '[ <A HREF="javascript:pointers[' + this.number + '].close()" TARGET="_self">Collapse</A> ]' : '';
      document.write(toprint.fontsize(2));
      document.write("<UL>");
      var j = 0;
      for (j = 0; j < this.section.length; j ++) {
         this.section[j].display();
      }
      document.write("</UL>");
   } else {
      document.write("<LI>",this.title.bold().link(this.link), " ");
      toprint = (this.section.length > 0) ? '[ <A HREF="javascript:pointers[' + this.number + '].open()" TARGET="_self">Expand</A> ]' : '';
      document.write(toprint.fontsize(2));
   }
}
function menuObj(menuName) {
   this.statusString = getCookie(menuName);
   this.name = menuName;
   this.section = new Array();
   this.title = "";
   this.display = displayMenu;
}
function initMenu(numSections) {
   for (i = 0; i < numSections; i++) {
      this.section[i] = new sectionObj();
   }
}
function displayMenu() {
   document.write(this.title.fontsize(5).bold());
   document.write("<HR>");
   document.write("<UL>");
   for (i = 0; i < this.section.length; i ++) {
      this.section[i].display();
   }
   document.write("</UL>");
}
var menu = new menuObj("JSDmenu");
menu.title = "An Example Menu";
menu.section[0] = new sectionObj();
menu.section[0].title = "First Menu Entry";
menu.section[0].link = "http://first.menu.link/";
menu.section[1] = new sectionObj();
menu.section[1].title = "Second Menu Entry";
menu.section[1].link = "http://second.menu.link/";
menu.section[2] = new sectionObj();
menu.section[2].title = "Third Menu Entry";
menu.section[2].link = "http://third.menu.link/";
menu.section[1].section[0] = new sectionObj();
menu.section[1].section[0].title = "Sub-menu Entry One";
menu.section[1].section[0].link = "http://fourth.menu.link/";
menu.section[1].section[1] = new sectionObj();
menu.section[1].section[1].title = "Sub-menu Entry Two";
menu.section[1].section[1].link = "http://fifth.menu.link";
menu.section[2].section[0] = new sectionObj();
menu.section[2].section[0].title = "Sub-menu Entry Three";
menu.section[2].section[0].link = "http://sixth.menu.link/";
menu.section[2].section[1] = new sectionObj();
menu.section[2].section[1].title = "Sub-menu Entry Four";
menu.section[2].section[1].link = "http://seventh.menu.link";
menu.section[2].section[2] = new sectionObj();
menu.section[2].section[2].title = "Sub-menu Entry Five";
menu.section[2].section[2].link = "http://eighth.menu.link/";
menu.section[2].section[3] = new sectionObj();
menu.section[2].section[3].title = "Sub-menu Entry Six";
menu.section[2].section[3].link = "http://ninth.menu.link";
menu.section[2].section[1].section[0] = new sectionObj();
menu.section[2].section[1].section[0].title = "Sub-menu Entry Seven";
menu.section[2].section[1].section[0].link = "http://tenth.menu.link";
</SCRIPT>
</HEAD>
<BODY>
<SCRIPT LANGUAGE="JavaScript">
menu.display();
</SCRIPT>
</BODY>
</HTML>

The resulting menu manager would like Figures 10.5 and 10.6.

Figure 10.5. Initially the menu is displayed entirely closed.

Figure 10.6. The user can use the expand and collapse links to manipulate the menu.

Incorporating a Menu into a Frameset

The menu manager is designed with the assumption that all the links in the menu are intended to be targeted at the same frame or window, which is why no provisions are made to include link targets in the sectionObj object definition.

By default, all the links are targeted at the same window in which the menu appears, which may defeat the purpose of hierarchical menu like the one produced by the menu manager.

In order to combine a menu into a frameset, it's necessary to specify the target frame for all the links in a menu. This is done with the <BASE> tag, which can be used to set an alternate default target for all links in a document. By using

<BASE TARGET="targetFrame">

in the HTML header of the menu file, all links in a menu are targeted at another frame.

For example, consider the following frameset:

<HTML>
<HEAD>
<TITLE>Menu Frameset</TITLE>
</HEAD>
<FRAMESET COLS="30%,*" BORDER=1>
   <FRAME SRC="menu.html">
   <FRAME SRC="output.html" NAME="output">
</FRAMESET>
</HTML>

As a result, the file menu.html could include a <BASE TARGET="output"> tag in the header and a menu definition:

var menu = new menuObj("FrameMenu");
menu.title = "FrameSet Menu Example";
menu.section[0] = new sectionObj();
menu.section[0].title = "Document One";
menu.section[0].link = "document1.html";
menu.section[0].section[0] = new sectionObj();
menu.section[0].section[0].title = "Sub-document One";
menu.section[0].section[0].link = "subdocument1.html";
menu.section[0].section[1] = new sectionObj();
menu.section[0].section[1].title = "Sub-document Two";
menu.section[0].section[1].link = "subdocument2.html";
menu.section[1] = new sectionObj();
menu.section[1].title = "Document Two";
menu.section[1].link = "document2.html";
menu.section[2] = new sectionObj();
menu.section[2].title = "Document Three";
menu.section[2].link = "document3.html";
menu.section[2].section[0] = new sectionObj();
menu.section[2].section[0].title = "Sub-document Three";
menu.section[2].section[0].link = "subdocument3.html";

This would produce results similar to those shown in Figures 10.7 and 10.8.

Figure 10.7. The menu loads in its own frameset and can be expanded and collapsed without affecting the output frame.

Figure 10.8. Clicking on a link in the menu opens the appropriate document in the output frame.

Summary


In this chapter an outline manager has been developed as an example of applying Cookies. This application highlights several important principles, including:

The next chapter moves from client-side JavaScript to server-side JavaScript and takes a look at LiveWire and LiveWire Pro.

Previous Page Page Top TOC Next Page See Page