This was a triumph.
I'm making a note here: HUGE SUCCESS.

Search This Blog

Friday, October 25, 2013

How to make an accordion menu for a subsite in SharePoint 2013

If you use term driven navigation to manage all the pages on your subsites, and you have a lot of pages for a certain subsite, then your navigation might turn out to be quite long. In situations like this, it might as well come in handy to have an accordion menu that just collapses or expands its contents (terms) upon click. I'm going to use jQuery and JavaScript for this.

Of course, it would also be important that terms having underlaying terms can't act as a link (the link behind them, if any, shouldn't activate and open a new page). Terms that have underlaying terms should expand and show their underlaying terms.

Here's a screenshot of a term driven navigation with all the terms and underlaying terms visible, no accordion used yet:

What we want is a navigation that looks like this:
 =>
 =>

So basically you have all the categories (the top terms) collapsed. Upon clicking on a term that has underlaying terms, it should expand and show the underlaying terms of that term you just clicked on. And so on.
In the example above, when you open the second category, the underlaying terms expand. In here, there is another term that has underlaying terms, which I named the subcategory. You can once again click on this one and it will expand, showing its respective underlaying terms. If a term no longer has underlaying terms, it cannot expand and it just opens the page.

And now the real work.

You will need to create a JavaScript file (or just add the code in an existing script file if you already use scripts on your site) and make sure to add it to your master page. Mine is named "script.js" and its path is "~sitecollection/Style Library/Scripts/script.js". You will also need jQuery, so make sure you have that as well. My jQuery file its path is "~sitecollection/Style Library/Scripts/jquery-1.10.2.min.js".

To correctly make a reference to your scripts, you must add them to your master page. I'm using a HTML master page. Edit your master page in advanced edit mode and add the reference code after the "ScriptLink" lines. The code below has five ScriptLink references (references to default SharePoint scripts), the code you must add has to be after those scripts.
// The following five lines are already in your master page
<!--SPM:<SharePoint:ScriptLink language="javascript" name="core.js" 
OnDemand="true" runat="server" Localizable="false"/>-->
<!--SPM:<SharePoint:ScriptLink language="javascript" name="menu.js" 
OnDemand="true" runat="server" Localizable="false"/>-->
<!--SPM:<SharePoint:ScriptLink language="javascript" name="callout.js" 
OnDemand="true" runat="server" Localizable="false"/>-->
<!--SPM:<SharePoint:ScriptLink language="javascript" name="sharing.js" 
OnDemand="true" runat="server" Localizable="false"/>-->
<!--SPM:<SharePoint:ScriptLink language="javascript" name="suitelinks.js"
OnDemand="true" runat="server" Localizable="false"/>--> 
     
// Now use these following two lines to reference to your JavaScript
// files, with first referencing to the jQuery script and then your 
// own script.
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink1" 
runat="server" OnDemand="false" Localizable="false" 
name="~sitecollection/Style Library/Scripts/jquery-1.10.2.min.js"/>-->
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink2" 
runat="server" OnDemand="false" Localizable="false" 
name="~sitecollection/Style Library/Scripts/scripts.js"/>-->

Now that you have correctly made a reference to the jQuery script and your own script, you can save the master page (for now, since we'll add some more later).

Let's start with the script.

This is what the code in my script.js file looks like. I will try to add comments in the code.
function accordionMe(selector, initalOpeningClass) {
   var speedo = 300;
   var $this = selector;
   var accordionStyle = true;

   // First of all, hide all ul's:
   $this.find("li>ul").hide(); 

   // Then for each li you find in that ul,
   $this.find("li").each(function(){ 
      // that isn't empty,
      if ($(this).find("ul").size() != 0) { 
         // find all the ones at the top,
         if ($(this).find("a:first")) { 
            // and make sure it won't do anything on click.
            $(this).find("a:first").click(function(){ return false; });
            // Optional: if you want to style the non-clickable links, 
            // then uncomment the code below. 
            //$(this).find("a:first").addClass("no-click");
         }
      }
   });

   // Open all items.
   $this.find("li."+initalOpeningClass).each(function(){ 
      $(this).parents("ul").slideDown(speedo); 
   });

   // Execute this function on click of li with an a tag.
   $this.find("li a").click(function(){ 
      if ($(this).parent().find("ul").size() != 0) {
         if (accordionStyle) { 
            if(!$(this).parent().find("ul").is(':visible')){
               // Fetch all parents.
               parents = $(this).parent().parents("ul"); 
               // Fetch all visible ul's.
               visible = $this.find("ul:visible"); 
               // Loop through.
               visible.each(function(visibleIndex){ 
                  var close = true;
                  // Check if the parent is closed.
                  parents.each(function(parentIndex){ 
                     if(parents[parentIndex] == visible[visibleIndex]){
                        close = false;
                        return false;
                     }
                  });
                  // If closed, slide the content of the ul up 
                  // (so collapse).
                  if(close){ 
                     if($(this).parent().find("ul") != 
                       visible[visibleIndex]){
                        $(visible[visibleIndex]).slideUp(speedo);
                     }
                  }
               });
            }
         }
         if($(this).parent().find("ul:first").is(":visible")) {
            $(this).parent().find("ul:first").slideUp(speedo);
         }
         else {
            $(this).parent().find("ul:first").slideDown(speedo);
         }
      }
   });
}

There you have it, your code is done! Now all we need to do is make sure it will execute the function as soon as a page is loaded. We need to get back to our master page for this. So open your master page again (in advanced edit mode) and find the following piece of code:
<script type="text/javascript">
//<![CDATA[
 var g_pageLoadAnimationParams = { elementSlideIn : "sideNavBox", 
 elementSlideInPhase2 : "contentBox" };
//]]>
</script>

We will not change that part, but we will have to paste some code underneath it. Here's the code:
<script type="text/javascript">
//<![CDATA[
 var g_pageLoadAnimationParams = { elementSlideIn : "sideNavBox", 
 elementSlideInPhase2 : "contentBox" };

 $(function runOnInitLoad() {
  accordionMe(jQuery("#zz9_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz10_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz11_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz12_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz13_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz14_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz15_RootAspMenu"), "selected");  
  accordionMe(jQuery("#zz16_RootAspMenu"), "selected");  
  runThisCode(); 
  moveScroller();
  setCustomFontName('#fseaFont-1-1-Menu');
 });

 ExecuteOrDelayUntilScriptLoaded(function() { runOnInitLoad(); }, 
 "init.js");
//]]>
</script>

The reason that I use the function on so many different ID's is because these menu ID's get used often, spread across the site. These are just all the ones I needed, so I added them all. Feel free to add/remove some.

After you have saved your master page, checked it in and published it as a major version (and don't forget to check in and publish your scripts as well), we're all done! You now should have a nice accordion menu, sliding up and down as you click on it. :) Enjoy!


PS.: I would like to express my thanks to Mathias Bosman, who seriously helped me with this (basically, he made the whole script work). Thanks Mathias!

14 comments:

  1. Hi Where can we add collapse / expand icon in this code ? Thanks

    ReplyDelete
    Replies
    1. I'm not sure if you need a separate button or icon to collapse/expand everything, or want an icon on the left side of each collapsable/expandable term. Currently any term in the accordion that has underlaying terms is collapsable/expandable upon clicking, but if you tweak the code a bit then I'm sure you can create an icon to collapse/expand every term that has underlaying terms.

      Delete
  2. Hi Magali, thanks for this post.
    I need to add in the third level a collapse/expand img what should i do ?
    Thanks

    ReplyDelete
    Replies
    1. If it is a small image, like an icon, then you could add it using CSS. For example, if an element at the third level has the text "Third element" in it, then you can use JavaScript to search through your accordion and look for an element that has an inner HTML equal to "Third element". If that requirement is found to be true, then use the CSS background attribute to assign an image to that element using "url('../enter/pathname/here.png') 0 7px no-repeat !important;" without the double quotes. You can even replace the value of that inner HTML of that element with nothing if you don't want to display a text.
      In case you need an image in every third level, you could write a piece of JavaScript to search through the accordion and for each third level element (for example: $this.find("li ul li ul li")...) add an image by either assigning a class to that element and give that class proper CSS styling with a background image, or insert an img tag in that element containing the url to the desired image.

      Delete
  3. Hi Magali, great post! but I have an issue with my sharepoint online, I get an error that $ is not defined, I tried everything but I can not avoid the error, you have an idea how I can correct? Thank's for your Help.

    ReplyDelete
    Replies
    1. Either your JavaScript file wasn't properly loaded into your page, or your version of jQuery is incorrect. In your JavaScript file, write a console log with dummy text in it and check in the console window of your browser if it came through. If not, then your JavaScript file isn't called properly. If it does, and you still get the error, then maybe you called the script before the jQuery script. Make sure to first call the jQuery script and then the JavaScript file. :)

      Delete
  4. var g_pageLoadAnimationParams

    Not found this code=(

    ReplyDelete
    Replies
    1. This code is not in your master page? Perhaps you need to search for something similar to that in your master page, or even add it to your master page. I cannot duplicate the situation here, sorry. :( Do let me know if you managed to find it or get it to work!

      Delete
  5. where can I find init.js that you refereed in the script? I am new to share point/scripting world.

    ReplyDelete
    Replies
    1. Basically, init.js is just a script used by SharePoint, it is an OOB file, an "out-of-the-box" file, located somewhere in the Layouts folder. You don't need the location of this file in order to use it, just referring to it (as I did in my code) will work. Under no circumstances should you try to edit this file.

      Delete
  6. What is the init.js in this statement referring to: ExecuteOrDelayUntilScriptLoaded(function() { runOnInitLoad(); }, "init.js");? Should this be "scripts.js" in the example?

    ReplyDelete
    Replies
    1. The "init.js" is just a file located in the Layouts folder (server-side, I believe) of the SharePoint site. It is required for my code to work properly. You don't need to change it to "scripts.js".

      Delete
  7. This seems to attach to the top navigation too, and there's another problem - they only expand if your section heading is a link, and the link then doesn't work. If it's not a link, it won't expand

    ReplyDelete
    Replies
    1. For the top navigation: to make the section headings work without using an actual link you can give them "/" as href. In my term store, top terms that have underlying terms all have a "simple hyperlink or header" as their type of navigation (I might get lost in translation here, my environment is in Dutch) and I just fill in "/". It's been a while since I looked at that code again but I think I've updated it last time, I will need to look at it soon to see if it is still up-to-date here.

      Delete