Enterprise Software

SuperScripter: Build a tabbed panel

Find out how to build a tabbed panel that works on version 4.0 browsers.

By Jamie Jaworski
(3/12/01)

In a previous column, we created a DHTML tabbed panel using simple, standards-based CSS and DOM scripting supported by Internet Explorer 5 and later, Navigator 6, and Mozilla releases M17 and above. In this column, I'll show how to retrofit the tabbed panel to make it work on older, nonstandard version 4.0 browsers, and I'll also slightly improve the panel's interface to make it more visually appealing.

When you retrofit the original tabbed panel, the tabs and panels will lack borders on Navigator 4.0 but should otherwise function the same whether you're using Netscape Navigator or Microsoft Internet Explorer. Clicking a tab will display the associated panel; clicking a tab in the back row with cause the tab's row to move to the front. This is the minor interface improvement I mentioned; it lets panels from all rows display in the same manner and makes the component's behavior more consistent. Unfortunately, it also makes the component no longer work on Opera, which doesn't yet support the CSS clip property.

The other significant improvement is that the component is no longer DOM-exclusive and now works on version 4.0 browsers. Internet Explorer 4.0 has fairly robust CSS support and required only a few small changes. Navigator 4.0, however, was a different story altogether.

Adapt it to Navigator 4.0

Getting the tabbed panel to work on Navigator 4.0 requires dusting off Netscape's maligned LAYER tag, a throwback to their prestandards crack at DHTML. The browser permits some DHTML using the same DIV elements as other browsers but with too many limitations to make the tabbed panel possible. If you want nested, positioned, fixed-size, solid-color regions on Navigator 4.0, it's LAYER or nothing.

But as adaptations go, LAYER has its advantages. For our tabbed panel, a few lines of code set the Navigator 4.0 code apart from the rest:

<NOLAYER>
   <!— DIV tags used by other browsers —>
</NOLAYER>
<LAYER id="p1" width="400" height="200" src="nav4.html">
</LAYER>

The LAYER tag's src attribute imports the Navigator 4.0 version of the tabbed panel from the file nav4.html. The NOLAYER element encompasses the DIV elements to be used by all other browsers. Navigator 4.0 will process the LAYER tag and ignore everything within NOLAYER, while all other browsers (including Netscape 6) will ignore both LAYER and NOLAYER. In this way, we cleanly separate the Navigator 4.0 system from the CSS-based code.

Within the nav4.html file, we have two series of LAYER tags, one for tabs and one for panels. Here's an example of a tab:

<LAYER bgcolor="yellow" style="font-weight:bold;text-align:center;font-family:sans-serif;" width="90" height="78" left="0" top="24" z-index="8" id="p1tab0" onfocus="selectTab(0)">
HTML
</LAYER>

Note that the style properties address only the contained text. LAYERs are inherently absolutely positioned (ILAYER is used for relative positioning) and use HTML attributes for size, placement, layering, and background color. The onfocus event handler responds to mouse clicks.

The panels are similar to the tabs, but with more contents:

<LAYER bgcolor="yellow" style="font-family:sans-serif;padding:6px;" width="400" height="200" left="0" top="54" z-index="8" id="p1panel0">
<H2>Hypertext Markup Language</H2>
<P><A href="http://www.w3.org/MarkUp/" target="external">HTML</A> is the language in which Web pages are written. HTML uses tags to identify how text is to be structured and formatted within a document. </P>
</LAYER>

Notice that padding is indicated in the style attribute. This is the only way to achieve padding within a Navigator 4.0 LAYER; if you try using style sheets, the padding just produces larger margins.

The final problem we face for Navigator 4.0 is that it can't render absolutely positioned content inside tables. It can, however, position content over tables. So first we move the LAYER that imports nav4.html (p1 above) to the end of the BODY, outside any tables. Then we put an empty ILAYER of the same size called panelLocator where we want the panel:

<ILAYER id="panelLocator" width=400 height=254></ILAYER>

Finally, we add a script that positions the tabbed panel LAYER on top of the ILAYER:

function positionPanel() {
   document.p1.top=document.panelLocator.pageY;
   document.p1.left=document.panelLocator.pageX;
}
if (document.layers) window.onload=positionPanel;

This adaptation has the drawback of requiring two separate copies of the same content, but it's a trade-off for efficiency. It is sometimes possible to nest LAYER within DIV on the same page to reference the same content but that introduces many complications. Many browsers that otherwise ignore LAYER will apply its style properties, and, in this case, browsers would have to relocate the tabbed panel from the end of the page. They can do it, but it would add complexity and potential instability. It's best to reserve that for Navigator 4.0's problematic DHTML.

Enhance the tabbed panel

Outside Navigator 4.0's special region, the HTML and script for the tabbed panel are much the same as in the previous column. Within the NOLAYER tags is a relatively positioned DIV element.

<DIV id="p1" style="background-color:transparent; position:relative; height:254px; width:400px; padding:6px;">

Note the padding property; even recent browsers differ in whether padding is rendered inside or outside width, so it is included here to make sure that this DIV allocates enough space. This DIV contains two series of absolutely positioned DIVs that make up the tabs and panels. These are similar to their LAYER counterparts but get their appearance entirely from styles and capture click events with onclick:

<DIV class="tab" style="background-color:yellow; left: 0px; top: 24px; z-index:8" id="p1tab0" onclick="selectTab(0)">
HTML
</DIV>

The tab and panel DIVs use class attributes to import common style properties from a STYLE element in the page's HEAD. You can also add style rules for LAYER for the Navigator 4.0 version, but they must apply to both tabs and panels, as Navigator 4.0 does not support class in LAYER.

The script is modified to support row swapping and non-DOM browsers. For the row swapping, it adds the divLocation and newLocation arrays for tracking the tabs' positions:

var divLocation = new Array(numLocations)
var newLocation = new Array(numLocations)

for(var i=0; i<numLocations; ++i) {
divLocation[i] = i
newLocation[i] = i
}

The function updatePosition() uses these arrays to position a tab based on whether or not it was clicked and should go in front.

function updatePosition(div, newPos) {
   newClip=tabHeight*(Math.floor(newPos/tabsPerRow)+1)
   if (document.layers) {
      div.style=div;
      div.clip.bottom=newClip; // clip off bottom
      } else {
      div.style.clip="rect(0 auto "+newClip+" 0)"
      }
   div.style.top = (numRows-(Math.floor(newPos/tabsPerRow) + 1)) * (tabHeight-vOffset)
   div.style.left = (newPos % tabsPerRow) * tabWidth + (hOffset * (Math.floor(newPos / tabsPerRow)))
}

Note that updatePosition() accounts for Navigator 4.0 (via document.layers) when adjusting the tab's clip property. The clip is adjusted because the tabs change height when they move to a new row.

To account for the different object models, the getDiv() function returns an object reference matching a tab or panel's id according to the browser. The setZIndex() function uses getDiv() to adjust an object's z-index.

function getDiv(s,i) {
   var div
   if (document.layers) {
      div = document.layers[panelID].layers[panelID+s+i]
   } else if (document.all && !document.getElementById) {
      div = document.all[panelID+s+i]
   } else {
      div = document.getElementById(panelID+s+i)
   }
   return div
}

function setZIndex(div, zIndex) {
   if (document.layers) div.style = div;
   div.style.zIndex = zIndex
}

Finally, the selectTab() function now uses getDiv() and setZIndex() to reorder the tabs and panels when a tab is clicked. selectTab() also uses updatePosition() to move the tabs to the appropriate rows.

function selectTab(n) {
   // n is the ID of the division that was clicked
   // firstTab is the location of the first tab in the selected row
   var firstTab = Math.floor(divLocation[n] / tabsPerRow) * tabsPerRow
   // newLoc is its new location
   for(var i=0; i<numDiv; ++i) {
      // loc is the current location of the tab
      var loc = divLocation[i]
      // If in the selected row
      if(loc >= firstTab && loc < (firstTab + tabsPerRow)) newLocation[i] = (loc - firstTab)
      else if(loc < tabsPerRow) newLocation[i] = firstTab+(loc % tabsPerRow)
      else newLocation[i] = loc
   }
   // Set tab positions & zIndex, update location
   for(var i=0; i<numDiv; ++i) {
      var loc = newLocation[i]
      var div = getDiv("panel",i)
      if(i == n) setZIndex(div, numLocations +1)
      else setZIndex(div, numLocations - loc)
      divLocation[i] = loc
      div = getDiv("tab",i)
      updatePosition(div, loc)
      if(i == n) setZIndex(div, numLocations +1)
      else setZIndex(div,numLocations - loc)
   }
}

An unfortunate casualty of this update is Opera, which doesn't yet support the clip property. clip is necessary only to make a seamless junction between tab and panel. If you don't mind a border between them, you can modify selectTab() to set the tab's z-index first, which will place the tab behind the panel.

Jamie Jaworski writes for CNET Builder.com and is the author of five books on JavaScript, Java, and HTML, including Mastering JavaScript and JScript and Java 2 Unleashed.

Editor's Picks