07 April 2010

Create a Featured News Section with jQuery

I was surfing the web the other day and took special notice of a news ticker on a popular online site. This set me to thinking, could I accomplish something similar with SharePoint news? Here is how I was able to accomplish a featured news section for a SharePoint site. Here’s a screenshot of the final result:

image

The site visitor is presented with the last five featured news items and can quickly switch between the stories by clicking on the tab located at the top of the page. This is a standard content editor web part and can be exported for use on other sites within the same web application.

In order to complete the steps in this blog, you will need to obtain the following scripts:

First, I created a resources document library on the top level of my SharePoint site and uploaded all of the scripts. I placed this in the top level site of the SharePoint web application so I could reference this library anywhere in SharePoint. I also created a “scratch pad page that I could edit in SharePoint designer”.

image

I created a new content type based off the Article Page content type and included a few more items that are needed for my implementation of SharePoint such as Corporate Location and Industry. I also added a Yes/No site column called Featured Item and a Publishing Image column I called Feature Image. These two columns will be used by the “Featured News”.

image

Finally, I created a new layout page and attached it to the new content type. Now, when authoring news my editors have the ability to choose if they want to feature a news item and if that news item should include a feature image.

With the content type configured and a few news articles published I opened SharePoint Designer with my scratch page open and ready for coding. The script reference needed to be added to the page.

  1: <script type="text/javascript" src="/Resources/jquery-1.4.2.min.js"></script>
  2: <script type="text/javascript" src="/Resources/jquery.SPServices-0.5.3.min.js"></script>
  3: <script type="text/javascript" src="/Resources/jquery.tools.min.js"></script>

I also placed the content holders into the page.

  1: <div id="FeaturedNews">
  2:    <!-- UL to hold all of the news items as tabs -->
  3:    <ul id="NewsTabs">
  4:    </ul>
  5:    <!-- div to hold all of the returned news snippets -->
  6:    <div id="NewsPages">
  7:    </div>
  8: </div>

To make it easier to work with my returned date, I created a javascript object that allows me to quickly reference the details of any single item. It also contains functions to aid in the HTML output that will be needed by the jQuery Tools library for rendering the tabs and news story snippets.

  1: function FeaturedNewsStory()
  2: {
  3:   this.Title = '';
  4:   this.CompanyLocation = '';
  5:   this.Comments = '';
  6:   this.RollupImage = '';
  7:   this.FeatureImage = '';
  8:   this.ByLine = '';
  9:   this.PublishDate = '';
 10:   this.ServerURL = '';
 11:   
 12:   this.FeatureImageURL = function () {
 13:     if (this.FeatureImage != null) {
 14:       var startPos = this.FeatureImage.indexOf("src=");
 15:       if (startPos < 0) {
 16:         return 'N/A';
 17:       }
 18:       startPos += 5;
 19:       var endPos = this.FeatureImage.indexOf("\"",startPos);
 20:       var ImageURL = this.FeatureImage.substring(startPos,endPos);
 21:       return ImageURL;
 22:     }
 23:     return 'N/A';
 24:   };
 25:   
 26:   this.RollupImageURL = function () {
 27:     if (this.RollupImage != null) {
 28:       var startPos = this.RollupImage.indexOf("src=");
 29:       if (startPos < 0) {
 30:         return 'N/A';
 31:       }
 32:       startPos += 5;
 33:       var endPos = this.RollupImage.indexOf("\"",startPos);
 34:       var ImageURL = this.RollupImage.substring(startPos,endPos);
 35:       return ImageURL;
 36:     }
 37:     return 'N/A';
 38:   };
 39:   
 40:   this.GetStory = function() {
 41:     var html = '';
 42:     html += "<div class='Page'>";
 43:     if (this.FeatureImageURL() != 'N/A') {
 44:       html += "<img src='" + this.FeatureImageURL() + "'/>";
 45:     }
 46:     html += "<h1>" + this.Title + "</h1>";
 47:     html += "<div class='Author'>" + this.ByLine + "</div>";
 48:     html += "<div>" + this.Comments + "</div>";
 49:     html += "<div class='ReadMore'><a href='" + this.ServerURL + "'>Read More...</a></div>";
 50:     html += "<cite>" + this.CompanyLocation + "</cite>";
 51:     html += "</div>";
 52:     return html;
 53:   };
 54:   
 55:   this.GetTab = function () {
 56:     var html = '';
 57:     html += "<li><a href='#'>";
 58:     if (this.RollupImageURL() != 'N/A') {
 59:          html += "<img src='" + this.RollupImageURL() + "' alt='" + this.Title + "'/>";
 60:     }
 61:     
 62:     var month = "";
 63:     var month = this.PublishDate.substring(5,7);
 64:     var day = "";
 65:     var day = this.PublishDate.substring(8,10);
 66:     var publishText = "";
 67:     switch(month)
 68:     {
 69:       case '01':
 70:         publishText = "JAN " + day;
 71:         break;
 72:       case '02':
 73:         publishText = "FEB " + day;
 74:         break;
 75:       case '03':
 76:         publishText = "MAR " + day;
 77:         break;
 78:       case '04':
 79:         publishText = "APR " + day;
 80:         break;
 81:       case '05':
 82:         publishText = "MAY " + day;
 83:         break;
 84:       case '06':
 85:         publishText = "JUN " + day;
 86:         break;
 87:       case '07':
 88:         publishText = "JUL " + day;
 89:         break;
 90:       case '08':
 91:         publishText = "AUG " + day;
 92:         break;
 93:       case '09':
 94:         publishText = "SEP " + day;
 95:         break;
 96:       case '10':
 97:         publishText = "OCT " + day;
 98:         break;
 99:       case '11':
100:         publishText = "NOV " + day;
101:         break;
102:       default:
103:         publishText = "DEC " + day;
104:         break;
105:     }
106:     html += "<div class='PubDate'>" + publishText + "</div>";
107:     html += "</a></li>";
108:     return html;
109:   };
110: }
111: 

With an object in place to make it easier to work with returned items, I make use of the SPServices library to call the SharePoint lists web service and execute the GetListItems method. This method returns a series of items from a list based on a CAML query. In order to limit the selection, the CAML query I need to execute is:

  1: <Query>
  2:    <Where>
  3:      <And>
  4:        <Eq>
  5:          <FieldRef Name='ContentType' />
  6:          <Value Type='Choice'>Company Press Release</Value>
  7:        </Eq>
  8:        <Eq>
  9:          <FieldRef Name='Featured_x0020_Item' />
 10:          <Value Type='Boolean'>1</Value>
 11:        </Eq>
 12:      </And>
 13:    </Where>
 14:    <OrderBy>
 15:      <FieldRef Name='ArticleStartDate' Ascending='False' />
 16:    </OrderBy>
 17: </Query>

This instructs SharePoint to only return items that are created based on my content type and have been set to be featured. It also instructs SharePoint to get the items based on the Article Date in descending order, placing the newest items at the top of the results.

Using SPServices, the call is executed and then parsed using jQuery into an array of FeaturedNewsStory objects.

  1: function loadNewsArticles() {
  2:   //Create an array to hold all of the returned results.
  3:   var itemList = new Array();
  4: 
  5:   // Set up a temporary object to hold default values for the list.
  6:   // The CAMLQuery is defined in this object.
  7:   var newsDefaults = {
  8:     webURL: "/News",
  9:     listName: "Pages",
 10:     CAMLViewFields: "<ViewFields/>",
 11:     CAMLQuery: "<Query><Where><And><Eq><FieldRef Name='ContentType' /><Value Type='Choice'>Company Press Release</Value></Eq><Eq><FieldRef Name='Featured_x0020_Item' /><Value Type='Boolean'>1</Value></Eq></And></Where><OrderBy><FieldRef Name='ArticleStartDate' Ascending='False' /></OrderBy></Query>",
 12:     CAMLRowLimit: 5,
 13:     CAMLQueryOptions: "<QueryOptions/>"
 14:   };
 15:  
 16:   // Make use of the SPServices library to call the SharePoint lists web service
 17:   $().SPServices({
 18:     // Set the operation
 19:     operation: "GetListItems",
 20:     // Required so something is returned.
 21:     async: false,
 22:     // Set the default WEBUrl where the news pages library is located
 23:     webURL: newsDefaults.webURL,
 24:     // Set the name of the list/library
 25:     listName: newsDefaults.listName,
 26:     // Set the View Fields
 27:     CAMLViewFields: newsDefaults.CAMLViewFields,
 28:     // Set the CAML Query
 29:     CAMLQuery: newsDefaults.CAMLQuery,
 30:     // Set the total number of items to return.
 31:     CAMLRowLimit: newsDefaults.CAMLRowLimit,
 32:     // Set the query options
 33:     CAMLQueryOptions: newsDefaults.CAMLQueryOptions,
 34:     // Set a callback function to handle the returned data.
 35:     completefunc: function(xData, status) {
 36:       // Iterate through all returned rows and create a new FeaturedNewsStory
 37:       $(xData.responseXML).find("z\\:row").each(function () {
 38:         var item = new FeaturedNewsStory();
 39:         item.Title = $(this).attr("ows_Title");
 40:         item.CompanyLocation = $(this).attr("ows_Corporate_x0020_Location");
 41:         item.Comments = $(this).attr("ows_Comments");
 42:         item.RollupImage = $(this).attr("ows_PublishingRollupImage");
 43:         item.FeatureImage = $(this).attr("ows_Featured_x0020_Image");
 44:         item.PublishDate = $(this).attr("ows_ArticleStartDate");
 45:         item.ByLine = $(this).attr("ows_ArticleByLine");
 46:         item.ServerURL = $(this).attr("ows_ServerUrl");
 47:         // Extend the array and place the new item into it.
 48:         itemList.push(item);
 49:       });
 50:     }  
 51:   });
 52:   // Return the array of items.
 53:   return itemList;
 54: }
 55: 

Finally, call the script once the page is ready and render the contents.

  1: // Use the jQuery way to queue a function for execution when
  2: // the page is ready. 
  3: $(document).ready(function () { initialize(); });
  4: 
  5: // Function that makes a call to the web service and then
  6: // renders the featured news content within the specified 
  7: // placeholders.
  8: function initialize() {
  9:   var newsPages = $("#NewsPages");
 10:   var newsTabs = $("#NewsTabs");
 11:   var Items = new Array();
 12: 
 13:   // Get all of the featured news items.
 14:   Items = loadNewsArticles();
 15:   for(i=0; i < Items.length; i++)
 16:   {
 17:     var item = Items[i];
 18:     // Make use of the FeaturedNewsStory object to get the appropriate markup for a tab.
 19:     $(newsTabs).append(item.GetTab());
 20:     // Make use of the FeaturedNewsStory object to get the appropriate markup for a news snippet.
 21:     $(newsPages).append(item.GetStory());
 22:   }
 23:   
 24:   // Use jQuery Tools to create tabs.
 25:   $(newsTabs).tabs("#NewsPages > div");
 26: }

While this may already be working, CSS is needed to improve the presentation. Here’s the full solution, including CSS, that is placed inside a standard content editor web part:

<script type="text/javascript" src="/Resources/jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="/Resources/jquery.SPServices-0.5.3.min.js"></script>
<script type="text/javascript" src="/Resources/jquery.tools.min.js"></script>
<script type="text/javascript">
var newsItems = new Array();
$(document).ready(function () { initialize(); });
function initialize() {
  var newsPages = $("#NewsPages");
  var newsTabs = $("#NewsTabs");
  var Items = new Array();
  Items = loadNewsArticles();
  for(i=0; i < Items.length; i++)
  {
    var item = Items[i];
    $(newsTabs).append(item.GetTab());
    $(newsPages).append(item.GetStory());
  }
  
  $(newsTabs).tabs("#NewsPages > div");
}
function FeaturedNewsStory()
{
  this.Title = '';
  this.CompanyLocation = '';
  this.Comments = '';
  this.RollupImage = '';
  this.FeatureImage = '';
  this.ByLine = '';
  this.PublishDate = '';
  this.ServerURL = '';
  
  this.FeatureImageURL = function () {
    if (this.FeatureImage != null) {
      var startPos = this.FeatureImage.indexOf("src=");
      if (startPos < 0) {
        return 'N/A';
      }
      startPos += 5;
      var endPos = this.FeatureImage.indexOf("\"",startPos);
      var ImageURL = this.FeatureImage.substring(startPos,endPos);
      return ImageURL;
    }
    return 'N/A';
  };
  
  this.RollupImageURL = function () {
    if (this.RollupImage != null) {
      var startPos = this.RollupImage.indexOf("src=");
      if (startPos < 0) {
        return 'N/A';
      }
      startPos += 5;
      var endPos = this.RollupImage.indexOf("\"",startPos);
      var ImageURL = this.RollupImage.substring(startPos,endPos);
      return ImageURL;
    }
    return 'N/A';
  };
  
  this.GetStory = function() {
    var html = '';
    html += "<div class='Page'>";
    if (this.FeatureImageURL() != 'N/A') {
      html += "<img src='" + this.FeatureImageURL() + "'/>";
    }
    html += "<h1>" + this.Title + "</h1>";
    html += "<div class='Author'>" + this.ByLine + "</div>";
    html += "<div>" + this.Comments + "</div>";
    html += "<div class='ReadMore'><a href='" + this.ServerURL + "'>Read More...</a></div>";
    html += "<cite>" + this.CompanyLocation + "</cite>";
    html += "</div>";
    return html;
  };
  
  this.GetTab = function () {
    var html = '';
    html += "<li><a href='#'>";
    if (this.RollupImageURL() != 'N/A') {
         html += "<img src='" + this.RollupImageURL() + "' alt='" + this.Title + "'/>";
    }
    
    var month = "";
    var month = this.PublishDate.substring(5,7);
    var day = "";
    var day = this.PublishDate.substring(8,10);
    var publishText = "";
    switch(month)
    {
      case '01':
        publishText = "JAN " + day;
        break;
      case '02':
        publishText = "FEB " + day;
        break;
      case '03':
        publishText = "MAR " + day;
        break;
      case '04':
        publishText = "APR " + day;
        break;
      case '05':
        publishText = "MAY " + day;
        break;
      case '06':
        publishText = "JUN " + day;
        break;
      case '07':
        publishText = "JUL " + day;
        break;
      case '08':
        publishText = "AUG " + day;
        break;
      case '09':
        publishText = "SEP " + day;
        break;
      case '10':
        publishText = "OCT " + day;
        break;
      case '11':
        publishText = "NOV " + day;
        break;
      default:
        publishText = "DEC " + day;
        break;
    }
    html += "<div class='PubDate'>" + publishText + "</div>";
    html += "</a></li>";
    return html;
  };
}
function loadNewsArticles() {
  var itemList = new Array();
  var newsDefaults = {
    webURL: "/News",
    listName: "Pages",
    CAMLViewFields: "<ViewFields/>",//"<ViewFields><FieldRef ID='Featured_x0020_Image'/><FieldRef ID='Title'/><FieldRef ID='Comments'/><FieldRef ID='PublishingRollupImage'/><FieldRef ID='Corporate_x0020_Location'/></ViewFields>",
    CAMLQuery: "<Query><Where><And><Eq><FieldRef Name='ContentType' /><Value Type='Choice'>Company Press Release</Value></Eq><Eq><FieldRef Name='Featured_x0020_Item' /><Value Type='Boolean'>1</Value></Eq></And></Where><OrderBy><FieldRef Name='ArticleStartDate' Ascending='False' /></OrderBy></Query>",
    CAMLRowLimit: 5,
    CAMLQueryOptions: "<QueryOptions/>"
  };
  $().SPServices({
    operation: "GetListItems",
    async: false,
    webURL: newsDefaults.webURL,
    listName: newsDefaults.listName,
    CAMLViewFields: newsDefaults.CAMLViewFields,
    CAMLQuery: newsDefaults.CAMLQuery,
    CAMLRowLimit: newsDefaults.CAMLRowLimit,
    CAMLQueryOptions: newsDefaults.CAMLQueryOptions,
    completefunc: function(xData, status) {
      $(xData.responseXML).find("z\\:row").each(function () {
        var item = new FeaturedNewsStory();
        item.Title = $(this).attr("ows_Title");
        item.CompanyLocation = $(this).attr("ows_Corporate_x0020_Location");
        item.Comments = $(this).attr("ows_Comments");
        item.RollupImage = $(this).attr("ows_PublishingRollupImage");
        item.FeatureImage = $(this).attr("ows_Featured_x0020_Image");
        item.PublishDate = $(this).attr("ows_ArticleStartDate");
        item.ByLine = $(this).attr("ows_ArticleByLine");
        item.ServerURL = $(this).attr("ows_ServerUrl");
        itemList.push(item);
      });
    }  
  });
  return itemList;
}
</script>
<style type="text/css">
#NewsTabs {
  margin: 0 !important;
  padding: 0px;
  height: 68px;
  border-bottom: 1px black solid;
}
#NewsTabs li {
  float: left;
  padding: 0;
  margin: 0;
  list-style: none;
}
#NewsTabs a {
  float:left;
  font-size:13px;
  display:block;
  padding:5px 30px;  
  text-decoration:none;
  border:1px solid #666;  
  border-bottom:0px;
  height:66px;
  background-color:#efefef;
  color:#777;
  margin-right:2px;
  -moz-border-radius-topleft: 4px;
  -moz-border-radius-topright:4px;
  position:relative;
  top:1px;  
}
#NewsTabs li img
{
  clear: both;
  text-align: center;
  width: 32px;
  margin-top: 8px;
  display: block;
  border: none;
}
#NewsTabs a:hover {
  background-color:#F7F7F7;
  color:#333;
}
  
/* selected tab */
#NewsTabs a.current {
  background-color:#ddd;
  border-bottom:1px solid #ddd;  
  color:#000;  
  cursor:default;
}
.Page
{
  display:none;
  border:1px solid #666;
  border-width:0 1px 1px 1px;
  min-height:150px;
  padding:15px 20px;
  background-color:#ddd;  
  overflow: auto;  
  height: 275px;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 12px;
}
.PubDate
{
  font-size: 8px;
  color: white;
  background: #444;
  margin-top: 4px;
  padding-left: 8px; 
  padding-right: 8px;
  padding-top: 2px;
  padding-bottom: 2px;
}
#FeaturedNews{
    width: 550px;
}
#NewsPages img
{
  float: right;
  width: 200px;
  margin: 0 3 0 15;
  border: 3px solid #666;
}
#NewsPages cite
{
  font-size: 12px;
  font-variant: normal;
  font-style: normal;
  padding-top: 0px;
  margin-top: 22px;
  display: inline-block;
}
#NewsPages h1
{
 font-size: 14px;
 padding-top: 0px;
 margin-top: 0px;
 padding-top: 8px;
 display: inline-block;
 border-top: 1px solid #666;
 border-bottom: 1px solid #666;
 padding-bottom: 8px;
 margin-bottom: 16px;
}
div.clear
{
  clear: both;
}
</style>
<div id="FeaturedNews">
<ul id="NewsTabs">
</ul>
<div id="NewsPages">
</div>
</div>

1 comment:

Anonymous said...

Chris:

Great stuff, and I'm glad you were able to use SPServices to good effect!

You'll have some problems with non-IE browsers due to this line:
$(xData.responseXML).find("z\\:row")
You should do this instead:
$(xData.responseXML).find("[nodeName=z:row]")

See my post on this.

M.