March 17, 2010

Hierarchical menu for web forms

Recently I was working on a web page that used several chained menus for selecting data in a form. We've all seen the type, pull down one menu, select a value, pick something from the next menu, maybe make a selection from yet a 3rd menu before you're done with just that one bit. There had to be a better way...

The thought of using optgroup tags to build a hierarchical select was thrown out, mainly because Safari's rendering of these basically just creates one big long list, with only slight formatting. What I needed was unordered list style menu, such as what many sites use for managing their menus and links. The issue, though, was that I could find no samples of this type of menu being used in a web form, so off I went into unexplored territory.

After a few days of research, trial, and plenty of error, I finally came up with something that fit the bill.

There were many challenges involved with working up this code, the list had to be usable without each element being a link, the selected item needed to be displayed after selected, and finally the menu had to collapse after a selection was made. Most of this was easily handled by pure CSS, but the last item needed a little javascript to make work.

Almost every article I came across dealing with a hierarchical menu using unordered lists came with plenty of code to fix problems in IE. Since my target audience was strictly Mac based, I left out all hacks that may be necessary for IE, IE users should well be accustomed to their browser not being standards compliant, so no harm done. Dropping the need for IE compatibility made the coding much easier. I needed a menu that would display a single option on the page until the user hovered over the menu, a primary list of selections would drop down, hovering over each of these could cause a secondary menu to appear to the right of each choice, each of these choices could have their own list of selections.

If you'd like to skip ahead to see the final result, click here.

That said, here is the required CSS code:
body {
font-size: 10px;
font-family: Helvetica, sans-serif;
}

.fancygroup, .fancygroup ul {
display: inline-block;
padding: 0px;
margin: 0px;
background-color:#eee;
font-size: 11px;
line-height: 18px;
color: #333;
border:1px solid #aaa;
-moz-border-radius:3px;
-webkit-border-radius:3px;
}
.fancygroup li {
white-space: nowrap;
padding-left: 8px;
padding-right: 8px;
list-style-type: none;
position: relative;
margin: 0px;
}

.fancygroup li > ul {
width: auto;
display: none;
}
.fancygroup li:hover {
background-color: #ccc;
background: #95a3b2;
color: #fff;
z-index: 1; /* Place this at a higher level so it appears on top of other page elements */
}
.fancygroup li:hover > ul {
display: block;
position: absolute;
top: 18px;
left: -1px;
}
.fancygroup li:hover li:hover ul {
position: absolute;
top: -5px;
left: 100%;
}

As I said earlier, some javascript was also needed to handle the menu. each LI element with selectable choices needed to use an onClick function to call a bit of code that would take the selected value, and populate a hidden input field on the form, and also update the menu to display this choice in the place of the 'select your entry' text, much like a normal menu would. At first, I had these combined, but overriding the input field style to be used within the menu was a pain, ultimately going with two separate items for this proved easier.

Next, I needed the menu to collapse itself after selecting an entry. Several techniques were tried to make this go away, but as long as the user's cursor was still hovering over the selected choice, the menu remained visible. So, time for some javascript.

In the HTML, the LI elements each have an onClick function that calls selGroup to choose the selection, and then a second function to change the CSS style of the parent element to hide it, thereby making the menu collapse since the user is no longer hovering over a displayed item. The problem with doing that, though, was that if the user went back to the menu, the hidden part of the menu remained hidden, preventing any of those selections from being chosen. After much more experimenting, two more bits of javascript were created, resetMenu, and a related function resetChild, which loops through the entire list resetting the display settings to make these visible again.


function selGroup(theValue)
{
document.frm.sel_location2.value = theValue.outerText;
document.getElementById('sel_location').innerHTML = theValue.outerText;
}

function resetMenu(theMenu)
{
navRoot = document.getElementById(theMenu);
for (var i=0; i node = navRoot.childNodes[i];
if (node.nodeName=='LI') {
resetChild(node);
}
}
}
function resetChild(node) {
for (var i=0; i if (node.childNodes[i].nodeName == 'UL') {
if (node.childNodes[i].style.display != '') {
node.childNodes[i].style.display = '';
}
resetChild(node.childNodes[i]);
}
if (node.childNodes[i].nodeName == 'LI') {
if (node.childNodes[i].style.display != '') {
node.childNodes[i].style.display = '';
}
resetChild(node.childNodes[i]);
}
}
}

Some last notes about the final code, when creating the menu, you'll need to set a width on the first UL element that is large enough to hold your largest selectable choice. A bit further down, the list items that appear first under the menu will need to have (almost) matching widths so that the sides line up, in the example provided, these numbers are 16 pixels off, because of the 8 pixel left/right padding used. Subsequent choices use an auto width setting, or we can quickly create a menu that is too wide for the page.

Posted by Jim at March 17, 2010 9:09 PM | TrackBack