JQuery for Everyone: Accordion Left-nav with Cookies Speed Test
We had a great run of Superstar! workshops this year. I enjoyed them all and learned a lot from our users. One of the more popular solutions, based on Accordion Left Nav and featured in Tony’s Deployment article, has only one improvement since the original article.
A few months ago, I added the ability to save a cookie that determined which navigation sections should be open when the page loads. As Tony pointed out, that works great going from page to page when deployed across an entire site.
However, I can’t leave well enough alone. I want to see if I can make it just a little better before I publish again.
The first thing I need to do is get a baseline for the script’s speed. Firebug has a timer method, console.time(’name of timer’); and console.timeEnd(’name of timer’);. It turns in a consistent 53ms.
With this script, as with many jQuery solutions, everything is packed into an anonymous function that runs on the document.ready event.
$(function(){ console.time('timer'); //document ready code goes here console.timeEnd('timer'); }); //53ms
The order of the instructions as well as which selectors and DOM-walking strategy you employ makes a huge difference. Experience will help but good old fashioned tenacity and testing can’t be beat. Here’s what I started with:
$(function(){ console.time('timer'); //find menu rows in the left nav //filter the array of rows for headers //set variables for open and closed states //close all rows //set the click event for headers //iterate over the headers and check for a cookie //fire the click event for any headers with cookies console.timeEnd('timer'); });
I added .timer() calls around each of the sections to get a picture of which parts take the longest. The first thing I noticed is that my goal (to hide) comes too late. I should hide the rows as soon as I identify them.

Next, I started trying to optimize my selectors to make the process faster. One really interesting experiment lead me to this:
$(function(){ console.time('menuRows'); var menuRows = $("table#zz2_QuickLaunchMenu tr[id]"); console.timeEnd('menuRows'); }); //23ms
$(function(){ console.time('menuRows'); var menuRows = $("table#zz2_QuickLaunchMenu > tbody > tr[id]"); console.timeEnd('menuRows'); }); //12ms (faster than baseline)
$(function(){ console.time('menuHd'); var menu = $("#LeftNavigationAreaCell"); var menuHd = $("table.ms-navSubMenu1 > tbody > tr[id]", menu); console.timeEnd('menuHd'); }); //3ms (WOW!)
That gave me the confidence to improve this script top to bottom.

Click “Read more…” to see the new version, enjoy!
<script type="text/javascript"> if(typeof jQuery=='undefined'){ var jQPath="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/"; document.write('<script src="',jQPath,'jquery.min.js" type="text/javascript"><\/script>'); } </script> <script type="text/javascript"> /* * Accordion Quicklaunch with Cookies * Copyright (c) 2009 Paul Grenier (endusersharepoint.com) * Licensed under the MIT (MIT-LICENSE.txt) */ $(function(){ var menu = $("#LeftNavigationAreaCell"), menuHd = $("table.ms-navSubMenu1 > tbody > tr[id]", menu), submenus = menuHd.next("TR:not([id])"); submenus.hide(); var menuHd = submenus.prev(), closedImg = "/_layouts/images/PLUS.gif", openedImg = "/_layouts/images/MINUS.gif", cssInit = { "background-image":"url('"+closedImg+"')", "background-repeat":"no-repeat", "background-position":"100% 50%", "cursor": "pointer" }, cssClosed = {"background-image":"url('"+closedImg+"')"}, cssOpen = {"background-image":"url('"+openedImg+"')"}; menuHd.find("td:last").css(cssInit); menuHd.bind("click", function(event) { if (event.target.tagName=="TD"){ var styleElm = $(this).find("td:last"); var nextTR = $(this).next("tr:not([id])"); if (nextTR.is(':visible')) { nextTR.hide(); styleElm.css(cssClosed); SetCookie(this.id, "", "/"); } else { nextTR.show(); styleElm.css(cssOpen); SetCookie(this.id, "open", "/"); } } }); $.each(menuHd,function(i,e){ var g = GetCookie(e.id); if (g && g == "open") { $(e).find("td:first").click(); } }); }); </script>
- JQuery for Everyone: Accordion Left Nav
- JQuery for Everyone: Print (Any) Web Part
- JQuery for Everyone: HTML Calculated Column
- JQuery for Everyone: Dressing-up Links Pt1
- JQuery for Everyone: Dressing-up Links Pt2
- JQuery for Everyone: Dressing-up Links Pt3
- JQuery for Everyone: Cleaning Windows Pt1
- JQuery for Everyone: Cleaning Windows Pt2
- JQuery for Everyone: Fixing the Gantt View
- JQuery for Everyone: Dynamically Sizing Excel Web Parts
- JQuery for Everyone: Manually Resizing Web Parts
- JQuery for Everyone: Total Calculated Columns
- JQuery for Everyone: Total of Time Differences
- JQuery for Everyone: Fixing Configured Web Part Height
- JQuery for Everyone: Expand/Collapse All Groups
- JQuery for Everyone: Preview Pane for Multiple Lists
- JQuery for Everyone: Preview Pane for Calendar View
- JQuery for Everyone: Degrading Dynamic Script Loader
- JQuery for Everyone: Force Checkout
- JQuery for Everyone: Replacing [Today]
- JQuery for Everyone: Whether They Want It Or Not
- JQuery for Everyone: Linking the Attachment Icon
- JQuery for Everyone: Aspect-Oriented Programming with jQuery
- JQuery for Everyone: AOP in Action - loadTip Gone Wild
- JQuery for Everyone: Wiki Outbound Links
- JQuery for Everyone: Collapse Text in List View
- JQuery for Everyone: AOP in Action - Clone List Header
- JQuery for Everyone: $.grep and calcHTML Revisited
- JQuery for Everyone: Evolution of the Preview
- JQuery for Everyone: Create a Client-Side Object Model
- JQuery for Everyone: Print (Any) Web Part(s) Plugin
- JQuery for Everyone: Minimal AOP and Elegant Modularity
- JQuery for Everyone: Cookies and Plugins
- JQuery for Everyone: Live Events vs. AOP
- JQuery for Everyone: Live Preview Pane
- JQuery for Everyone: Pre-populate Form Fields
- JQuery for Everyone: Get XML List Data with OWSSVR.DLL (RPC)
- Use Firebug in IE
- JQuery for Everyone: Extending OWS API for Calculated Columns
- JQuery for Everyone: Accordion Left-nav with Cookies Speed Test
- JQuery for Everyone: Email a List of People with OWS
- JQuery for Everyone: Faster than Document.Ready
- jQuery for Everyone: Collapse or Prepopulate Form Fields
- jQuery for Everyone: Hourly Summary Web Part
- jQuery for Everyone: "Read More..." On a Blog Site
- jQuery for Everyone: Slick Speed Test
- jQuery for Everyone: The SharePoint Game Changer
- JQuery For Everyone: Live LoadTip
Everything works except the last line after reading the cookie doesn’t fire:
$(e).find(”td:first”).click();
Is that correct?
What exactly does not work? Because it will only fire the click event for a valid cookie with the value open. All other sub-menu sections will default to closed.
I do get a hit on the value (”open”) in the conditional. So it is setting and reading properly from the cookie. It just doesn’t fire the next line…find a tag and force the click on open. I’m new to jquery so I don’t know how to force a known tag to click to troubleshoot. Other than that, everything works. Is there an alternative to the fin method, or maybe some other reason it doesn’t get a hit?
@David,
The $.each method passes the element and iterator (i,e). If you customized the nav or .master, you may need to adjust what gets the click.
If clicking manually works, add a console.log(event); line in the .bind() area. When it fires, click the event in Firebug and go to the DOM panel.
Drill down the DOM panel to find the target. Look at that element (if it’s working it should be a TD).
Now compare that element to the position of the elements returned by console.log(menuHd); (anywhere after it’s set). The event.target should be the first TD inside a menuHd element for the last section to work.
If not, say if you used a custom theme or something, you may need to change TD:first to something else.
Jan is there a reason not to put the img paths directly in the CSS statements? Seems like it would be faster and the only reason to have the variable is to facilitate changing the image, but with it only appearing once or twice in the code, not sure the benefit is worth it.
@Vinny,
Speed is a funny thing. Sometimes, using a separate SCRIPT tag slows things down because it blocks other elements during loading. If you have a custom CSS already being loaded by the .master, by all means, add the images to a pair of classes and toggle the classes.
With jQuery 1.3.2 you need to change the following:
if (nextTR.is(’:hidden’)) {
nextTR.show();
styleElm.css(cssOpen);
SetCookie(this.id, “open”, “/”);
} else {
nextTR.hide();
styleElm.css(cssClosed);
SetCookie(this.id, “”, “/”);
}
I tried the change for jQuery 1.3.2 (version I am using) and it does not work – maybe there need to be more specific instructions?. Prior to making the change, the menus show colapsed, then when I click on a header they open up for a second and then go back to closed. Does anyone have a solution to make this work with version 1.3.2?
Does anybody tried to add ‘help balloons / tool tips / dialog balloons’ or whatever that can be called…. when someone move the pointer over the nav menu and get the a box with comments?
I can the accordeon to work (it expands and collapses when I click on the heading next to the heading text), but the pictures for opening (+) and closing (-) are not displayed. Any thoughts how to solve this? My knowledge of jQuery does not go much beyond copy/paste, so please, be gentle.
@mark,
The urls in this script point to default images installed with WSS. If you don’t have them, moved them, or changed their location, simply update the url or fix the permission issue.
The images are still there and in the browser I can open them directly with the link from the code. If I try other images they also won’t show (the functionality keeps working). Other ideas?
I replaced the jquery version to 4-1 and now is not working …..
hi paul,
because i am new to sp2010 i do not know where exactly to put this javascript code
Can you tell us what steps to follow to get this Accordion Left-nav working?
Thanks
husso