Previous Section Next Section

10.11 Creating Collapsible XML Menus

NN 6, IE 5(Win)

10.11.1 Problem

You want to present a navigation menu that looks and operates like the collapsible hierarchy shown in the lefthand frame of many popular products (Windows Explorer, Outlook Express, Adobe Acrobat PDF bookmarks, and so on), but the data needs to come from an XML data source.

10.11.2 Solution

Use the XMLoutline.js library shown in Example 10-7 in the Discussion to convert a specially formatted XML outline document to an interactive collapsible menu like the one shown in Figure 10-4 of Recipe 10.10. Include a simple, empty div element in the HTML portion of your page where the outline is to appear:

<div id="content"></div>

In the body, assign the menu initialization function, initXMLOutline( ), to the onload event handler, specifying the filename of the XML file:


Also include at the bottom of the page an <object> tag that tries to load the relevant ActiveX control in Internet Explorer for Windows before it's needed in the script code:

<!-- Try to load Msxml.DOMDocument ActiveX to assist support verification -->
<object id="msxml" WIDTH="1" HEIGHT="1" 
        classid="CLSID:2933BF90-7B36-11d2-B20E-00C04F983E60" ></object>

Other pieces that you need to provide or customize, as described in the Discussion, are the following:

  • The OPML source for the data

  • Images for the outline graphics

  • Script global variable values for precached images and outline item link target

  • Style sheet rule dimensions to match your image designs and font specifications

This recipe works with Internet Explorer 5 or later for Windows and Netscape 6 or later.

10.11.3 Discussion

The recipe shown here is similar to the JavaScript data-based solution shown in Recipe 10.10. The difference is that the data is formatted in outline-flavored XML: OPML (Outline Processing Markup Language) designed by Userland's outline (and other things) guru Dave Winer ( Thus, while all of the toggling and state-switching code is identical to Recipe 10.10, the loading of the external OPML file and creation of the outline is different. For the sake of completeness and context, however, we treat this recipe separately.

Participating in this recipe are a few style sheet rules that control the appearance and layout of elements that scripts create on the fly. You may include them in the HTML page or import them:

<style type="text/css">
    .row {vertical-align:middle; font-size:12px; font-family:Arial,sans-serif}
    .OLBlock {display:none}
    img.widgetArt {vertical-align:text-top}

The XMLoutline.js library is shown in Example 10-7. Because all of the data for the outline comes from a separate file, this library consists entirely of interactive code.

Example 10-7. The XMLoutline.js library
          Global Variables
// precache art files and sizes for widget styles and spacers
// (all images must have same height/width)
var collapsedWidget = new Image(20, 16);
var collapsedWidgetStart = new Image(20, 16);
var collapsedWidgetEnd = new Image(20, 16);
var expandedWidget = new Image(20, 16);
var expandedWidgetStart = new Image(20, 16);
var expandedWidgetEnd = new Image(20, 16);
var nodeWidget = new Image(20, 16);
var nodeWidgetEnd = new Image(20, 16);
var emptySpace = new Image(20, 16);
var chainSpace = new Image(20, 16);
// miscellaneous globals
var widgetWidth = "20";
var widgetHeight = "16";
var currState = "";
var displayTarget = "contentFrame";
// XML document object
var xDoc;
  Toggle Display and Icons
// invert item state (expanded to/from collapsed)
function swapState(currState, currVal, n) {
    var newState = currState.substring(0,n);
    newState += currVal ^ 1 // Bitwise XOR item n;
    newState += currState.substring(n+1,currState.length);
    return newState;
// retrieve matching version of 'minus' images
function getExpandedWidgetState(imgURL) {
    if (imgURL.indexOf("Start") != -1) {
        return expandedWidgetStart.src;
    if (imgURL.indexOf("End") != -1) {
        return expandedWidgetEnd.src;
    return expandedWidget.src;
// retrieve matching version of 'plus' images
function getCollapsedWidgetState(imgURL) {
    if (imgURL.indexOf("Start") != -1) {
        return collapsedWidgetStart.src;
    if (imgURL.indexOf("End") != -1) {
        return collapsedWidgetEnd.src;
    return collapsedWidget.src;
// toggle an outline mother entry, storing new state value;
// invoked by onclick event handlers of widget image elements
function toggle(img, blockNum) {
    var newString = "";
    var expanded, n;
    // modify state string based on parameters from IMG
    expanded = currState.charAt(blockNum);
    currState = swapState(currState, expanded, blockNum);
    // dynamically change display style
    if (expanded =  = "0") {
        document.getElementById("OLBlock" + blockNum).style.display = "block";
        img.src = getExpandedWidgetState(img.src);
    } else {
        document.getElementById("OLBlock" + blockNum).style.display = "none";
        img.src = getCollapsedWidgetState(img.src);
function expandAll( ) {
    var newState = "";
    while (newState.length < currState.length) {
        newState += "1";
    currState = newState;
    initExpand( );
function collapseAll( ) {
    var newState = "";
    while (newState.length < currState.length) {
        newState += "0";
    currState = newState;
    initExpand( );
   Outline HTML Generation
// apply default expansion state from outline's header
// info to the expanded state for one element to help 
// initialize currState variable
function calcBlockState(n) {
    var ol = xDoc.getElementsByTagName("body")[0];
    var outlineLen = ol.getElementsByTagName("outline").length;
    // get OPML expansionState data
    var expandElem = xDoc.getElementsByTagName("expansionState")[0];
    var expandedData = (expandElem.childNodes.length) ? 
        expandElem.firstChild.nodeValue.split(",") : null;
    if (expandedData) {
        for (var j = 0; j < expandedData.length; j++) {
            if (n =  = expandedData[j] - 1) {
                return "1";
    return "0";
// counters for reflexive calls to drawOutline( )
var currID = 0;
var blockID = 0;
// generate HTML for outline
function drawOutline(ol, prefix) {
    var output = "";
    var nestCount, link, nestPrefix, lastInnerNode;
    ol = (ol) ? ol : xDoc.getElementsByTagName("body")[0];
    prefix = (prefix) ? prefix : "";
    if (ol.childNodes[ol.childNodes.length - 1].nodeType =  = 3) {
        ol.removeChild(ol.childNodes[ol.childNodes.length - 1]);
    for (var i = 0; i < ol.childNodes.length ; i++) {
        if (ol.childNodes[i].nodeType =  = 3) {
        if (ol.childNodes[i].childNodes.length > 0 && 
            ol.childNodes[i].childNodes[ol.childNodes[i].childNodes.length - 1].nodeType 
            childNodes[i].childNodes.length - 1].nodeType =  = 3) {
                 ol.childNodes[i].childNodes.length - 1]);
        nestCount = ol.childNodes[i].childNodes.length;
        output += "<div class='OLRow' id='line" + currID++ + "'>\n";
        if (nestCount > 0) {
            output += prefix;
            output += "<img id='widget" + (currID-1) + "' src='" + 
                ((i=  = ol.childNodes.length-1) ? collapsedWidgetEnd.src : (blockID=  =0) ? 
                    collapsedWidgetStart.src : collapsedWidget.src);
            output += "' height=" + widgetHeight + " width=" + widgetWidth;
            output += " title='Click to expand/collapse nested items.' onClick= " +  
              "'toggle(this," +  blockID + ")'>";
            link =  (ol.childNodes[i].getAttribute("uri")) ?
                ol.childNodes[i].getAttribute("uri") : "";
            if (link) {
                output += "&nbsp;<a href='" + link + "' class='itemTitle' title='" + 
                link + "' target='" + displayTarget + "'>" ;
            } else {
                output += "&nbsp;<a class='itemTitle' title='" + link + "'>";
            output += "<span style='position:relative; top:-3px; height:11px'>&nbsp;" + 
                ol.childNodes[i].getAttribute("text") + "</span></a>";
            currState += calcBlockState(currID-1);
            output += "<span class='OLBlock' blocknum='" + blockID + "' id='OLBlock" + 
                blockID++ + "'>";
            nestPrefix = prefix;
            nestPrefix += (i =  = ol.childNodes.length - 1) ? 
                       "<img src='" + emptySpace.src + "' height=" + widgetHeight + 
                           " width=" + widgetWidth + ">" :
                       "<img src='" + chainSpace.src + "' height=" + widgetHeight + 
                           " width=" + widgetWidth + ">"
            output += drawOutline(ol.childNodes[i], nestPrefix);
            output += "</span></div>\n";
        } else {
            output += prefix;
            output += "<img id='widget" + (currID-1) + "' src='" + 
                ((i =  = ol.childNodes.length - 1) ? nodeWidgetEnd.src : nodeWidget.src);
            output += "' height=" + widgetHeight + " width=" + widgetWidth + ">";
            link =  (ol.childNodes[i].getAttribute("uri")) ? 
                ol.childNodes[i].getAttribute("uri") : "";
            if (link) {
                output += "&nbsp;<a href='" + link + "' class='itemTitle' title='" + 
                link + "' target='" + displayTarget + "'>";
            } else {
                output += "&nbsp;<a class='itemTitle' title='" + link + "'>";
            output +="<span style='position:relative; top:-3px; height:11px'>" +  
                ol.childNodes[i].getAttribute("text") + "</span></a>";
            output += "</div>\n";
    return output;
     Outline Initializations
// expand items set in expansionState OPML tag, if any
function initExpand( ) {
    for (var i = 0; i < currState.length; i++) {
        if (currState.charAt(i) =  = 1) {
            document.getElementById("OLBlock" + i).style.display = "block";
        } else {
            document.getElementById("OLBlock" + i).style.display = "none";
function finishInit( ) {
        // get outline body elements for iteration and conversion to HTML
        var ol = xDoc.getElementsByTagName("body")[0];
        // wrap whole outline HTML in a span
        var olHTML = "<span id='renderedOL'>" + drawOutline(ol) + "</span>";
        // throw HTML into 'content' div for display
        document.getElementById("content").innerHTML = olHTML;
        initExpand( );
function continueLoad(xFile) {
    // IE needs this delay to let loading complete before reading its content
    setTimeout("finishInit( )", 300);
// verify that browser supports XML features and load external .xml file
function loadXMLDoc(xFile) {
    if (document.implementation && document.implementation.createDocument) {
        // this is the W3C DOM way, supported so far only in NN6
        xDoc = document.implementation.createDocument("", "theXdoc", null);
    } else if (typeof ActiveXObject != "undefined") {
        // make sure real object is supported (sorry, IE5/Mac)
        if (document.getElementById("msxml").async) {
            xDoc = new ActiveXObject("Msxml.DOMDocument");
    if (xDoc && typeof xDoc.load != "undefined") {
        // Netscape 6+ needs this delay for loading; start two-stage sequence
        setTimeout("continueLoad('" + xFile + "')", 50);
    } else {
        var reply = confirm("This example requires a browser with XML support, such as " +
            "IE5+/Windows or Netscape 6+.\n \nGo back to previous page?");
        if (reply) {
            history.back( );
// initialize first time -- invoked onload
function initXMLOutline(xFile) {

The script begins by defining and precaching the small images that become components of the finished outline display (called widgets in this example). Images created for this solution are shown in Figure 10-5 with Recipe 10.10. All images are the same size. Each image object is assigned to a global variable, as are some other default values, including xDoc, which holds a reference to the hidden XML document.

The section marked "Toggle Display and Icons" includes functions that control the change of state between expanded and collapsed. A pair of functions named getExpandedWidgetState( ) and getCollapsedWidgetState( ) (both invoked by the toggle( ) function) retrieve one of three expanded or collapsed images depending on the name (specifically, a portion of the name) of the current widget image. The swapState( ) helper function (also invoked from toggle( )) performs binary arithmetic on the value of the currState variable to change a specific character from zero to one or vice versa (these characters represent the state of each branch node).

At the center of user interaction is the toggle( ) function, which is activated by onclick event handlers assigned to the each clickable widget. Because the event handlers are assigned while a script builds the outline, they can include parameters that indicate which item is being clicked. Thus, toggle( ) receives the widget's current image URL (used to determine which image should take its place) and a numeric ID associated with the span containing nested items. Although the function is small, it uses some helper functions — notably swapState( ), getExpandedWidget( ), and getCollapsedWidget( ) — to do the job. The two basic tasks of this function are to change the clicked widget image and display style sheet setting of the element containing nested items below it.

Two more functions, expandAll( ) and collapseAll( ), stand ready to fully expand and collapse the entire outline, if your user interface design provides user control of that feature.

The next-to-last block of code devotes itself to the creation of the HTML for the outline menu content. One helper function, calcBlockState( ), is invoked repeatedly during the HTML construction, and looks to see if the particular line number of the outline is supposed to be expanded by default. The data for these settings consists of a comma-delimited list of line numbers for expanded items (read from the expansionState tag of the OPML data).

Assembly of the outline's HTML in the drawOutline( ) function iterates through the node tree of the xDoc object (described later). But a major part of that iteration entails recursive calls to the same drawOutline( ) function to build the nested items. Therefore, a pair of counting variables (used to compose unique IDs for elements) are declared in the global space as currID and blockID.

Now we reach the drawOutline( ) function, which accumulates the HTML for the rendered outline. The content is assembled just once, while all subsequent adjustments to the expanded or collapsed states get controlled by style sheet settings. Layout of the various widget images is governed by the structure of the xDoc element hierarchy. Among the more complex tasks that the drawOutline( ) recursive code needs to keep track of is whether an image column position requires a vertical line to signify a later connection with an earlier item or just a blank space. The regularity of all widget image sizes lets the script build the widget image parts of each line as if the images were mosaic tiles.

The final code block performs all initializations. First is a function (initExpand( )) that iterates through the currState variable to establish the expand/collapse state of each nested block. This function is invoked not only by the following finishInit( ) function, but also by the expandAll( ) and collapseAll( ) function.

A three-stage sequence of functions (cascaded through setTimeout( ) to accommodate different timing issues in IE and NN) loads the external XML document and triggers the drawOutline( ) function. The sequence consists of three functions: loadXMLDoc( ), continueLoad( ), and finishInit( ). In the source code, the functions are defined in reverse order in which they execute. The execution sequence begins with validating that the browser supports reading external XML documents (via the IE/Windows and Mozilla/Netscape techniques). If validation succeeds, the document is loaded into the variable xDoc. Finally, the body portion of the OPML document is read from the hidden XML document, and passed to drawOutline( ) to generate the outline's HTML.

The main initXMLOutline( ) function, which is invoked by the onload event handler, simply gets the ball rolling, and is provided in the code to create a space for any other initializations that the page may include. Importantly, the URL of the OPML file is passed as a parameter to the initXMLOutline( ) function, although it could be applied at any point convenient to your code.

OPML is an extensible format for outline data. An OPML document is divided into two blocks, head and body. The body element contains all of the items that belong to the outline. Each item is called an outline element. Hierarchy (nesting) of outline items is determined entirely by the nesting of outline elements. You may add whatever attributes you like to an outline element and still conform to the format (provided the attribute/value syntax is well-formed XML). An excerpt of the OPML document that produces an outline like the one shown in Figure 10-4 (but with truncated URLs for space reasons) follows:

<?xml version="1.0"?> 
<opml version="1.0">
        <title>HTML Sections Outline</title>
        <dateCreated>Mon, 10 Sep 2002 03:40:00 GMT</dateCreated>
        <dateModified>Fri, 22 Sep 2002 19:35:00 GMT</dateModified>
        <ownerName>Danny Goodman</ownerName>
        <outline text="Forms">
            <outline text="Introduction" uri=""/>
            <outline text="Controls" uri="">
                <outline text="Control Types" 
            <outline text="FORM Element" uri=""/>
            <outline text="INPUT Element" uri="">
                <outline text="INPUT Control Types"  
                <outline text="Examples" 
        <outline text="Scripts">
            <outline text="Introduction" 
            <outline text="Designing Documents for Scripts" 
                <outline text="SCRIPT Element" 
                <outline text="Specifying the Scripting Language" 
                    <outline text="Default Language" 
                    <outline text="Local Language Declaration" 
                    <outline text="References to HTML Elements" 

Notice in the OPML document's structure that branch nodes contain other outline elements between their start and end tags, while leaf nodes contain no other outline elements.

If you issue the OPML content from a document on the server with an .opml extension, be sure that your server configuration maps that extension to the content type of text/xml. Similarly, any server-published content in this format should also be sent with a content type header of text/xml.

You cannot simply load an XML document into an Internet Explorer browser window or frame and expect to access the document's element hierarchy. IE has built-in processing that converts the raw XML into pretty-printed (displayed) HTML. In the process, the document's object model becomes an HTML document, cluttered with all kinds of formatting markup.

To facilitate the loading and reading of raw XML data, IE- and Mozilla-based browsers provide separate virtual documents, which are not rendered for viewing and, more importantly, maintain the document hierarchy of the raw XML. The IE mechanism is an ActiveX control (Msxml.DOMDocument) that resides in Windows desktop systems starting with IE 5 (but is not available in IE 5.x for the Mac). On the Mozilla side, the W3C DOM standard provides an object and method for creating this kind of virtual document (via document.implementation.createDocument( )). For a symmetrical cross-browser approach to loading external XML content in the loadXMLDoc( ) function, an <object> tag in the HTML loads an instance of the IE ActiveX control. If the loading is successful, the IE branch of loadXMLDoc( ) is able to proceed with its creation of the document container used by the rest of the scripts.

Once the DOM-specific virtual XML documents (empty at this stage) are created, the script invokes the load( ) method, which, fortunately, exists for both objects (although not specified for the W3C DOM Level 2), takes the same parameters, and does the same job on both platforms. To prevent IE from pre-processing subsequent script statements ahead of the loading, a setTimeout( ) forces a delay prior to the scripts diving into the content of the virtual XML document.

Parsing the XML document hierarchy (in the drawOutline( ) function) takes advantage of the regularity of the body element of an OPML document. One nuisance arises, however, in Mozilla-based browsers. If the OPML document is transmitted with carriage returns between lines, these are treated as text nodes in the hierarchy. Thus, in the drawOutline( ) code, you see a couple of instances where for loop execution is modified slightly when a node of type 3 is encountered. We're interested only in element nodes (nodeType of 1) because they contain attributes with the text and link URIs. The rest of the function operates with the same recursive calls to build nested lines of the outline as in Recipe 10.10.

Because attributes for OPML outline elements are extensible, you can add whatever information your outline needs for your version. This includes information about images (URIs, alternate text, and so on) if you prefer to use images rather than text as entries. Also, don't forget to look into the OPML elements in the head as sources of data that may be useful to render for the user, such as dates, title, and initial expansion state other than fully expanded or collapsed.

10.11.4 See Also

Recipe 1.1 for building large strings from smaller segments; Recipe 4.5 for other uses of setTimeout( ); Recipe 10.10 for a comparable navigation outliner using a JavaScript data source; Recipe 12.1 for precaching images; Recipe 12.7 for hiding and showing elements.

    Previous Section Next Section