-->
Bookmark and Share

Revenge of the Menu Bar

See the demo page for the finished version of the code.

Fixing the Menu Display

If you recall, the first time a given menu needs to be displayed, menuInit() is called for it. The first step in this function is to replace the arrow character for IE browsers.

function menuInit(menu) {

  var itemList, spanList;
  var textEl, arrowEl;
  var itemWidth;
  var w, dw;
  var i, j;

  // For IE, replace arrow characters.

  if (browser.isIE) {
    menu.style.lineHeight = "2.5ex";
    spanList = menu.getElementsByTagName("SPAN");
    for (i = 0; i < spanList.length; i++)
      if (hasClassName(spanList[i], "menuItemArrow")) {
        spanList[i].style.fontFamily = "Webdings";
        spanList[i].firstChild.nodeValue = "4";
      }
  }

It does this first setting an explicit line height for the menu. This is necessary because the replacement arrow character will increase the text height when used. Forcing a line height will keep all the menu items evenly spaced, regardless of whether they have the arrow or not.

It then checks every SPAN element in the menu DIV for the menuItemArrow class name. For these, it changes the font-family style to "Webdings" and the actual text is replaced with the '4' character. In this font, that character represents a right-pointing triangle very similar to the original Unicode character.

The "Webdings" font has been included with Internet Explorer since version 4, so it's fairly safe to assume IE users will have it installed. But you can always use some other character if you want, such as '>' or '»'.

Next it works on making the arrows flush to the right. To do so, it must first find out how wide the items are.

  // Find the width of a menu item.

  itemList = menu.getElementsByTagName("A");
  if (itemList.length > 0)
    itemWidth = itemList[0].offsetWidth;
  else
    return;

Since the style class for the A tags within a menu DIV specify display:block, they should all be the same width. So the width of the first item element can be used.

Now it goes through each item link, one at a time, looking for those with both a menuItemText-class SPAN and a menuItemArrow-class SPAN.

  // For items with arrows, add padding to item text to make the
  // arrows flush right.

  for (i = 0; i < itemList.length; i++) {
    spanList = itemList[i].getElementsByTagName("SPAN");
    textEl  = null;
    arrowEl = null;
    for (j = 0; j < spanList.length; j++) {
      if (hasClassName(spanList[j], "menuItemText"))
        textEl = spanList[j];
      if (hasClassName(spanList[j], "menuItemArrow"))
        arrowEl = spanList[j];
    }
    if (textEl != null && arrowEl != null) {
      textEl.style.paddingRight = (itemWidth 
        - (textEl.offsetWidth + arrowEl.offsetWidth)) + "px";
      // For Opera, remove the negative right margin to fix a display bug.
      if (browser.isOP)
        arrowEl.style.marginRight = "0px";
    }
  }

When it finds one, it measures the width of both SPAN elements and adds just enough padding to the item text SPAN to match the item width found earlier.

Additionally, for Opera browsers, the margin-right style of the SPAN containing the arrow character is set to zero. This will leave some extra space to the right of the arrow when viewed with that browser, but it's better than no arrow at all.

The next step is to address the hover problem in IE. For whatever reason, setting an explicit style width on any link tag within the menu seems to correct it.

  // Fix IE hover problem by setting an explicit width on first item of
  // the menu.

  if (browser.isIE) {
    w = itemList[0].offsetWidth;
    itemList[0].style.width = w + "px";
    dw = itemList[0].offsetWidth - w;
    w -= dw;
    itemList[0].style.width = w + "px";
  }
}

The function does this with the first item link in the menu. Using the item element's offsetWidth property it calculates a corresponding pixel value for its style.width property.

Note that in CSS, the width style applies to the content of an element. This does not include any padding or borders. The value of an element's offsetWidth property does include padding and borders, however.

The code above accounts for this by setting an arbitrary value on the style width and comparing that to the resulting offsetWidth of the element. The difference equals the width of any padding and borders.

It can then subtract that difference from the original offsetWidth to get a value for the style width that leaves the element the size unchanged.

Finally, a user defined property is set on the menu element to mark it as having been initialized.

  // Mark menu as initialized.

  menu.isInitialized = true;

Below, you can see the result, both before and after menuInit() has been called. The background color of the item and arrow SPAN elements has been altered so that you can better see the space occupied by each.

With the menus looking presentable, and the hover effect acting as expected in IE, we can go about setting up the event handling and code to control them.