Universal Related Popup Menus / Two, Three, or More! Related Menus | 2 | WebReference

Universal Related Popup Menus / Two, Three, or More! Related Menus | 2

Two, Three or More (!) Related Menus

Universal Related Popup Menus 2.01

It seems our readers are never satisfied. A few readers have asked for four- and even five-level menus, to help navigate huge Web sites. Some have also asked about having multiple related menus in one page. Ask and ye shall receive. Rather than extend our nonrecursive technique for our relate function (relate calls relate2...) let's explore a more elegant solution to both requests.

Recursive Relate for Unlimited Levels

The problem with our current relate function is that it's not recursive, so each time we add another level we need another relate function. This technique is inefficient, and cries out for recursiveness. By making relate recursive (where it calls itself if there are more levels to relate), we can add as many levels as we want. A recursive relate, however, requires a different array structure (nested) to work. Fortunately, switching to a nested array also helps solve our second request, multiple menus per page.

Passing Arrays for Multiple Menus

Our previous 2- and 3-level technique used a hard coded array, named "a" to define our menus on the fly. This limited us to one related menu per page. Some users asked for the ability to include two or more different related menus per page. By passing our menu array as a parameter, we can make the relate function even more generic, to handle different menu arrays. Passing arrays as parameters has an added benefit:

  1. External Arrays - they can be defined externally and centralized in one location. For example, larger sites could have a site-wide pop-up menu nav bar that could reference one external file, making maintenance and updates easy.

Note: we found a bug in Netscape 3 with external .js files, which are rendered as text. The correct TYPE attribute for the SCRIPT tag is TYPE="application/x-javascript", but IE chokes on this, so most folks use TYPE="text/javascript". Netscape 3 displays external .js files as text, unless you configure your server explicitly to serve .js files as "application/x-javascript" content (as we have). Another solution is to SSI the .js file into your page.

So with these two key changes in mind, let's get to the code. But first, a little demonstration.

Pick an Internet.com Channel:
-> Pick a Web Site:

WebReference.com Nav Bar Example

Choose a subject:
-> Choose a topic:
-> Choose a subtopic:
-> Choose a sub-subtopic:

The first two-level related menu shows an abbreviated version of internet.com, using our new technique. Notice the first menu has a "leaf" (an option with no submenu) that is also live (in this example, Internet Marketing).. The second four-level menu expands our previous three-level WebReference nav bar. Both use the same relate function, but use two different arrays read in from an external file. (the fourth menu is a mockup for illustration, all links point to 3D)

The Code

First I'd like to extend special thanks to Michaël Guitton for his tireless help with refining this code. He took our existing 3-level code and added external arrays, plus a recursive relate. We've been refining the code ever since. Isn't open source great?

First let's look at what's changed in the relate function call, that we use within your SELECT FORM elements. Instead of referencing a hard-coded two- or three-dimensional array (i.e., a[i][j][k]...) we reference the appropriate level of our nested array "m," which represents the entire menu tree (less the first menu, which is hard coded into the HTML). The reference to m is now passed into relate, at the level matching the select menu's level thus:

    <SELECT NAME="m4" ID="m4" onChange="relate(this.form, 0, m, 1)">

Much cleaner. The first two parameters are the same (form, elt), the third has changed from a reference to this.selected index to the menu array "m" and the depth of this select menu (1st, 2nd, 3rd in a series). Relate finds a reference to the menu array (m) at the proper depth (1) and calls update (new function, encapsulates the select menu Option updates) to redefine the submenus. To use the relate function on a second level menu you just change the depth to 2. This is a much simplified version of our original rewrite that looked like this (5-level menu):

    <SELECT NAME="m4" ID="m4" CLASS=saveHistory onChange="var" formNum = get(this.form); relate(this.form, 0, ref(m[ sindex(formNum, 3, 0)][sindex(formNum, 2, 0) ][ sindex(formNum, 1, 0) ], this.selectedIndex))">

Yikes! In our next version we simplified this call to look like this:

    <SELECT NAME="m4" ID="m4" onChange="relate(this.form, 0, ref(this.form, 0, m, 4))" >

This encapsulates most of the referencing complexity, but this.form is redundant here, and ref is unnecessary. The current version (2.01) is simplified even more, and the relate function call looks like this:

    <SELECT NAME="m4" ID="m4" onChange="relate(this.form, 0, m, 4)">

We've eliminated the ref function here. So to use relate you just have to change the menu array name, and the depth of the menu, the code does the rest. This newest version relegates the referencing complexity to the relate function, and makes for easier select menu setup.

The Relate Function

The relate function itself has changed in our newest version. Relate updates the current menu's submenus based on the selection of this.form. It first finds the level in the menu array where the current pop-up menu is stored, then passes that reference to the menu array to update(), which updates (redefines) the submenus. Since we aren't testing for new Option constructor support (if (v)) each time we dig thru the menu tree this makes for cleaner and faster code.

    function relate(form, elt, tree, depth) { // relate submenus based on sel of form - calls update to redef submenus if (v) { var num = get(form); // fetch the current form index var a = tree; // set a to be the tree array while (a != null && --depth != -1) // traverse menu tree until we reach the corresponding option record a = a[sindex(num, -depth, elt)]; // at depth 3, should end up w/ something like a[i][j][k] // where each index holds the value of s(elected )index in related form select elts if (a != null && a.length) { // if a array exists and it has elements, // feed update() w/ this record reference update(num, elt, a); return; } } // if a hasn't any array elements or new Option unsupported then end up here ;) jmp(form, elt); // make like a live popup }

Let's step through the relate code. get(form) works as before, looping through the document.forms property until it returns the current form's index.

s(elected)index Function

The s(elected)index function returns the selected index of any given form. Specifically it finds the selected index of the forms elt (0) in the neighborhood (offset) of the current form (num = get(this.form)).

    function sindex(num, offset, elt) { // sel finds selected index of num + offset's form elt's element // in this case elt is always 0, or first select menu in each form var sel = document.forms[ num + offset ].elements[elt].selectedIndex; if (sel

Here's how you use sindex.

    sindex(get(this.form), 0, 0)
returns the selected option index for the current form
    sindex(get(this.form), 1, 0)
returns the selected option index for the next form
    sindex(get(this.form), -1, 0)
returns the selected option index for the previous form

Array Referencing

The key to relate is finding the right level in the menu array tree ("a" above) that corresponds to the current pop-up menu's level. The while loop in relate, specifically the code in green, moves towards the leaves of the menu tree until we reach the level where the current pop-up menu data is stored. Here's how it works:

    Ex: num

Update Submenus

Once we've a found a reference within the menu array to the right level we're home free. We pass the form's number and the menu array (referenced correctly) to update, which updates the submenus recursively.

    function update(num, elt, m) { // updates submenus - form(num)'s menu options, and all submenus // if refd form exists if (num != -1) { num++; // reference next form, assume it follows in HTML with (document.forms[num].elements[elt]) { for (var i = options.length - 1; 0 options[i] = new Option(m[i].text, m[i].value); // fill up next menu's items } options[0].selected = true; // default to 1st menu item, windows bug kludge } if (m[0].length != 0) { update(num, elt, m[0]); // update subsequent form if any grandchild menu exists } } }

Update works similarly to the way relate did before, nulling out the next menu's array and redefining it. Only now, we pass in the nested array (m) referenced at the right place, and recursively redefine subsequent menus.

Once we run out of child menus we return out of relate. If in the course of traversing our menu tree we run into a leaf (a tree with unequal branches, m.length != 0) we bypass the return and act like a live popup. Note that childless option records shouldn't be first in menu records. Pretty nifty, huh?

Now, on to our new external arrays. The new code allows you to define multiple arrays for different related menus in one page. The structure of each array has changed from the old code to the new. Instead of a two- or three-dimensional array (a[i][j][k]...) which must be hard coded into different relate functions we create a nested array, like a C hacker does, thus:

    if (v) { i = new Array( new Array( // internet.com news channels new O("InternetNews.com","http://www.InternetNews.com",null), new O("InternetNews Radio","http://stream.internet.com/",null), new O("atNewYork","http://www.atnewyork.com/",null), new O("NewsLinx","http://www.newslinx.com",null) ), new Array( // internet.com web dev channels new O("WebDeveloper.com","http://www.WebDeveloper.com",null), new O("WDVL.com","http://WDVL.com/",null), new O("WebReference.com","http://www.WebReference.com/",null) ) ); }

This example shows a simple two-level related menu tree, which is an abbreviated version of internet.com's sites. We define a nested array, i, with the first level array empty, representing the first menu (internet.com channels), in the second level arrays representing each channel. The O method assigns the option's text and value to the current array's element. It also adds another nested dimension for any submenus.

So our menu tree array "m" (passed in as "i" above) is in fact an array of hierarchical menus. relate() maps the child menus to the subsequent form's select elts.

    function O(text, value, submenu) { this.text = text; this.value = value; this.length = 0; if (submenu != null) { // submenu is an array of options... for (var i = 0; i

That's it! This technique works for any level related menu, and has been tested up to five levels. To extend this technique up to four levels for example, you would do the following.

First menu:

    <SELECT NAME="i1" ID="i1" onChange="relate(this.form, 0, m, 1))">

Second menu:

    <SELECT NAME="i2" ID="i2" onChange="relate(this.form, 0, m, 2))">

etc. and define the nested menus like this: (4-D menus here)

    if (v) { m = new Array( new Array( new O("3-D Animation","/3d/", new Array( // new O("Glossary","/3d/glossary/", null), new O("Lesson56","/3d/lesson56/", new Array( new O("56.1","/3d/lesson56/", null), new O("56.2","/3d/lesson56/part2.html",null), ... )) ), new O("Design","/dlab/", new Array( new O("About","/dlab/about.html", new Array( new O("About.1","/3d/lesson56/", null), new O("About.2", ...

What about Frames?

Some of you have asked about using the URPM in a FRAMESET. Not a problem. Frame-enabling these popups just take a couple minor changes related to the jmp() function. We create an 'urpm' object, which points to the current window by default (self). Then in the jmp() function we change 'location = ...' to 'urpm.location = ...' and voila! All you need to do then is to set 'urpm' to the relevant frame using the parent property. All the "4d" code has been changed to work with frames, and you can also see a demonstration (zip file).

Here are the changes we made:

    var urpm = self; // frame work-around urpm = parent.dummy; // Set the target to the relevant frame function jmp(form, elt) { // urpm.location added for optional navigation to named frames if (form != null) { with (form.elements[elt]) { if (0

To see how we did it in this page just view source. A stripped down version of this page with only the two related menus is available here. As always it's open source, so if you think of any improvements let us know.

Comments are welcome

Created: Sept. 28, 1999
Revised: Nov. 4, 1999

URL: http://webreference.com/dev/menus/intro4.html