Monday, October 15, 2012

JavaScript to add a global link next to Welcome menu in SharePoint 2010

Similarly to my post on adding a link next to the Site Actions menu in the Left Ribbon, you can add a link on the right ribbon on SharePoint 2010 pages, next to the Welcome menu.

ExecuteOrDelayUntilScriptLoaded(ExecuteDefaultLoad, "sp.js");

var _rightRibbonContainer = null;

function ExecuteDefaultLoad()
{

 var isWikiEdit = false;
 if ( document.forms[MSOWebPartPageFormName]._wikiPageMode != null )
 {
  if( document.forms[MSOWebPartPageFormName]._wikiPageMode.value == "Edit" )
    {
     isWikiEdit = true;
    }
  }

 var inDesignMode = document.forms[MSOWebPartPageFormName].MSOLayout_InDesignMode.value;
 if (inDesignMode == "1" || isWikiEdit) 
 {  // page is in edit mode 
 } 
 else 
 {  
  LoadRightRibbon(); 
 } 
}

function LoadRightRibbon()
{
 
 var ribbonContainerRowRight = document.getElementById("RibbonContainer-TabRowRight");

 if( ribbonContainerRowRight != null )
 {
  if( ribbonContainerRowRight.children != null && ribbonContainerRowRight.children[2] != null )
  {
   if( ribbonContainerRowRight.children[2].children != null )
   {
    _rightRibbonContainer = document.getElementById("RibbonContainer-TabRowRight").children[2].children[0];

    if( _rightRibbonContainer != null )
    {     
     AddHelloLink();
    }
   }
  }
 }
}


function AddHelloLink()
{
 if( _rightRibbonContainer != null )
 {

  var newSpan = document.createElement("span");
  newSpan.innerHTML = '<a class="ms-menu-a" style="cursor:pointer;white-space:nowrap;"    href="javascript:;" title="Hello!" onclick="window.location=\'/sites/test123\';return false;"><span><font color=\'#8ce352\'><b><i>Hello World!</i></b></font></span></a>';
  newSpan.className = 'ms-SPLink ms-SpLinkButtonInActive ms-welcomeMenu';
  
  newSpan.onmouseover= function() {  this.className = "ms-SPLink ms-SpLinkButtonActive ms-welcomeMenu"}; 
  newSpan.onmouseout= function() {  this.className = "ms-SPLink ms-SpLinkButtonInActive ms-welcomeMenu"}; 
  

  _rightRibbonContainer.insertBefore(newSpan, _rightRibbonContainer.children[0]);
 }
}

How to create a SharePoint Delegate Control that injects JavaScript to all SharePoint pages

Recently, I needed a way to insert some JavaScript files into all pages in all of the SharePoint site collections and sites under an entire web application.

The thought of changing all the master pages in every site collection seemed not only tedious, but unmanageable. Every time a new site collection is created, we would have to remember to modify the master pages. Also, if a site collection owner decided to use their own master pages, we would have no way of maintaining those either.

So, after banging my head trying to come up with a better solution, I stumbled upon the delegate control!

A detailed description of how it works and what it does can be found here:


The beauty of the delegate control is that you can basically overwrite anything in the master page.

I wanted to add a simple JavaScript file to all of the pages in the master page.

So I started by following the example below. I modified this code in step 12 from the hardcoded script to instead read from a JavaScript file that is located in my layouts folder.

  protected override void CreateChildControls()
        {
            base.CreateChildControls();

            string srcScript = "/_layouts/Company/CompanyScript.js";
            this.Controls.Add(new ScriptLink() { Name = srcScript, Language = "javascript", Localizable = false });
        }


The step by step process can also be found here as well: http://msdn.microsoft.com/en-us/library/ms470880.aspx

Step by step process to creating a delegate control:
1.      Start SharePoint development tools in Microsoft Visual Studio 2010.
2.      On the File menu, point to New, and then click Project.
3.      In Project Types, under Visual Basic or C#, select Empty SharePoint Project.
4.      Type EcmaScriptDelegate as the project name. Click OK.
5.      In the SharePoint Customization Wizard, choose Deploy as a farm solution. Click Finish.
6.      In the Solution Explorer, right-click the EcmaScriptDelegate project. Select Add and then New Item.
7.      In the Add New Item dialog box, click the Code group and choose the Class template. Type EcmaScriptDelegateControl as the Name and then click Add.
8.      Next, you must add a reference to System.Web. In the Solution Explorer, right-click the References folder and select Add Reference. In the Add Reference dialog, click the .NET tab and find System.Web in the list. Click OK.
9.      In the EcmaScriptDelegateControl file that is displayed, add the following using statement.
using System.Web.UI.WebControls;
10.  Change the base class of EcmaScriptDelegateControl to WebControl by modifying the following line.
class EcmaScriptDelegateControl : WebControl

11.  Override the OnLoad method by adding the following code.

protected override void OnLoad(EventArgs e)
{
  base.OnLoad(e);
}

12.  Inside the OnLoad method, add the following code to put JavaScript on the page.

string helloAlert = "alert('Hello, world!');";
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "popup", helloAlert, true);

Now, you have built the delegate control for the project. Next, you will create the Feature to deploy the control.
To create a Feature to deploy the control
1.      In the Solution Explorer, right-click the EcmaScriptDelegate project and select Add and then New Item.
2.      In the Add New Item dialog box, choose the Empty Element template and type EcmaScriptDelegateFeature as the Name. Click Add.
3.      Insert the following XML inside the Elements element. The Id attribute identifies the delegate where the control is rendered. The ControlAssembly and ControlClass attributes are unique to your control. For more information about how to find the full assembly name, see How to: Create a Tool to Get the Full Name of an Assembly.


The most flexible thing about the delegate control is that it can be defined at the 4 scopes:

·         Web
·         Site collection
·         Web application
·         Farm

Thus, I can set the feature of this scope to activate at any level, and have it immediately show up in ALL of my pages.

The best part is, when you deactivate the feature, it is completely gone and has no impact! No messing with the master page!

JavaScript to add a global link near Site Actions menu in SharePoint 2010

The goal is to add a link next to the Site Actions menu that would always lead back to the portal home, similar to below.




Using IE Developer Tools, I was able to determine the location where I wanted to place my link was inside of the RibbonContainer-TabRowLeft element.

You can modify the master page and inject the following javascript to achieve this:

Read my following post to see how to make this work on all site collections across a web application by using a delegate control!




ExecuteOrDelayUntilScriptLoaded(ExecuteDefaultLoad, "sp.js");

function ExecuteDefaultLoad()
{

 var isWikiEdit = false;
 if ( document.forms[MSOWebPartPageFormName]._wikiPageMode != null )
 {
  if( document.forms[MSOWebPartPageFormName]._wikiPageMode.value == "Edit" )
    {
     isWikiEdit = true;
    }
  }

 var inDesignMode = document.forms[MSOWebPartPageFormName].MSOLayout_InDesignMode.value;
 if (inDesignMode == "1" || isWikiEdit) 
 {  
  // this page is currently in edit mode 
 } 
 else 
 {  
  AddHomeLink();
 } 
}


function AddHomeLink()
{
 var ribbonContainerRowLeft = document.getElementById("RibbonContainer-TabRowLeft");
 if( ribbonContainerRowLeft != null )
 {
  if( ribbonContainerRowLeft.children != null && ribbonContainerRowLeft.children[0] != null )
  {  
  var newSpan = document.createElement("span");
  newSpan.innerHTML='<a class="ms-menu-a" style="cursor:pointer;white-space:nowrap;"    href="javascript:;" title="SharePoint Portal Home" onclick="window.location=\'/\';return false;"><img src="/_layouts/images/hhome.png"  border="0px"/></a>';
  newSpan.className = 'ms-SPLink ms-SpLinkButtonInActive ms-welcomeMenu';
  newSpan.onmouseover= function() {  this.className = "ms-SPLink ms-SpLinkButtonActive ms-welcomeMenu"}; 
  newSpan.onmouseout= function() {  this.className = "ms-SPLink ms-SpLinkButtonInActive ms-welcomeMenu"}; 
  ribbonContainerRowLeft.insertBefore(newSpan, ribbonContainerRowLeft.children[0]);
  }
 }
}


Wednesday, August 15, 2012

Show All Sites I Have Access to in SharePoint with Filtering using JQuery and Javascript

Suppose you want your users to see all the SharePoint sites that they have access to on one page, instead of having to drill down to each subsite to find their content. Instead of creating a site directory, I want to show them all the sites that they can access and allow them to filter based on the site name, site path and site description.

Laura Rogers has a blog post that goes through a step by step process of using the Search Core Results web part, which can be found here: http://sharepoint911.com/blogs/laura/Lists/Posts/Post.aspx?ID=90

However, I want to do this by using some scripting. By default, it will show all the sites the current user has access to (with a scroll bar on the right hand side if it is an extensive list). The search textbox will automatically filter the entire set of sites (with no post backs) on the site title, path and description. The end result will be similar to below:


First, we will want to use the SharePoint search service and get all the sites and webs using jquery/ajax. The query we will use gets only the indexed items that are of contentclass type "STS_SITE" or "STS_WEB:

SELECT Title, Rank, Size, Description, Path FROM Scope() WHERE "scope" = 'All Sites' AND (contentclass = 'STS_Site' OR contentclass = 'STS_Web') ORDER BY "Rank" DESC"
Next, we will call the query using the search service, where the url being passed in is the url to the search.asmx path.

 $.ajax({ 
url: "http://mydomain/_vti_bin/search.asmx" ,  
type: "POST",   
dataType: "xml",       
data: soapEnv,      
async:true,
complete: processResult,   
contentType: "text/xml; charset=\"utf-8\""
 });   
The processResult function will be executed to iterate through the results and create our table once the ajax call has been made.

Finally, we will use the dataTable js plugin to allow scrolling and filtering on the sites list
$('#mySitesTable').dataTable( {
"sScrollY": "300px",
"bPaginate": false,
"bSort": false
} );

Simply add this script to a SharePoint page and reference the jquery library, which you can grab from http://docs.jquery.com/Downloading_jQuery and the jquery.dataTables.min.js, which you can download from http://datatables.net/:

<div id="errorMsg"></div>
<div id="showQueryResults" style="display: none">false</div>
<div id="searchResults"></div>
<div id="testoutput" style="display: none"></div>

<script src="/assets/js/jquery/jquery-1.7.2.min.js" type="text/javascript"></script>
<script src="/assets/js/jquery/plugins/dataTables/jquery.dataTables.min.js" type="text/javascript"></script>

<style>
 div.table_Wrapper { border:10px solid blue; }
 .dataTables_filter 
 {
  width: 50%;
  float: right;
  text-align: right;
 }
 .dataTables_info
 {
  width: 100%;
  font-weight:bold;
  float: left;
  border: 2px solid #ddd;
  background-color: Gainsboro;
  color: #999;
  text-align: right;

 }
 
</style>


<script type="text/javascript">

var searchURL = "http://mydomain/_vti_bin/search.asmx";
var arraySearchResults = new Array();
var arrayListToSearch = new Array();
var displayQueryResults = false;
var searchCap = 5000;
 
//used to display the output of the query; 
//if you would like to see the query result, make set the innerHTML of showQueryResults to true
if( document.getElementById("showQueryResults").innerHTML == "true")
{
 displayQueryResults = true;       
}

RunSearch(); 

   
function RunSearch()
{
 arraySearchResults = new Array();
 

 var myQuery = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>";
 myQuery += "<Query>"; 
 myQuery += "<SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document</Format></SupportedFormats>";  
 myQuery += "<Range><Count>" + searchCap + "</Count></Range>";  
 myQuery += "<Context>";
 myQuery += "<QueryText language='en-US' type='MSSQLFT'>";
 myQuery += "SELECT Title, Rank, Description, Path FROM Scope() WHERE \"scope\" = 'All Sites' AND (contentclass = 'STS_Site' OR contentclass = 'STS_Web') ORDER BY \"Rank\" DESC";
 myQuery += "</QueryText>";
 myQuery += "</Context>"; 
 myQuery += "</Query>";
 myQuery += "</QueryPacket>";   

  
 var soapEnv = "<?xml version=\"1.0\"?>"+
 "<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>" + 
 "<soap:Body>" +
 "<Query xmlns='urn:Microsoft.Search'>"+
 "<queryXml>" + escapeHTML(myQuery) + "</queryXml>"+   
 "</Query>"+    
 "</soap:Body>"+ 
 "</soap:Envelope>";


 $.ajax({  
  url: searchURL,   
  type: "POST",    
  dataType: "xml",        
  data: soapEnv,       
  async:true,

  complete: processResult,    
  contentType: "text/xml; charset=\"utf-8\""

 });         
 }
 
  

function processResult(xData, Status)
{
 if (Status == "error") 
 {
  DisplayErrorMesssage(Status, xData);
  return;
 }

// alert($(xData.responseXML).text());
 
   var queryResult = $(xData.responseXML).find("QueryResult").text();
   $("#testoutput").text(queryResult);
 
   if( displayQueryResults )
   {
       document.getElementById("testoutput").style.display = "";
   }
   else
   {
       document.getElementById("testoutput").style.display = "none";
   }
 
   $(xData.responseXML).find("QueryResult").each(function() {  
    var xml = $("<xml>" + $(this).text() + "</xml>");  
    xml.find("Document").each(function() 
    {  
  var curPath = $("Action>LinkUrl", $(this)).text();  
  curPath = curPath.toLowerCase();

  var curTitle = "";  
  var curDesc = "";
 
 
  $(this).find("Property").each(function() 
  {  
   if ($("Name", $(this)).text() == "TITLE") 
   {  
    curTitle = $("Value", $(this)).text(); 
   }  
   if ($("Name", $(this)).text() == "DESCRIPTION") 
   {  
    curDesc = $("Value", $(this)).text(); 
   }  
  });  
  
  arraySearchResults.push([curTitle, curPath, curDesc]);
    
    });  

  });
 
 PrintOutput();
}
 
 

function PrintOutput()
{ 
 arraySearchResults.sort(sortSearchResults);

 var output = "";

 output += '<table id="mySitesTable" cellpadding="0" cellspacing="0" border="0" class="display">';
 output += "<thead><tr><th align='left'><font color='steelblue' size='2pt'><u><b>Sites that I have access to</b></u></font><br/><br/></th></tr></thead>"; 
 output += "<tbody>";

 
 for( var x = 0; x < arraySearchResults.length; x++ )
 {
  var title = arraySearchResults[x][0];
  var path = arraySearchResults[x][1];
  var desc = arraySearchResults[x][2];
  
  output += PrintRow(path, title, desc);
 } 
 
 output += "</tbody></table>";
 
 document.getElementById("searchResults").innerHTML = output;
 
  
 $('#mySitesTable').dataTable( {
  "sScrollY": "300px",
  "bPaginate": false,
  "bSort": false
 } );
}
 
function PrintRow(path, title, desc)
{
 var output = "";

 if( desc != null && desc != "" )
  output += '<tr><td><a href="' +  path +  '" target="_blank">' + title   + '</a><br/><font color="green">' + path + '</font><br/>'+desc+'<br/><br/></td></tr>';
 else
  output += '<tr><td><a href="' +  path +  '" target="_blank">' + title   + '</a><br/><font color="green">'+path+'</font><br/><br/></td></tr>';

 return output; 
}
 
 
function DisplayErrorMesssage( Status, xData)
{
 document.getElementById("errorMsg").innerHTML = "Error occurred. " + Status ;

 if( xData.responseXML != null )
 {
  document.getElementById("errorMsg").innerHTML += $(xData.responseXML).text();
 }
}
 
 
function sortSearchResults(a, b){

 var aTitle = a[0]; 
 var bTitle = b[0];
  
  
 var x = aTitle.toLowerCase(), y = bTitle.toLowerCase();   
 
 return x < y ? -1 : x > y ? 1 : 0;   
}


function escapeHTML (str) 
{  
 return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');   
}     
 </script>



Tuesday, August 7, 2012

Track SharePoint Attachments Uploaded and Deleted in the EditForm using JavaScript

To track the changes to attachments in a SharePoint item for auditing/record keeping purposes:

1. Create a new SharePoint edit form for the list using SharePoint Designer
2. Make sure that your Edit Form is updated and can upload attachments (use this MSDN article if you are having issues http://support.microsoft.com/kb/953271/en-us)

3. **Updated step**

Find <tr id="idAttachmentsRow" > and directly underneath of that closing </tr> add the following:


<tr style="display:none">
      <td colspan="2" valign="top" class="ms-formbody" nowrap="" height="20px">
       <H3 class="ms-standardheader">
              <SharePoint:AttachmentsField ControlMode="Display" FieldName="Attachments" runat="server" Visible="true"/>
       </H3>
      </td>
     </tr>
(By adding this row and this field, it will retain the name of the attachment)
    
4. To get the newly added attachments, you will want to grab the "attachmentsOnClient" element. This is where the references to the newly added attachments are stored. Next, you will want to iterate through all the INPUT tags and get the "value" attribute to get the names of the files that were uploaded.

5. To get the removed attachments from the SharePoint list item, we will grab the "attachmentsToBeRemovedFromServer" element. All of the GUIDs of the list item attachments to be removed are stored in this element. We will parse out each guid and use it to identify the attachmentRow found under the attachmentsTable. This row will have a reference to the removed items.

6. To view the changes that have been made when a user clicks to save the form, add the script below. Of course, instead of alerting the user of the changes made, you can record it in a status or audit trail field to make it more seamless!


 
<script type="text/javascript">

function GetNewlyUploadedAttachments()
{
 var uploadedAttachments = new Array();
 var oAttachments = document.getElementById("attachmentsOnClient");
 if( oAttachments.innerHTML != null )
 {
  var attachmentTable = oAttachments.getElementsByTagName("INPUT");
  for (var i = 0; i < attachmentTable.length; i++) 
  {    
   var value = attachmentTable[i].getAttribute("value");    
   if ( value != null && value.length > 0 ) 
   {   
    var lastIndex = value.lastIndexOf("\\");
    var fileName = value.substring(lastIndex+1);
    uploadedAttachments.push(fileName); 
   } 
  } 
 }

 return uploadedAttachments.toString();
}

function GetRemovedAttachments()
{
 var removedAttachments = new Array();
 var attachmentsToBeRemoved = document.getElementsByName("attachmentsToBeRemovedFromServer").item(0).value;

 if( attachmentsToBeRemoved != null && attachmentsToBeRemoved != "")
 {
  var array = attachmentsToBeRemoved.split(';');
  for(var i =0; i < array.length; i++ )
  {
   attachmentGuid = array[i];
   if( attachmentGuid != null && attachmentGuid != "" )
   {
    var attachmentRow =  document.getElementById(attachmentGuid);
    var span = attachmentRow.getElementsByTagName('span')[0];
    var links = span.getElementsByTagName("a");
    for (z = 0; z < links.length; z++) 
    {
     removedAttachments.push(links[z].firstChild.nodeValue);
    }
   }
  }
 }
 return removedAttachments.toString();
}

function PreSaveAction()
{
 var newUploadedAttachments = GetNewlyUploadedAttachments();
 var removedAttachments = GetRemovedAttachments();
 if( removedAttachments != null && removedAttachments != "" )
 {
  alert("User removed attachments: " + removedAttachments );
 }
 if( newUploadedAttachments != null && newUploadedAttachments != "" )
 {
  alert("User uploaded attachments: " + newUploadedAttachments );
 }
}
</script>



Monday, July 9, 2012

Display SharePoint List Item Attachments as Images

Suppose you want a visual display of the images that you attach to a SharePoint list. Clicking on each attachment to open it can be a hassle instead of just having it automatically display the images on load.


By default, the attachments are listed as links:




If you view the properties of a SharePoint list item, you will see that the link to each attachment is calculated by: 

[Site Url]/Attachments/[Item ID]/[Attachment Name]

So, if we can grab the item id of the list item, and then determine the attachment names, we will be able to use those properties to display the image with an img tag.

First thing's first. If you view the source of that default SharePoint item view page, you will see that the attachments are store din the element: idAttachmentsTable. We want to grab all of the attachment names in that element. To do so, we will iterate through the span elements and parse out the attachments:  


 

<script type="text/javascript">
spanTag = document.getElementById("idAttachmentsTable").getElementsByTagName("span");
var attachmentArray = new Array();
for (var i = 0; i < spanTag.length; i++) 
{
  filename = spanTag[i].innerHTML;
  var index  = filename.indexOf('>')+1;
  var lastindex = filename.lastIndexOf('<');

  var name = filename.substring(index, lastindex);

  name = name.replace(/ /g, '%20');
  name = name.replace(/'/g, '%27');

  for( var x = 0; x < imgExtensions.length; x++ )
  {
   if( name.indexOf(imgExtensions[x]) >=0 )
   {
    attachmentArray.push([name]);
    break;
   }
  }
}
</script>

Now that we have the names of the attachments, we want to grab the current item id of our list item, which we can simply grab from the source url.

Call to get the item ID:

var paramID = getParameterByName("ID");

Function to grab the parameter from the source url:
function getParameterByName(name) 
{   
 name = name.replace(/[\[]/, "
\\\[").replace(/[\]]/, "\\\]");  
 var regexS = "[\\?&]" + name + "=([^&#]*)"; 
 var regex = new RegExp(regexS);  
 var results = regex.exec(window.location.href); 
 if(results == null) 
  return "";
 else   
  return decodeURIComponent(results[1].replace(/\+/g, " "));
}

Now, we will use the paramID and the attachment names to create our img tags and append it to the Attachments element:

var ctrl = document.getElementById("idAttachmentsTable");
 var attachmentString = "";
 for( i = 0; i < attachmentArray.length; i++)
 {
  attachmentString += 
  " <img src='" + listAttachmentUrl + paramID + "/"+ attachmentArray[i] + "' width='400px' border='2'>";
 }
 ctrl.parentNode.innerHTML += attachmentString;


After adding our script:





To use this script, you will need to use SharePoint designer to edit the "View" page of the list. Navigate to the end of the PlaceHolderMain content placeholder, and right before the closing "</asp:Content>" tag, place the script below in it's entirety. Make sure to modify the listAttachmentUrl to reflect your site and list names:

<script language="javascript" type="text/javascript"> 
var listAttachmentUrl = '/mysite/Lists/mylist/Attachments/';
var imgExtensions = new Array(".jpg",".jpeg", ".png", ".bmp", ".tif", ".tiff");
LoadAllAttachments();

function LoadAllAttachments()
{
 spanTag = document.getElementById("idAttachmentsTable").getElementsByTagName("span");
 var attachmentArray = new Array();
 for (var i = 0; i < spanTag.length; i++) 
 {
  filename = spanTag[i].innerHTML;
  var index  = filename.indexOf('>')+1;
  var lastindex = filename.lastIndexOf('<');

  var name = filename.substring(index, lastindex);

  name = name.replace(/ /g, '%20');
  name = name.replace(/'/g, '%27');
  for( var x = 0; x < imgExtensions.length; x++ )
  {
   if( name.indexOf(imgExtensions[x]) >=0 )
   {
    attachmentArray.push([name]);
    break;
   }
  }
 }

 var paramID = getParameterByName("ID");
 var ctrl = document.getElementById("idAttachmentsTable");
 var attachmentString = "";
 for( i = 0; i < attachmentArray.length; i++)
 {
  attachmentString += 
  " <img src='" + listAttachmentUrl + paramID + "/"+ attachmentArray[i] + "' width='400px' border='2'>";
 }
 ctrl.parentNode.innerHTML += attachmentString;
}

function getParameterByName(name) 
{   
 name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");   
 var regexS = "[\\?&]" + name + "=([^&#]*)";  
 var regex = new RegExp(regexS);   
 var results = regex.exec(window.location.href);  
 if(results == null)  
  return ""; 
 else    
  return decodeURIComponent(results[1].replace(/\+/g, " "));} 
</script>

Friday, July 6, 2012

SharePoint Color Coded list based on a Field in the Row

SharePoint doesn't provide an out of the box way of color coding lists. But, thank goodness, we can use a little scripting to edit the styling of our lists!

Mike Smith provided a great script that allows you to color the background of the row if the cell's value matches the value it's looking for. But, what if another field in the same row has one of the other values and throws off your color coding?

Case in point: color coding is thrown off because the Description field
can have the same value that is being used to color code on
  

Well, this script below will search only the cell index of the row that you would like to base your color coding on. In our case, we would like to search only on the Status field/column. To get started:

1. Add a content editor web part directly underneath the list
2. Open the source editor and place the script below into it
3. Modify the columnIndexToSearch and the columnSpan variables to match the cell/field index to search for and the number of cells in each row respectively.

Note: In this case, the attachment field counts as a cell. Since we are searching only on the Status, our columnIndexToSearch is 2 (0 = Attachment field and 1 = Title field). The total number of columns we are displaying on our list is 5. If you add or remove columns in the view, the color coding will get thrown off. So, make sure to update these variables if you do make updates to the list view.

Final Result: Color coded based on the Status column only
   







<script type="text/javascript">


var listView = getElementsByClass("ms-listviewtable"); //get the list on the page
var tRows = listView[0].childNodes[0].childNodes;

var columnIndexToSearch = 2; //the index of the column that should be searched
var columnSpan = 5; //number of columns visible on the list

var j = 0;
var i = 0; 
var cellcounter = 0; //used to keep track of the cells searched on the row

for( j = 0; j < tRows.length; j++)
{
 if( tRows[j].className == "" || tRows[j].className == "ms-alternating")
 {
  var x = tRows[j].childNodes; // find all of the TRs
  for (i = 0; i < x.length; i++) 
  { 
   if( cellcounter == columnIndexToSearch) 
   {
    if (x[i].innerHTML.indexOf("In Progress") >= 0)
    { 
     x[i].parentNode.style.backgroundColor='lightgreen'; 
    }

    else if (x[i].innerHTML.indexOf("Completed") >= 0)
    { 
     x[i].parentNode.style.backgroundColor='lightblue'; 
    }

    else if (x[i].innerHTML.indexOf("Deferred") >= 0)
    { 
     x[i].parentNode.style.backgroundColor='lightgrey'; 
    }

    else if (x[i].innerHTML.indexOf("Waiting on someone else") >= 0)
    { 
     x[i].parentNode.style.backgroundColor='orange'; 
    }
  }

   cellcounter++;
   if( cellcounter >= columnSpan)
   {
    cellcounter = 0;
   }
  } 
 }
}

function getElementsByClass(searchClass, node, tag) 
{
 var classElements = new Array();
 if ( node == null )
  node = document;
 if ( tag == null )
  tag = '*';
 var els = node.getElementsByTagName(tag);
 var elsLen = els.length;
 var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
 for (i = 0, j = 0; i < elsLen; i++) 
 {
  if ( pattern.test(els[i].className) ) 
  {
   classElements[j] = els[i];
   j++;
  }
 }
 return classElements;
}


</script>



SharePoint Calendar Jump to Next or Previous Month

I had a SharePoint calendar and I wanted a way to jump to the Next or Previous month from the left hand navigation/quick launch menu. 

I couldn't hardcode this url into the navigation, because it would change every month. I needed a dynamic way to do this. To address this, I created a new blank SharePoint page in my site, called "MonthRedirector". I added a script that would determine the current month, and then either skip to the next or previous month based on the query string parameters passed in.


1. Create a blank SharePoint page, call it MonthRedirector
2. Add a content editor web part to the page and paste in the script below or (View Code)

Make sure to set the "Chrome Type" to be "none" and modify the site url in the script to reflect your site's url

3. Add 2 new links to the Navigation menu, 1 for the previous month and 1 for the next month:

[siteurl]/Pages/MonthRedirector.aspx?Jump=Prev&Calendar=[Your calendar name]

[siteurl]/Pages/MonthRedirector.aspx?Jump=Next&Calendar=[Your calendar name]


Now, when a user clicks on the links on the left hand navigation, it will automatically direct them to the previous month or the next month on the calendar!


 
<style>
#sidebar,#sidebar-footer {display : none !important;}
</style>

<script type="text/javascript">
var siteURL = "http://mySiteUrl";
//if you do not wish to hardcode the name, use something
//like SPServices to grab the current site url
//var siteURL = $().SPServices.SPGetCurrentSite();

var curDate = new Date();
var paramfilterJump = getParameterByName("Jump");
var paramCalendarName = getParameterByName("Calendar");

if( paramCalendarName != null && paramCalendarName !="" && paramfilterJump != null 
 && paramfilterJump != "" && paramfilterJump == "Next")
{
 var fullDateString =  PadDigits(curDate.getMonth() + 2, 2)  + "%2F" + PadDigits(curDate.getDate(), 2) + "%2F" +  curDate.getFullYear();  
 var redirectURL = siteURL + "/Lists/" + paramCalendarName + "/calendar.aspx?CalendarDate=" + fullDateString; 
 window.location.href = redirectURL;
}
else if( paramCalendarName != null && paramCalendarName != "" && paramfilterJump != null 
 && paramfilterJump != "" && paramfilterJump == "Prev")
{
 var fullDateString =  PadDigits(curDate.getMonth(), 2)  + "%2F" + PadDigits(curDate.getDate(), 2) + "%2F" +  curDate.getFullYear();  
 var redirectURL = siteURL + "/Lists/" + paramCalendarName + "/calendar.aspx?CalendarDate=" + fullDateString; 
 window.location.href = redirectURL;
}

function PadDigits(n, totalDigits) 
{ 
 n = n.toString(); 
 var pd = ''; 
 if (totalDigits > n.length) 
 { 
  for (i=0; i < (totalDigits-n.length); i++) 
  { 
   pd += '0'; 
  } 
 } 
 return pd + n.toString(); 
} 

function getParameterByName(name) {   name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");   var regexS = "[\\?&]" + name + "=([^&#]*)";   var regex = new RegExp(regexS);   var results = regex.exec(window.location.href);   if(results == null)     return "";   else     return decodeURIComponent(results[1].replace(/\+/g, " ")); } 

</script>



Tuesday, June 26, 2012

Full Text Search in SharePoint on multiple document libraries using SPServices

A while ago, I needed a way to do a full text search of several SharePoint document libraries and display it in one page in a friendly way. I decided to use SPServices to do my querying and output the results using Javascript.

This script does a full text search on the keywords passed into an input box on the current scope of the site. After getting the query results, it then iterates through each search result and displays only the results where the path matches the list names passed in.

Documents in 2 different libraries:











Search output:














Refined search output:









Steps:
1. Ensure search is configured and working on your site
2. Add this script to a content editor web part on your page (View Code)
3. Update the script tags with the location of SPServices and JQuery on your site
4. Make sure to update the listUrl div with the correct list paths that you would like to restrict the output to









<div id="divSearch">
	<div>
		<b>Search Keyword: </b><br/><input type="text" id="tbSearch" size="50%" />
		<input type="button" id="btnSearch" value="Search Policies" onclick="RunSearch();" />
	</div>
</div>

<div id="divErrorMsg"></div>
<div id="divSearchResults"></div> 
<div id="divTestQueryResult"></div>
<div id="divTestQuery"></div>

<script language="javascript" type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" language="javascript" src="/policy/SiteAssets/js/jquery.SPServices-0.7.2.min.js"></script>

<script type="text/javascript"> 

//author: sparsee
//DCSharePointchick.blogspot.com
/**configurable**/
var m_searchCap = 5000;
var m_listUrls = "https://site/policy/Procedure;https://site/policy/Policy;https://site/policy/Supplement;";
var m_displayQueryResults = false;
/**************/

var m_searchQueryForList = "";
var m_arrSearchResults = new Array();
var m_searchResultCount = 0;
var m_output = "";

var imgTxt = '<img src="_layouts/images/ictxt.gif" BORDER=0>';
var imgDoc = '<img src="/_layouts/images/icdocx.gif" BORDER=0>';
var imgPdf = '<img src="_layouts/images/pdf16.gif" BORDER=0>';
var imgDefault = '<img src="_layouts/images/STS_ListItem16.gif" BORDER=0>';

$(document).ready( function() 
{
	//get document properties
	var listUrls = m_listUrls;
	listUrls = listUrls.slice(1,listUrls.length);  
	listUrls = listUrls.substring(0, listUrls.length-1); 
	var arrlistUrls = listUrls.split(';');

	for( var y = 0; y < arrlistUrls.length; y++)
	{
		var curUrl = arrlistUrls[y];
		
		if( curUrl != null && curUrl != "" )
		{
			var url = curUrl.toLowerCase();
			if( m_searchQueryForList != "" )
			{
				m_searchQueryForList += "OR CONTAINS(Path, '\""+ url +"*\"')";
			}
			else
			{
				m_searchQueryForList += "CONTAINS(Path, '\""+ url +"*\"')";
			}
		}
	} 
	
	$("#tbSearch").keyup(function(){
		RunSearch();
	});
});


function RunSearch()
{
	$("#divSearchResults").html("Searching...");
	$("#divErrorMsg").html("");
	m_arrSearchResults = new Array();

	var tbSearch = document.getElementById("tbSearch").value; 

	var myQuery = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>" + 
		"<Query>" + 
		"<Range><Count>" + m_searchCap + "</Count></Range>" +
		"<Context>" +
		"<QueryText language='en-US' type='MSSQLFT'>" +
		"SELECT Title, Rank, Size, Author, HitHighlightedSummary, Description, Path, Write FROM Scope() WHERE FREETEXT('" + tbSearch + "') AND (" + m_searchQueryForList + ") ORDER BY \"Rank\" DESC" +
		"</QueryText>" +
		"</Context>" +
		"</Query>" +
		"</QueryPacket>";


	$().SPServices({
		operation: "Query",
		async: true,
		queryXml: myQuery,
		//debug: true,
		completefunc: function (xData, Status) {

			$("#divSearchResults").html("Searching...");
			$("#divErrorMsg").html("");
			m_arrSearchResults = new Array();


			if (Status != "success") 
			{
				DisplayErrorMesssage(Status);
				return;
			}

			var queryResult = $(xData.responseXML).find("QueryResult").text();

			if( m_displayQueryResults )
			{
				$("#divTestQueryResult").text(queryResult);
				$("#divTestQuery").text("<br/>" + myQuery);
			}
			else
			{
				$("#divTestQueryResult").text("");
				$("#divTestQuery").text("");
			}

			$(xData.responseXML).find("QueryResult").each(function() 
			{  
				var xml = $("<xml>" + $(this).text() + "</xml>");  
				xml.find("Document").each(function() 
				{  
					var curPath = $("Action>LinkUrl", $(this)).text().toLowerCase();  

					var curTitle = "";  
					$(this).find("Property").each(function() 
					{  
						if ($("Name", $(this)).text() == "TITLE") 
						{  
							curTitle = $("Value", $(this)).text(); 
						}  
					});  

					var curHithighlighted = "";
					$(this).find("Property").each(function() 
					{  
						if ($("Name", $(this)).text() == "HITHIGHLIGHTEDSUMMARY") 
						{  
							curHithighlighted = $("Value", $(this)).text(); 
						}  
					});  

					var curWrite = "";
					$(this).find("Property").each(function() 
					{  
						if ($("Name", $(this)).text() == "WRITE") 
						{  
							curWrite = $("Value", $(this)).text(); 
						}  
					});  
		 
					var curAuthor = "";
					$(this).find("Property").each(function() 
					{  
						if ($("Name", $(this)).text() == "AUTHOR") 
						{  
							curAuthor = $("Value", $(this)).text(); 
						}  
					});   
		
					var arrayRows = new Array();
					arrayRows.push([curTitle, curPath, curHithighlighted, curWrite, curAuthor]);

					m_arrSearchResults.push([ curTitle, arrayRows ]);
				});  
			});
			
			PrintOutput();
		}
	});
}

function PrintOutput()
{ 
	m_searchResultCount = 0;
	m_output = '<table class="section-body">';
	
	var tb = document.getElementById("tbSearch").value; 

	for( var x = 0; x < m_arrSearchResults.length; x++ )
	{
		var searchTitle = m_arrSearchResults[x][0];
		var arrayRows = m_arrSearchResults[x][1];

		if( arrayRows.length > 0 )
		{
			var tableID = '"' + x + 'Table"';
			
			m_output += "<tr><td><table id=" + tableID + " class='tablesorter section-table table_summary' width='800px' style='margin-top:0px !important;'>";

			for( var y = 0; y < arrayRows.length; y++)
			{			
				var row = arrayRows[y];
				
				var title = row[0];
				var path = row[1];
				var highlighted = row[2];
				var write = row[3];
				var author = row[4];

				highlighted = GetFormattedHighlightedText(highlighted);
				title = GetHighlightedTitle(title, tb);
				title = GetFormattedTitle(path, title);

				write = GetFriendlyDate(write);

				var highlightedPath = GetHighlightedPath(path, tb);

				m_output += PrintRow(path, title, highlighted, highlightedPath, write, author);
				m_searchResultCount++;  
			}
			
			m_output += "</table></td></tr>";
		}
	}

	var returnedResults = "<br/><span style='color:red'>Showing Results for: <b>" + document.getElementById("tbSearch").value + "</b></span> (Returned: <font color='green'>" + m_searchResultCount + "</font> results)";

	m_output += "</table>";
	m_output = returnedResults + m_output;

	$("#divSearchResults").html(m_output);
}

function PrintRow(path, title, highlighted, highlightedPath, write, author)
{
	var row = '<tr><td><a href="' + path + '" target="_blank">' + title + '</a></td></tr>' + 
	'<tr><td>' + highlighted + '</td></tr>' +
	'<tr><td><a style="color:green;" href="'+ highlightedPath + '">' + highlightedPath +'</a><font color="grey"> - ' + author + ' - ' +  write + '</font></td></tr>';
	
	return row;
}

function GetFormattedTitle(path, title)
{
	if( path.indexOf(".pdf") >= 0 )
		title = imgPdf + title;
	else if( path.indexOf(".doc") >= 0 )
		title = imgDoc + title;
	else if( path.indexOf(".txt") >= 0 )
		title = imgTxt + title;
	else
		title = imgDefault + title;
		
	return title;
}

function GetHighlightedTitle(title, searchText)
{
	if( searchText != "" )
		return highlight(title, searchText);
	else
		return title;
}

function GetHighlightedPath(path, searchText)
{
	if( searchText != "" )
		return highlight(path, searchText);
	else
		return path;
}

function highlight( data, search ) { return data.replace( new RegExp( "(" + preg_quote( search ) + ")" , 'gi' ), "<b>$1</b>" ); } 
function preg_quote( str ) {  return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); } 


function GetFormattedHighlightedText(highlighted)
{
	if( highlighted != null )
	{
		highlighted = highlighted.replace("<c0>", "<font color='green'><b>");
		highlighted = highlighted.replace("</c0>", "</b></font>");

		highlighted = highlighted.replace("<c1>", "<font color='green'><b>");
		highlighted = highlighted.replace("</c1>", "</b></font>");

		highlighted = highlighted.replace("<c2>", "<font color='green'><b>");
		highlighted = highlighted.replace("</c2>", "</b></font>");

		highlighted = highlighted.replace("<c3>", "<font color='green'><b>");
		highlighted = highlighted.replace("</c3>", "</b></font>");

		highlighted = highlighted.replace("<c4>", "<font color='green'><b>");
		highlighted = highlighted.replace("</c4>", "</b></font>");

		//highlighted = "<i>" + highlighted + "</i>";

		return highlighted;
	}
	return "";
}

function GetFriendlyDate(dateField)
{
	var datetimeparts = dateField.split('T');
	var dateparts = datetimeparts[0].split('-');
	var month = dateparts[1];
	var date = dateparts[2];
	var yr = dateparts[0]

	var formatteddateandtime = month + "/" + date + "/" +  yr ;
	
	return formatteddateandtime;
}

function DisplayErrorMesssage( errorMsg )
{
	$("#divErrorMsg").html("Error occurred. " + errorMsg );
}

function CheckSubmit() 
{
    if (event.keyCode == 13) 
	{
		$("#btnSearch").focus();
    }
}
</script>

Friday, June 22, 2012

SharePoint Calendar by Fiscal Year with FullCalendar and SPServices

A little background:

Last week at work, I had a requirement to display a calendar view in SharePoint that would display a full fiscal quarter of a year on one page. It would have to be a color coded calendar, filter by Fiscal Year, Fiscal Quarter and by the Department and show a full quarter view (3 months) on one page.

The first thing I thought of was to add 3 calendar web parts on one page (one for each month of the quarter) and then add some javascript that would redirect the calendars to the current quarter's months. Well, that didn't work out too well because the calendars are controls, and I can't pass in multiple parameters in the url for each one.

So next, I started down the path of creating a gannt chart that would be filtered by each quarter. Each quarter and fiscal year would be dynamically set based on calculated column fields. The first bump in the road was that the gannt chart doesn't go past 2 years. The second bump in the road was that reoccuring events would show one line from the first date to the very end date (there were no tick boxes). I had to scratch that plan.


After doing some research, I came across this blog by Josh McCarty. It utilizes the FullCalendar jQuery library with SPServices to query a SharePoint calendar and display it in a friendly way.

I used his code as a base, and expanded it to create multiple calendar views on one page (one calendar view per month (up to 12!)), allow filtering by the fiscal year and fiscal quarter, filter by department, add a legend of colors for the department and enable tooltips for each event. 

The entire color-coded calendar view, with a legend and drop down fields to filter on fiscal year, fiscal quarter and department (by default, it will set the Time Frame to be the current fiscal year and quarter):

Ability to show the calendar by the week:


Ability to filter by Department:




Steps:
1. Create a simple calendar on your SharePoint site. Add a column called "Department" and make it a choice field. Add the choice options. Ex.:
Dept1
Dept1: SubDept
Dept2
Dept2: SubDept

2. Add a calculated column called "Color" and set it to:


=IF(ISNUMBER(FIND("Dept1",Department)),"LightBlue",IF(ISNUMBER(FIND("Dept2",Department)),"Yellow")
Modify this field with the correct department names and colors you would like to use. 
3. Add the javascript/spservices/jquery libraries to a document library on your site.
4. Insert this code into a Content Editor web part on a page and modify the listCalendarName variable to be the name of your calendar and update the script tags. (View Code)

5. Voila! You should be able to see a color coded calendar similar to the one above!