DHTML Lab: Hierarchical Menus Ver. 2 (Cross-Browser/Frames); Menu Item Setup | WebReference

DHTML Lab: Hierarchical Menus Ver. 2 (Cross-Browser/Frames); Menu Item Setup


Logo

Hierarchical Menus Ver. 2 (Cross-Browser/Frames)
item setup

Webreference

Parameters used for the menus on this page:

menuWidth = 120;
childOverlap = 50;
childOffset = 5;
perCentOver = null;
secondsVisible = .5;
fntCol = "blue";
fntSiz = 10;
fntBold = false;
fntItal = false;
fntFam = "sans-serif";
backCol = "#DDDDDD";
overCol = "#FFCCCC";
overFnt = "purple";
borWid = 1;
borCol = "black";
borSty = "solid";
itemPad = 0;
imgSrc = "tri.gif";
imgSiz = 10;
separator = 1;
separatorCol = "red";
isFrames = false;

Menu Animated GIF
Animated GIF demonstrating heirarchical menus for non-DHTML browsers.

In longer script listings, cross-browser code is blue, Navigator-specific code is red, and Explorer code is green.

The [cc] symbol denotes code continuation. The code is part of the preceding line. It is placed on a new line for column formatting considerations only.

The complete item setup function follows.

function itemSetup(whichItem,whichArray) {
  this.onmouseover = itemOver;
  this.onmouseout = itemOut;
  arrayPointer = (whichItem-1)*3;
  this.dispText = whichArray[arrayPointer];
  this.linkText = whichArray[arrayPointer + 1];
  this.hasMore = whichArray[arrayPointer + 2];
  if (this.linkText.length > 0) {
    if (NS4) {
      this.captureEvents(Event.MOUSEUP)
      this.onmouseup = linkIt;
    }
    else {
      this.onclick = linkIt;
      this.style.cursor = "hand";
    }
  }
  htmStr = (this.hasMore) ?
[cc]       imgStr + this.dispText : this.dispText;
  if (NS4) {
    if (fntBold) htmStr = htmStr.bold();
    if (fntItal) htmStr = htmStr.italics();
    htmStr = htmStr.fontcolor(fntCol);
    htmStr = "<FONT FACE=" + fntFam +
[cc]        " POINT-SIZE=" + fntSiz + ">" +
[cc]        htmStr+ "</FONT>";
    this.document.write(htmStr);
    this.document.close();
    this.bgColor = backCol;
    this.visibility = "inherit";
    this.container = this.parentLayer;
    if (whichItem == 0) {
      this.top = borWid + itemPad;
    }
    else {
      this.top = this.prevItem.top +
[cc]    this.prevItem.clip.height + separator;
    }
    this.left = borWid + itemPad;
    this.clip.top = this.clip.left = -itemPad;
    this.clip.bottom += itemPad;
    this.clip.right = menuWidth-(borWid*2)-itemPad;
  }
  else {
    with (this.style) {
      padding = itemPad;
      color = fntCol;
      fontSize = fntSiz + "pt";
      fontWeight = (fntBold) ? "bold" : "normal";
      fontStyle =  (fntItal) ? "italic" : "normal";
      fontFamily = fntFam;
      borderBottomWidth = separator + "px";
      borderBottomColor = separatorCol;
      borderBottomStyle = "solid";
      backgroundColor = backCol;
    }
    this.innerHTML = htmStr;
    this.container = this.offsetParent;
    if (whichItem == 0) {
      this.style.pixelTop = 0;
    }
    else {
      this.style.pixelTop =
[cc]          this.prevItem.style.pixelTop +
[cc]          this.prevItem.offsetHeight;
    }
    this.style.pixelLeft = 0;
  }
}

In Version 2, the first argument is the item count, declared internally by itemSetup() as whichItem.

First off, itemSetup() assigns functions to the mouseover and mouseout event handlers. In Version 1, there were two extra lines used to perform this assignment. My mistake.

  this.onmouseover = itemOver;
  this.onmouseout = itemOut;

Next, we subtract one from the item count, and multiply that by three. This gives us the first array element that relates to the item. For example, if this is the second item in a menu, whichItem would be 2. (2-1)*3 = 3. Therefore the first relevant array element would be element 3 (the fourth one).

  arrayPointer = (whichItem-1)*3;

Using this value, we assign the relevant array entries to item properties, as in Version 1:

  this.dispText = whichArray[arrayPointer];
  this.linkText = whichArray[arrayPointer + 1];
  this.hasMore = whichArray[arrayPointer + 2];

If our item is a link, we must navigate to a new page when the user clicks it. In Navigator, remember, the click event cannot be captured by an element, only by its document, which excludes its text. We opted for a workaround in Version 1, using the focus event. This, in turn, led to the new page loaded also being in focus, with unwanted highlighted content. For Version 2, we will capture the mouseup event (half a click). This solves the focus problem.

The Explorer code remains the same as before.

  if (this.linkText.length > 0) {
    if (NS4) {
      this.captureEvents(Event.MOUSEUP)
      this.onmouseup = linkIt;
    }
    else {
      this.onclick = linkIt;
      this.style.cursor = "hand";
    }
  }

We now start building the item content, assigned to htmStr. First of all, if the item opens a new menu, the triangle image is appended to the displayed text:

  htmStr = (this.hasMore) ?
[cc]       imgStr + this.dispText : this.dispText;

Item Styling in Navigator

We have done away with CSS text styling for Navigator. This leaves us with HTML and JavaScript options, only. First, we use the old JavaScript 1.0 bold(), italics() and fontcolor() string methods to make our display string bold, italicized or of a different color:

    if (fntBold) htmStr = htmStr.bold();
    if (fntItal) htmStr = htmStr.italics();
    htmStr = htmStr.fontcolor(fntCol);

The final font styling (font family and point size) is achieved by enclosing htmStr in a FONT tag. The standard FACE= attribute is given the value of fntFam. Navigator 4 introduced a proprietary attribute for <FONT>: POINT-SIZE=, which takes an integer value.

    htmStr = "<FONT FACE=" + fntFam +
[cc]        " POINT-SIZE=" + fntSiz + ">" +
[cc]        htmStr+ "</FONT>";

The styled string is written to the item element:

    this.document.write(htmStr);
    this.document.close();

Next, we have to style our item element, using JavaScript and layer properties. We first set the background color, the visibility and assign the containing menu to the container property.

    this.bgColor = backCol;
    this.visibility = "inherit";
    this.container = this.parentLayer;

Those of you who studied our Puzzle code will recall that we created a border around our puzzle, by offsetting a child element's position by a value equal to the requested border width. The slightly wider-and-taller parent element's background color created the illusion of a border around the child element. That's exactly what we will do again for Version 2 of our menus. This will solve all the CSS-related problems. Navigator's use of CSS for regular elements is fine. Conversely, because it translates CSS positioned elements to layers, internally, handling of CSS styling for positioned elements is poor.

Our valiant attempt in Version 1 to work around these limitations failed.

Therefore, we first position the item within the menu. If it is the first item, its top position is the value of the border width plus the padding. Otherwise, it is placed below the previous item, leaving space for the separator line, which again is just the parent menu element showing through.

    if (whichItem == 1) {
      this.top = borWid + itemPad;
    }
    else {
      this.top = this.prevItem.top +
[cc]    this.prevItem.clip.height + separator;
    }

The item's left position is the border plus the padding:

    this.left = borWid + itemPad;

Now, we apply padding (space around content) to the item through creative clipping! The top and left clip properties are given a negative value, expanding the background color area of the item to give the impression of padding. The bottom clip is expanded by the same amount, for the same effect. The right clip is just inside the right border, but the padding on the right has been achieved by the width argument when the element was created!

 this.clip.top = this.clip.left = -itemPad;
 this.clip.bottom += itemPad;
 this.clip.right = menuWidth-(borWid*2)-itemPad;

We have therefore created an element and styled it using only JavaScript, HTML and layer properties, all extremely well-supported and stable in Navigator.

Item Styling in Explorer

Explorer loves CSS and all CSS properties are exposed to JavaScript. We simply assign values to the relevant properties using our parameter variables:

with (this.style) {
   padding = itemPad;
   color = fntCol;
   fontSize = fntSiz + "pt";
   fontWeight = (fntBold) ? "bold" : "normal";
   fontStyle =  (fntItal) ? "italic" : "normal";
   fontFamily = fntFam;
   borderBottomWidth = separator + "px";
   borderBottomColor = separatorCol;
   borderBottomStyle = "solid";
   backgroundColor = backCol;
}

Notice that the separator line is a true border, created by adding a border to only the bottom side of the item element. This means that the separator can also be a different color from the menu border. We have hard-coded the border style as "solid" as befits a separator. You could change this if you like or add yet another parameter variable to assign different separator styles to different pages.

Notice, also, that since Version 2 limits the font size to a point value, we append the "pt" string to fntSiz, before assigning it to the item's style.fontSize property.

We write our display string to the item, set the container property and position the item within the menu, exactly as we did for Version 1:

    this.innerHTML = htmStr;
    this.container = this.offsetParent;
    if (whichItem == 1) {
      this.style.pixelTop = 0;
    }
    else {
      this.style.pixelTop =
[cc]          this.prevItem.style.pixelTop +
[cc]          this.prevItem.offsetHeight;
    }
    this.style.pixelLeft = 0;

The one exception is the pixelTop assignment. We no longer need to overlap top and bottom item borders to create a single-line look, as we did in Version 1. We simply place each item element under the previous one.

When all the items are set up, and styled, the containing menus are set up.


Produced by Peter Belesis and

All Rights Reserved. Legal Notices.
Created: May. 22, 1998
Revised: May. 22, 1998

URL: http://www.webreference.com/dhtml/column20/hier2Item.html