A mega menu for SharePoint with a DVWP and a list

Mega Menus are a hot topic in current user interfaces. There are many JavaScript and jQuery tools out on the interweb that provide the JS code to present those Mega Menus in an appealing fashion. All these solutions use hard-wired html that hold the nested UL and LI items for the menu, though.

Within SharePoint, how can you create a SharePoint list to feed a Mega menu?

This post explains step by step how to create a SharePoint list that holds Mega Menu items and how to customise a DVWP to present the menu items in a format of nested lists. These lists will then be rendered by jQuery and JavaScript to create the completely configurable Mega Menu for SharePoint. Embed the DVWP in your master page and empower your staff to take control of the mega menu.

The Vision:

A MegaMenu for a SharePoint portal home page

The Mission:

Make it easy to maintain. Allow rich content and sub headers as well as standard links. Factor in frequent changes without involving developers or a lengthy Dev/Test/Prod release cycle. The personal assistant of the marketing boss should be able to make changes on the fly. Instantaneous. Without any knowledge of HTML or CSS, so editing code is out. If it’s more complicated than filling in a time sheet, it won’t fly. And do all that with just the browser interface and SharePoint Designer. No Visual Studio, no custom code.

What’s a MegaMenu, anyway?

There are quite a few sites out there describing how to create impressive MegaMenus if you Bingle a bit. Rave reviews of the concept from Jacob Nielsen. Flashy sites from developers strutting their stuff. JavaScript, jQuery – the choice is yours. Most of them even work.

None of those take into account a SharePoint background, though. The MegaMenu content is always somehow “there already”, nicely configured in a nested construct of UL and LI tags, with hard-coded tags and titles. Not something the Marketing Assistant will want to get his head around if he wants to add a few items and a flashy “Hot and new” icon to a new menu entry.

So, to achieve the vision and make the mission possible, we need to come up with some practicable steps.

Here’s the plan:

  • Create a SharePoint list that stores all the items to feature in the MegaMenu
  • Create a view on that list with a Data View Web Part (DVWP)
  • Modify the DVWP to show as nested lists instead of the standard table structure
  • Apply the CSS and the jQuery

What you need:

  • A SharePoint 2010 site.
  • SharePoint Designer 2010
  • jQuery Hover Intent plug-in: The only non-standard SharePoint elements required are the jQuery core and the library for Hover Intent. This is a variation of a JavaScript functionality that displays stuff if the mouse hovers over specific screen elements. Download the Hover Intent here: http://cherne.net/brian/resources/jquery.hoverIntent.html
  • Why use Hover Intent? JavaScript mouse hover events normally fire immediately when the mouse hovers over the item. This can lead to a lot of screen flicker and a general perception of “nervous” behavior. Hover Intent waits for the mouse to pause before the event is triggered, Check out the link above for a demo of what it does. Don’t forget to download the jQuery core file.
  • The CSS and jQuery script calls were originally posted at http://www.sohtanaka.com/web-design/mega-drop-downs-w-css-jquery/but this web site seems no longer available. Thanks to the WayBack machine, though, it can still be viewed in the Internet Archive and I have salvaged the MegaMenu Demo and placed a copy on my site. All credits for developing that demo go to Soh Tanaka.
I also assume that you have a basic knowledge of the SharePoint 2010 browser interface , basic knowledge of SharePoint Designer 2010, Data View Web Parts (DVWP) and how to style screen elements with CSS.

The final look and feel we’re after can be experienced in the MegaMenu Demo. In my Sharepoint site, it looks like this:

Have a look around and try out the demo to see what the end product should look like. Once you’re comfortable with the concept, come back and let’s take action.

1. Create a SharePoint list for menu items

If we want the MegaMenu to be configurable, then a SharePoint List will be the most logical way to achieve that. For the sake of normalizing data, I suggest an approach with two lists.

List for MegaMenu headers

This list is called MegaMenu Headers and has three columns:

Title: — Single line of text – The MegaMenu tab title (if you use graphic files for the MegaMenu tab background, these won’t ever be visible, but they will help with the general orientation).

Order – number – the order in which the headers appear on the final page. This column will be the first sorting and grouping criterion of the mega menu. The column will be inherited by the list that stores the MegaMenu details.

CSSClass – Single line of text – This value needs to be manually created in the CSS file that formats the MegaMenu. Every header tab will have a specific width and position, defined by a CSS class. To make the formatting easier to maintain, assign the CSS class name here and then make sure that the CSS file actually has an entry that defines the properties for that class.

List for MegaMenu Content

The Headers provide the outer envelope, but the meat of the functionality will be with the individual content items of the menus. For this, we need another SharePoint List. The list is called MegaMenu Content and has these columns:

Column Type
MegaHeader Lookup
MenuRow Number
MenuColumn Number
ItemNumber Number
Title Single line of text    (Don’t make this required!! It will not always have data)
MenuLink Hyperlink or Picture
ItemImage Hyperlink or Picture
ItemBody Multiple lines of text  (This is rich text)
Published Yes/No
ItemWidth Choice  (the choices are: “default, 2, 3, 4”. Make the default value “default”. The code below will look out for that.)
MegaHeader:Order Lookup
MegaHeader:CssClass Lookup

The last two columns, MegaHeader:Order and MegaHeader:CssClass are created by ticking their column names when defining the Lookup column for MegaHeader.

Now fill your list with some content. For each item, make sure you select a MegaHeader value and specify MenuRow, MenuColumn and ItemNumber. These numbers will influence the order of the items in a menu panel.

Then specify at least one of the columns Title, MenuLink, ItemImage or ItemBody If a menu item has a Title specified, it will be formatted as a h2 element. A MenuLink for an item with a title is optional.

ItemWidth is optional. Leave it at default unless you want an item to span more than one column. In that case, the first item in that column requires the ItemWidth to be set.

2. Creating a DVWP

Fire up SharePoint Designer 2010, open an existing Web Part Page or a Wiki Page and create a DVWP:

Insert > Data View > Empty Data View

In the new, empty data view, click the link to select a data source and select the list MegaMenu Content. It does not really matter which fields you select for the display, because we will gut the DVWP content and replace it with a custom XSL Template. So select a few fields and click “Insert Selected Fields as > Multiple Item View”.

By default, the DVWP only shows 10 items. Fix that by clicking “Paging > Display All Items”.

Next, click the Sort & Group icon and add these fields to the sort order:

– MegaHeader:Order

– MenuRow

– MenuColumn

– ItemNumber

3. Customising the DVWP

By default, a DVWP is displayed as a table. We need to change this to a nested list with this structure:

<ul id="topnav">
    <li>Header Tab
<div class="”sub”">
<div class="”row”">
<ul>
    <li>Menu Item</li>
    <li>Menu Item</li>
    <li>Menu Item</li>
</ul>
</div>
</div></li>
    <li>Next Header tab ... etc</li>
</ul>

This image shows the nested classes and divs:

red = <div class=”sub”>
orange = <div class=”row”>
purple = <ul> or <ul class=”span2″>, depending on the data

So, let’s apply the custom template. Find the first template in the XSL and delete all template code down to the last tag. Make sure to keep the opening tags intact.

Without further ado, here is the template code that I used, starting with the tag:

<Xsl>
<xsl:stylesheet
    version="1.0"
    xmlns:x="http://www.w3.org/2001/XMLSchema"
    xmlns:d="http://schemas.microsoft.com/sharepoint/dsp"
    xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime"
    xmlns:asp="http://schemas.microsoft.com/ASPNET/20"
    xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    xmlns:SharePoint="Microsoft.SharePoint.WebControls"
    xmlns:ddwrt2="urn:frontpage:internal"
    exclude-result-prefixes="x d asp xsl msxsl ddwrt ddwrt2">
<xsl:output method="html" indent="no"/>
<xsl:decimal-format NaN=""/>
<xsl:param name="dvt_apos">&apos;</xsl:param>
<xsl:param name="ManualRefresh">
</xsl:param><xsl:variable name="dvt_1_automode">0</xsl:variable>
 
<xsl:key name="MHeaders" match="Row" use="@MegaHeader_x003a_Order"/>
<xsl:key name="MRows" match="Row" use="concat(@MegaHeader_x003a_Order, '-', @MenuRow)"/>
<xsl:key name="MColumns" match="Row" use="concat(@MegaHeader_x003a_Order,'-', @MenuRow,'-', MenuColumn)"/>
 
  <xsl:template match="/" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:SharePoint="Microsoft.SharePoint.WebControls">
    <xsl:call-template name="Setup"/>
  </xsl:template>
  <xsl:template name="Setup">
    <xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row"/>
    <!-- start unordered list for the navigation top tabs -->
    <ul id="topnav">
      <xsl:variable name="HeaderLoop" select="//Row[generate-id() = generate-id(key('MHeaders', @MegaHeader_x003a_Order)[1])]"/>
      <xsl:for-each select="$HeaderLoop">
        <xsl:variable name="LinkStart" select="string('&lt;a href=&quot;#&quot; class=&quot;')"/>
        <xsl:variable name="LinkClass"><xsl:value-of select="@MegaHeader_x003a_CssClass"/></xsl:variable>
        <xsl:variable name="LinkEnd" select="string('&quot; &gt;')"/>
        <xsl:variable name="LinkVar" select="concat($LinkStart,$LinkClass,$LinkEnd)"/>
        <xsl:variable name="CloseATag" select="string('&lt;/a&gt;')"/>
        <!-- build the list items for the top tabs -->
        <li>
          <xsl:value-of select="$LinkVar" disable-output-escaping="yes"/>
          <xsl:value-of select="substring-after(@MegaHeader., ';#')"/>
          <xsl:value-of select="$CloseATag" disable-output-escaping="yes"/>
          <!-- create a container for each menu panel -->
          <div class="sub">
            <xsl:variable name="RowLoop" select="key('MHeaders', @MegaHeader_x003a_Order)[generate-id() = generate-id(key('MRows', concat(@MegaHeader_x003a_Order, '-', @MenuRow))[1])]"/>
            <xsl:for-each select="$RowLoop">
              <!-- create a container for each row -->
              <div class="row">
                <xsl:variable name="ColumnLoop" select="key('MRows', concat(@MegaHeader_x003a_Order, '-', @MenuRow))[generate-id() = generate-id(key('MColumns', concat(@MegaHeader_x003a_Order,'-', @MenuRow,'-', @MenuColumn))[1])]"/>
                <xsl:for-each select="$ColumnLoop">
                  <!-- start a new unordered list for each column -->
                  <!-- if column width is specified, use that as a class -->
                  <xsl:variable name="SpanTagStart" select="string('&lt;ul ')"/>
                  <xsl:variable name="SpanTagClass">
                    <xsl:choose>
                      <xsl:when test="@ItemWidth != string('default')">
                        <xsl:value-of select="concat(string('class=&quot;span'),@ItemWidth,string('&quot; '))"/>
                      </xsl:when>
                      <xsl:otherwise>
                        <xsl:value-of select="''"/>
                      </xsl:otherwise>
                    </xsl:choose>
                  </xsl:variable>
                  <xsl:variable name="SpanTagEnd" select="string('&gt;')"/>
                  <xsl:variable name="SpanTagOpen" select="concat($SpanTagStart,$SpanTagClass,$SpanTagEnd)"/>
                  <xsl:variable name="SpanTagClose" select="string('&lt;/ul&gt;')"/>
                  <xsl:value-of select="$SpanTagOpen" disable-output-escaping="yes"/>
                  <!-- start a new unordered list for each column -->
                  <xsl:variable name="ItemLoop" select="key('MColumns', concat(@MegaHeader_x003a_Order,'-', @MenuRow,'-', @MenuColumn))"/>
                  <xsl:for-each select="$ItemLoop">
                    <!-- build the a href tag if we have a link -->
                    <xsl:variable name="ItemLinkStart" select="string('&lt;a href=&quot;')"/>
                    <xsl:variable name="ItemLinkURL"><xsl:value-of select="@MenuLink"/></xsl:variable>
                    <xsl:variable name="ItemLinkVar">
                      <xsl:choose>
                        <xsl:when test="string-length(@MenuLink) &gt; 0">
                          <xsl:value-of select="concat($ItemLinkStart,$ItemLinkURL,$LinkEnd)"/>
                        </xsl:when>
                        <xsl:otherwise><xsl:value-of select="''"/></xsl:otherwise>
                      </xsl:choose>
                    </xsl:variable>
                    <!-- build the list items -->
                    <li>
                      <!-- if we have a Title, format as h2 -->
                      <xsl:choose>
                        <xsl:when test="string-length(@MenuLink) &gt; 0 and string-length(@Title) &gt; 0">
                        <!-- title and link -->
                          <h2>
                            <xsl:value-of select="$ItemLinkVar" disable-output-escaping="yes"/>
                            <xsl:value-of select="@Title"/>
                            <xsl:value-of select="$CloseATag" disable-output-escaping="yes"/>
                          </h2>
                        </xsl:when>
                        <xsl:when test="string-length(@MenuLink) = 0">
                        <!-- title only -->
                          <h2>
                            <xsl:value-of select="@Title"/>
                          </h2>
                        </xsl:when>
                        <xsl:otherwise>
                          <xsl:value-of select="$ItemLinkVar" disable-output-escaping="yes"/>
                          <xsl:value-of select="@MenuLink.desc"/>
                          <xsl:value-of select="$CloseATag" disable-output-escaping="yes"/>
                        </xsl:otherwise>
                      </xsl:choose>
                      <xsl:if test="string-length(@ItemImage) &gt; 0">
                        <img border="0" src="{@ItemImage}" alt="{@ItemImage.desc}"/>
                      </xsl:if>
                      <xsl:if test="string-length(@ItemBody) &gt; 0">
                        <xsl:value-of select="@ItemBody" disable-output-escaping="yes"/>
                      </xsl:if>
                    </li>
                  </xsl:for-each>
                  <xsl:value-of select="$SpanTagClose" disable-output-escaping="yes"/>
                </xsl:for-each>
              </div>
            </xsl:for-each>
          </div>
        </li>
      </xsl:for-each>
    </ul>
  </xsl:template>
</xsl:stylesheet>
</Xsl>

Note: If you want to use this XSL, I strongly suggest that you download MegaMenuXSLT text file with the code. When rendered in a web browser, some characters in the code may be replaced, resulting in faulty XSL.

Some Explanations

The biggest challenge was to figure out how to do the grouping in XSL. After several approaches, I found that the Muenchian grouping works best for my purposes. For each of the three grouping levels, I created filter keys that get progressively more complex, concatenating the header order, row order and column order.

<xsl:key name="MHeaders" match="Row" use="@MegaHeader_x003a_Order"/>
<xsl:key name="MRows" match="Row" use="concat(@MegaHeader_x003a_Order, '-', @MenuRow)"/>
<xsl:key name="MColumns" match="Row" use="concat(@MegaHeader_x003a_Order,'-', @MenuRow,'-', @MenuColumn)"/>

These keys are created above the first template. Since each header tab needs its own class assigned, I created variables that store the field CssClass for the current item and then concatenate that with the appropriate opening and closing brackets for the tag.

<xsl:variable name="LinkStart" select="string('&lt;a href=&quot;#&quot; class=&quot;')"/>
<xsl:variable name="LinkClass"><xsl:value-of select="@MegaHeader_x003a_CssClass"/></xsl:variable>
<xsl:variable name="LinkEnd" select="string('&quot; &gt;')"/>
<xsl:variable name="LinkVar" select="concat($LinkStart,$LinkClass,$LinkEnd)"/>
<xsl:variable name="CloseATag" select="string('&lt;/a&gt;')"/>
<!-- build the list items for the top tabs -->
<li>
    <xsl:value-of select="$LinkVar" disable-output-escaping="yes"/>
    <xsl:value-of select="substring-after(@MegaHeader., ';#')"/>
    <xsl:value-of select="$CloseATag" disable-output-escaping="yes"/>

The resulting html looks like this:

<a class="”MyClass”" href="”#”">TheFirstTab</a>

The same technique is used later on to create the ul tag for the column, so the default column width can be overridden. With a default column width, we need a simple

      tag, but when a column width is specified with a 2, for example, then we need <ul class=”span2”>.
<xsl:variable name="SpanTagStart" select="string('&lt;ul ')"/>

<xsl:variable name="SpanTagClass">

   <xsl:choose>

      <xsl:when test="@ItemWidth != string('default')">

         <xsl:value-of select="concat(string('class=&quot;span'),@ItemWidth,string('&quot; '))"/>

      </xsl:when>

      <xsl:otherwise>

         <xsl:value-of select="''"/>

      </xsl:otherwise>

   </xsl:choose>

</xsl:variable>

<xsl:variable name="SpanTagEnd" select="string('&gt;')"/>

<xsl:variable name="SpanTagOpen" select="concat($SpanTagStart,$SpanTagClass,$SpanTagEnd)"/>

<xsl:variable name="SpanTagClose" select="string('&lt;/ul&gt;')"/>

<xsl:value-of select="$SpanTagOpen" disable-output-escaping="yes"/>

Several rows below that, the ul tag is closed with

<xsl:value-of select="$SpanTagClose" disable-output-escaping="yes"/>

4. Create the CSS

The CSS file that was on the demo site is here: MegaMenu.css. Either plug it into your custom CSS file, if you use one, or load it via script. I’ve added the following CSS classes to allow for the multi-column spanning of items:

[css]
ul#topnav li .sub ul{
list-style: none; /*–This is in the original CSS–*/
margin: 0; padding: 0;
width: 180px;
float: left;
}

ul#topnav li .sub ul.span2{
width: 360px; /*–This is a new definition–*/
}

ul#topnav li .sub ul.span3{
width: 540px; /*–This is a new definition–*/
}

ul#topnav li .sub ul.span4{
width: 720px; /*–This is a new definition–*/
}[/css]

Also, make sure that in the section for the #topnav you have definitions for each of the CssClass items you specified in the MegaMenu Headers list.

For testing purposes I used a Web Part Page to contain the DVWP. I created a Content Editor Web Part below the DVWP and linked it to a MegaMenuScript.txt file that has the following script:

[javascript]
/managedPath/siteName/Resource%20Library/jquery.min.js
/managedPath/siteName/Resource%20Library/jquery.hoverIntent.minified.js

$(document).ready(function() {
//On Hover Over
function megaHoverOver(){
$(this).find(“.sub”).stop().fadeTo(‘fast’, 1).show(); //Find sub and fade it in
(function($) {
//Function to calculate total width of all ul’s
jQuery.fn.calcSubWidth = function() {
rowWidth = 0;
//Calculate row
$(this).find(“ul”).each(function() { //for each ul…
rowWidth += $(this).width(); //Add each ul’s width together
});
};
})(jQuery);

if ( $(this).find(“.row”).length > 0 ) { //If row exists…

var biggestRow = 0;

$(this).find(“.row”).each(function() { //for each row…
$(this).calcSubWidth(); //Call function to calculate width of all ul’s
//Find biggest row
if(rowWidth > biggestRow) {
biggestRow = rowWidth;
}
});

$(this).find(“.sub”).css({‘width’ :biggestRow}); //Set width
$(this).find(“.row:last”).css({‘margin’:’0′}); //Kill last row’s margin

} else { //If row does not exist…

$(this).calcSubWidth(); //Call function to calculate width of all ul’s
$(this).find(“.sub”).css({‘width’ : rowWidth}); //Set Width

}
}
//On Hover Out
function megaHoverOut(){
$(this).find(“.sub”).stop().fadeTo(‘fast’, 0, function() { //Fade to 0 opactiy
$(this).hide(); //after fading, hide it
});
}

//Set custom configurations
var config = {
sensitivity: 2, // number = sensitivity threshold (must be 1 or higher)
interval: 100, // number = milliseconds for onMouseOver polling interval
over: megaHoverOver, // function = onMouseOver callback (REQUIRED)
timeout: 500, // number = milliseconds delay before onMouseOut
out: megaHoverOut // function = onMouseOut callback (REQUIRED)
};

$(“ul#topnav li .sub”).css({‘opacity’:’0′}); //Fade sub nav to 0 opacity on default
$(“ul#topnav li”).hoverIntent(config); //Trigger Hover intent with custom configurations

});

[/javascript]

I keep all my scripts in a Resource Library. The exact path to your script will be different, so please make sure to adjust the path. If you don’t use a Custom CSS file, you can load the CSS in that script as well.

Now it’s time to test. If all goes well in the web part page, you can use the DVWP in your Master Page or a Page Layout. In this case, plug the script straight into the page instead of calling it via a CEWP.

For the Master Page version of the DVWP I created a filter, so the DVWP will only display items where the column “Published” is checked.

Now the marketing assistant can create new menu items in the MegaMenu Content list. He can check if all items behave by opening the Web Part page that has the single, unfiltered DVWP. If he’s happy with that, he can tick the “Publish” check box for the new menu items and they will appear in the filtered DVWP on the Master Page.

Mission Accomplished.