10.11 Creating Collapsible XML MenusNN 6, IE 5(Win) 10.11.1 ProblemYou 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 SolutionUse 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:
onload="initXMLOutline('SpecOutline.xml')"
Also include at the bottom of the page an
<object>
tag that
<!-- 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:
This recipe works with Internet Explorer 5 or later for Windows and Netscape 6 or later. 10.11.3 DiscussionThe 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 (http://www.opml.org). 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}
</style>
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);
collapsedWidget.src="oplus.gif";
var collapsedWidgetStart = new Image(20, 16);
collapsedWidgetStart.src="oplusStart.gif";
var collapsedWidgetEnd = new Image(20, 16);
collapsedWidgetEnd.src="oplusEnd.gif";
var expandedWidget = new Image(20, 16);
expandedWidget.src="ominus.gif";
var expandedWidgetStart = new Image(20, 16);
expandedWidgetStart.src="ominusStart.gif";
var expandedWidgetEnd = new Image(20, 16);
expandedWidgetEnd.src="ominusEnd.gif";
var nodeWidget = new Image(20, 16);
nodeWidget.src="onode.gif";
var nodeWidgetEnd = new Image(20, 16);
nodeWidgetEnd.src="onodeEnd.gif";
var emptySpace = new Image(20, 16);
emptySpace.src="oempty.gif";
var chainSpace = new Image(20, 16);
chainSpace.src="ochain.gif";
// 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) {
continue;
}
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].removeChild(ol.childNodes[i].childNodes[
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 += " <a href='" + link + "' class='itemTitle' title='" +
link + "' target='" + displayTarget + "'>" ;
} else {
output += " <a class='itemTitle' title='" + link + "'>";
}
output += "<span style='position:relative; top:-3px; height:11px'> " +
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 += " <a href='" + link + "' class='itemTitle' title='" +
link + "' target='" + displayTarget + "'>";
} else {
output += " <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) {
xDoc.load(escape(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) {
loadXMLDoc(xFile);
}
The script begins by defining and precaching the small images that become
The section
At the center of
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
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
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
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-
<?xml version="1.0"?>
<opml version="1.0">
<head>
<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>
<ownerEmail>dannyg@dannyg.com</ownerEmail>
<expansionState></expansionState>
<vertScrollState>1</vertScrollState>
<windowTop></windowTop>
<windowLeft></windowLeft>
<windowBottom></windowBottom>
<windowRight></windowRight>
</head>
<body>
<outline text="Forms">
<outline text="Introduction" uri="http://w3.org/.../forms.html#h-17.1"/>
<outline text="Controls" uri="http://w3.org/.../forms.html#h-17.2">
<outline text="Control Types"
uri="http://w3.org/.../forms.html#h-17.2.1"/>
</outline>
<outline text="FORM Element" uri="http://w3.org/.../forms.html#h-17.3"/>
<outline text="INPUT Element" uri="http://w3.org/.../forms.html#h-17.4">
<outline text="INPUT Control Types"
uri="http://w3.org/.../forms.html#h-17.4.1"/>
<outline text="Examples"
uri="http://w3.org/.../forms.html#h-17.4.2"/>
</outline>
...
</outline>
<outline text="Scripts">
<outline text="Introduction"
uri="http://w3.org/.../scripts.html#h-18.1"/>
<outline text="Designing Documents for Scripts"
uri="http://w3.org/.../scripts.html#h-18.2">
<outline text="SCRIPT Element"
uri="http://w3.org/.../scripts.html#h-18.2.1"/>
<outline text="Specifying the Scripting Language"
uri="http://w3.org/.../scripts.html#h-18.2.2">
<outline text="Default Language"
uri="http://w3.org/.../scripts.html#h-18.2.2.1"/>
<outline text="Local Language Declaration"
uri="http://w3.org/.../scripts.html#h-18.2.2.2"/>
<outline text="References to HTML Elements"
uri="http://w3.org/.../scripts.html#h-18.2.2.3"/>
</outline>
...
</outline>
...
</outline>
</body>
</opml>
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
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 AlsoRecipe 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. |