Aliaspooryorik
ColdFusion ORM Book

Expanding menus with jQuery

I was recently asked by a client to do a menu which would expand to show pages when the visitor moved their mouse over the menu heading to show sub pages of that section. Their second requirement was that the menu would stay expanded if the visitor clicked on one of those sub pages.

Update: I've posted an updated version of this code

The code for the menu was a simple list with nested lists inside for the sub pages. For example:


<ul id="nav">
    <li>Heading 1
        <ul>
            <li>Sub page A</li>
            <li>Sub page B</li>
            <li>Sub page C</li>
        </ul>
    </li>
    <li>Heading 2
        <ul>
            <li>Sub page D</li>
            <li>Sub page E</li>
            <li>Sub page F</li>
        </ul>
    </li>
    <li>Heading 3
        <ul>
            <li>Sub page G</li>
            <li>Sub page H</li>
            <li>Sub page I</li>
        </ul>
    </li>
</ul>

Please note that I have removed the "a" tags for ease of reading.

This presented a few problems; the first was how the collapse the menu for visitors who have JavaScript (and stay expanded for visitors without for accessibility reasons). This is simple with jQuery:


$(function(){
    $('#nav>
li>ul').hide();
});

Note the use of the ">" to make sure that the function is only called on direct descendants of the id of nav.

The second problem was to make the menu expand when the visitor pointed at a heading.


$(function(){
    $('#nav>
li>ul').hide();
    $('#nav>li').mouseover(function(){
        // open current menu if it's closed
        $(this).find('ul:hidden').slideDown(500);
    })
});

So far so good. Now to get the menu to collapse when the visitor moves their mouse over another heading.


$(function(){
    $('#nav>
li>ul').hide();
    $('#nav>li').mouseover(function(){
        // close open sub menus
        $(this).siblings().find('ul:visible').slideUp(500);
        // open current menu if it's closed
        $(this).find('ul:hidden').slideDown(500);
    })
});

This works but can make you feel a bit sea-sick so we can improve on it by waiting for the submenus to close before opening the one we want by using the callback functionality of the slideUp method.


$(function(){
    $('#nav>
li>ul').hide();
    $('#nav>li').mouseover(function(){
        // create a reference to the active element (this)
        // so we don't have to keep creating a jQuery object
        $heading = $(this);
        // close open sub menus
        $heading.siblings().find('ul:visible').slideUp(500, function(){
            // open current menu if it's closed
            $heading.find('ul:hidden').slideDown(500);
        });
    })
});

The eagle eyed will spot that this won't work. Why? Because if none of the submenus are expanded then then callback will never be fired as no menus will slide up. To get round this we need to do this:


$(function(){
    $('#nav>
li>ul').hide();
    $('#nav>li').mouseover(function(){
        // create a reference to the active element (this)
        // so we don't have to keep creating a jQuery object
        $heading = $(this);
        // check to see if any sub menus are open
        if ($heading.siblings().find('ul:visible').size()!=0) {
            // close open sub menus
            $heading.siblings().find('ul:visible').slideUp(500, function(){
                // open current menu if it's closed
                $heading.find('ul:hidden').slideDown(500);
            });
        }
        else {
            // open current menu if it's closed
            $heading.find('ul:hidden').slideDown(500);
        }
    })
});

This all works nicely except when you open the sub menu of Heading 1, then try to open the Heading 2 submenu. The problem is that when the menu contracts, it moves the headings under the mouse firing the mouseover event. So if you try and point at Heading 2, the chances are that you'll end up with Heading 3 expanded.

To stop this happening, jQuery has a handy filter called "animated" which checks to see if an element is currently being animated. We can use this to disable the mouseover event. Our final code looks like this.

The JavaScript file (to include in your HTML head tag)


$(function(){
    $('#nav>
li>ul').hide();
    $('#nav>li').mouseover(function(){
        // check that the menu is not currently animated
        if ($('#nav ul:animated').size() == 0) {
            // create a reference to the active element (this)
            // so we don't have to keep creating a jQuery object
            $heading = $(this);
            // create a reference to visible sibling elements
            // so we don't have to keep creating a jQuery object
            $expandedSiblings = $heading.siblings().find('ul:visible');
            if ($expandedSiblings.size() > 0) {
                $expandedSiblings.slideUp(500, function(){
                    $heading.find('ul').slideDown(500);
                });
            }
            else {
                $heading.find('ul').slideDown(1000);
            }
        }
    });
});

The HTML menu


<ul id="nav">
    <li>Heading 1
        <ul>
            <li>Sub page A</li>
            <li>Sub page B</li>
            <li>Sub page C</li>
        </ul>
    </li>
    <li>Heading 2
        <ul>
            <li>Sub page D</li>
            <li>Sub page E</li>
            <li>Sub page F</li>
        </ul>
    </li>
    <li>Heading 3
        <ul>
            <li>Sub page G</li>
            <li>Sub page H</li>
            <li>Sub page I</li>
        </ul>
    </li>
</ul>

 


32 comments

  1. When I use this menu example on a stand alone page the menu is displayed unexpanded when the page loads.

    When I include it in a source page in a cflayoutarea the menu opens expanded.

    Can you think of what I am doing wrong? I am using the same code except that I am placing the scripts in a cfsavecontent block and then returning that value in a cfhtmlhead.

    Comment by Michael Brennan-white – May 13, 2008
  2. Coldfusion 8 ships with the Yahoo! User Interface Library (YUI) and the EXT2 library so if you are using any of the AJAX tags or CFGRID then it may cause a conflict. I'd try running jQuery in the "jQuery.noConflict();" mode. Let me know if that works!

    Comment by John Whish – May 13, 2008
  3. John,

    That worked in Firefox very well but the slideup method in IE7 is very jerky even when I run the code on a page outside the RIA.

    I personally like the idea of punishing IE users but this is not a way to win over Hearts and minds.

    Comment by Michael Brennan-white – May 13, 2008
  4. I've just tried it in IE7 and it's quite smooth for me although I do get a slight flickering effect, just before the menu slides down. You may want to try using the hide and show effects (which had the same problem for me):

    if ($expandedSiblings.size() > 0) {
      $expandedSiblings.hide(500, function(){
        $heading.find('ul').show(500);
      });
    }
    else {
      $heading.find('ul').show(1000);
    }

    or the fade method which works well

    if ($expandedSiblings.size() > 0) {
      $expandedSiblings.fadeOut(500, function(){
        $heading.find('ul').fadeIn(500);
      });
    }
    else {
      $heading.find('ul').fadeIn(1000);
    }

    Comment by John Whish – May 13, 2008
  5. Hi,

    have you tried out one of the CSS menu's by CSS Play (cssplay.co.uk)? They're excellent, free and webstandard ;-)

    Comment by Sebastiaan – May 19, 2008
  6. Absolutely spectacular. I have been working on this exact behaviour for longer than I want to admit! My jQuery-fu isn't as strong as yours...

    I do wonder about the
    <quote>
    Their second requirement was that the menu would stay expanded if the visitor clicked on one of those sub pages.
    </quote>

    how did you do *this* part? AJAX? Although easy enough with AJAX, I can't think of how to make it degrade gracefully, short of having 'regular' hrefs which are stripped of their target by a script onLoad and the Ajax built from them.

    Your thoughts would be really appriciated!

    Comment by Evan – October 30, 2008
  7. Hi Evan, glad my post was useful - always good to hear :)

    On this project I used ColdFusion to add a class to the clicked link. When the page loads jQuery iterates through and hides the lists without an "a" tag with the class of "current" something like this:

    $('#nav>li ul.subMenu').each(function(){
    if ($(this).find('li a.current').size()==0) {
    // current page is not in this menu list so hide
    $(this).hide();
    }
    });

    You could make this simpler with the parent() method, but I do some other stuff in the loop.

    In the past I've used a cookie to record which link was clicked and then used a similar technique to achieve the same thing. jQuery has some cool cookie plugins.

    Does that answer your question?

    Comment by John Whish – October 31, 2008
  8. Hi john,

    I am trying your code but somehow i can't close the already opened sub menus

    bldd.nl/jsproblems/convertToJQuery.html

    In the end i want the submenu's slideIn from the left but first i like to fix this one.

    Any pointers???

    regards

    Comment by alex – February 28, 2009
  9. @alex, had a quick look at what you've done and think this should point you in the right direction:

    $(function(){
    $('#nav>li').click(
    function(){
    $heading = $(this);
    if ( $('ul:visible', $heading).size() == 0 ) {
    $heading.siblings().find('ul:visible').slideUp(500);
    $('ul', $heading).slideDown(500);
    }
    return false;
    }
    )
    });

    Comment by John Whish – February 28, 2009
  10. Tx John,

    For thinking along, i forgot to mention it only occurs in Firefox (mine is 3.x). In IE the menu works as aspected.

    Anything i am overlooking for Firefox??

    Comment by alex – March 03, 2009
  11. The code in the post is specifically for using with a mouseover event and is much more complicated than it needs to be for use with a click event. Try the version in the comments above, which worked for me in FF and Chrome.

    Comment by John Whish – March 03, 2009
  12. What about in the case below where sub page 1 is a sub category of sub page A? I can't seem to get this to function correctly... I don't want it to be visible until it is rolled over.

    Thanks for any thoughts on this.

    <ul id="nav">
    <li>Heading 1
    <ul>
    <li>Sub page A</li>
    <ul>
    <li>Sub page 1</li>
    </ul>
    <li>Sub page B</li>
    <li>Sub page C</li>
    </ul>
    </li>

    Comment by Anthony Creek – April 27, 2009
  13. Hi Anthony, are you trying to get it so that when you mouseover "Heading 1" you see "Sub page A" and when you mouseover that you see "Sub page 1"?

    I would have thought that the a filter like ul>li:hidden would do the trick.

    Comment by John Whish – April 28, 2009
  14. Yeah, that's what I'm (unsuccessfully) trying to do. I'm still messing with it, but I still haven't been able to get it to function properly.

    Comment by Anthony Creek – April 29, 2009
  15. Hi Anthony, I did intend to write you an example but I haven't had the time. You might want to check out this link:
    www.filamentgroup.com/examples/menus/flyout.php
    I think it does what you want and looks great as well!

    Comment by John Whish – April 30, 2009
  16. hey John,
    I've got it working correctly in FF, but not IE. I've got it setup so the selected pages stay open by adding a class "selected" then the regular class name and calling to it in my javascript file. We'll see how it goes... I'll be working on it again this weekend.

    Comment by Anthony Creek – May 01, 2009
  17. Thank you for this post. I am a new website developer and found this very easy to implement.

    I opted to use your "sea sick" example because in this case I have only one menu item with a sub-list. Looks great!

    Comment by JackJames – May 13, 2009
  18. Hi JackJames, glad you found it useful. :)

    Comment by John Whish – May 13, 2009
  19. is there somewhere where i can view this?

    Comment by dave – May 14, 2009
  20. Hi Dave, no I haven't got a hosted version anywhere but it would be easy enough to copy paste the last 2 "code" boxes into a HTML doc to view (don't forget to link to the jquery library!)

    Comment by John Whish – May 15, 2009
  21. Hi, how can I change the Code, that the Submenu fades out, when I leave the submenu area. actually the submenu only closes if i go on another top menu point than the one with the submenu.

    Comment by Dominik – August 14, 2009
  22. I'm using this in conjunction with wordpress and the wp_list_pages php code...

    I got this to work using this html/php

    <ul id="nav">
    <?php wp_list_pages('include=2,3,5,7&sort_column=menu_order&title_li='); ?>
    </ul>

    and the javascript from above.

    But I want the current page's menu to stay expanded. I tried the suggestion from John Whish:
    $('#nav>li ul.subMenu').each(function(){
    if ($(this).find('li a.current').size()==0) {
    // current page is not in this menu list so hide
    $(this).hide();
    }
    });

    but used li a.current_page_item because that's what wordpress names the class... but I'm having trouble with ul.subMenu because I don't have a way set a class to the submenus created by wp_list_pages.. Anyone know how to get around this:

    It can be found here
    stephenemlund.com/bridalshoppe/?page_id=2

    Comment by Stephen Emlund – October 27, 2009
  23. Hi Stephen, looking at the source of your code, the 'current_page_item' class is on the li, not the a, so the selector will return nothing.

    You should be able to just use simple selectors to find child elements. something like $('#nav>li>ul') which means direct decendant.

    Comment by John Whish – October 27, 2009
  24. Awesome work. Actually this is same what i wanted. Thanks

    Comment by vijay – March 16, 2010
  25. Cool stuff, John. Is it possible to have the current sub-menu collapse and the next one expand simultaneously instead of serially? See the FogBugz website for an example. Click around in the menu on the left and you'll see what I'm trying to achieve.

    Also, is there an easy way to default to having the first sub-menu expanded when the page loads?

    www.fogcreek.com/FogBugz/LearnMore.html />
    Thanks,
    Jay

    Comment by Jay – March 25, 2010
  26. thanks, EXACTLY what I was looking for!

    Comment by
    tjobbe – April 03, 2010
  27. To solve the flickering issues on IE7+, just add a valid DOCTYPE to your page. XHTML Transitional works perfectly with this menu.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> />
    It's always recommended to run jQuery on a validated document.

    Comment by
    Danny Herran – April 06, 2010
  28. How can I use in .NET 3.5. any experts

    Comment by azurebug – June 29, 2010
  29. Hi John,
    Is it possible on clicking the Heading a second time to make the sublist close again?
    Cheers,
    Fi

    Comment by Fiona Smith – August 17, 2010
  30. @Fiona, off the top of my head you should be able to do:

    $('#nav>li').click(function(){
    $(this).children().hide();
    });

    Is that what you want?

    Comment by John Whish – August 17, 2010
  31. Hey awesome turtorial, like it's so simple! But there are a problem when there are mulitiple sub menues... have bin looking for a code, to sovled it, but i find i hardt to find... I'm useing this function:
    function getMenu($tableName, $postParentName, $postMatchName, $parentNr = 0){
    global $db;

    $result = $db->query("SELECT * FROM $tableName WHERE $postParentName = $parentNr ORDER BY orden ASC");

    $rowCount = $result->num_rows;
    if ($rowCount == 0) {
    return null;
    }

    static $output;

    $output .= "<ul>";
    while ($row = $result->fetch_assoc()) {

    $output .= "<li ><a href='index.php?page=$row[title]'>$row[title]</a>";
    $parentNr = $row[$postMatchName];
    getMenu($tableName, $postParentName, $postMatchName, $parentNr);
    $output.="</li>";
    }

    $output .="</ul>";

    return $output;
    , to make it recursiv... and i would like to get around hardkoding the style an jquery...
    Hope you kan help...
    Thanks, anyway!

    Comment by Kate Munch Piper – September 21, 2011
  32. This is fantastic and really sleek. However I'm trying to get it to work so that the current page's sub menu is expanded, or if you are on a sub page, the menu is also expanded to show that page in the navigation. I know it's something simple but I can't figure it out.

    Any help would be appreciated :)

    If it helps, I also have the js set up so that the current page's menu link has a class of 'selected' on it:

    $(function(){
    var path = location.pathname.substring(1);
    if ( path )
    $('#nav a[href$="' + path + '"]').attr('class', 'selected');
    });
    $(function(){
    $('#nav>li>ul').hide();
    $('#nav>li').mouseover(function(){
    // check that the menu is not currently animated
    if ($('#nav ul:animated').size() == 0) {
    // create a reference to the active element (this)
    // so we don't have to keep creating a jQuery object
    $heading = $(this);
    // create a reference to visible sibling elements
    // so we don't have to keep creating a jQuery object
    $expandedSiblings = $heading.siblings().find('ul:visible');
    if ($expandedSiblings.size() > 0) {
    $expandedSiblings.slideUp(500, function(){
    $heading.find('ul').slideDown(500);
    });
    }
    else {
    $heading.find('ul').slideDown(1000);
    }
    }
    });
    });

    Comment by Emma – October 29, 2011

Leave a comment

If you found this post useful, interesting or just plain wrong, let me know - I like feedback :)

Please note: If you haven't commented before, then your comments will be moderated before they are displayed.

Please subscribe me to any further comments
 

Search

Wish List

Found something helpful & want to say ’thanks‘? Then visit my Amazon Wish List :)

Categories

Recent Posts