ben tedder : code things

Create a jQuery accordion with a SharePoint CQWP

Today we'll be creating 5 (don't be scared!) small files that will result in you being able to add a Content Query Web Part to a page, and display the results in a jQuery accordion format. It should look something like this:

Click for a demo or download the source files

Steps to create your own:

Step 1: Add, then Export a standard CQWP {#acc_exportCQWP}

This method of editing a CQWP to do what you want involves no custom coding, no packaging, no .wsp files. All you need to do is go to any page and insert a CQWP Web Part (OOTB). Once it is on your page, click the little black arrow that displays the Web Parts context menu and select Export. This will download a .webpart file called Content. Open this file in your favorite code editor (at the moment mine are Notepad++ for Windows and TextMate for Mac), and go to step 2.

Step 2: Edit the .webpart file to read custom .xsl files {#acc_webpart}

To see the full .webpart file, check out the source code, but basically we're going to edit 4 lines and put links to our custom .xsl files:

  • Title
  • ItemXslLink
  • MainXslLink
  • XsL

Once you have edited these 4 lines, upload this webpart to the root Web Part gallery by clicking Site Actions > Site Settings > Web parts.

<property name="Title" type="string">Accordion</property>
<property name="ItemXslLink" type="string">/Style Library/XSL Style Sheets/AccordionStyle.xsl</property>
<property name="MainXslLink" type="string">/Style Library/XSL Style Sheets/AccordionCQWP.xsl</property>
<property name="Xsl" type="string"><xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:cmswrt="http://schemas.microsoft.com/WebPart/v3/Publishing/runtime" exclude-result-prefixes="xsl cmswrt x" > <xsl:import href="/Style Library/XSL Style Sheets/Header.xsl" /> <xsl:import href="/Style Library/XSL Style Sheets/AccordionStyle.xsl" /> <xsl:import href="/Style Library/XSL Style Sheets/AccordionCQWP.xsl" /> </xsl:stylesheet></property>

Step 3: Create AccordionStyle.xsl {#acc_accordionstyle}

Instead of using SharePoint's ItemStyle.xsl (not best practice to edit that directly) we're going to create our own stylesheet (the one we linked to in step 2). It has the same header as ItemStyle.xsl, so feel free to copy from there or from below. However, it only has one template. I've called the template Accordion, so when you're editing the Web Part properties in the browser you'll see this as the only selectable style in the Presentation pane.

Basically all this style does is wrap the header field (represented by @Title) in a div with a class of accordion-header, and the body field (represented by @Body) in a div with a class of accordion-content. We'll use these classes later in the jQuery functions.

Once you've created this file, put it in /Style Library/XSL Style Sheets/ using SharePoint Designer.

<xsl:stylesheet
  version="1.0"
  exclude-result-prefixes="x d xsl msxsl cmswrt"
  xmlns:x="http://www.w3.org/2001/XMLSchema"
  xmlns:d="http://schemas.microsoft.com/sharepoint/dsp"
  xmlns:cmswrt="http://schemas.microsoft.com/WebParts/v3/Publishing/runtime"
  xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt">

<!-- Accordion Template -->
    <xsl:template name="Accordion" match="Row[@Style='Accordion']" mode="itemstyle">

       <div class="accordion-header"><xsl:value-of select="@Title"/></div>
       <div class="accordion-content"><xsl:value-of select="@Body" disable-output-escaping="yes" /></div>

    </xsl:template>
</xsl:stylesheet>

Step 4: Create AccordionCQWP.xsl {#acc_accordioncqwp}

The next page we'll make starts off as a clone of ContentQueryMain.xsl, so go there real quick and copy all of that code into your new file.

There are three things we will do to this code.

  1. Replace dfwp-list with accordion (on lines 32 and 83)
  2. Replace all occurrences of dfwp-item with accordion-item (line 34)
  3. Add (using html character codes…annoying) "expand all" and "collapse all" divs before the list.

Come to think of it, you wouldn't have to do #3 if you just used jQuery to prepend those buttons to the list. Yeah, that's the easier way…I'll update the post soon.

Once you've done those 3 edits, upload this file to /Style Library/XSL Style Sheets/ (the same place as the file in step 3)

<!-- beginning of code from ContentQueryMain.xsl goes here -->
  <xsl:variable name="BeginList" select="string('<ul class="accordion"><div class="expand-all">Expand All</div><div class="collapse-all">Collapse All</div>')" />
  <xsl:variable name="EndList" select="string('</ul>')" />
  <xsl:variable name="BeginListItem" select="string('<li class="accordion-item">')" />
  <xsl:variable name="EndListItem" select="string('</li>')" />
  <xsl:template match="/">
        <xsl:call-template name="OuterTemplate" />
    </xsl:template>
    <xsl:template name="OuterTemplate">
        <xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row" />
        <xsl:variable name="RowCount" select="count($Rows)" />
        <xsl:variable name="IsEmpty" select="$RowCount = 0" />
            <div id="{concat('cbqwp', $ClientId)}" class="cbq-layout-main">
                 <xsl:if test="$cbq_iseditmode = 'True' and string-length($cbq_errortext) != 0">
                    <div class="wp-content description">
                        <xsl:value-of disable-output-escaping="yes" select="$cbq_errortext" />
                    </div>
                  </xsl:if>
                  <xsl:choose>
                      <xsl:when test="$IsEmpty">
                           <xsl:call-template name="OuterTemplate.Empty" >
                               <xsl:with-param name="EditMode" select="$cbq_iseditmode" />
                           </xsl:call-template>
                      </xsl:when>
                      <xsl:otherwise>
                           <xsl:call-template name="OuterTemplate.Body">
                               <xsl:with-param name="Rows" select="$Rows" />
                               <xsl:with-param name="FirstRow" select="1" />
                               <xsl:with-param name="LastRow" select="$RowCount" />
                          </xsl:call-template>
                      </xsl:otherwise>
                  </xsl:choose>
            </div>
            <xsl:if test="$FeedEnabled = 'True' and $PageId != ''">
                <div class="cqfeed">
                    <xsl:variable name="FeedUrl1" select="concat($SiteUrl,$FeedPageUrl,'xsl=1&web=',$WebUrl,'&page=',$PageId,'&wp=',$WebPartId,'&pageurl=',$CBQPageUrl,$CBQPageUrlQueryStringForFilters)" />
                    <a href="{cmswrt:RegisterFeedUrl( $FeedUrl1, 'application/rss+xml')}"><img src="\_layouts\images\rss.gif" border="0" alt="{cmswrt:GetPublishingResource('CbqRssAlt')}"/></a>
                </div>
            </xsl:if>
    </xsl:template>
    <xsl:template name="OuterTemplate.Empty">
        <xsl:param name="EditMode" />
            <xsl:if test="$EditMode = 'True' and string-length($cbq_errortext) = 0">
                <div class="wp-content description">
                    <xsl:value-of disable-output-escaping="yes" select="$cbq_viewemptytext" />
                </div>
            </xsl:if>
    </xsl:template>
    <xsl:template name="OuterTemplate.Body">
      <xsl:param name="Rows" />
      <xsl:param name="FirstRow" />
      <xsl:param name="LastRow" />
      <xsl:variable name="BeginColumn1" select="string('<div class="expand-all">Expand All</div><div class="collapse-all">Collapse All</div><ul class="accordion" style="width:')" />
      <xsl:variable name="BeginColumn2" select="string('%" >')" />
      <xsl:variable name="BeginColumn" select="concat($BeginColumn1, $cbq_columnwidth, $BeginColumn2)" />
      <xsl:variable name="EndColumn" select="string('</ul>')" />
      <!-- rest of code from ContentQueryMain.xsl goes here -->

Step 5: jQuery functions {#acc_jquery}

Our jQuery code is going to do several things:

  • Start every content pane hidden and make sure all headers are deactivated
  • When a header is clicked, it toggles the "expanded" class on the header (giving it a different style) and uses slideToggle (a jQuery animation) to show or hide the content pane.
  • It adds functions to the "expand all" and "collapse all" buttons we made earlier

When you're done copy and pasting (or modifying) this code, save it in /Style Library/scripts/accordion.js. If you've got other javascript files there you should consider combining and condensing your code for less calls, but for the sake of this tutorial, it will be a standalone .js file. Once you've dropped the file into your system, link to it from the Master Page. Here's an article on how to call custom javascript from your Master Page.

function accordionLoad() {

    $(".accordion-header").removeClass("expanded");
    $(".accordion-content").hide();

    $(".accordion-header").bind("click", function(){
        $(this).toggleClass("expanded");
        $(this).siblings(".accordion-content").slideToggle();
    })

    $(".expand-all").bind("click",function(){
        $(this).siblings(".accordion").find(".accordion-content").slideDown();
        $(this).siblings(".accordion").find(".accordion-header").addClass("expanded");
    })

    $(".collapse-all").bind("click",function(){
        $(this).siblings(".accordion").find(".accordion-content").slideUp();
        $(this).siblings(".accordion").find(".accordion-header").removeClass("expanded");
    })
}

$(document).ready(function(){
    accordionLoad();
});

side note: I would love to rewrite this jQuery to be a controller, that way it can be re-used across the site anytime somebody adds .accordion() to an element using jQuery.

Step 6: CSS {#acc_css}

This is the way I want my accordion to look, so feel free to modify. This is also the style used in the demo of this tutorial.

When you're done, drop this file in /Style Library/css/accordion.css (again, you could also include this in you main css, but for the sake of the tutorial it's standalone).

/* Accordion Style
********************************************************** */
ul.accordion {
    list-style:none;
    margin:0px;
    padding:0px;
}
.accordion-item {
    border-top:1px solid #ccc;
}
.accordion-header {
    font-size:1.2em;
    cursor:pointer;
    padding:10px;
}
.accordion-header:hover {
    background:#efefef;
}
.accordion-header.expanded {
    background:#dfdfdf;
}
.accordion-content {
    padding:20px;
}
.expand-all,
.collapse-all {
    display:inline-block;
    cursor:pointer;
    padding:5px 10px;
}
.expand-all:hover,
.collapse-all:hover {
    background:#efefef;
}

Step 7: Putting it all together {#acc_final}

So, once you've made all of the above files, your structure should look like this:

  • /Style Library/XSL Style Sheets/AccordionStyle.xsl
  • /Style Library/XSL/Style Sheets/AccordionCQWP.xsl
  • /Style Library/scripts/accordion.js
  • /Style Library/css/accordion.css
  • Accordion.webpart (uploaded to the Web Part Gallery)

Now go to your page, add the Accordion Web Part, and use it to display a list of items (FAQs and announcements work great in this format). It should be quite easy to read in the is jQuery Accordion format.

Click for a demo or download the source files