Tackling Hierarchical Menu Issues with Drupal and jQuery

One of the decisions we make when creating a hierarchical menu for a website is whether to create a "landing page" for each section. In other words, does each parent menu item have its own page?

There are three options. Each parent item can:

  1. link to a unique page,
  2. activate a drop-down menu, but link to nothing, or
  3. link to the same page of one of its children (duplicate links to the same page).

That decision typically depends on several factors, including placement of the main and secondary menus, depth of the menus, and the overall organization of the site.

The problem with #1 is that it's sometimes hard for the user to figure out how to navigate back to the "index" page for the section. Since the link to that page isn't duplicated in the children menu items, it generally doesn't appear in the secondary menu.

Drop-down menus are nice, but they don't work in all circumstances. For example, drop-downs begin to fail when the menus are too deep. (How many times have you gotten frustrated trying to traverse 4 or 5 levels of "drop-down tunnels" on some ecommerce sites?)

On my most recent project, the site is very product-heavy... hundreds of products. Some products will have children pages, some will not. We decided that, regardless of whether the product has children pages, we would like to include an "Introduction" link that points to the same page as the parent item (option #3 above).

Existing Menu Structure

So the product menu (containing approximately 500 pages) will be organized as follows:

Category 1 Menu
    Subcategory 1
        Product A
            Introduction (links to same page as parent)
            Page 2
            Page 3
            Page 4
        Product B
            Introduction (links to same page as parent)
    Subcategory 2
        Product C
            Introduction (links to same page as parent)
            Page 2
        Product D
            Introduction (links to same page as parent)
            Page 2
            Page 3
Category 2 Menu
    etc.
Category 3 Menu
    etc.
Category 4 Menu
    etc.
Category 5 Menu
    etc.
Category 6 Menu
    etc.

For each product, we wanted to show a secondary menu like this in the left sidebar:

Product A (<h2> header)
    Introduction
    Page 2
    Page 3
    Page 4

Anyone who has built a site in a CMS probably sees the problem immediately. Creating each "Introduction" link requires us to create two different menu items, both pointing to the same page.  This can be problematic for a few reasons:

  • If you have to do it often, it's very labor intensive.
  • Having multiple menu items with the exact same link can confuse some CMSs... they don't know which one is active (class="active"). I try to avoid it.
  • Managing a very large menu can be cumbersome; adding unnecessary links to each level makes it even more so.

This web site has hundreds of products, and so having my client create each duplicate "Introduction" link was proving to be very time-consuming. We decided to insert the Inroduction links automatically using jQuery.

The Goal

So I needed to write some jQuery to turn this:

 

into this:

Note that the second image has an "Introduction" link as the first child. The goal is to write jQuery that automatically inserts that text and links it to the same page is the <h2> header (parent) above it.

Getting Started: Creating the Secondary Menu Block

First, I had to create the secondary menu block itself. I did this through the Menu Block module. If you haven't used this yet, I highly recommend it. It allows you to create a block from any level of any menu. So, for example, you could create a menu block from the 2nd level (child) of the primary menu. You can specify the exact starting child, or you can tell Menu Block to follow you around all the 2nd level children of that menu as you enter different sections. The module is very flexible, and I find it indespensible.

I was able to use the Menu Block module to create a single block that follows the user from product to product, inserting the correct submenu starting at the approprate 3rd level menu item. (Note that, in the above photos, the text "Swiftclip L-Series Clip Angles" header in the left sidebar is actually a menu link. It's in the 3rd level of one of the product menus. The gray rectangular links below that header are the 4th level children.)

Oops, There's Always an Exception

No problem, I thought! I'll just write a short jQuery script to insert the link. But, like any web project, it wasn't quite as simple as that. Some of the products have no children pages. And so, as with other Drupal blocks, because that menu is empty, the block will not show up. So there's nothing to search for. In those cases, I need to turn this:

 

into this:

 

Make that Two Exceptions

Additionally, my client sometimes wants to add children pages within the "Introduction" link. So sometimes he needs the following:

 

In the cases where he needs to place children within the Introduction link, he'll need to go ahead and create the Intro link himself. But, since this was the exception to the rule, creating the other Intro links via JavaScript is going to save him a ton of time.

A Quick Sidebar, Please

If you're not confused by now, either you've already dealt with something very similar, or you have the ability to follow a complex written narrative really well. This is a pretty darned complex problem, not only for the developer, but for the client. From a CMS development standpoint, this is the type of thing that separates beginner developers from those with experience (and the right tools). From the client standpoint, his requirements are complex enough to give him fits if he doesn't remember exactly how it works. (Additionally, I generally stand by my creed: a CMS ought not require much more than a page of documentation.)

To be honest, I'd rather not create an automated process this complex. The main reason is that, although my client and I both understand it, if you bring another developer or client into it, it's difficult to explain. But if I do it right, it's exactly what my client wants, and it will save him a lot of time. So I probably would have kept things a little more simple for any other client; in the long run, that would help ensure the client would have no surprises. But when your client is an engineer, you can sometimes bend the rules a bit. :)

Writing the jQuery

OK, now to get to work.

Using Firebug, I found that the menu's containing <div> is <div id="block-menu_block-1">. So that's where I need to start.

In jQuery, first find out if this is a product page. Drupal does a great job of adding handy classes to the <body> tag. So I just needed to check to see if we're in section-product. But I want to ignore two individual pages within the products section:

if (
    $('body').hasClass('section-products')
    && !$('body').hasClass('page-products')
    && !$('body').hasClass('page-products-sitemap')
) {

Next, see if the menu block in question exists:

    // create menu if menu block exists
    if ($('#block-menu_block-1').length > 0) {

If it does, then check to see if an "Introduction" link already exists (remember the exception above) as the first child <li>. If not, create it:

        if ($('#block-menu_block-1 ul.menu li.first a').html() != 'Introduction') {
            // grab the URL from the menu block <h2> header
            var my_url = $('#block-menu_block-1 h2 a').attr('href');
            if (my_url == $(location).attr('pathname')) {
                // we're actually on the intro page... add clases to make it active
                var li_html = '<li class="leaf first last active active-trail"><a class="active-trail active" title="" href="' +  my_url + '">Introduction</a></li>';
            } else {
                // we're on some other child page... no need to make it the active link
                var li_html = '<li class="leaf first last"><a class="" title="" href="' +  my_url + '">Introduction</a></li>';
            }
            $('#block-menu_block-1  .menu_block_wrapper').prepend(li_html);
        }

Lastly, for the cases where no menu block exists, create one from scratch:

    } else {
        // the menu block doesn't exist.
        // grab the product name form the <h1> title, and put it into an <h2> header
        var h2_html = '<h2 class="title"><a href="#">' + $('h1').html() + '</a></h2>';
        var ul_html = '<ul class="menu"><li class="leaf first last active active-trail"><a class="active-trail active" title="" href="#">Introduction</a></li></ul>';
        // add the <h2> and <ul> to the block
        $('#block-block-11 .menu_block_wrapper').append(h2_html);
        $('#block-block-11  .menu_block_wrapper').append(ul_html);
        $('#block-block-11').show();
    }
}

In the code above, <div id="block-block-11"> is a placeholder block of sorts. I created it in Drupal, placed it in the sidebar, and filled it with the following HTML:

<div class="menu_block_wrapper"><!-- save --></div>

Via CSS, block-block-11 is set to display: none. If I hadn't created this block, the sidebar would have disappeared on those pages where the menu block didn't exist, and there would have been nowhere to place the menu.

VoilĂ , it works!