Converting SharePoint 2013 Quick Launch to accordion menu

before/after

In this article I'm going to tell you how to turn default SharePoint quick launch menu to an accordion menu. It can be useful if site structure is large and the quick launch takes too much space.

I decided that it will be better if this solution can be stand-alone, so I didn't use any frameworks. I also used a default SharePoint image file(spcommon.png) to create dropdown triangles for both expanded and collapsed levels:
spcommon

This solution also accepts a couple of parameters, so you can tweak its behaviour. Parameters are discussed in "How to use" section.

Here is a shortcut straight to "How to use" section where you can view available options and download this solution:

"How to Use" and Download section.

Script workflow

As we can't change the HTML structure of default SharePoint control, we will use JavaScript to manually insert all the changes to menu structure just after the page loads.

The script will do the following:
1. Find all li elements inside quick launch.
2. Create and append switch nodes to all li elements that has child levels. Switch nodes will work as buttons to collapse and expand levels.
3. Attach an event to switch nodes to toggle levels visibility on click.
4. Expand selected level and all his parents, so it will be visible when the page loads.

Let's now review how it is made step by step:

Main code

First, we need to find all li elements inside the quick launch menu. After that we check if the menu is not empty, iterate through all levels, and check if they have descendants:

var levels = document.querySelectorAll('.ms-core-listMenu-verticalBox li');

if (levels.length) {
  for (var i = 0; i < levels.length; i++) {
    if (levels[i].querySelector('ul')) {
      ...
    }
  }
}

After that we create switch nodes and append them to every level:

var switchSpan = document.createElement('div');
switchSpan.className = 'switch';
switchSpan.innerHTML = '<span><img alt="" src="/_layouts/15/images/spcommon.png"/></span>';

levels[i].insertBefore(switchSpan, levels[i].firstChild);

Now we need to check if current level or his descendants are selected (have 'selected' class) and add 'expanded' class to them and 'collapsed' class to all others:

levels[i].className += (levels[i].querySelector('.selected') || levels[i].className.indexOf('selected') != -1) ? ' expanded' : ' collapsed';

Here the loop through levels closes and we proceed to the events.

As animation is made by CSS transitions that are not supported in old browsers, so we detect IE8 or lower to turn off animation:

if (document.all && !document.addEventListener) SP2013QLAccordion.useAnimation = false;

Here we find all switch elements, check if they are present, iterate through them and add collapse/expand event to them:

var switches = document.querySelectorAll('.ms-core-listMenu-verticalBox .switch');

if (switches.length) {
  for (var j = 0; j < switches.length; j++) {
    AddEvent(switches[j], 'click', ExpandCollapse);
  }
}

Functions

We were using a couple of functions in the main code block, let's take a look at them now.

ExpandCollapse function

The ExpandCollapse function main purpose is to change level class from 'expanded' to 'collapsed' and visa versa, which determine if the level is visible or hidden. But there are some more actions involved.

At the beginning of the click event we need to find all the nodes that will be used, after that we close other levels if a collapseOtherLevels parameter is set to true.

ExpandCollapse = function(param) {
  var level = this.parentNode,
      sublevel = level.querySelector('ul'),
      sublevelHeight = CalculateHeight(sublevel),
      otherLevels = level.parentElement.children;

  if (SP2013QLAccordion.collapseOtherLevels && level.className.indexOf('collapsed') != -1 && !param) {
    for (var i = 0; i < otherLevels.length; i++) {
      if (otherLevels[i].className.indexOf('expanded') != -1) ExpandCollapse.call(otherLevels[i], 'collapse');
    }
  }     

  if (SP2013QLAccordion.useAnimation) {
    ...
  } else {
    ...
  }
}

Then two same blocks go, both doing the same, but one uses animation, and the other doesn't, depending on useAnimation parameter.

Nonanimated menu GIF

Let's take a look at nonanimated one first. It just changes the level node class, so it becomes visible or not.

if (level.className.indexOf('expanded') != -1 || param == 'collapse') {
  level.className = level.className.replace(' expanded',' collapsed');
} else {
  level.className = level.className.replace(' collapsed',' expanded');
}

And here comes the animated one. It is quite complex, because it uses a workaround for CSS transitions.

Animated menu GIF

None of browsers support CSS transition to or from an 'auto' value.

Though it should work by specification, none of browsers can make a CSS transition to an auto value. There are some solutions for this problem. The easier one is to use max-height instead of height in your CSS, but it has some drawbacks:

  1. You need to know the approximate size of the element to set max-size;
  2. There will be a delay if the element size differs much from its max-size.

This doesn't suit current solution, because menu size can be differ greatly depending on the number of elements, so I implemented a more neat solution by Nikita Vasilyev. The main idea is to manually calculate height of an element, and use it for the transition.

    if (level.className.indexOf('expanded') != -1 || param == 'collapse') {
      sublevel.style.height = sublevelHeight + 'px';
      level.className = level.className.replace(' expanded',' collapsed');
      sublevel.style.transition = SP2013QLAccordion.collapseTransition;
      sublevel.offsetHeight; // Force repaint
      sublevel.style.height = 0;
    } else {
      sublevel.style.height = 0;
      level.className = level.className.replace(' collapsed',' expanded');
      sublevel.style.transition = SP2013QLAccordion.expandTransition;
      sublevel.offsetHeight; // Force repaint
      sublevel.style.height = sublevelHeight + 'px';
      sublevel.addEventListener('transitionend', function transitionEnd(event) {
        if (event.propertyName == 'height') {
          sublevel.removeAttribute('style');
          sublevel.removeEventListener('transitionend', transitionEnd, false);
        }
      }, false);
    }

CalculateHeight function

Previous function involves the node height determination. Since it is not trivial because a node can be invisible or can contain a different number of hidden and visible descendants, I used a separate function for this task.

To determine size of a node, it has to be visible, but we don't want other elements to be affected by the size of a hidden element, so this function uses CSS rules position: absolute; and visibility: hidden; to make this possible. After calculation, it returns initial styles of a node.

var CalculateHeight = function(node) {
  var initialStyles = node.style.cssText,
      nodeHeight;

  node.style.position = 'absolute';
  node.style.visibility = 'hidden';
  node.style.height = 'auto';
  nodeHeight = node.offsetHeight;
  node.style.cssText = initialStyles;
  return nodeHeight;
}

AddEvent function

A small function to create a crossbrowser event attaching:

AddEvent = function(htmlElement, eventName, eventFunction) {
  if (htmlElement.attachEvent)
    htmlElement.attachEvent("on" + eventName, function() {eventFunction.call(htmlElement);}); 
  else if (htmlElement.addEventListener)
    htmlElement.addEventListener(eventName, eventFunction, false);
}

ExecuteOrDelayUntilBodyLoaded function

It is a default SharePoint body onload function. You can read more about it in my article SharePoint 2013 page onload event.

Level amount limit

By default SharePoint quick launch menu can hold two levels of items. You can increase this number by changing StaticDisplayLevels parameter inside AspMenu control with V4QuickLaunchMenu ID in your masterpage code:

<!--SPM:<SharePoint:AspMenu
    id="V4QuickLaunchMenu"
    runat="server"
    EnableViewState="false" 
    DataSourceId="QuickLaunchSiteMap"
    UseSimpleRendering="true"
    Orientation="Vertical"
    StaticDisplayLevels="4"
    AdjustForShowStartingNode="true"
    MaximumDynamicDisplayLevels="0"
    SkipLinkText=""
/>-->

Amount of levels menu can display is less by 1 from the value of StaticDisplayLevels parameter. To display 3 levels set 4, to display 4 set 5, and so on.

Though it is enough for menu control to be able to display more than two levels, you can't achieve it by using default navigation settings. Use metadata navigation in this case.

How to use

To use this solution, place SP2013Accordion.css and SP2013Accordion.js to the Style Library folder using SharePoint Designer. Then modify your master page to add links to these files.

Feel free to modify the SP2013Accordion.js file to change some options. Available options with default values:

useAnimation: true,
collapseOtherLevels: false,
expandTransition: 'height 0.15s ease-out',
collapseTransition: 'height 0.15s ease-out',
  1. useAnimation determines if animation is used when you expand/collapse menu levels;
  2. collapseOtherLevels determines if other levels will be closed when you open another one (closes only sibling levels);
  3. expandTransition - transition style on level expanding;
  4. collapseTransition - transition style on level collapsing;

Download

SP2013Accordion.rar (3 KB)

Now you can use this solution to create more flexible Quick Launch menu.