Add a jQuery accordion with a SharePoint CQWP

Requirements:

  • Create a web part to display categories and procedures.
  • Group the procedures by category.
  • Can add, edit and delete categories and procedures.
  • Can view the full detail of a procedure

Final delivery:

result

Features:

  • No custom solution developed. Applied custom CSS and JavaScript to an OOTB content query web part.
  • Used jQuery accordion to toggle the categories and items belonging to the category.
  • Edit category and procedure in a modal dialog box.

Solution:

1. Create two lists, Category and Procedure

Category list columns:

Title: Single Line of Text

Description: Multiple Line of Text

                Procedure list columns:

Procedure Name: Single line of text

Category: Lookup (Lookup to Category list, tick the ID checkbox)

ProcID: Calculated (Formular: =TEXT(Created,”ddMMyyhhmmss”))

ProcLink: Calculate (Formular: =”/pages/DisplayFullProc.aspx?ProcID=”&ProcID)

 

2. Create DisplayFullProc.aspx page with a data view web part. The DVWP displays a single Procedure item. (This step is optional)

3. Go to a page and add a CQWP. Export it to your local desktop.

4. Open the exported .webpart file in a text editor.

5. Change the following code:

<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">&lt;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" &gt; &lt;xsl:import href="/Style Library/XSL Style Sheets/Header.xsl" /&gt; &lt;xsl:import href="/Style Library/XSL Style Sheets/AccordionStyle.xsl" /&gt; &lt;xsl:import href="/Style Library/XSL Style Sheets/AccordionCQWP.xsl" /&gt; &lt;/xsl:stylesheet&gt;</property>
     <property name="UseCache" type="bool">False</property>

6. Upload the edited .webpart file to the Web Part Gallery in the root site.

7. Open your root site in SharePoint Designer. Navigate to All Files->Style Library->XSL Style Sheets. Create a new file and call it AccordionStyle.xsl.  This is the style file is for each procedure item in the list. Copy the following code to the document.

<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">

<!-- Procedure Item Template -->
    <xsl:template name="Accordion" match="Row[@Style='Accordion']" mode="itemstyle">
       <!--Need to pass the category ID value to the group header, but the ID doesn't need to be displayed-->          
                   <div style="display:none">
                                <xsl:value-of select="@CategoryID" />
                   </div>
       <div>
                  <!--Edit Proc Button-->
                      <a href="#">
                                                <xsl:attribute name="onclick">
                                                                javascript:portal_openEditProModalDialog('<xsl:value-of select="@ProcID"/>')
                                                </xsl:attribute>
                                             <img src="/Style Library/Images/edit-icon-thumb.png" border="0"></img>
                                  </a>

                                  <!--Delete Proc Button-->
                                  <a href="#">
                                                <xsl:attribute name="onclick">
                                                                deleteProc('<xsl:value-of select="@ProcID"/>')
                                                </xsl:attribute>
                                                <img src="/Style Library/Images/delete-icon-thumb.png" border="0"></img>
                                  </a>

                      <xsl:text>                </xsl:text>

                                  <a href="{@ProcLink}" target="_blank">
                                <xsl:value-of select="@Title"/>
                      </a>
       </div>
    </xsl:template>
</xsl:stylesheet>

8. In the XSL Style Sheets folder, make a copy of the ContentQueryMain.xsl file and rename it to AccordionCQWP.xsl. Make the following modifications:

  • Replace dfwp-list with accordion (Line 32 and 83)
  • Replace dfwp-item with accordion-item (Line 34)
  • Replace Line 83 with the following code:

This code displays the Expand All, Collapse All, Add Category and Add Procedure links before the list. To make it easier to see, visit this site and this site for conversion.

<xsl:variable name="BeginColumn1" select="string('&lt;div class=&quot;expand-all&quot;&gt;Expand All&lt;/div&gt;&lt;div class=&quot;collapse-all&quot;&gt;Collapse All&lt;/div&gt;&lt;div class=&quot;add-cat&quot;&gt;&lt;a href=&quot;/ippreserver/Lists/Category/NewForm.aspx?IsDlg=0&quot;&gt;Add Category&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;add-proc&quot;&gt;&lt;a href=&quot;/ippreserver/Lists/Procedure/NewForm.aspx?IsDlg=0&quot;&gt;Add Procedure&lt;/a&gt;&lt;/div&gt;&lt;ul class=&quot;accordion&quot; style=&quot;width:')" />

9. In the XSL Style Sheets folder, back up the Header.xsl file by making a copy of the original file and rename it to Header_original.xsl. The Header.xsl is the style file for each category group the list.  Edit the Header.xsl file. Paste the following code before </xsl:stylesheet>

<xsl:template name="IPCat" match="*[@GroupStyle='IPCat']" mode="header">
    <div>
      <a href="#">
              <xsl:attribute name="onclick">
                      javascript:portal_openEditCatModalDialog('<xsl:value-of select="@CategoryID"/>')
              </xsl:attribute>
              <img src="/Style Library/Images/edit-icon-thumb.png" border="0"></img>
      </a>                  
      <a href="#" >
              <xsl:attribute name="onclick">
                       deleteCategory('<xsl:value-of select="@CategoryID"/>')
              </xsl:attribute>
              <img src="/Style Library/Images/delete-icon-thumb.png" border="0"></img>
      </a>

      <xsl:text>                </xsl:text>

      <xsl:call-template name="OuterTemplate.GetGroupName">
        <xsl:with-param name="GroupName" select="@*[name()=$Group]"/>
        <xsl:with-param name="GroupType" select="$GroupType"/>
      </xsl:call-template>
    </div>
  </xsl:template>

10. Go back to the SharePoint site. Open the site in SharePoint Designer. Navigate to Site Pages. Upload an empty .txt file. This file will be used to store JavaScript and CSS. A content editor web part will reference this file. It actually can be uploaded to any location.

11. Paste the following code to the .txt file:

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
 
<script type="text/javascript">
/*--------------------------Expand All, Collapse All and Category Header Click--------------------------*/
function accordionLoad() {
 
    $(".accordion-header").removeClass("expanded");
    $(".accordion-content").hide();
 
    $(".accordion-header").bind("click", function(){
        $(this).toggleClass("expanded");
        $(this).siblings(".accordion").find(".accordion-content").slideToggle();
        $(this).siblings(".accordion").find(".accordion-content").css("border-bottom","1px solid #ccc");
    })
 
    $(".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");
    })
}
/*--------------------------End Expand All, Collapse All and Category Header Click--------------------------*/
 
/*--------------------------Pop up IPPreserver Category Edit Form--------------------------*/
function portal_openEditCatModalDialog(id){
    var options = {
        url:"/ippreserver/Lists/Category/EditForm.aspx?ID="+id+"&IsDlg=1",
        dialogReturnValueCallback: CatDialogCallback
        };
 
    SP.UI.ModalDialog.showModalDialog(options);
}
 
function CatDialogCallback(dialogResult, returnValue){
                if (dialogResult != 0)
                {
                                document.location.reload(true);
                }
}
/*--------------------------End Pop up IPPreserver Category Edit Form--------------------------*/
 
/*--------------------------Delete the selected category--------------------------*/
var oListItem;
var categoryName;
var clientContext;
function deleteListItem(id) {
 
    this.itemId = id;
                var siteUrl = '/ippreserver';
 
    clientContext = new SP.ClientContext(siteUrl);
    var oList = clientContext.get_web().get_lists().getByTitle('Category');
                oListItem = oList.getItemById(id);
    
    clientContext.load(oListItem, 'Title');
    clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
}
 
function onQuerySucceeded() {                 
                categoryName = this.oListItem.get_item('Title');
    oListItem.deleteObject();
    clientContext.executeQueryAsync(Function.createDelegate(this, this.onSecondQuerySucceeded), Function.createDelegate(this, this.onSecondQueryFailed));    
}
 
function onQueryFailed(sender, args) {
 
    alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
 
function onSecondQuerySucceeded() {                                  
    alert('Category deleted: ' + categoryName);
    location.reload();
}
 
function onSecondQueryFailed(sender, args) {
 
    alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
 
 
function deleteCategory(id)
{
                var x;
                var r=confirm("Are you sure you want to delete this category?");
                if (r==true)
                {
                                deleteListItem(id);                           
                }              
}
/*--------------------------End Delete the selected category--------------------------*/
 
/*--------------------------Pop up IPPreserver Procedure Edit Form--------------------------*/
function portal_openEditProModalDialog(id){
    var options = {
        url:"/ippreserver/Lists/Procedure/EditForm.aspx?ID="+id+"&IsDlg=1",
        dialogReturnValueCallback: ProDialogCallback
        };
 
    SP.UI.ModalDialog.showModalDialog(options);
}
 
function ProDialogCallback(dialogResult, returnValue){
                if (dialogResult != 0)
                {
                                document.location.reload(true);
                }
}
/*--------------------------End Pop up IPPreserver Category Edit Form--------------------------*/
 
/*--------------------------Delete the selected procedure--------------------------*/
var pListItem;
var procName;
var pContext;
function deleteProcItem(id) {
 
    this.itemId = id;
                var siteUrl = '/ippreserver';
 
    pContext = new SP.ClientContext(siteUrl);
    var oList = pContext.get_web().get_lists().getByTitle('Procedure');
                pListItem = oList.getItemById(id);
    
    pContext.load(pListItem, 'Title');
    pContext.executeQueryAsync(Function.createDelegate(this, this.onPQuerySucceeded), Function.createDelegate(this, this.onPQueryFailed));
}
 
function onPQuerySucceeded() {                              
                procName = this.pListItem.get_item('Title');
    pListItem.deleteObject();
    pContext.executeQueryAsync(Function.createDelegate(this, this.onSecondPQuerySucceeded), Function.createDelegate(this, this.onSecondPQueryFailed));    
}
 
function onPQueryFailed(sender, args) {
 
    alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
 
function onSecondPQuerySucceeded() {                                                
    alert('Procedure deleted: ' + procName);
    location.reload();
}
 
function onSecondPQueryFailed(sender, args) {
 
    alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
 
 
function deleteProc(id)
{
                var x;
                var r=confirm("Are you sure you want to delete this procedure?");
                if (r==true)
                {
                                deleteProcItem(id);                         
                }              
}
/*--------------------------End Delete the selected category--------------------------*/
 
$(document).ready(function(){
    accordionLoad();
       
});
 
</script>
 
/*--------------------------Style Sheet--------------------------*/
<style>
 
ul.accordion {
                list-style:none;
                margin:0px;
                padding:0px;
}
.accordion-item {
/*           border-top:1px solid #ccc;*/
}
.accordion-header {
                font-size:1.3em;
                cursor:pointer;
                padding:10px;
                border-bottom:1px solid #ccc;
}
.accordion-header:hover {
                background:#efefef;
}
.accordion-header.expanded {
                background:#dfdfdf;
}
.accordion-content {
                padding:5px 20px;
}
.expand-all,
.collapse-all {
                display:inline-block;
                cursor:pointer;
                padding:5px 10px;
zoom:1; *display: inline; _height: 30px;
 
}
.expand-all:hover,
.collapse-all:hover {
background:#efefef;
}
 
.add-cat,
.add-proc {
                display:inline-block;
                cursor:pointer;
                padding:5px 10px;
zoom:1; *display: inline; _height: 30px;
 
}
.add-cat:hover,
.add-proc:hover {
background:#efefef;
}
 
.editIcon
{
                vertical-align:middle
}
 
.deleteIcon
{
                vertical-align:middle;
                width:20px;
}
</style>
/*--------------------------End Style Sheet--------------------------*/

12. Upload the edit and delete icon to Style Library-> Images folder in SharePoint Designer

13. Go to the SharePoint page. Add a CEWP and enter the .txt file’s link to the Content Link field.

14. Add the uploaded CQWP from Step 6.

15. Edit the CQWP’s properties.

Browse to the Procedure list.

1

2

3

4

16. Overall structure:

  • Category and Procedure lists
  • OOTB CQWP, CEWP
  • AccordionStyle.xsl
  • AccordionCQWP.xsl
  • Header.xsl
  • text file to store CSS and JavaScript

References:

Demo: http://www.isb.bj.edu.cn/admissions/Pages/Frequently-Asked-Questions.aspx

Source file: https://bentedder.qx.ly/2yPB

Steps to create your own: http://www.bentedder.com/create-a-jquery-accordion-with-a-sharepoint-cqwp/

HTML Encoder/Decoder:

http://www.web2generators.com/html/entities

http://www.textfixer.com/html/html-character-encoding.php

 

SP 2010 – Content Query Web Part filter items by both date and time

Environment:

SharePoint Server 2010

Problem:

We have a list which stores company news added by internal staff. We use the content query web part on the home page to display the last added news item. Sometimes we have news that needs to be published at a certain time of a day, e.g. the CEO just announced something at 10 am and we would want that news to be displayed right after on the home page. Ideally, the announcement would be pre-entered to the list and set a time to be displayed.

The content query web part only has filters that can be used to filter a date, for example, it can filter the publish date to be less than today. For my scenario, I need it to filter the current time as well.

Solution:

After some searches on the net, I found two approaches. One is to use a workflow to move list items from list A to list B on a certain time. List A has all the pre-entered items and List B has the items to be displayed on home page. The other approach is to modify the .webpart file and override the existing filters.

I took the latter approach since it’s simpler for me to do.

    1. Open the page which has the content query web part, check it out and edit it.
    2. Export the content query web part to local desktop.
    3. Make a copy of it and rename it to something new, e.g. CQWP_Updated.webpart
    4. Open the CQWP_Updated.webpart in a text editor, e.g. Notepad
    5. Search for QueryOverride, you should be able to find <property name=”QueryOverride” type=”string”/>, remove that line.
    6. Move to the bottom of the file.
    7. Add the  following code  before the closing </properties>
      <property name="QueryOverride" type="string">
        <![CDATA[
                      <Where>
                       <Leq>
                         <FieldRef Name="Start_x0020_Date" Nullable="True" Type="DateTime"/>
                         <Value IncludeTimeValue='TRUE' Type="DateTime"><Today /></Value>
                       </Leq>
                     </Where>
                   <OrderBy>
                      <FieldRef Name="Start_x0020_Date" Nullable="True" Type="DateTime"
                      Ascending="FALSE"/>
                   </OrderBy>]]>
       </property>

      Note: The code basically specifies that filter the list by Start Date, display the items that has the Start Date less or equal to now and order the list in decending order of Start Date.

    8. Save the file and go back to the browser.
    9. Remove the original Content Query web part and add the modified one to the page. When you open the “Edit Web Part” panel, you’ll see a message: “Some properties in this Web Part are not available because they are configured to have fixed values.” Also you’ll notice the Additional Filters, Grouping and Sorting sections are grayed out. That means you’ll need to modify the .webpart file to change filtering, grouping and sorting.

References:

Customize the SharePoint Content By Query Web Part by Using Custom Properties: http://msdn.microsoft.com/en-us/library/aa981241.aspx

Filter a CQWP on time portion of datetime field

http://go4answers.webhost4life.com/Example/cant-customized-content-query-web-part-22681.aspx

Create contact form using ECMAScript/JavaScript Client Object Model

My client has asked to create a request form based on a list which records all the requests in SharePoint 2010.

Problems/Requirements:

  • Only one column in the request list which is the default column created when the list is created. It has a column type of “Single line of Text”. This column can’t be deleted or changed to a different column type.
  • Display that column as a muli-line text box rather than a single-line input box and have the message wrapped.
  • Once the request is sent successfully from the form, display a message to the user “Request Sent!”

Final delivery:

Request

Features:

  • The user can only enter 255 characters in the box as specified in the Request column in the list. Anything over 255 will be truncated straight away. The reason it’s set to 255 is because in the list, the column has “Maximum number of characters” set to 255.
  • Once a request is submitted successfully, a message will pop up to indicate the user.
  • If something accidently clicked the “Send” button without typing a message, an error message will pop up and the empty request won’t be added to the list.
  • All done through a content editor web part using ECMAScript, javascript and html.
  • This web part can be reused by changing the field names in the script to match with the list. However, I haven’t tried with lists containing fields other than single/multiple line of text.

Code:

<script type="text/ecmascript">  
function SendEnquiry() {  
  var el = document.getElementById("Message");
  if (el.value == '')  {   
    alert('Please enter a message.');   
    return false; 
  }  
  else  {   
    var MessageField = document.getElementById("Message");     
    SaveInContactUs(MessageField.value);  
  } 
}   
function SaveInContactUs(Message) {  
  var context = new SP.ClientContext.get_current();   
  var web = context.get_web();   
  var list = web.get_lists().getByTitle('ImageRequest');   
  var listItemCreationInfo = new SP.ListItemCreationInformation();   
  var newItem = list.addItem(listItemCreationInfo);     
  newItem.set_item('Title', Message);    
  newItem.update();    
  context.executeQueryAsync(Function.createDelegate(this, this.success), Function.createDelegate(this, this.failed)); 
}   
function success() {  alert('Your message is submitted!'); }   

function failed(sender, args) {  alert('failed. Message:' + args.get_message()); }
function textLimit(field, maxlen) { if (field.value.length > maxlen)  field.value = field.value.substring(0, maxlen);  } 

</script>
<div style="width:300px">  
  <p><strong>Image Request Form</strong></p>  
  <p>I would like to have an image available in the gallery depicting.</p>  
  <div>   
    <textarea id="Message" name="Message" onkeyup="textLimit(this, 255);" style="font-size:11pt; width:300px; height:150px" ></textarea>     
    <br/>   
    <div style="text-align:center;">    
      <input type="submit" onclick="Javascript:SendEnquiry();" value="Send" style="width: 100px; height: 30px; font-weight: bold;">   
    </div>  
  </div> 
</div>

Reference:

This article really got me started. Thanks so much!

http://www.learningsharepoint.com/2011/10/03/create-contact-form-using-ecmascriptjavascript-client-object-model-sharepoint-2010/

Say if I have a contact form instead and I would like to get the user to receive a copy of their submitted message in their email, I can probably include the code in this article. (In theory)

http://stackoverflow.com/questions/7097363/send-email-with-javascript

If the form included a dropdown list, the code in this article may be useful.

http://haseebakhtar.wordpress.com/2012/07/10/how-to-retrieve-all-choice-field-values-using-ecmascript-sharepoint-2010/

Form based validation in js

http://stackoverflow.com/questions/6810985/validate-form-required-fields-based-on-class

Further improvement:

The validation on the form is pretty lame. If someone just entered spaces in the text box, it would still submit the form successfully. So how can I have a better validation for a required field? And what if I have a email or number field, what validation would I need? You are welcome to post solutions/thoughts in the comment area.

XSLT displays a multi-select column

Thanks to Marc again for this helpful article.

Here is the xsl code:

<xsl:template name=”MultiSelectDisplay”>
<xsl:param name=”MultiSelectValue”/>
<xsl:param name=”MultiSelectSeparator”/>
<xsl:choose>
<xsl:when test=”contains($MultiSelectValue, ‘;’)”>
<xsl:value-of select=”concat(substring-before($MultiSelectValue, ‘;’), $MultiSelectSeparator)” disable-output-escaping=”yes”/>
<xsl:call-template name=”MultiSelectDisplay”>
<xsl:with-param name=”MultiSelectValue” select=”substring-after($MultiSelectValue, ‘;’)”/>
<xsl:with-param name=”MultiSelectSeparator” select=”$MultiSelectSeparator”/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select=”$MultiSelectValue” disable-output-escaping=”yes”/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Other useful SharePoint xsl templates written by Marc & Co can be found here.

How to make SharePoint 2010 Content Query Web Part (CQWP) AJAX Enabled

This post has more details.

Basically, just export an CQWP, open the file in notepad, search for the following properties to change.

<property name=”InitialAsyncDataFetch” type=”bool”>False</property>
<property name=”AsyncRefresh” type=”bool”>False</property>
<property name=”ManualRefresh” type=”bool”>False</property>
<property name=”AutoRefresh” type=”bool”>False</property>
<property name=”AutoRefreshInterval” type=”int”>60</property>

The above properties map as below:

InitialAsyncDataFetch: Enable Asynchronous Load

AsyncRefresh: Enable Asynchronous Update

ManualRefresh: Show Manual Refresh Button

AutoRefresh: Enable Asynchronous Automatic Refresh

AutoRefreshInterveral: Automatic Refresh Interval (seconds)

Enable AJAX option for Content Query Web Part (CQWP) in SharePoint 2010

Enable AJAX option for Content Query Web Part (CQWP) in SharePoint 2010

Export your CQWP and open it in a text editor

Look for the following lines:

<property name=”InitialAsyncDataFetch” type=”bool”>False</property>
<property name=”AsyncRefresh” type=”bool”>False</property>
<property name=”ManualRefresh” type=”bool”>False</property>
<property name=”AutoRefresh” type=”bool”>False</property>
<property name=”AutoRefreshInterval” type=”int”>60</property>

The above properties map as below:

InitialAsyncDataFetch: Enable Asynchronous Load
AsyncRefresh: Enable Asynchronous Update
ManualRefresh: Show Manual Refresh Button
AutoRefresh: Enable Asynchronous Automatic Refresh
AutoRefreshInterveral: Automatic Refresh Interval (seconds)

After setting the property “InitialAsyncDataFetch” to true, the web part loaded asynchronously