For RW Devs; Smart Navigation Explained

When we launched our latest theme, Havnit, we made mention of a new “smart navigation” we were using. It was always my intent to write this explanation of why and how — because we like to share like that — but it surprised me just how much attention the concept was getting and the questions I was receiving about smart navigation. So, before I put this off any longer, here is the why and how of smart navigation… [just skip to the how]

Why Smart Navigation, Why Now

On June 23 2006, seyDoggy launched our first commercial RapidWeaver theme to use a technique called split navigation. By now you are all familiar with the system. It’s a popular technique today, in part because we shared the information of how to create split navigation for those who had not yet figured it on their own.

As you are well aware, the trouble with the original split navigation method has always been that it generates a lot of duplicate code with each use of the %toolbar% syntax. Traditionally we’ve just hidden the bits of each instance that we’ve not needed with CSS.

I’ve wanted to create a better, smarter system for some time now but have been waiting to see how our 2008 decision to start including jQuery in our themes played out in terms of support load. Originally used for ExtraContent, it was my hope was to use jQuery to create a dynamic navigation system that would reduce the need for multiple %toolbar% instances.

Shortly after the release of our first jQuery enabled theme, YourHead Software came out with Stacks which soon cultivated it’s own ecosystem of JavaScript enabled objects. The RapidWeaver content space became a veritable mine field of potential conflicts, so I felt I needed to know more about jQuery before I went messing around with as vital a RapidWeaver component as navigation.

By 2010 I was becoming familiar enough with what Stack developers would and wouldn’t do that I felt it was time to push the theme boundaries again. By March 2010 I had released the Flavorite which featured several dynamic components using jQuery, all of which held up in the JavaScript rich RapidWeaver content space that now prevailed thanks to Stacks.

With Flavorite functioning without so much as a hiccup I knew there was no better time to make a smarter navigation system using JavaScript.

How to Build a Smart Navigation

First, let me start by clarifying that I’m not trying to coin the phrase “smart navigation”. I just have no idea what else to call it… dynamic navigation? Appended navigation? jQuery navigation? Smart navigation just seems easy, but you can call it whatever you like.

Second, I didn’t invent anything here, so don’t give me credit for it. I just put together a few simple methods that come default in jQuery. Just like the original split navigation system, this is more for those who haven’t figured it out on their own yet.

Third, there are many ways to skin a cat — which is a really gross saying — and it holds true here. I am going to show you a jQuery method or two, because they are easy. This can be achieved in JavaScript proper or any other JavaScript library flavor you like.

The Markup

First you need to know what RapidWeaver’s %toolbar% syntax generates. In its rawest form — depending on what you set in the Theme.plist — it will generate a basic unordered list. These examples were taken from the built in Alpha theme where I toggled between RWAlwaysDisplayFullNavigation true and false. So the navigation looks like this if RWAlwaysDisplayFullNavigation is false:

    <ul>
        <li><a href="../../" rel="self">Home</a></li>
        <li><a href="../../aboutus/" rel="self">About us</a></li>
        <li><a href="../../store/" rel="self" class="currentAncestor">Store</a>
            <ul>
                <li><a href="./" rel="self" id="current">Widget 1</a>
                    <ul>
                        <li><a href="../../store/widget_1/details/" rel="self">Details</a></li>
                    </ul>
                </li>
                <li><a href="../../store/widget_2/" rel="self">Widget 2</a></li>
            </ul>
        </li>
        <li><a href="../../contactus/" rel="self">Contact us</a></li>
    </ul>

And like this if RWAlwaysDisplayFullNavigation is true:

<ul>
    <li><a href="../../" rel="self">Home</a></li>
    <li><a href="../../aboutus/" rel="self">About us</a></li>
    <li><a href="../../store/" rel="self" class="currentAncestor">Store</a>
        <ul>
            <li><a href="./" rel="self" id="current">Widget 1</a>
                <ul>
                    <li><a href="../../store/widget_1/details/" rel="self">Details</a></li>
                </ul>
            </li>
            <li><a href="../../store/widget_2/" rel="self">Widget 2</a>
                <ul>
                    <li><a href="../../store/widget_2/details/" rel="self">Details</a></li>
                </ul>
            </li>
        </ul>
    </li>
    <li><a href="../../contactus/" rel="self">Contact us</a></li>
</ul>

The difference between RWAlwaysDisplayFullNavigation: true/false will be key in the operation of this method across either option.

Many theme developers, like myself, will include a more robust set of id’s and classes in the creation of their navigation systems:

<ul class="toolbarList">
    <li class="normalListItem"><a class="normal">Home</a>
        <ul class="toolbarList">
            <li class="normalListItem"><a class="normal">Legal</a></li>
        </ul>
    </li>
    <li class="currentAncestorListItem"><a class="currentAncestor">Store</a>
        <ul class="toolbarList">
            <li class="currentListItem"><a class="current">Widget</a></li>
        </ul>
    </li>
</ul>

And it’s this kind of structure that’s going to help us get around RWAlwaysDisplayFullNavigation: true/false differences.

The Styles

If you’re a theme developer already, you are well aware of these structures and the methods of styling them. In the original example (assume .menu as a wrapper):

/* parent */
.menu ul { /* some styles */}
.menu ul li { /* some styles */}
.menu ul li a { /* some styles */}
.menu ul li a#current { /* some styles */}
.menu ul li a.currentAncestor { /* some styles */}

/* child 1 */
.menu ul ul { /* some styles */}
.menu ul ul li { /* some styles */}
.menu ul ul li a { /* some styles */}
/* and so on, and so on... */

And in the advanced sample:

/* parent */
.menu ul.toolbarList { /* some styles */}
.menu ul li.normalListItem { /* some styles */}
.menu ul li a.normal { /* some styles */}
.menu ul li a.current { /* some styles */}
.menu ul li a.currentAncestor { /* some styles */}

/* child 1 */
.menu ul ul.toolbarList { /* some styles */}
.menu ul ul li.normalListItem { /* some styles */}
.menu ul ul li a.normal { /* some styles */}
/* and so on, and so on... */

You can begin see how detailed you could potentially make your selections when you include more for CSS (and jQuery) to grab onto. For instance:

.menu ul.toolbarList
    li.currentAncestorListItem
        ul.toolbarList
            li.currentListItem
                a.current { /* some styles*/ }

The jQuery

jQuery comes with some really great, pre-defined functions like .append(), .prepend() and .appendTo(). If you’ve made an ExtraContent theme before then you are familiar with .appendTo(). So already you can see that there are likely many ways to move sub navigation items — the nested ul‘s — from where they are created in source to another happy little place in the DOM.

If all you ever had to worry about was RWAlwaysDisplayFullNavigation: false then you could simply go about it this way:

$('.menu ul ul').appendTo('.sub_menu');

Unfortunately you’ll have an awful mess trying to use this method with RWAlwaysDisplayFullNavigation: true because all the the nested items are shown, not just those children of the currently selected item. So the above method will capture all ul‘s that are children of other ul‘s, regardless of whether they are children of our current page.

That’s where using more robust identification comes in. With a few more classes to reference we can get more specific about which sub menu items we want to move. In our case — referencing the more advanced markup — we don’t want the children of the “Home” page to be appended to our .sub_menu, but we do want the children of the “Store” moved because the widget page is currently selected in the menu.

Keep in mind that navigation items in RapidWeaver can have 3 states:

  • normal – the default state of the menu item
  • current – the state applied to the menu item of any page currently being viewed
  • ancestor – the parent of menu items of any page currently being viewed

So you don’t want to fetch the sub menu items of pages you’re not viewing but you do want the sub menus of the top level page both when it’s the current page and when it’s the ancestor page. So here is what you can do for the current menu items:

$('.menu ul.toolbarList li.currentListItem ul.toolbarList').appendTo('.sub_menu');

And then the ancestor menu items:

$('.menu ul.toolbarList li.currentAncestorListItem ul.toolbarList').appendTo('.sub_menu');

Of course you would combine these two selections into one, but it’s easier to illustrate them separately for now.

So anyone familiar with RWAlwaysDisplayFullNavigation: true option knows that we use it to create drop menus. If you’re inclined to do this on the top level menu and wish to use some sort of DOM manipulation (like an animation), then you would soon discover that .append(), .prepend() and .appendTo() will all perform a move operation on the selected elements, effectively removing them from that portion of the DOM. To counter this we can use .clone() prior to our chosen operation so that we can retain access to those sub level menu items of the top menu to create our drop menu system. It would look something like this:

$('.menu ul.toolbarList li.currentListItem ul.toolbarList').clone().appendTo('.sub_menu');

And finally, for those curious as to what method I chose in Havnit, I went with .prepend() and a healthy serving of child selectors, as follows:

$('.sub_menu').prepend($('.menu > .toolbarList > .currentListItem > .toolbarList, #menu > .toolbarList > .currentAncestorListItem > .toolbarList').clone());

Comments are closed.