About two months ago, a colleague of mine told me of a requirement he had to build an accordion-style menu for an intranet MOSS portal he was building. Not knowing where to start, he asked me what he could do. I advised him that he could either try to work with the <SharePoint:AspMenu> control used by the Quick Launch out of the box or he could use the Accordion control found in the AJAX.NET Control Toolkit and (combined with the SharePoint navigation providers), he could roll his own menu control. They never got around to actually doing this as more critical requirements came about and they eventually postponed this accordion-style menu to a later phase.

Last weekend, I finally decided to look into the examples and documentation around jQuery. I had heard of it before but never really had a chance to play around with it. But I decided that it would be a worthwhile investment of a few hours to learn it, especially since I had read a few weeks ago that Microsoft had put it full jQuery support into Visual Studio 2010. What I found after a few hours of digging in impressed me. I can see why everyone seems to be buying in and using jQuery. It simply makes client-side, rich internet application development much simpler. I have never enjoyed writing Javascript (frankly, I've tried to avoid it as much as possible) but with jQuery, it makes it so easy.

So, being as excited as I was, I started to dream up some ideas I had on how I could use jQuery. I have kicked around a few ideas with some of my colleagues and acquaintances and the ones they thought were good ideas, I will start to build during the upcoming days and months. I will release the code either here (if it is just small snippet code) or in CodePlex if I build out libraries or toolkits. But yesterday, as I was laying in my bed freezing (it was 45 degrees in my house because my furnace went out and had to be repaired this morning), I thought about the accordion control my colleague mentioned. I thought to myself, now this is something I think could do quickly with jQuery. So today, as I waited for the service guy to fix my furnace and for my new treadmill to be delivered, I wrote some jQuery code to make the SharePoint Quick Launch menu work like an accordion.

Getting Started

By default, the following image is the default appearance of the Quick Launch menu in my dummy test portal:

I annotated the image in order to better explain what I did. First, in order to understand the javascript I wrote, it is important to understand the html output that is emitted by the Quick Launch menu. Every site in the menu is outputted as a table (item B). That table has the CSS class 'ms-navheader' applied by default (I say by default because you can always override the CSS class assigned in the <SharePoint:AspMenu> control; all references to CSS classes here are in reference to the default CSS classes assigned out of the box). Within this table, a link (item A) is added to the actual site. Each of these anchor tags also have the 'ms-navheader' CSS class applied to it. Lastly, each sub-menu section (item C) is another table with the CSS class 'ms-navSubMenu2' applied.

My accordion would function as follows (the UI requirements, if you will):

Only one sub menu at a time should be visible.

If you click on a site's navigation bar (item B), the sub menu (item c) should appear and all other sub-menus would disappear.

If you click on a site's navigation bar to attempt to expand the sub-menu but the site has no sub-items, nothing should happen (the currently visible sub-menu should still remain).

Clicking on the site link (item A) should still continue to take you to the site.

By default, when the page is first rendered, the current site the user is on should have its sub-menu expanded out.

The following zip file contains the javascript I wrote to satisfy these UI requirements (the zip also contains both the minified and Visual Studio annotated 1.2.6 version of jQuery): quicklaunch_accordion.zip (58.59 kb)

Some highlights and explanations of the code:

Finding each sub menu

//For each Quick Launch navigation sub menu:

$("table.ms-navSubMenu2").each(function(){

//Find any navigation items under the sub menu that have been selected.

var selectedNavItems = $(this).find("a.ms-selectednav");

//Find the corresponding navigation header of the current sub menu being processed

//if the navigation header for this sub menu is selected or if there are any

//selected navigational items in this submenu, show the submenu.

$(this).show();

}

else

{

//otherwise, hide the submenu

$(this).hide();

}

});

When the page is first rendered, the above snippet finds each sub-menu and automatically hides or displays them based on the UI requirements above. A sub-menu item link that is selected will have the 'ms-selectednav' CSS class applied. Similarly, if the user is currently on the welcome page of a site that appears on the Quick Launch, the 'ms-selectednavheader' CSS class will be applied to the corresponding table element in the Quick Launch html.

Handling a site's link (item A) click event

//When a user clicks a navigation header, the user should be taken directly

//to the site link. The javascript event handler to hide/display the submenus

//should not be triggered.

$("a.ms-navheader").click(function(e){

e.stopPropagation();

});

Normally, the javascript that is triggered when clicking on the table navigation header (item B) would execute because this anchor tag is within that table. Calling stopPropogation() prevents that click event on the table from firing.

Event handler for site menu items

//Finally, this adds a click event handler for the navigation header table

Nice article! I've been searching for something like this for the last couple of weeks.

I have a question though. What happen if I MANUALLY add new Headings and new Links? In this case, it shouldn't be a parent-child relationship between the content of a Heading and a link. I tried to run your code on this scenario and I only see the Headings. Whatever heading I clicked, no sub-menu appears.

That was actually not a scenario I tested. But it still shouldn't matter. There should be no change whatsoever that needs to be made to the JS. It would have to be changes made to the sitemapprovider and your navigation settings. Remember that the JS works off of what is already rendered down to the page. And the quick launch is dependent on Current Navigation in the Navigation settings. So if it doesn't appear there, it won't appear in the Quick Launch.

With headings, by default (second option selected in the Current Navigation section in the Navigation settings page), when you add a new heading, it simply becomes an item inside of the submenu. It doesn't itself become a new header (like a subweb does). Same thing with links that you manually add. They also just become an item inside of the submenu. If you make the link a child of the heading (in the Navigation settings, making sure it's underneath the heading and indented), that won't appear at all on the Quick Launch (by default). That's not the JS that's causing that. You can try it yourself by removing the reference to the javascript in your master file.

Now if you did want to have the heading show up as an actual heading like how subwebs show up, you can choose the third option under the Current Navigation settings. That would make the heading show up as a header in the Quick Launch but that would make every link directly under that web also become a Quick Launch header. Or you could try one of the other site map providers (or roll your own site map provider) and swap out which provider the quick launch is using to get the effect you need.

I tried adding these JS files, Quick Launch menu headers are collapsed and Menu items are hidden, but when I clicked on Header its its taking to viewlsts.aspx page. I removed link from Quick Launch header, not if I click on Header, its taking me to same page.

Hi Florian,
I had the same problem. I added some menu items with headings and links in quick launch menu. but those were not hidden or shown on clicking. So, instead of Click event of table, add code to the anchor click event. But if you have urls specified in the headers, those wont work.
$("a.ms-navheader").click(function(e){
var subMenu = $(this).parents("tr:eq(0)").next("tr").find("table.ms-navSubMenu2:eq(0)");
if (subMenu.length > 0)
{
//only if we have a submenu should we hide the other submenus and show the current one.
$("table.ms-navSubMenu2").hide("slow");
subMenu.show("slow");
}
// e.stopPropagation();
});

I understand this is very late response. I hope this is what you were looking for.

Dear Moderator,
Please edit my previous comment with the following
change this line
But if you have urls specified in the headers, those wont work.
to this line
In the headings, don't specify any URLs. specify javascript:. This will avoid post back and you will get the expanded menu for the selected item

Great start and I like the perceived animation. How could you add session cookie management, so the last group opened (navheader) would be visible by default on the next page? That's the only thing that keeps this from totally rocking.

Thanks Bart!! Just finished rolling out your accordion navigation across my Sharepoint site and it has been well received! Phase II is providing a manual way to expand the navigation instead of auto expanding. Example: a "+" on the sites with subsites to enable the menu to expand and a "-" to collapse. I will test this on my beta site and see what happens!! Thanks for your script and response.

That's great to hear. Thanks for letting me know. What you're trying to do shouldn't be hard to accomplish; if you get stuck somewhere, let me know, I can probably help out a little. I can't promise anything since I'm so busy with work that I often have little time left to do anything else but it doesn't hurt to try. Anyways, good luck!

Most likely that's a permissions issue with the user you're logged in as. Are you sure that user has access to the sub items that are in the menu? If not, those items won't appear as part of their menu. The links are security-trimmed so items (pages, lists, document libraries, subsites, etc) that a user has no access to will not be returned by the navigation provider and therefore won't be part of the Quick Launch menu.

For some reason the allitems.aspx in the _layouts folder doesn't seem to be picking up the script in the master page. Is that because the inacessible _layouts folder (at least to me as I am not Sharepoint central admin) uses a different master page?

Hi Bart,
Its really great job from you, well i have been trying for this quicklaunch functionality but you acheived it with simple jquery, but my requirement is like providing collapse and expandable effect for four levels.I am able to display subsites upto 4 levels but was not able to give same effect to subsites.That is subsites are getting displayed under without any effect.Any idea regarding my requirement.Thanks.

Hey Bart,
This isnt working !. I am not sure how you guys have succeeded in getting this to work. Loge's code too doesnt work. The last section - click event handler is where I am stuck. Can you suggest how to get this working?

does anyone know why my selected libraries/lists in the quick launch are NOT highlighted? in the masterpage i added the
<StaticSelectedStyle CssClass="selectednav" .... to the SharePoint aspmenu and that didn't do anything. i see if the people and groups if i click on a group, the group gets highlighted, but i just couldn't see why the doc libraries/lists are not.... any help is appreciated.

Thanks for this accordion code. Look forward to having it work. I don't have access to the /scripts on the site so I was hoping to reference the JQuery code on google like so: (this works on another Quick Lauch semi-accordion script:

When I run your accordion the quick launch nav is not affected in any way. I am obvisouly new to this so I'd appreciate any advice user's of this have. When I get this working I am lobbying the site owners to integrate the JQuery into the site. this is for a prototype.

I have used your script and modified it to work in a SharePoint 2013 environment. What I did:
- Edit the code to match the id's and classes from SharePoint 2013
- Pasted the code in a separate .js file
- Made a reference to that .js file from the master page
- Added this to the body of the master page: onload="initQuickLaunch()"

So far, it does hide all the submenu's I have. But what I can't manage to do is to show the submenu of a certain header once that header has been clicked on. It just goes directly to the page that has been linked to that header, but what I am looking for is to toggle the show functionality so that all the subsites inside that submenu are shown.

If you would care to please take a look at this code:

function initQuickLaunch() {
alert('Start script'); //To test if the script has started
//For each Quick Launch navigation sub menu:
$("ul#zz16_RootAspMenu>li.static>ul.static>li.static>a.static").each(function(){
//Find any navigation items under the sub menu that have been selected.
var selectedNavItems = $(this).find("a.selected");

//Find the corresponding navigation header of the current sub menu being processed
var menuHeader = $(this).parents("a:eq(0)").prev("a").find("ul#zz16_RootAspMenu>li.static>a.static:eq(0)");

if ($(menuHeader).hasClass("selected") || selectedNavItems.length > 0)
{
//if the navigation header for this sub menu is selected or if there are any
//selected navigational items in this submenu, show the submenu.
$(this).show();
alert('Show submenu'); //To test if the submenu is shown
}
else
{
//otherwise, hide the submenu
$(this).hide();
alert('Hide submenu'); //To test if the submenu is hidden
}
});

//When a user clicks a navigation header, the user should be taken directly
//to the site link. The javascript event handler to hide/display the submenus
//should not be triggered.
$("ul#zz16_RootAspMenu>li.static>a.static").click(function(e){
e.stopPropagation();
alert('User clicked on navigation header'); //To test if the click is registered
});

//When the user hovers over the navigation header, it would be nice
//to have an indicator that they can click on the header. Usually,
//browsers use the hand icon to indicate clickable items.
$("ul#zz16_RootAspMenu>li.static>a.static").hover(function(e){
$(this).css("cursor", "hand");
}, function(e){
$(this).css("cursor", "default");
});

//Finally, this adds a click event handler for the navigation header table
$("ul#zz16_RootAspMenu>li.static>a.static").click(function(e)
{
var subMenu = $(this).parents("a:eq(0)").next("a").find("ul#zz16_RootAspMenu>li.static>ul.static>li.static>a.static:eq(0)");
if (subMenu.length > 0)
{
//only if we have a submenu should we hide the other submenus and show the current one.
$("ul#zz16_RootAspMenu>li.static>ul.static>li.static>a.static").hide("slow");
subMenu.show("slow");
}
});
};

Could you please take a look at it? This might be useful for others in the future, hence the reason I'd appreciate it of someone would care to see what might be the problem. ;)