[ Team LiB ] Previous Section Next Section

10.8 Creating Drop-Down Navigation Menus

NN 6, IE 5

10.8.1 Problem

You want navigation menus to drop down from a menu bar on the page.

10.8.2 Solution

This solution demonstrates one of dozens of ways to implement drop-down menus. It relies on some simple images for the always visible menu headers, a single external style sheet called menus.css (shown in Example 10-3 in the Discussion), and an external JavaScript library called menus.js (shown in Example 10-4 in the Discussion). You can see the results in Figure 10-2.

Figure 10-2. Drop-down menus in action
figs/jsdc_1002.gif

To implement this solution, you must create (or borrow) menu header images for normal and highlighted versions. In several places within the menus.js library, you fill in the text and links for the menu items. The library does the rest to assemble the DHTML components for the menus, under guidance of the menus.css style sheets.

10.8.3 Discussion

The menu bar is hardwired into the page's HTML as a single div containing three img elements. Each img element is surrounded by a hyperlink (a) element containing basic navigation action for use by drop-down menu users and simple clicking. Mouse event handlers for the img elements are assigned by a script:

<div id="menubar">
<a href="index.html"><img id="menuImg_1" class="menuImg" 
src="home_off.jpg" border="0" height="20" 
width="80"></a><a href="catalog.html"><img id="menuImg_2" 
class="menuImg" src="catalog_off.jpg" border="0" height="20" 
width="80"></a><a href="about.html"><img id="menuImg_3" 
class="menuImg" src="about_off.jpg" border="0"
height="20" width="80"></a>
</div>

An imported style sheet, menus.css, shown in Example 10-3, contains specifications for the drop-down menu containers (class menuWrapper, which is assigned by script during initialization) and the individual items in the menus in both normal and highlighted states.

Example 10-3. menus.css style sheet
.menuWrapper {
              position:absolute; 
              width:162px; 
              background-color:#ff9933; 
              visibility:hidden; 
              border-style:solid; 
              border-width:2px; 
              border-color:#efefef #505050 #505050 #efefef; 
              padding:3px; 
             }
.menuItem    {
              cursor:pointer; 
              font-size:16px; 
              font-family:Arial, Helvetica, sans-serif; 
              border-bottom:1px solid #505050; 
              border-top:1px solid #efefef;
              padding-left:10px; 
              color:black; 
              background-color:#ff9933; 
              text-decoration:none; 
              position:absolute; 
              left:0px; 
              width:159px; 
              height:22px; 
              line-height:1.4em
             }
.menuItemOn  {
              cursor:pointer; 
              font-size:16px; 
              font-family:Arial, Helvetica, sans-serif; 
              border-bottom:1px solid #505050; 
              border-top:1px solid #efefef;
              padding-left:10px; 
              color:#0099ff; 
              background-color:#ffcc99; 
              text-decoration:underline; 
              position:absolute; 
              left:0px; 
              width:159px; 
              height:22px; 
              line-height:1.4em
             }

Example 10-4 shows the menus.js library for all the scripts used with the menus.

Example 10-4. menus.js drop-down menu library
// global menu state
var menuReady = false;
  
// precache menubar image pairs
if (document.images) {
    var imagesNormal = new Array( );
    imagesNormal["home"] = new Image(20, 80);
    imagesNormal["home"].src="home_off.jpg";
    imagesNormal["catalog"] = new Image(20, 80);
    imagesNormal["catalog"].src  = "catalog_off.jpg";
    imagesNormal["about"] = new Image(20, 80);
    imagesNormal["about"].src  = "about_off.jpg";
  
    var imagesHilite = new Array( );
    imagesHilite["home"] = new Image(20, 80);
    imagesHilite["home"].src="home_on.jpg";
    imagesHilite["catalog"] = new Image(20, 80);
    imagesHilite["catalog"].src  = "catalog_on.jpg";
    imagesHilite["about"] = new Image(20, 80);
    imagesHilite["about"].src  = "about_on.jpg";
}
  
function getElementStyle(elem, IEStyleProp, CSSStyleProp) {
    if (elem.currentStyle) {
        return elem.currentStyle[IEStyleProp];
    } else if (window.getComputedStyle) {
        var compStyle = window.getComputedStyle(elem, "");
        return compStyle.getPropertyValue(CSSStyleProp);
    }
    return "";
}
  
// carry over some critical menu style sheet attribute values
var CSSRuleValues = {menuItemHeight:"18px",
                     menuItemLineHeight:"1.4em",
                     menuWrapperBorderWidth:"2px",
                     menuWrapperPadding:"3px",
                     defaultBodyFontSize:"12px"
                    };
  
// specifications for menu contents and menubar image associations
var menus = new Array( );
menus[0] = {mBarImgId:"menuImg_1",
            mBarImgNormal:imagesNormal["home"],
            mBarImgHilite:imagesHilite["home"],
            menuItems:[  ],
            elemId:""
           };
menus[1] = {mBarImgId:"menuImg_2",
            mBarImgNormal:imagesNormal["catalog"],
            mBarImgHilite:imagesHilite["catalog"],
            menuItems:[ {text:"Deluxe Line", href:"catalog_deluxe.html"},
                        {text:"Budget Line", href:"catalog_budget.html"},
                        {text:"Export", href:"catalog_export.html"},
                        {text:"Order Print Catalog", href:"catalog_order.html"}
                      ],
            elemId:""
           };
menus[2] = {mBarImgId:"menuImg_3", 
            mBarImgNormal:imagesNormal["about"],
            mBarImgHilite:imagesHilite["about"],
            menuItems:[ {text:"Press Releases", href:"press.html"},
                        {text:"Executive Staff", href:"staff.html"},
                        {text:"Map to Our Offices", href:"map.html"},
                        {text:"Company History", href:"history.html"},
                        {text:"Job Postings", href:"jobs.html"},
                        {text:"Contact Us", href:"contact.html"}
                      ],
            elemId:""
           };
  
// create hash table-like lookup for menu objects with id string indexes
function makeHashes( ) {
    for (var i = 0; i < menus.length; i++) {
        menus[menus[i].elemId] = menus[i];
        menus[menus[i].mBarImgId] = menus[i];
    }
}
  
// assign menu label image event handlers
function assignLabelEvents( ) {
    var elem;
    for (var i = 0; i < menus.length; i++) {
        elem = document.getElementById(menus[i].mBarImgId);
        elem.onmouseover = swap;
        elem.onmouseout = swap;
    }
}
  
// invoked from initMenu( ), generates the menu div elements and their contents.
// all this action is invisible to user during construction
function makeMenus( ) {
    var menuDiv, menuItem, itemLink, mbarImg, textNode, offsetLeft, offsetTop;
    
    // determine key adjustment factors for the total height of menu divs
    var menuItemH = 0;
    var bodyFontSize = parseInt(getElementStyle(document.body, "fontSize", "font-size"));
    // test to see if browser's font size has been adjusted by the user
    // and that the new size registers as an applied style property
    if (bodyFontSize =  = parseInt(CSSRuleValues.defaultBodyFontSize)) {
        menuItemH = (parseFloat(CSSRuleValues.menuItemHeight));
    } else {
        // works nicely in Netscape 7
        menuItemH = parseInt(parseFloat(CSSRuleValues.menuItemLineHeight) * bodyFontSize);
    }
    var heightAdjust = parseInt(CSSRuleValues.menuWrapperPadding) + 
        parseInt(CSSRuleValues.menuWrapperBorderWidth);
    if (navigator.appName =  = "Microsoft Internet Explorer" && 
        navigator.userAgent.indexOf("Win") != -1 && 
        (typeof document.compatMode =  = "undefined" || 
        document.compatMode =  = "BackCompat")) {
        heightAdjust = -heightAdjust;
    }
    
    // use menus array to drive div creation loop
    for (var i = 0; i < menus.length; i++) {
        menuDiv = document.createElement("div");
        menuDiv.id = "popupmenu" + i;
        // preserve menu's ID as property of the menus array item
        menus[i].elemId = "popupmenu" + i;
        menuDiv.className = "menuWrapper";
        if (menus[i].menuItems.length > 0) {
            menuDiv.style.height = (menuItemH * menus[i].menuItems.length) - 
            heightAdjust + "px";
        } else {
            // don't display any menu div lacking menu items
            menuDiv.style.display = "none";
        }
        // define event handlers
        menuDiv.onmouseover = keepMenu;
        menuDiv.onmouseout = requestHide;
  
        // set stacking order in case other layers are around the page
        menuDiv.style.zIndex = 1000;
        
        // assemble menu item elements for inside menu div
        for (var j = 0; j < menus[i].menuItems.length; j++) {
            menuItem = document.createElement("div");
            menuItem.id = "popupmenuItem_" + i + "_" + j;
            menuItem.className = "menuItem";
            menuItem.onmouseover = toggleHighlight;
            menuItem.onmouseout = toggleHighlight;
            menuItem.onclick = hideMenus;
            menuItem.style.top = menuItemH * j + "px";
            itemLink = document.createElement("a");
            itemLink.href = menus[i].menuItems[j].href;
            itemLink.className = "menuItem";
            itemLink.onmouseover = toggleHighlight;
            itemLink.onmouseout = toggleHighlight;
            textNode = document.createTextNode(menus[i].menuItems[j].text);
            itemLink.appendChild(textNode);
            menuItem.appendChild(itemLink);
            menuDiv.appendChild(menuItem);
        }
        // append each menu div to the body
        document.body.appendChild(menuDiv);
    }
    makeHashes( );
    assignLabelEvents( );
    // pre-position menu
    for (i = 0; i < menus.length; i++) {
        positionMenu(menus[i].elemId);
    }
    menuReady = true;
}
  
// initialize global that helps manage menu hiding
var timer;
  
// invoked from mouseovers inside menus to cancel hide
// request from mouseout of menu bar image et al.
function keepMenu( ) {
    clearTimeout(timer);
}
  
function cancelAll( ) {
    keepMenu( );
    menuReady = false;
}
  
// invoked from mouseouts to request hiding all menus
// in 1/4 second, unless cancelled
function requestHide( ) {
    timer = setTimeout("hideMenus( )", 250);
}
  
// "brute force" hiding of all menus and restoration
// of normal menu bar images
function hideMenus( ) {
    for (var i = 0; i < menus.length; i++) {
       document.getElementById(menus[i].mBarImgId).src = menus[i].mBarImgNormal.src;
       var menu = document.getElementById(menus[i].elemId)
       menu.style.visibility = "hidden";
    }
}
  
// set menu position just before displaying it
function positionMenu(menuId){
    // use the menu bar image for position reference of related div
    var mBarImg = document.getElementById(menus[menuId].mBarImgId);
    var offsetTrail = mBarImg;
    var offsetLeft = 0;
    var offsetTop = 0;
    while (offsetTrail) {
        offsetLeft += offsetTrail.offsetLeft;
        offsetTop += offsetTrail.offsetTop;
        offsetTrail = offsetTrail.offsetParent;
    }
    if (navigator.userAgent.indexOf("Mac") != -1 && 
        typeof document.body.leftMargin != "undefined") {
        offsetLeft += document.body.leftMargin;
        offsetTop += document.body.topMargin;
    }
    var menuDiv = document.getElementById(menuId);
    menuDiv.style.left = offsetLeft + "px";
    menuDiv.style.top = offsetTop + mBarImg.height + "px";
}
  
// display a particular menu div
function showMenu(menuId) {
    if (menuReady) {
        keepMenu( );
        hideMenus( );
        positionMenu(menuId);
        var menu = document.getElementById(menuId);
        menu.style.visibility = "visible";
    }
}
  
// menu bar image swapping, invoked from mouse events in menu bar
// swap style sheets for menu items during rollovers
function toggleHighlight(evt) {
    evt = (evt) ? evt : ((event) ? event : null);
    if (typeof menuReady != "undefined") {
        if (menuReady && evt) {
            var elem = (evt.target) ? evt.target : evt.srcElement;
            if (elem.nodeType =  = 3) {
                elem = elem.parentNode;
            }
            if (evt.type =  = "mouseover") {
                keepMenu( );
                elem.className ="menuItemOn";
            } else {
                elem.className ="menuItem";
                requestHide( );
            }
            evt.cancelBubble = true;
        }
    }
}
  
function swap(evt) {
    evt = (evt) ? evt : ((event) ? event : null);
    if (typeof menuReady != "undefined") {
        if (evt && (document.getElementById && document.styleSheets) && menuReady) {
            var elem = (evt.target) ? evt.target : evt.srcElement;
            if (elem.className =  = "menuImg") {
                if (evt.type =  = "mouseover") {
                    showMenu(menus[elem.id].elemId);
                    elem.src = menus[elem.id].mBarImgHilite.src;
                } else if (evt.type =  = "mouseout") {
                    requestHide( );
                }
                evt.cancelBubble = true;
            }
        }
    }
}
  
// create menus only if key items are supported
function initMenus( ) {
    if (document.getElementById && document.styleSheets) {
        setTimeout("makeMenus( )", 5);
        window.onunload=cancelAll;
    }
}

Scripts begin with one global variable declaration, menuReady, that is ultimately used as a flag to let other functions know when the menus are available for animation. Next is code for precaching all menu button images in two states (á là Recipe 12.1). A utility function, getElementStyle( ), is a variation of the function from Recipe 11.12, which this script uses to keep menu item font sizes in sync with user-selected font sizes in Mozilla-based browsers.

Because browser security restrictions (at least in IE 6 and Netscape 6 and 7) prevent scripts from reading rule property values of style sheets, the script includes a global object that replicates some key style sheet values that the scripts use for help with menu positioning and sizing. You can get these values from the style sheet settings.

Next is the creation of objects (inside an array called menus) that contain vital menu details needed later when they are built. Each object has five properties, which are described in more detail later.

Scripts for hiding and showing the menus frequently require that a menu's reference be capable of pointing to the swappable image at the top of the menu, and vice versa. To speed this process along (i.e., to avoid looping through all of the menu's array items in search of properties that match either the image or menu IDs), it is more convenient during initialization to create a one time, simulated hash table (in the makeHashes( ) function), whose string index values consist of both the image and menu element IDs (see Recipe 3.9). The result is a hash table that has two pointers to each menus array entry�one for the image ID and one for the menu ID. Another function invoked during initialization, assignLabelEvents( ), assigns the mouse rollover event handlers to the image elements at the top of each menu.

The biggest function of the recipe, makeMenus( ), assembles the menu elements, using W3C DOM node creation syntax. This routine is invoked at initialization time and depends on the menus array, defined earlier, to help populate each menu with the text and link for each menu item.

There are two rollover concerns for this application: the menu bar image swapping and the display of the menus. While their actions are different, the actions work with each other. But first, some support code to do the dirty work is needed. Because of the interaction between menu bar image and menus, a setTimeout( ) timer is used to assist in cleaning up menus that are no longer necessary. The timer identifier (created when setTimeout( ) is invoked) is preserved as a global variable called timer.

You can't just hide the menu when a mouseout event occurs in one of the menu bar images, since the mouse may be headed to the currently displayed menu and you want that menu to remain in place. To assist with this task is the keepMenu( ) function, which cancels a timer set in the requestHide( ) function, and thus makes sure the menu stays visible.

A related function, cancelAll( ), which is invoked by an onunload event handler, guards against potential problems (particularly in Netscape 6 and later) in states between page loadings, while the cursor may still be rolling around a swappable image. Global variables are in transient states and may be valid when a function begins, but be gone by the time the value is needed. The cancelAll( ) function puts the page in a quiet state during the transition.

All mouseout events from the menu bar and menus start the 1/4-second delay clock ticking in the requestHide( ) function. If the timer is still valid in 1/4 second, the hideMenus( ) function runs. The hideMenus( ) function performs a blanket restoration of menu bar images and menu display.

A separate positioning function, positionMenu( ), is invoked before any menu is displayed. Therefore, if the menu bar changes position on the page (perhaps due to dynamic content or a resized browser window), the menu is displayed correctly with respect to the menu title image.

Invoked by all mouseover events in menu bar images and menu components, the showMenu( ) function turns off the timer so that any pending hideMenu( ) call won't occur. Then it immediately hides all menus (rather than waiting for the timer) and shows the menu div element whose ID is accessed from the menus array (index passed as a parameter) elemId property.

As the user rolls the mouse pointer over items within a displayed menu, the mouseover and mouseout events trigger style sheet changes to the entries (by changing the element's class assignment in the toggleHighlight( ) function). In the case of a mouseover, the keepMenu( ) function fires to make sure any pending menu hiding gets cancelled. For a mouseout, the hide request is made, in case the mouseout motion is toward some other region on the page.

The swap( ) function, invoked by the mouse event handlers of the menu bar images, is the main trigger to display a drop-down menu. In response to a mouseover event, the menu is displayed, and the menu bar image changes to the highlighted version. For a mouseout event, a hide request is made. If the hide timer isn't cancelled in time, the menu disappears and the menu bar image returns to its default image.

Invoked by the onload event handler, the initMenus( ) function certifies that the browser has the right stuff for the menuing system. It looks not only for basic W3C DOM support, but also for the more esoteric document.styleSheets property (lacking in Opera through Version 6). If the browser supports the necessary facilities, the makeMenus( ) function generates the menu div elements. It also assigns an onunload event handler to the window so that any pending menu-hide request is cancelled before the page goes away.

One of the goals of the design shown in this recipe is to minimize the amount of custom work needed to implement the drop-down menu in a variety of visual contexts. Most of the menuing libraries available from other sources go to even further extremes in this regard, building very complex and thorough systems of custom objects and dynamically written HTML. General-purpose libraries, especially those designed to work with outdated object models (e.g., the Navigator 4 DOM), need the extra complexity to accommodate as wide a range of deployment scenarios as possible. This example, on the other hand, is pared to a smaller size, and might require a bit more work to blend into your design (particularly around the images for the menu bar). But there should plenty of ideas here that you can use as-is for a largely automatic menuing systems compatible with IE 5 or later and NN 6 or later.

Reliance on style sheets for the visual aspects of the menus simplifies your experimentation with different looks, such as color combinations, font specifications, and sizes. The JavaScript code supporting the style sheets makes only a few assumptions:

  • All drop-down menus are the same width, regardless of the widths of their menu title images (which may vary as you see fit).

  • Widths of menu items are determined by subtracting the menu wrapper's padding from the wrapper's width (162 pixels for the wrapper and 159 pixels for menu items in this recipe).

  • Style sheets for the two states of menu item (normal and highlighted) should specify the same dimensions and font sizes.

  • Several menu item height and menu wrapper border style sheet properties must be replicated in the CSSRuleValues object defined in the menu JavaScript code. If you modify your style sheet, duplicate the changes in CSSRuleValues.

You need to customize a few parts of the JavaScript code to fit the menu system to your graphical menus. First, establish the image precaching for the individual graphics in your menu bar (your menu bar may be a vertical list of menu titles or other element arrangement). Image array string index values and src properties are set just like they are in Recipe 12.1.

Next are the specifications for the content of the menus. The menus array contains custom objects corresponding to the menus that get created elsewhere. Each custom object has numerous properties that get used through various stages of creating, displaying, and hiding the menus:

mBarImgId

String containing the ID of the img element for the menu bar menu title image associated with this menu.

mBarImgNormal

Reference to the precached image object for the normal state of the menu title image.

mBarImgHilite

Reference to the precached image object for the highlighted state of the menu title image.

menuItems

Array of objects, one for each item in the menu (or an empty array if the menu title has no drop-down menu items). Each object consists of two properties: one for the text that shows in the drop-down menu; the other with the URL to which the user will navigate when selecting the menu item.

elemId

Initialized as an empty string, this property gets its value filled in automatically when the menus are created. Do nothing with this property.

The final item to customize is the menu bar. Each menu title must be its own img element (and have two states�normal and highlighted�as fed to the precaching code earlier). You must add onmouseover and onmouseout event handlers to each img element's HTML code. Both event handlers invoke the same method (swap( )), and pass as the first parameter the zero-based index integer corresponding to the index of the menus array entry bearing the menu specifications for this menu title.

Although it is not a requirement for the menuing system, I recommend that each menu title image also be surrounded by a hyperlink. This is so that underpowered browsers and search engines are able to follow paths to the next lower levels of your web site, even if the destinations of those links are simple pages offering traditional links to the same items in your drop-down menus. Users of your menus will be able to bypass those pages.

All of the other code in the recipe works on its own to build the menus and handle their display activities. Items in the menus are created as traditional links so that users who expect to see URLs of links in the status bar will be right at home.

Code in the makeMenus( ) function assumes that the menus are to be deployed as true drop-down menus by positioning the menus flush left and just below the images in the menu bar. If you need the menus to push to the right or upward, you'll need to adjust the statements that set values of the menuDiv.style.left and menuDiv.style.top properties. Notice that in the drop-down recipe, the left position is lined up with the mBarImg.offsetLeft value, and that the top is pushed to the bottom of the image by the addition of mBarImg.height. If you want to make the menu push to the right, the top would be flush with the mBarImg.offsetTop, and the left would be extended to the right by the amount of mBarImg.width. To make the menu appear to pop upward, the top of the menu would be at the mBarImg.offsetTop minus the height of the menuDiv (set earlier in this function). In all cases, leave the plain offsetLeft and offsetTop variable adjustments in the formulas because they take care of some nonstandard position alignment behaviors of Internet Explorer.

It's unfortunate that misplaced security restrictions prevent scripts from reading style sheet rule attributes directly. As a workaround, we have to duplicate some important values in the script code (in the CSSRuleValues global variable) to refer to them while sizing the menus. If, in the future, browsers are able to access style sheet rules without offending security restrictions, you should be able to let the definitions in the style sheet rules govern the menu positioning and sizing. For future reference, the following function adheres to the W3C DOM (and IE syntax idiosyncrasies) to read individual rule values from a style sheet embedded in a style or link element bearing an ID. While the function works as-is from a local hard disk, it generates security-related errors when the page is accessed from a server:

// utility function invoked from makeMenus( ) 
// returns style sheet attribute value.
// parameters are: ID of <style> element, selector name, and attribute name
function getCSSRuleValue(styleID, selector, attr) {
    var sheet, styleElem, i;
    for (i = 0; i < document.styleSheets.length; i++) {
        sheet = document.styleSheets[i];
        styleElem = (sheet.ownerNode) ? sheet.ownerNode : 
           ((sheet.owningElement) ? sheet.owningElement : null);
        if (styleElem) {
            if (styleElem.id =  = styleID) {
                break;    
            }
        }
    }
    var rules = (sheet.cssRules) ? sheet.cssRules : ((sheet.rules) ? 
       sheet.rules : null);
    if (rules) {
        for (i = 0; i < rules.length; i++) {
            if (rules[i].selectorText =  = selector || rules[i].selectorText =  =        
               "*" +  selector) {
                return rules[i].style[attr];
            }
        }
    }
    return null;
}

If you design your menu bar to live in one frame and expect the pop-up menu to appear in another frame, you have some more coding to do because div elements exist within the context of a browser window and do not extend into adjacent frames. To begin, the code that generates the div elements in the changeable frame has to be incorporated into each document that loads into that other frame. But that code needs to be modified to look to the menu bar title images in the navigation frame for position information. You also have to take into account any possible scrolling that occurs in the changeable frame, since it influences the position of the menu within the page, even though the menu title is static. Additional cross-frame communication is needed to synchronize the image swapping and menu showing/hiding actions.

As a last note, you may be interested in the rationale behind the requestHide( ) function and the use of the setTimeout( ) method to hide a menu. Notice that two different elements' states change: the menu title image's src and the visibility of the associated menu. A simple onmouseout event handler on the image to swap the image and hide the menu works only if the user moves the pointer in a direction other than downward into the menu. In the latter case, you don't want the menu title to change back to its original state or hide the menu. Instead, the image's onmouseout event handler sets the timer to execute the restoration process in 250 milliseconds via setTimeout( ). However, the onmouseover event handlers of menu components, which fire before the 250 milliseconds are up, clear the timer so that the state stays the same, all while the cursor is in the menu (or goes back up to the menu title).

If the user slides over to another menu title, the time-out timer is also cleared so that the hideMenus( ) method can restore initial state instantly (which is a little faster than 250 milliseconds, so response feels quicker). Only when the user slides the pointer out and away from the menu bar or a visible menu does the timer have a chance to invoke hideMenus( ), which puts everything back the way it was when the page loaded.

With the requestHide( ) function setting a timer to go off in the future (no matter how soon the future will be), you must set the onunload event handler for the page to invoke cancelHide( ). Failure to do so will allow the hideMenus( ) function call in the timer queue to execute after the page has gone away � taking its scripts with it. The result is a script error. If your pages assign another onunload event handler via script properties for other purposes, you need to define yet another function that invokes both cancelHide( ) and your other function, so that the onunload event can invoke both functions.

10.8.4 See Also

Recipe 3.9 for simulating a hash table; Recipe 12.1 for precaching images; Recipe 12.7 for hiding and showing elements; Recipe 13.1 for creating a positioned element.

    [ Team LiB ] Previous Section Next Section