Chapter 19

Using Frames and Cookies in Advanced Applications


CONTENTS

One of the tremendous advantages of hypertext is its ability to link together many pieces of information in a nonlinear fashion. Rather than having to read a book one page at a time, you can leap from one link to the next as you explore different topics and their interconnections. However, sometimes this easy access to information can have unfortunate and cumbersome side effects.

One such side effect occurs when you find yourself moving back and forth between a small set of pages, over and over again. This happens because of a problem with presentation: the information you want is scattered over more than one page. If you are reading a tutorial, for example, you may find that you are revisiting the Table of Contents page with dreary repetitiveness. You can avoid this by having multiple browser windows open at the same time, but this often takes up too much of your display.

This chapter focuses on the emerging HTML frames technology, which addresses this issue of presentation. Both the Netscape Navigator browser and Microsoft's Internet Explorer version 3.0 enable you to split a single browser window into multiple, independent subwindows, each containing its own URL. These sub-windows are known as frames. We will explore frames technology and its close relationship to JavaScript in some detail.

To do this we must first address the related topic of data storage. Sophisticated applications often require both data presentation and data storage/management capabilities. Unfortunately, information or parameter storage is particularly difficult. In Netscape data may be temporarily saved between reloads using the location.search property, and also on the command line. In addition, both Netscape and Microsoft provide a persistent means of storage via cookies. We explore both methods in the first section of this chapter, and then subsequently illustrate how such data can then be used to format a page on the fly.

Parameter Specification and Data Storage

Because Netscape is extremely security-conscious, it has made it difficult to store or load data-even between document reloads. Chapter 8briefly described one method of storing simple data between document reloads via a string saved in location.search. This special location.search property is often referred to as the command line. A script can retrieve this string and redraw itself based on information in the string. We first examine this approach to data storage in more detail. This is followed by a discussion of the more permanent storage option offered by cookies.

Storing Parameters and Other Data

If you have a lot of data, you might appreciate having it accumulated and stored for you. You can use a submit widget or form.submit() to have the browser collect all of your data, encode it, and store it in the command line. As we will see in the next section, the output is very hard to read and looks like scrambled text.

Another possibility is to store data in dynamic arrays in a window or, better, in the frameset document. Unfortunately, this too is unstable. If you have database-like data, it must all be hand coded into the document that will host it, although you could use an HTML builder or other programming tool to automatically create the HTML document.

The only possibility that offers any permanence is cookies. Cookies are lines in a file that the browser enables you to write to disk. This file will be named cookies.txt on a Windows machine, or just cookies on a UNIX or Macintosh. Under Netscape Navigator this file will be in your Netscape directory or folder. Cookies are limited in size and number. Nevertheless, cookies are extremely useful. They are frequently used in the same way as a Windows .ini file, a Macintosh Preferences file, or a UNIX.rc file. The next two subsections examine both the command-line approach and the cookie approach.

Command-Line Parameters

Using the submit() routine to store form field values in the command line leads to a location that looks like listing 19.1.


Listing 19.1  A Typical Value for the location.search Property

?myname=Mona+M.+Everett%2C+Ph.D.&tc=navy&lc=blue&vc=orange
&aname=Mona+M.+Everett%2C+Ph.D.&myimage=DBLACE4.jpg&mytext
=navy&mylink=blue&myvlink=orange&myurl=http%3A%2F%2Fwww2.b
est.com%2F%7Edsiegel%2Ftips%2Ftips_home.html%22&cmmt=Welco
me+to+my+little+home+page+builder.++There%27s+no+tellin%27
+just+how+much+this+page+can+be+expanded.++What+do+you+thi
nk%3F%0D%0A&subbtn=Submit

If you examine this output, you may be able to discern some patterns. First of all, it begins with the characteristic question mark (?), which delineates the location.search string. Second, most of the information seems to occur in pairs with the field name as the left-hand member and the field value as the right-hand member. The pairs are separated by the character ampersand (&).

There are absolutely no spaces in this output. Every space has been replaced by a plus sign (+). Various escape sequences containing the percent sign (%) occur in this string. These sequences are used to encode non-alphanumeric characters in the input. In particular, any punctuation has been replaced by such a sequence.

JavaScript does not give you a lot of tools with which to dissect this sequence. Let's see how we can use the ones we have. First, there are a pair of built-in functions to handle the escape sequences. These are escape(), which takes a non-alphanumeric character and hands you back the coded sequence, and unescape(), which reverses the process. Second, we can use the substring method of the String object to "walk" through a string. A statement of the form

myString.substring(start,stop)

extracts all the characters starting from position start and ending at the last character just before the position stop. This enables us to examine each character in the string, one at a time, if we want. Based on the character encountered, we can replace it or take some action.

The file encdec.htm on the CD-ROM has two functions that use this approach. The first one, which decodes the command-line search string, provides the core of the page rewrite code. The second changes all of the < and > to &lt; and &gt;, respectively, so that you can write out HTML to the page. This function is not actively called in the page, but is used for debugging using document.write(). The latter function is also really useful if you want to write HTML dynamically to your page to show your user how to do something. Let's examine the function that decodes the command-line search string, arraySubParms(), shown in listing 19.2. Note that this function presupposes that an array named parms has been declared as a global variable, and also initialized.


Listing 19.2  encdec.htm  A Function That Dissects the Command Line into its Component Parts

function arraySubParms(astr)
{
     k = astr.length
     astr = astr.substring(1,k)
     bstr = ''
     counter = 1
     for (i = 0 ; i <= k ; i++)
     {
          ccStr =''
          ccStr = astr.substring(i,i+1)
     
          if (ccStr == '+') ccStr = ' '
          if (ccStr == '%')
               {
                 var xx = astr.substring(i,i+3)
                 ccStr = unescape(xx)     
                 i += 3     
               }
          // car  
          if (ccStr == '=')
               {
                    parms[counter] = bstr
                    bstr = ''
                    continue
               }
          //right-hand member of pair
          if (ccStr == '&')
               {
                    parms[parms[counter]] = bstr
                    counter++
                    bstr = ''
                    ccStr=''
                    continue
               }

          bstr += ccStr
     }
}

When arraySubParms(astr) receives a string, it immediately finds the string's length and uses that value to chop off the first character, which is the question mark (?), that starts the location.search string. It then begins a loop that cycles through every character in the string with the statement ccstr = astr.substring(i,i+1). The variable ccstr is then checked to see if it is equal to the plus sign (+). If it is, then that character is replaced with a space. If ccstr is a percent sign, the function uses the substring function again to grab three characters, starting from its current position. It then uses the built-in function, unescape, to turn these three characters back into an ASCII character.

Because three characters instead of one are used up, the pointer into the string, i, must be advanced by three with the statement i+=3. In either of these two cases (+ or &), the next two conditional tests will fail, and ccstr, which may have been modified, is added to bstr.

The browser places all of the form element names and their values into the command line as name=value pairs. Each pair is terminated by an ampersand (&). The next two conditional tests extract the left and right members of the pair and place them into the left and right members of the associative array parms. If the function finds that equal sign (=), it knows that bstr-which has steadily been accumulating characters-now holds the name of the element.

Another variable, counter, is used to keep track of the current index into the parms array. The left member is set with the statement, parms[counter] = bstr. The variable, bstr, is set to the empty string at this point so that it can start accumulating characters anew. The value of counter is not advanced. A continue statement is used to bypass the rest of the loop so that the equal sign (=), which was just seen, is not added into the new value of bstr.

NOTE
JavaScript associative arrays are one-dimensional arrays of pairs. The left and right members of a pair are set differently. Set the left member using a numerical index into the array, for example, myArray[n]=lvar. Set the right member with an index equal to the value that you placed into the left member, for example, myArray[myArray[n]] = rvar. You can also use myArray[lvar] = rvar if lvar has not changed between setting the left and right sides of the pair.

If the next test, for the ampersand (&), yields true, the function knows that it has now accumulated the right-hand member of the array in bstr. It sets the right-hand member with the statement, parms[parms[counter]] = bstr. Remember that you set the right-hand member with the index as the name of the left-hand member, not the index itself. The processing of the name=value pair is now complete. Again, bstr is set to the empty string in anticipation of the next loop iteration. In this particular case, however, counter is now incremented with the counter++ statement.

When the function finally reaches the end of the location.search string, you will then have all of the variables in the global parms array. You can now use them anywhere within the current window or in any window that you create.

The second noteworthy function from the encdec.htm file is the toprint() function, which changes all occurrences of the < and > characters in any string into their corresponding control codes. This seemingly trivial operation is, in fact, very important. This is because the < and > characters are interpreted by HTML. If you want to write HTML to your document, you must somehow prevent them from being interpreted as HTML delimiters. Converting them to control codes does the trick. This function is very useful for debugging or to show HTML example code on your pages.

We will conclude this discussion of command-line parameters by examining the remaining code from file encdec.htm. It uses the global parms array that you have just prepared to rewrite your page according your specifications. Listing 19.3 shows the page rewrite code itself.


Listing 19.3  encdec.htm  Rewriting a Web Page using Command-Line Data

var astr = location.search
if (astr != null && astr != ''){ // start conditional

var parms = new createArray(12)

arraySubParms(astr)

astr = '<BODY BGCOLOR="linen" '
astr += 'TEXT="'+ parms['mytext'] + '" '
astr += 'LINK="'+ parms['mylink'] + '" '
astr += 'VLINK="'+ parms['myvlink'] + '" '
astr += 'ALINK="red" '
astr += 'BACKGROUND = "NewImages/' + parms['myimage'] + '" '
astr += '><BR>'
document.write(astr)
//document.write(toprint(astr))
} // end conditional
else document.write('<BODY>')
document.write('<TABLE ALIGN=RIGHT WIDTH=350 BORDER=1>')
document.write('<TR><TD>')
document.write('<FONT SIZE=7 COLOR= "indianred">')
document.write('<CENTER>' +document.title + '</CENTER>')
document.write('</TD></TR></TABLE>')
document.write('<LEFT><B>')
document.write('This page is an example of dynamically revised by ¬
      a header script which acts on information stored in the ¬
      command line.  That information is based on user\' choices.')
document.write('</B></LEFT>')
document.write('<BR CLEAR ALL>')
document.write('<HR>')

var astr = location.search
if (astr != null && astr != ''){ // start conditional
astr ='<CENTER><FONT SIZE=7 COLOR="' + parms['link'] + '"><B> '
astr += parms['aname'] + '</B></FONT></CENTER>'
document.write(astr)
} // end conditional
document.write('<HR><BR>')

Let us examine the operation of this script in some detail. There are several points worth noting. First of all, the location.search property is examined to make sure that it is not null or the empty string. If location.search does not contain a valid string, then most of the script processing is skipped. Two if...else statements are used for this purpose.

After the search string has been obtained and the parms array filled in by the call to arraySubParms(), the script starts building the <BODY> statement. Note that it builds it into a string and does not write it immediately with document.write. Note, too, the commented-out call to printit(), which was used during debugging to see if the string was built correctly.

Once the string has been assembled, a <BODY...> statement, which sets the background image and colors, is written to the document. If there was no search string, a plain <BODY> statement is written.

The script then writes a nice header for the document. The script next uses a second conditional clause to write your name in large letters. It had to check for the existence of a search string in order to do so. If the search string is present, you get your name; if it is absent, you get brief directions on using the page. When the header script is complete, the HTML on the page is interpreted by the browser.

Because all the form elements can be cleared with a submit, this program is polite and restores all of them from the global parms array. Instead of writing each one separately, it iterates through the form.elements array. Remember that the array was created with the element name as the left-hand member of the array. This makes it easy to get the correct variable in the form element. This could have also been done using numerical indexing. The routine shown is particularly useful if you have a large number of elements to restore.

Notice that a couple of form elements were included that were not used to construct the page. They were included here in order to provide a lot of escaped characters and a longer string of text with which to test the script.

Storing Persistent Data in Cookies

The only method you can use to store variables between invocations of Netscape Navigator and Internet Explorer v3 is the cookie approach. This is also the only approach that works with windows in different hierarchies. Since the Internet Explorer release is still in beta testing at the time of this writing we will focus on the version of cookies used by Netscape Navigator.

Cookies were originally designed to enable a server to save information on the client's disk. When the client contacted that same host at a later time, the previously saved cookie would be sent back to the server. Cookies are therefore useful if a browser connection is interrupted and you want to pick up where you left off. They are also useful in the event the server crashes and later wants to pick up where it left off. Cookies are now available for general use in JavaScript.

Cookies have the following five parameters:

NAME=VALUE
expires=DATE
path=PATH
domain=DOMAIN_NAME
secure

Only the first one, which is a familiar NAME=VALUE pair, is required. All of the others are optional. However, if you do not save an expiration date, the cookie automatically expires when you close Netscape-not something you want to happen if you want to keep information from session to session. The various parameters are separated by semicolons. If you create a cookie with the same name and path as a cookie already in existence, the new one overwrites the existing one.

Although servers can write named cookies one at a time, JavaScript cannot. You can set an individual named cookie with the statement, document.cookie='cookiename=xxxx', but when you retrieve document.cookie, you get a string consisting of all of the cookies. Currently, the only way to retrieve an individual cookie is to search through the entire set of cookies obtained from document.cookie. Consequently, it helps to add a prefix or suffix to the names of your cookies with little-used characters. This makes them easy to find with IndexOf().

Let's examine each of the cookie parameters in turn. As stated, the NAME=VALUE parameter is an associative pair. This means that it lends itself nicely to being stored in arrays and placed in form elements. This is the only required element of the cookie. The expires=DATE parameter is used to describe the expiration date for the cookie. As defined by Netscape, the date format must be "Wdy, DD-Mon-YY HH:MM:SS GMT" with the separators exactly as given. If you do not want persistent data between browser invocations, leave out this expiration date. If you want your cookie to never expire, give it a date several years in the future.

The path=PATH parameter is used to limit the search path of a server that can see your cookies. This is analogous to specifying a document BASE in an HTML document. If you use a slash (/), then everything in the domain can use your cookies.

The domain=DOMAIN_NAME parameter is only useful if the server is setting the cookie or, if for some reason, you want to generate a cookie that is available to the server. If the server generated the cookie, then the default domain is the domain name of the server that generated it. Finally, the parameter, secure, indicates that the cookie should only be sent if there is a secure client-server relationship.

Listing 19.4 shows the cookie versions of the routines for saving and restoring persistent information. This code can be found on the CD-ROM in the file c20-2.htm. In this case, the information goes to, and comes from, the document cookie rather than the command-line search string. These routines have been liberally modified from the original versions, which were written by Bill Dortch and placed in the public domain.


Listing 19.4  c20-2.htm  Saving and Restoring Document Cookie Information

function fixSep(what)
// escapes any semicolons you might have in your data
{
     n=0
     while ( n >= 0 )
          {
               n = what.indexOf(';',n)
               if (n < 0) return what     
               else
                    {
                         what = what.substring(0,n) + escape(';') ¬
                               + what.substring(n+1,what.length)
                         n++
                    }          
          }
     return what
}
function toCookie()
{
     document.cookie = ''
     nform = document.data
     for (i=0 ; i<nform.length; i++)
          {
               expr =makeYearExpDate(1)
               astr = fixSep(nform.elements[i].value)
               //astr = nform.elements[i].value 
               astr= nform.elements[i].name + '=' + astr + ';expires=' ¬
                     + expr + ';path=/'
               document.cookie=astr
          }
}
function makeYearExpDate(yr)
{
     var expire = new Date ();
     expire.setTime (expire.getTime() + ((yr *365) *24 * 60 * 60 * 1000));
     expire = expire.toGMTString()
     return expire
}
function getCookieAt(n)
{
  e = document.cookie.indexOf (";", n);
     if (e == -1)
          e = document.cookie.length 
     rstr= unescape(document.cookie.substring(n,e)) 
     return rstr
}
function fromCookie()
//restores summary fields from cookie
{
     nform = document.data
     astr = document.cookie
     alert(astr)
     cl = astr.length
     counter=0
     for (i = 0 ; i < nform.length ; i++)
{
               nstr = nform.elements[i].name + '='
               ll = nstr.length
               
               jx  = 0;
                 while (jx < cl) 
               {
                   k = jx + ll; 
                    xstr = astr.substring(jx,k);
if (xstr == nstr)
                     {
                              nform.elements[i].value = getCookieAt(k);
                              break ;
                         }
                   jx = document.cookie.indexOf(" ", jx) + 1;
                   if (jx == 0) break ;
                 }
               
          }
}
function arrayFromCookie()
// fills global array from cookie
{
     astr = document.cookie
     cl = astr.length
     k=0
jx  = 0;
     for (i = 0 ; i < 6 ; i++)
          {
                    
              jx=astr.indexOf(' ',jx)
               k = astr.indexOf('=',jx);
               xstr = astr.substring(jx+1,k);
parms[i]=xstr;
               parms[parms[i]] = getCookieAt(k+1);
              jx = astr.indexOf(";", jx) + 1;
              if (jx <= 0 || i > 10) break ;
               
          }

The function makeYearExpDate() enables you to set the expiration date for several years in the future. It was designed for really persistent cookies. If you want a shorter time, you can easily modify this routine. Note that this function uses the Date object heavily. The static method, Date.getTime(), returns a neatly formatted date string, while the method, Date.toGMTime(), returns the date converted to Greenwich Mean Time, which is what the cookie expiration mechanism expects your cookies to contain.

The function fixSep() escapes any semicolons that your variables might have. It is highly undesirable to store semicolons in the cookie parameters because the semicolon is the parameter separator. You could, in fact, escape all the non-alphanumeric characters in the entire string. However, this would make it difficult to read, especially if you simply want to look at the cookie.

The function, GetCookieAt(n), retrieves the cookie value starting at an offset of n characters into the cookie string. It replaces all escape sequences with their ASCII values. The function, FromCookie(), restores all of the summary forms variables from the cookie. It is really an undo function.

The final function, arrayFromCookie(), is called by the page rebuilding routines to build the global array, parms, from which the page is rewritten. Notice that we did not have to change the page rebuilding code from that of listing 19.3. We only changed the routine to build the parms array. Notice also that we can retrieve the value of a single cookie entry by indexing into the parms array.

Frames and JavaScript

Frames are one of the most important new features to be added to HTML. Frames allow multiple subwindows-or panes-in a single Web page. This gives you the opportunity to display several URLs at the same time, on the same Web page. It also allows you to keep part of the screen constant while other parts are updated. This is ideal for many Web applications that span multiple pages, but also have a constant portion (such as a Table of Contents). Before you learn about the implications of frames on JavaScript, a very brief tutorial on frames will be presented.

Specifying Frames in HTML

Frames in HTML are organized into sets which are known, appropriately enough, as framesets. In order to define a set of frames one must first allocate screen real estate to this frameset, and then place each of the individual frames within it. We will examine the syntax for the HTML FRAMESET and FRAME directives in order to understand how frames and framesets are organized.

One of the most important, and most confusing, aspects of frames is the parent/child relationships of frames, framesets, and the windows that contain them. The first frameset placed in a window has that window as its parent. A frameset also can host another frameset, in which case the initial frameset is the parent. Note that a top-level frameset itself is not named, but a frameset's child frames can be named. Frames can be referred to by name or as an index of the frames array. Figure 19.1 shows the overall hierarchy of frames, framesets, documents, and windows in Netscape Navigator.

Figure 19.1 : The hierarchy of windows, documents, and frames employs a complex, but consistent set of referencing rules.

You can divide your window real estate with a statement of the form <frameset cols=40%,*>. This frameset statement divides the window horizontally into two frames. It tells the browser to give 40 percent of the window width to the left-hand frame, frames[0], and anything remaining to the right-hand frame, frames[1]. You can explicitly give percentages or pixel widths for all frames, but it is more useful to use an asterisk (*), for at least one parameter. Use the wildcard character (*) for the widest frame, or for the frame that is least likely to be resized. This helps ensure that the entire frameset is displayed on a single screen. You can also divide the window vertically with a statement like <frameset rows=20%,*,10%>. This statement gives 20 percent of the available window height to the top frame, frames[0], 10 percent to the bottom frame, frames[2], and anything left to the middle frame, frames[1].

CAUTION
You cannot divide a window both horizontally and vertically with one frameset. To do that, you must use nested framesets.

The subsequent <FRAME...> statements define the name, source (URL), and attributes of each frame in the frameset. For example,

<FRAME SRC='menu.htm' NAME='menuframe' MARGINWIDTH=2 MARGINHEIGHT=2 
 SCROLLING=YES>

defines a frame into which the menu.htm file will be loaded. This frame is named menuframe.

Unless you are designing a ledge (a frame that never changes) and you know it will always be displayed in the frame, make the frame scrollable. You can enter an explicit SCROLLING attribute, which should be the value YES or NO, but the frame will default to SCROLLING=YES. Scrolling is much kinder to your users. You might have a very high resolution display, but a lot of computers, particularly laptops, do not. The MARGINWIDTH=xx and MARGINHEIGHT=xx attributes also allow you some latitude in how you present your document within a frame.

NOTE
Many browsers do not yet understand frames. Ideally, you should provide a version of your document that does not use frames for such browsers. At a minimum, you should warn the users about the presence of frames in your document using a <NOFRAMES>...</NOFRAMES> clause.

Make sure you have an initial URL to load into the frame, even if that URL is just a stub. Otherwise, you might find that the browser has loaded an index to the current directory. If you want to use a frame in the frameset to load other documents from a link, you must specify the target frame like this:

<A HREF='netcom.com/home' TARGET='menuframe'>Netscape</A>

Frames are a sophisticated way to build Web pages; you can keep your menu in one frame and display your content in another. However, it is easy to go overboard and have too many frames. If you present too much information in several different small frames, the user will probably be scrolling quite often. Since the whole purpose of frames is to present information in a pleasing manner, it is important not to try the user's patience. Frames can be a powerful tool, but they should be used judiciously.

Building a Frameset

Framesets are easy to build, although their hierarchy can become complex if they are nested. Listing 19.5 shows a simple frameset document. For it to display correctly, there must be HTML documents with the names given by the SRC attribute in each FRAME definition. When this code is loaded into Netscape Navigator, the page shown in figure 19.2 appears. This code appears in the file c20-3.htm on the CD-ROM.

Figure 19.2 : Framesets contain multiple frames and reference multiple URLs.


Listing 19.5  c20-3.htm  A Simple Frameset

<HTML>
<HEAD>
<TITLE><Simple Frame</TITLE>
<SCRIPT></SCRIPT>
</HEAD>
<FRAMESET cols=40%,*>
     <FRAME SRC="menu_2.htm" NAME="menuFrm" SCROLLING=YES
      MARGINWIDTH=3 MARGINHEIGHT=3>
     <FRAME SRC="display.htm" NAME="displayFrm" SCROLLING=YES
      MARGINWIDTH=3 MARGINHEIGHT=3>
     <NOFRAMES>
       You must have a frames-capable browser to
       <A HREF="noframes.htm">view this document</A> correctly.
     </NOFRAMES>
</FRAMESET>
</HTML>

NOTE
When building a frameset, always remember the following rules:
  1. The <FRAMESET>...</FRAMESET> block replaces the <BODY>...</BODY> block. It is incorrect to have both.
  2. Always use a <NOFRAMES>...</NOFRAMES> clause for browsers that do not support frames.
  3. Make all your frames scrollable except in exceptional circumstances.
  4. Make sure the HTML documents referenced by the SRC attributes are "live" before the frameset is displayed.

One of the most difficult concepts about framesets and frames is how they are referenced. For the simple frameset previously shown, you can make a simple roadmap of the object references. When you want to reference the child frames from the frameset, you can use the following references:

  • menu_2.htm
= frames[0]ormenuFrm
  • display.htm
= frames[1]or displayFrm

When one of the frames references its parent frameset, this object reference is used:

  • Either frame
= parent

The contents of each frame are referenced as properties of the frame. For example, the frameset can access the document object of menu_2.htm as frames[0].document or menuFrm.document.

Frames within Frames

Frames can be nested in two ways. We will illustrate both types of nesting by putting another frameset inside the displayFrm frame object defined in listing 19.5. To understand the first method, call the original frameset Frameset A. The frameset declaration shown in listing 19.6 nests a second frameset, referred to as Frameset B, within Frameset A. It does this by replacing frames[1] (the displayFrm frame) with another frameset. This code can be found in the c20-4.htm file on the CD-ROM. The auxiliary files menu_3.htm, pics.htm, and text.htm are also required.


Listing 19.6  c20-4.htm  Example of Nested Frames in which a Frame is Replaced with Another Frameset

<HTML>
<HEAD>
<SCRIPT>
</SCRIPT>
</HEAD>
<frameset cols = 30%,*>
     <frame src = 'menu_3.htm' name='menuFrame' marginwidth=3 ¬
           marginheight=3>
          <frameset rows=66%,*>
               <frame src='pics.htm' name='picFrame' scrolling=yes
                marginwidth=3 ¬
                     marginheight=3>
               <frame src='text.htm' name= 'textFrame' scrolling=yes ¬
                     marginwidth=3 marginheight=3>
          </frameset>
      <noframes>
          You must have a frames-capable browser to ¬
                <a href=text.htm>view this document</a> correctly.
      </noframes>
</frameset>
</HTML>

Referencing in this type of nested frameset is no different than the type of object references described for a simple frameset. When a frameset references a child frame, the following object references are used:

  • menu_3.htm
= frames[0]or menuFrame
  • pics.htm
= frames[1]or picFrame
  • text.htm
= frames[2]or textFrame

When any of the component frames refers to the frameset that contains it, the following reference is used:

  • Any frame increase =
parent

The second method uses URLs to achieve nested framesets. We will set Frameset B's displfrm to a URL that contains a framed document. This URL will come from the file displfrm.htm and will create the frames picFrame and textFrame. In this case, the object references are somewhat more complex. When the parent refers to its child frames it uses the following:

  • menu_4.htm
= frames[0]or menuFrm
  • displfrm.htm
= frames[1]or displayFrm

  • pics.htm
= frames[1].frames[0]or displayFrm.picFrame
  • text.htm
= frames[1].frames[1]or displayFrm.textFrame

When the child frames refer to their frameset parent, these object references are used:

  • menu_4.htm
= parent
  • displfrm.htm
= parent

  • pics.htm
= parent.parent
  • text.htm
= parent.parent

CAUTION
Specifying an empty URL in a frame declaration can cause the index file in the server's current directory to be loaded into the frame. Anyone can then open any of the documents listed in that index file. This can be considerably detrimental if you do not want to give users unrestricted read access to that particular directory.

Examples of Interframe Communication

At this point, you know how to refer to parent framesets in frames, and also know the correct object references for child frames of a frameset. The next topic explores interframe communication. This example uses the CD-ROM files c20-3.htm, menu_2.htm (shown in listing 19.7), and display.htm. Make sure you place all these files in the same directory, and then load c20-3.htm into your browser. The file, menu_3.htm, that's loaded into the left frame provides a simple and useful example of interframe communication.

The menu3.htm file contains some links to well known sites. A TARGET that points to the displayFrm frame is given for each link. If you click a link, the URL loads into the displayFrm frame instead of into the menuFrm frame. Note that you cannot refer to the "parent" object when you use a TARGET. To experiment with the page, click several links. After you have done this a few times, try to go backwards using Netscape's Back button. Notice that the whole frameset disappears; it is replaced by the document you were looking at before you loaded the c20-3.htm frameset.

TIP
Netscape's Forward and Back buttons work on the entire document-not on individual frames.

This limitation can certainly make life difficult, especially if you follow several links in the displayFrm and now want to get back to an intermediate one. Fortunately, there is a way to do this, but you must specifically provide for it. Notice the two small image buttons below the links. If you click the left arrow, the displayFrm frame reverts to the previously visited URL. Similarly, the right arrow takes you forward in the frame.


Listing 19.7  menu_2.htm  Using URLs to Create Nested Framesets

<HTML>
<HEAD>
<TITLE>MENU.HTM</TITLE>
<SCRIPT>
function writetopic(what)
{
   aWin = self.parent.displayFrm
     //aWin = self.parent.frames[1]
     aWin.document.close()
     aWin.document.open()
     aWin.document.write('<CENTER><H2><B>' + what + '</B> </H2>
      </CENTER>')
     aWin.document.close()
}
</SCRIPT>
</HEAD>
<BODY BGCOLOR='darkslateblue' TEXT='linen' LINK='corel' ¬
      VLINK='darkcorel' ALINK='yellow' >
<CENTER><FONT SIZE=7 COLOR="yellow"><B>MENU<B></FONT><CENTER>
<H3><HR></H3>
<FORM NAME="menuForm">
<INPUT TYPE='button' NAME="writeDisp" VALUE='Write to Display
      Frame' onClick='writetopic("Coming to you from ¬
      <BIG><I>menuFrm</I></BIG>..." )'>
</FORM>
<H3><BR><HR><BR></H3>
<H3><BR><HR SIZE=5 WIDTH=80%><BR></H3>
</BODY>
</HTML>

Another interesting aspect of frames is revealed if you attempt to use the View Source option of Netscape Navigator. Only the code for the frameset appears-the code for the frames contained in it does not. This is one approach to provide some simple protection for your source code. However, it only keeps novice users from seeing your code; experienced users can defeat this by loading the URLs referenced by the individual frames into a single browser window, and then using View, Source on that window.

CAUTION
The current release of Netscape Navigator does not reliably reload documents containing frames. This means that if you are editing a document and you press the Reload button, the most recent version of that document might not be reloaded.

Writing a New Document into a Frame

The examples in the files c20-4.htm and c20-5.htm do simple rewrites of documents into adjacent and foreign frames. We will now expand on that by taking the Text Object example from chapter 8 and writing it to a frame, rather than to a new window. The file c20-6.htm on the CD-ROM defines the frameset that loads the subsidiary documents setnote.htm and note.htm into its frames. The file setnote.htm contains a button that calls a writeNote() routine to write the new HTML into the frames[1] frame. The file note.htm is just a stub so you don't have to write an empty URL. Listing 19.8 shows the code for the writeNote() function. Figure 19.3 shows what happens when the note is written into the frame.

Figure 19.3 : Documents can be dynamically written into frames.


Listing 19.8  c20-6.htm  The Code for the writeNote() Function

function writenote(topic)
{
     topic = 'This is a little note about rewriting adjacent frames."
     topic += " You do it the same way as you would to rewrite"
     topic += " or originally write a window."
       aWin = self.parent.displayFrm
     ndoc= aWin.document
     ndoc.close()
     ndoc.open()
     astr ='<HTML><HEAD><BR><TITLE>' + topic + '</TITLE>'
     astr +='</HEAD>'
     astr +='<SCRIPT>'
     astr +='function closeNote(aName){'
     astr +='self.close()'
     astr +='}'
     astr +='function saveNote(aName){'
     astr +='}'
     astr +='<\/SCRIPT>'
     astr +='<BODY>'
     astr +='<FORM>'
     astr +='<TABLE ALIGN=LEFT BORDER><TR ALIGN=CENTER><TD>'
     astr +='<INPUT TYPE=button NAME=saveBtn VALUE="Save"
     ONCLICK="saveNote()" >'
     astr +='</TD>'
     astr +='<TD ROWSPAN=4>' + topic
     astr +='</TD>'
     astr +='</TR><TR ALIGN=CENTER><TD>'
     astr +='<INPUT TYPE=button NAME=closeBtn VALUE="Close"
     ONCLICK="closeNote()" >'
     astr +='</TD></TR>'
     astr +='<TR><TD><BR></TD></TR>'
     astr +='<TR><TD><BR></TD></TR>'
     astr +='</TABLE>'
     astr +='</FORM>'
     astr +='<BR CLEAR=ALL><H3><BR></H3>'
     astr +='Note:  Save button is not active yet'
     astr +='</BODY></HTML>' 
     ndoc.write(astr)
     ndoc.close()
}

Building a JavaScript Function Library in a Frameholder

You have already learned that a frameset document cannot contain any HTML other than frame definitions. It can, however, contain a script. In this script, you can keep window global variables and functions. We will define a minimal string manipulator in a frameset. With this tool you can do the following:

The first two operations merely require calls to string functions. The latter two can be accomplished by routines that we have already written. You store these functions in the frameset of the file c20-7.htm on the CD-ROM. This frameset requires the files funcs.htm and editor.htm to be in the same directory.

The frame named menuFrm will contain buttons to call your frameset functions. These functions must be able to refer to objects in their own frame as well as the adjacent frame editFrm. In addition, you must be able to call these functions from the parent frame. The true value of a frameset function library is its reusability. It is easy to copy the HTML file that defines the library and create a new document by changing a small amount of code-the code that builds the frameset itself. In this way, you can reuse your code.

Another way to reuse code is to have all the functions in a small or hidden frame. When you want to use those functions, you simply load that frame. If you take this approach, you don't have to change the frameset code. In both cases, however, it is more difficult to address an adjacent frame than it is to address a parent or child.

The menuFrm frame loads the document defined in the CD-ROM file funcs.htm. This file defines the buttons that access the frameset functions. Some of the object references are quite long, so this file makes liberal use of aliasing to shorten them. In fact, it does it so well that sometimes the whole procedure can be placed in the onClick event handler for the button. The file funcs.htm is loaded into the frames[0] object, while a simple editor window is placed in frames[1]. This editor is implemented as a textarea. When this frameset is loaded, the browser will display something like what is shown in figure 19.4.

Figure 19.4 : A frameset library can be used to implement a string processor.

The file funcs.htm also has a test window and a button so you can try out the functions it provides. These functions act on objects in its document. The code is such that the button always calls a routine called Testit. The Testit function has calls to the four routines in the function library. You can easily adapt this code for your own purposes by replacing the Testit function.

The most complex part of using functions stored in the frameset is determining the appropriate name of the frameset window. This depends on the window from which the function call is made. The example previously shown offers a very simple solution: just use self.parent.myfunc(). The self portion of this expression can be omitted, but you might want it to discourage ambiguity.

A Bug Database in JavaScript

This section presents a working application written entirely in JavaScript. The bugs in this chapter are software bugs, not the kind that crawl. The database is limited to 30 bugs because, at present, the only way to store persistent data is in cookies, which are limited in size and number. You can easily modify the code in this database to use the Image objects or Text objects presented in chapter 9. Indeed, you can even make it a database to hold descriptions of the six-legged kind of bug.

The Top-Level Structure of the Bug Database

Bugs of any kind are usually considered objectionable. You're going to turn them into objects that can be dealt with in an organized fashion. When you design an object, you first ask what information needs to be stored with that object. In the case of a software bug, you might want to track the date the bug was reported, who reported the bug, what the bug is, a description or comments, and the current status of the bug.

This is the type of application you might expect to find written in Delphi or Visual Basic. In that case, you would have a great deal of work to do to port the application to other platforms. This application, however, runs in the Netscape Navigator browser window and, thus, will run on any of the platforms Netscape supports. The main shortcoming of this application is that it has no place to store its data except in Netscape cookies, which are limited.

You will need a number of files for this application, which consists of five frames. Make sure you have the following files from the CD-ROM: c20-8.htm, menu.htm, bugs.htm, function.htm, traymenu.htm, indicatr.htm, bmenu.htm, buginput.htm, and notepad.htm. The top level of this application is, of course, the framesets it uses. Listing 19.9 shows these framesets (from the CD-ROM file bugs.htm).


Listing 19.9  bugs.htm  The Major Frameset for the Bugs Application

<HTML>
<HEAD>
<!- Created 12 Feb 1996 a6:59 PM 06:59 PM ->
<TITLE>Bugs Main Frameset</TITLE>
<SCRIPT>
//This function is a workaround to make sure that the table overlay is  drawn correctly.
function forceRewrite()
{
     blankWin=window.open('','blankWin','toobar=no,location=no,directories=no,
    status=yes,
    scrollbars=no,resizable=no,copyhistory=no,width=600,height=450')
     blankWin.close()
}
</SCRIPT>
</HEAD>
<FRAMESET ROWS= "80,*"  onLoad='forceRewrite()'>
     <FRAME SRC="menu.htm" NAME="menuFrm" MARGINHEIGHT=3 MARGINWIDTH=3>
     <FRAME SRC="bugs.htm" NAME="bugsFrm" SCROLLING=YES MARGINHEIGHT=3
     MARGINWIDTH=3>
     <NOFRAMES>
          You must have a frames-capable browser to 
          <a href=noframes.htm>view this document</a> correctly.
     </NOFRAMES>
</FRAMESET>
<SCRIPT>
var func = self.frames[0].frames[0]
var tray = self.frames[0].frames[1]
var smenu = self.frames[1].frames[0]
var work = self.frames[1].frames[1]
function passDownEdit(which)
{
  smenu.toEditor(which)
}
</SCRIPT>
</HTML>  

Before you examine this frameset definition, recall that Netscape draws images in an order that seems unpredictable. Its image drawing order is influenced by the state of your browser and by the size of the images.

One way around this is to force a screen refresh by forcing another window to obscure the screen. You can automate this approach by opening and closing another window rapidly above your application to force the redraw. You could use this seemingly worthless window creatively by making it a splash screen and closing it with a timer.

CAUTION
Script references to child frames or framesets cannot be made until after the framesets are declared. If you want to alias individual frames, do this in a footer script rather than in the header script.

We will now examine the structure of the frame declarations. The major frame holds three script items in addition to the frameset. A function called forceRewrite is defined in the header script. This function simply opens and closes a large window. This is done to overcome the image drawing limitation just mentioned. Notice that this function is called in the frameset tag via an onLoad handler. The onLoad handler executes after the page has been completely loaded. In this case, the point of this code is to allow all of the images to be loaded and then force a screen refresh so the table overlays are drawn correctly. Note that both FRAMESET and BODY statements may contain onLoad handlers.

The footer script contains aliases to all of the frames that will eventually be loaded. It has to be in the footer because the frameset does not know about its own child frames until you define them. The small footer function passDownEdit is used by the NotePad function to pass a command down to the child window smenu. Its purpose is to simplify window references.

The frameset itself defines two frames. Both of these hold other framesets. Table 19.1 lists the framesets, the frames they contain, the files loaded into those frames, their aliases, and their roles in the overall application. Figure 19.5 shows the page the bug database application creates.

Figure 19.5 : When the bug tracking application is loaded it produces this top-level display.

Table 19.1  Definitions for Five Frames, Their Aliases, and Their

Functions
FramesetFrame FileAlias Function
menu.htm0functions.htm funcHolds the function
     library.
 1traymenu.htm trayHolds a table
overlay with
linked image
buttons. Each
button points
to a predefined
function name
in the bottom-
left frame.
 2indicatr.htm indHolds a dummy
document that can
have messages
written to it.
Bugs.htm0bmnue.htm smenuHolds all of the
application code.
 1buginput.htm workInput form for
entering bug
information.

The Bug Object Defined

Because you're going to store bugs in your database, you must define a bug object. Because you can easily expect that there will be more than one bug in your database, you must further arrange for an array of bug objects to be created. Listing 19.10 shows the constructor for a single bug object.


Listing 19.10  bmenu.htm  Constructor Function for a Single Bug

function createBug(number,name,date,source,contact,progress,desc)
{
     this.length      = 11               
     this.number      = number          // a sequential number
     this.bname      = name               // name of this bug
     this.index      = ''          // current index of this bug in dBugs
     this.date           = date               // date first reported
     this.source      = source          // what module is bug in
     this.contact      = contact       	   // who reported bug, telephone no
     this.prog           = progress     // how far along are you on  debugging
     this.desc           = desc          // description of bug
     this.show           = showBug  // method to make a display string  for bug
     return this
}

The array of bug objects will be called dBugs. It will be declared as a global variable. The constructor for this array is shown in listing 19.11. The dBugs object is an extended array. This means you have given the array properties in addition to the length property. You still want to use it as an array, however. As you learned in chapter 9, failure to initialize the entire array will lead to erratic results. Your constructor does initialize it, in fact. This extended array has a number of methods designed to maintain the array and give the user information about the array. Not every method will be used in this application.


Listing 19.11  bmenu.htm  The Constructor for the Bug Object Array dBugs

function createBugsArray(program)
{
     this.length          = 16                    
     this.index           = 16                    // bug being looked at  NOW
     this.next                = 17                    // next open slot
     this.program           = program               // program being  debugged
     this.seq                = 1                  // next sequential  number
     this.corrected      = 1                         // apparent index
     this.add                = addBug        // method to add a bug to  array
     this.remove           = delTheBug    // method to remove a bug to  array
     this.see                = seeABug           // method to display a  bug
     this.count           = countBugs     // method to return a count of  bugs
     this.list                = listBugs           // method to list all  bugs
     this.check           = checkName      // method to check for unique  name
     this.find                = findBug      // method to find a bug in  array
     this.pack                = packBugs   // method to clean up array
     this.seeMe           = seeMyself   // method to display the entire  array.
      this.start           = 16            // place where array really 
                                               begins 
     this.length = this.start + 30
 var i
 for (i = this.start; i < this.start + 30; i++)
   this[i] = 'bug' + i
 for (i = this.start; i < this.start + 30; i++)
          this[this[i]] = 'temp ' + i

     return this
}

Entering a New Bug into the Database

The first task this application must do is solicit user input. It does this via an input screen in frame[1], which is aliased as work. All of the fields are in the same HTML form and would be accessed as work.document.forms[0]. Therefore, this latter object reference has also been aliased to wrkf, and declared as such in each of the other frames. An application menu (smenu) button calls newBug, which initializes some of the fields of the input form and then allows the user to fill out the form.

Listing 19.12 shows the code for newBug. This function goes through a number of steps to do its work. It checks the flag newInProgress to see if you are already in the process of entering a new bug; if so, it warns the user that it is not allowed and exits. If this test fails, it sets the flag newInProgress to true. It examines a check-box on the form to see if the user wants the form elements cleared automatically with each new bug entry. It arranges that certain fields will have default values. The user will not be allowed to change the index, the date, or the sequence number fields.

Notice that this function does not make a new bug entry into the dBugs array. You do not do this until the user indicates that he is serious about the entry by clicking the Finished button on the input form.


Listing 19.12  bmenu.htm  Setting Up for a New Bug Entry

function newBug()
{ 
     if (newInProgress)
            alert('You have not finished with the last new bug. Only one at ¬
                     a time, please!')
     else if (dBugs.next > dBugs.length)
          alert('You have reached your maximum of 30 bugs. You must fix some ¬
             of the bugs which you have recorded before you can enter any
             more!')
     else
               {
                    newInProgress = true
                    wrkf.notxt.value = dBugs.seq
                    wrkf.seqtxt.value= dBugs.next
                    var now = new Date;
                    wrkf.datetxt.value= now.toLocaleString()
                    if (wrkf.autoclchk.checked) work.clearBug()
                    var thisbugname = dBugs.program + dBugs.seq
                    wrkf.nametxt.value= thisbugname
                    wrkf.progbtn[0].checked = true
                    wrkf.progtxt.value= 'new'
                    wrkf.destxt.value = 'New Bug'
                    wrkf.mystatus.value = 'new'
          }
}

When the user does indicate that he is ready to enter the bug, the function saveBug() is called by the input form's Finished button. Note that saveBug has not been made a method of the bug object. However, you can make it into a method by changing all references to dBugs into references to this. You would also have to increase dBugs.start, dBugs.next, and the array length in this case. Listing 19.13 shows the code for the saveBug() function.


Listing 19.13  bmenu.htm  Function to Save a Bug into the dBugs Array and Store it as a Cookie

function saveBug()
{ 
     readVar()
     var ii = dBugs.next
     var curbug =new createBug(''+ ii,'bugtemp' + ii,'','','','','')
     dBugs[ii] = nm
     dBugs[nm] = curbug
     dBugs[dBugs[ii]].contact     = ct 
     dBugs[dBugs[ii]].source          = sr
     dBugs[dBugs[ii]].prog          = pr 
     dBugs[dBugs[ii]].desc          = ds 
     dBugs[dBugs[ii]].index          = dBugs.next
     dBugs[dBugs[ii]].number          = dBugs.seq
     dBugs[dBugs[ii]].bname          = nm
     var now = new Date
     dBugs[dBugs[ii]].date = now
     dataToCookie(nm,dBugs[dBugs[ii]],dBugs.index)
     saveSequential()
     dBugs.add()
     newInProgress = false
     updateStatus(ii,'......')
     flagDone()
}

First, saveBug calls a function named readVar(), shown in listing 19.14. The ReadVar() function might seem a little peculiar to you. Why not just read the form values directly into a bug? Because of a Netscape bug (which might not apply to all platforms and might be fixed by the time you read this). Pulling the input data directly from the form appears to wreak havoc with the dBugs array. The form input value is concatenated with an empty string; this forces it to be interpreted as a string, thereby bypassing this bug. Note that the variables used in readVar are global variables.


Listing 19.14  bmenu.htm  The Function readVar() Retrieves Input from the Form

function readVar()
{
     ct = '' + wrkf.contxt.value 
     sr = '' + wrkf.srctxt.value
     pr = '' + wrkf.progtxt.value
     ds = '' + wrkf.destxt.value
     nm = '' + wrkf.nametxt.value
      st     =  '' + wrkf.mystatus.value
     if (st == 'new')
     {
          var badname=true
          while (badname == true)
               {
                    badname = dBugs.check(nm)
                    if (badname == true ) {
               var prst = "This name has already been used."
               prst += " Please choose another"
               nm = prompt(prst, nm)
               }
               }
     }
}

ReadVar() has a second purpose: it makes sure new bugs have unique bug names. It checks the hidden field mystatus to be sure it is looking at a new bug, rather than an edited bug. In the bug database program, bugs are usually accessed by numerical indexes. The extended array has been set up so you can access a bug by name, as well. If the left side values of an associative array are not unique, this dual form of access is impossible. The code that does the checking calls the dBug.check method, which points to the function checkName(). The checkName() function simply cycles through the left side values of the associative array to see if the name passed to it appears there. If so, it returns true in a variable called badname; if not, it returns false. If badname is true, the user is asked to give it another name, which is again passed to checkName (see fig. 19.6). The cycle continues until a unique name has been found.

Figure 19.6 : Duplicate bug names are flagged by the input processing function.

When you have all of the information you need, you can create a new bug object (in the variable curbug). Where this bug should go in the dBugs array is dictated by the statement var ii = dBugs.next. The left side of that array element is filled with nm, the unique name of the bug. The right side of the array is filled with curbug. You could have filled various properties of curbug with information generated by the application or gleaned from the form by readVar() before you placed it into the dBugs array. Instead, we did it the hard way, by adding curbug to the array first and then filling it-just to demonstrate the double indirection into the bug object that is part of the dBugs array. This illustrates clearly that you are dealing with an array of arrays.

The final task is to add the date and then save the bug data in a cookie. The function dataToCookie() rolls up the bug information into a delimited string that can be saved in this manner, as shown in listing 19.15.


Listing 19.15  bmenu.htm  The Function dataToCookie Condenses a Bug into a Delimited String

function dataToCookie(bugname,bug,ndx)
{ 
     var astr = ''
     var aname = '@' + bugname
     var k = bug.length
          for (i = 1 ; i < k-1 ; i++) 
          {
                astr += bug[i]  + '_'
          }
     astr += '$'
     func.saveOneCookie(aname,astr,'year')
}

To save dataToCookie() a little trouble, we pass it the bug name as a separate parameter, although it could be extracted from the bug object itself, which is passed in as the second parameter. The DataToCookie() function prefixes the cookie name with '@' because you probably want to store more than one kind of cookie, and you need a way to tell the various cookies apart. The routine then cycles through each of the bug object's elements (except the last one, which is a method), and concatenates all of the values with the underscore (_) character as the separator.

It ends the string using the dollar sign ($) character as a terminator, and then calls the library function func.saveOneCookie(aname,astr,'year'). Note that this saveOneCookie function is stored in the function library contained in the frames[0] object. frames[0] has been aliased to func. Any function in the function library can be accessed by preceding the method or value name with func. Listing 19.16 shows the code for the saveOneCookie function.


Listing 19.16  function.htm  The saveOneCookie Function from the Function Library

function saveOneCookie(name,what,how)
{ 
               var expr
                var cc = how.substring(0,1)
               cc = cc.toUpperCase()
               if (cc == 'Y')
                      expr =makeYearExpDate(1)
               if (cc== 'T')
                         expr = ''
               if (cc == 'D')
                     expr = makeDeleteExpDate()
               what = fixSep(what)
               var astr= name + '=' + what + ';expires=' + expr +  ';path=/'
               document.cookie=astr               
}

The function saveOneCookie() is important, so we will examine it closely. First, it extracts the first letter of the how parameter and converts it to uppercase. It then tests this value to determine the expiration date for the cookie. If the how parameter has the value 'year', the cookie expiration date is set to one year from the current date. If how is the string 'Temporary', no expiration date is attached to the cookie-the cookie expires at the end of the Netscape session. Finally, if how is 'Delete', the expiration date is set for a time before now. The cookie automatically expires and is removed.

The function fixSep() changes all semicolons (;) to escape codes. The saveOneCookie function arranges to separate all cookies with semicolons, so you don't want any in the cookie body. The bug database application uses its own special separator, but the fixSep() routine can be used generically.

Finally, the cookie is constructed from the value name and what parameters passed to the routine, the expiration date, and the path (which generally is '/'). The cookie is then stored. Remember that document.cookie stores cookies separately but retrieves them jointly.

Because you're assigning each bug an absolute sequential number that isn't dependent on the dBugs array, you need to be able to store this number between invocations of the program. The saveSequential() function simply prefixes the name with a caret (^) to distinguish this cookie from others, defines the cookie, and then calls func.saveOneCookie().

At this point, the bug database application is finished storing the bug. It now has nothing left to do except clean up and notify the user that the bug has been saved. The flag newInProgress is reset to false so a new bug can be entered. A save message is placed in the status bar. However, this is not really an attention-getter, so another mechanism is used as well. You might have wondered about the odd frame in the upper-right corner of the bug database display. This frame is there only to provide the user visual cues. The FlagDone() function rewrites the document of ind to say 'DONE' (vertically) and sets a timer to trigger a 'go back to original' function. The code for this function is shown in listing 19.17. This could be made more elegant if you modified it so it plays a small animated sequence in its tiny window.


Listing 19.17  bmenu.htm  A Function To Give User a Visual Cue that Something Has Happened

function flagDone()
{
                    ind.document.open()
                    ind.document.write('<FONT SIZE=2 COLOR="maroon"><B>')
                    ind.document.write('D<BR>')
                    ind.document.write('O<BR>')
                    ind.document.write('N<BR>')
                    ind.document.write('E<BR>')
                    ind.document.write('</B></FONT>')
                    ind.document.close()
                    TimerID = setTimeout('ind.history.go(-0)',2000)
}

Retrieving a Bug

Now that you have successfully entered a bug, how do you retrieve it? A crude way is to have the user enter a number at a prompt or, perhaps, in an input field in the application menu. But users rarely remember what numbers are assigned to items, nor should they be required to do so. How about retrieval by name? That's better, but can the user remember the names? The most user-friendly solution is to present the user with a list of names.

The most obvious implementation of this list would be a select form element. Unfortunately, this element often misbehaves in current manifestations of Netscape, particularly if there are a large number of items on the list. A hotlist would also be nice. The bug database program produces two kinds of hotlists, in similar ways. The first is invoked by the System Menu List button. This is the first time you have used one of these buttons. Remember that the function of these buttons is to access a function in the resident application code. This function must have a standard name; in this case the function is alist(). The alist() function contains a single line of code:

showInWindow(dBugs.list('<BR>'),'Bugs')

The function showInWindow() puts the string argument given as its first para-meter into a pop-up window whose name is given as the second parameter. The dbugs.list() method it calls cycles through the valid dBugs entries and calls the dBugs.see method for each. This method is actually a reference to the function seeABug(), shown in listing 19.18. The resulting hotlist is shown in figure 19.7.

Figure 19.7 : The System Menu List button pops up a descriptive hotlist of bugs.


Listing 19.18  bmenu.htm  The Function seeABug() Converts Bug Information into a String

function seeABug(which,trm,cit)
{
     var astr = ''
     var nstr = ''
          nstr = this[which]
          var abug = this[nstr]
          if (cit) 
               astr = '<A HREF=JavaScript:self.creator.toEditor('
                astr +=      abug.index + ')>'
          astr += which + '     ' + nstr
   if (cit) astr += '</A>'
          astr += trm
          astr += 'Index:        ' + abug.index + trm
          astr += 'Name:         ' + abug.bname + trm
          astr += 'Number:       ' + abug.number + trm
          astr += 'Date:         ' + abug.date + trm
          astr += 'Source:       ' + abug.source + trm
          astr += 'Contact:      ' + abug.contact + trm
          astr += 'Progress:     ' + abug.progress + trm
          astr += 'Description   ' + abug.desc + trm
          return astr
     
}

Both the dBugs.list and the dBugs.see methods take a terminator parameter trm, so these functions can produce output that is properly formatted for HTML or for an alert. dBugs.list passes along the terminator to dBugs.see, along with the index of the bug and a variable called cit. If cit is true, dBugs.see will write the first line as a hotlink containing a JavaScript call to a function named toEditor(), along with its parameters. dbugs.list writes out most of the properties of each bug and presents it in a pop-up window for the user's perusal.

If the user clicks the hotlink, a call is made to the toEditor function, which retrieves the appropriate bug, unwraps it into the input window, and then sets dBugs.index to point to it. If the call came from a window and toEditor() finds that window open, toEditor() closes the window. The toEditor() function also sets the value of the input form's hidden field, mystatus, to 'edit' so saveBug() cannot insist that a new bug name be entered.

Editing a Bug

The editBug button (a bug with a pencil beside it) on the Application menu performs a similar function, except in this case, the hotlist is composed only of bug names. Each name calls toEditor() if it is clicked. This hotlist is shown in figure 19.8.

Figure 19.8 : A simple hotlist is popped up by the Application menu Edit button.

The bugs application lets you edit a bug as many times as you like, so you can update notes and report progress on the bug. It does not have a lot of error checking to ensure that the user enters data properly. You might want to build in the following mechanisms to improve the program:

Deleting a Bug

What do you do when you delete a bug? This implementation of the bug database program does nothing automatically. Ideally, you would archive the bug and then delete it from the list of active bugs. At the moment, Netscape makes no provision for writing to files other than the document cookie, so there is no way to archive the bugs. The Application menu, though, does provide a Delete button so you can delete bugs. The function delTheBug() is used to delete bugs from the dBugs array (see listing 19.19).


Listing 19.19  bmenu.htm  The Function delTheBug() Deletes a Bug from the dBugs Array and the Document Cookie

function delTheBug(which)
{
     
     if (which < this.start || which >= this.next)
     {
          alert('Cannot delete ' + which)
          return - 1
     }
     else
     {
          alert('deleting ' + which)
             var nstr = this[which]
          this[which] = '---'
          this['---'] = ''
          this.pack()
            func.saveOneCookie('@' + nstr,'','delete')
          return 1
     } 
}

This function places '---' in the bug's name and wipes out the bug object referenced by it. It then calls the method dBug.pack(), which eliminates the first '---' it finds in the dBugs array (there should only be one) and moves everything below it up one slot. It then decrements the dBugs.next property. Finally, it calls the func.saveOneCookie() function with the cookie name, an empty string, and the delete flag. This final operation removes the bug from the document cookie.

Other Buttons

The Application menu, which is the vertical menu on the left side of the page, has some unexplained items on it. These include a Load button, a Save button, and a Back Arrow button. Most of the buttons on the System menu have not been explained, and not all are functional.

Application Menu  The Load button loads all of the bugs from the document.cookie into the dBugs array. It calls a function named cookieToDBug() to do this. cookieToDBug() is also called in the document onLoad event handler so the application starts out with the array loaded.

The Back Arrow button reloads smenu (from the file bmenu.htm). This is really a debugging function and you might want to remove it in a final application.

System Menu  The System menu is the horizontal menu on the top of the page. The Forward and Backward Arrow buttons apply to the top window only. If you want to go backward and forward in a frame, you need to supply buttons to do so. These buttons act on the work frame that holds the input form. In the bug database application, this form does not change, so these buttons have no function in this application as it stands.

The Notes button points to a function called aNote() in smenu. If you click the button, a plain little notepad pops up and enables you to save and load notes. Figure 19.9 shows this notepad.

Figure 19.9 : The System menu Notes button pops up a rudimentary notepad.

The Bugs button is meant for debugging purposes during development. It pops up a list of warnings or bugs when an application is finished. At the moment, it points to storeSequential, which often needs to be adjusted during debugging.

The List button, as you have seen, pops up a hotlist that gives information on each bug.

The Links button is meant to pop up a list of links related to what is in the application. It points to the function aLink() in smenu, which is not currently used in this application.

You typically use the Go button to execute an application-specific aGo() function in smenu. In this implementation, aGo pops up an alert with the contents of the document cookie.

The Done button closes the entire application.

Other HTML Elements  The Input Screen is the large data entry area located at the center of the page. The Input screen Forward and Backward buttons scroll backward and forward in the dBugs array. When the user reaches the beginning or end of that portion of dBugs, the user notification area flashes NO.

The User Notification Area is in the upper-right frame; it usually has a picture in it. However, at the end of a load or when a scrolling record hits an end point, it displays a one-word message to the user.