This was a triumph.
I'm making a note here: HUGE SUCCESS.

Search This Blog

Thursday, January 23, 2014

How to get a term-driven breadcrumb trail with full hierarchy in SharePoint 2013

On the 6th of May, 2013, I had asked a question on Stack Overflow on how one could create a term-driven hierarchical breadcrumb trail in SharePoint 2013. Alas, never really got an answer that suited my needs.

Yesterday, I suddenly had the idea to just try and find a solution with JavaScript. The idea was to loop through the subsite navigation and see which list item resembled the current page, then find all the parent elements of that list item and fetch their values. And in case I was on a page that wasn't part of a subsite (but rather just a page at the top site collection), I would just fetch the title of the page and add that to the breadcrumb trail.

Here's a screenshot of how my breadcrumb looked like earlier:



And here's a screenshot of how it looks like now:



Pretty nice, huh?

Let's just get started, I'll give you the code.

The code

// Just add the following line.
SP.SOD.executeOrDelayUntilScriptLoaded("SP.UserProfiles.js", 
   "~sitecollection/Style Library/Scripts/jquery.SPServices-2013.01.js");

var siteCollection = "Your site collection name";
var siteCollectionUrl = "/";     
var pageName; 
var subsite;
// Fetch the name of the subsite you're currently on.
$().SPServices({
    operation: "SiteDataGetWeb",
    async: false,
  completefunc: function (xData, Status) {
   subsite = $(xData.responseXML).SPFilterNode("Title").text();
  }
});
// Fetch the url of the subsite you're currently on.
var subsiteUrl = $().SPServices.SPGetCurrentSite();
// Fetch the title of the page, remove a piece of text from the title
// (in my case, "Pages - "), then remove any white spaces before and 
// after the title.
var pageTitle = document.getElementsByTagName("title")[0].innerHTML
   .replace("Pages - ", "").replace(/^\s\s*/, "").replace(/\s\s*$/, "");

// If the current site is the same as the site collection (meaning it is
// not a subsite), then use the url of the current page.
if (subsite == siteCollection) {
 // Create the string that will contain the breadcrumb trail.
 var text = "<a href='" + siteCollectionUrl + "'>" + siteCollection 
 + "</a> > <a href='" + document.URL + "'>" + pageTitle + "</a>";
}
// In any other case, use the url of the subsite. 
else {
 // Create the string that will contain the breadcrumb trail.
 var text = "<a href='" + siteCollectionUrl + "'>" + siteCollection 
 + "</a> > <a href='" + subsiteUrl + "'>" + subsite + "</a>";
}

$(function runMe() {
 // Set the ID of your subsite navigation. 
 var $this = $("#NavRootAspMenu");
 if($this != null) {
  // Remove the class "static" from the last item in the navigation 
  // (because it(s a link to edit the navigation) and remove the last
  // child element (since this doesn't have a href attribute).
  $(".ms-listMenu-editLink").removeClass("static");
  $("ul[id*='RootAspMenu'] li.ms-navedit-editArea:last-child").remove();
  $this.find("li").each(function(i){
   var elem = $($this).find("li.static")[i]; 
   // If elem finds an anchor tag that contains the following class, then
   // add a new class. We'll use this class for the parent elements.
   if ($(elem).find("a").hasClass("ms-core-listMenu-selected")) {
    $(elem).addClass("parentSelected");
   }  
  });  
  // If the subsite navigation contains elements with the class 
  // "parentSelected" and that element contains an anchor, a span and
  // another span with the class "menu-item-text", then for each of those
  // elements do the following. 
  $this.find(".parentSelected > a span span.menu-item-text")
   .each(function(j) { 
   $(this).addClass("bcn");  //bcn = breadcrumbnode
   var crumbLink = $($this).find(".parentSelected > a")[j].href;
   var crumbName = $("span.bcn")[j].innerHTML;
   // If the link equals the url of the site collection, then this list
   // item is a category and it does not require an anchor tag.
   if (crumbLink == "https://your-site-collection.com/") {
    text = text + " > " + crumbName;
   }
   // In any other case, this list item has a page and we will add an
   // anchor tag to the breadcrumb trail. 
   else {
   text = text + " > <a href='" + crumbLink + "'>" + crumbName + "</a>";
   }
  });
 }
// When we ran through the navigation, apply the new breadcrumb trail to
// the element in the master page. 
document.getElementById("customBreadcrumb").innerHTML = text;
});

You'll also need to have the jQuery library for SharePoint Web Services, you can find it here.
In order for our code to work properly, we will need to replace some code from the master page and add a reference to the breadcrumb script on the master page. Be sure to read the JavaScript code first, you will need to fill in a name for your site collection (var siteCollection) and give the url of your site collection followed by a slash (var crumbLink).

Adding a reference to the master page

If we want to apply this code on the site collection and all subsites, then we should add a reference to our script in the master page. Please do note that I'm using a HTML master page. This is the code you should add:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink1"
runat="server" name="~sitecollection/Style Library/Scripts/breadcrumb.js"
OnDemand="false" Localizable="false"/>-->

Do note that the ID might be different. You must make sure that you do not already have a scriptlink with the same ID, so change the number of the ID and make it unique.
Also, the name (path to your script) might be different. Make sure that it matches the path of where your script is located.

Replacing code in the master page

Next, we need to replace some code in the master page. In my HTML master page, I replaced any piece of code that had to do with the breadcrumb. Just to give you an idea, the following pieces of code are the ones that I REMOVED from my master page:
<!--SPM:<asp:sitemappath runat="server" 
 sitemapproviders="SPSiteMapProvider,SPXmlContentMapProvider" 
 rendercurrentnodeaslink="true" 
 nodestyle-cssclass="breadcrumbNode" 
 currentnodestyle-cssclass="breadcrumbCurrentNode" 
 rootnodestyle-cssclass="breadcrumbRootNode" 
 hideinteriorrootnodes="false"
 SkipLinkText=""/>-->
<!--SPM:<SharePoint:AjaxDelta id="DeltaPlaceHolderPageTitleInTitleArea" 
 runat="server">-->
<!--SPM:</SharePoint:AjaxDelta>-->
<!--SPM:<SharePoint:AjaxDelta BlockElement="true" 
 id="DeltaPlaceHolderPageDescription" CssClass="ms-displayInlineBlock 
 ms-normalWrap" runat="server">-->
 <a href="javascript:;" id="ms-pageDescriptionDiv" 
  style="display: none;">
  <span id="ms-pageDescriptionImage"></span>
 </a>
 <span class="ms-accessible" id="ms-pageDescription">
  <!--SPM:<asp:ContentPlaceHolder id="PlaceHolderPageDescription" 
   runat="server"/>-->
 </span>
 <!--SPM:<SharePoint:ScriptBlock runat="server">-->
  <!--SPM:_spBodyOnLoadFunctionNames.push("setupPageDescriptionCallout");-->
 <!--SPM:</SharePoint:ScriptBlock>-->
<!--SPM:</SharePoint:AjaxDelta>-->

It is very important that you keep the following code in your master page, but wrap something around it with a class. Like so:
<span class="hideDefaultBreadcrumb">
 <!--SPM:<asp:ContentPlaceHolder id="PlaceHolderPageTitleInTitleArea" 
  runat="server">-->
  <!--SPM:<SharePoint:SPTitleBreadcrumb runat="server"
   RenderCurrentNodeAsLink="true"
   SiteMapProvider="SPContentMapProvider"
   CentralAdminSiteMapProvider="SPXmlAdminContentMapProvider">-->
   <!--SPM:<pathseparatortemplate>-->
    <!--SPM:<SharePoint:ClusteredDirectionalSeparatorArrow 
     runat="server"/>-->
   <!--SPM:</PATHSEPARATORTEMPLATE>-->
  <!--SPM:</SharePoint:SPTitleBreadcrumb>-->
 <!--SPM:</asp:ContentPlaceHolder>-->
</span>

Then add the class to your style sheet:
.hideDefaultBreadcrumb {
 display: none;
}

The reason that we must keep this content placeholder, is that otherwise you'll suddenly miss a great portion of the "Apps you can add". I forgot about this at first and then suddenly had only three apps left, but after reading about it here I learned that I should have left in the content placeholder with ID PlaceHolderPageTitleInTitleArea. So that's fixed now.

I also removed a span with the ID "ctl00_DeltaPlaceHolderPageTitleInTitleArea" in the master page.

Bear in mind that your code might be different from mine, so always check your site with source view in your browser, to see the ID's and classes of the elements in the breadcrumb.
Now, ADD the following code to the location of where you just removed the piece of code that was previously your breadcrumb:
<span id="customBreadcrumb"></span>

In that tiny span, your breadcrumb trail will appear. This will be dynamically filled with text and links once your script is running.

Make sure your master page and your script are checked in. Now you can finally see the full breadcrumb trail, with the correct hierarchy!
No need to deploy solutions, no need to find a workaround or use webparts to do the trick, just some simple JavaScript is all you need. ;)

Enjoy!

Friday, December 20, 2013

How to redirect all but a few users to a custom page in SharePoint 2013

Imagine that you are working in a production environment, you're close to your deadline on which all users will be granted access to your site. But what if you're not ready yet? What if your supervisor wants you to "deny" all users access to the site by redirecting them to one page, all users except for a select few?

In such a case, you'll need a script that will check the name of the user, see if he/she is allowed full access to the site, and if not redirect the user to a custom page. I used Jane Doe and John Doe as the users that are allowed full access to the site (I advise you to add your own name as well).

The code

We need to make a JavaScript file (I named mine "custom-redirect.js") and we will add the following code to the file:
SP.SOD.executeFunc('sp.runtime.js');
SP.SOD.executeOrDelayUntilScriptLoaded('SP.UserProfiles.js', 
 "~sitecollection/Style Library/Scripts/jquery.SPServices-2013.01.js");

var url = window.location.pathname;
if (url.indexOf('Redirect.aspx') > -1) {
 // Do not run the script if we're already on the page.
}
else {
 function redirectMe() {
  var userListArray = new Array();
  userListArray.push("Jane Doe"); 
  userListArray.push("John Doe"); 

  var user = $().SPServices.SPGetCurrentUser({ 
   webURL: "", 
   fieldName: "Title", 
   fieldNames: {}, 
   debug: false
  });
 
  function include(arr, obj) {
   for (var i = 0; i < arr.length; i++) {
    if (arr[i] == obj) return true;
   }
  }

  if(include(userListArray,user)) {}
  else { 
   window.location = 
    "http://your-site-here.com/Pages/Redirect.aspx";
  }
 }
}

The code will check if we're not on the redirect page already, since there's no need to keep running the script when the user has already been redirected. If we're not, then we'll continue with the function called redirectMe().

We need to make an array that will hold the names of all users that will be granted full access to the site. Push the names of those users to the array.

Then, we use SPServices to check the "Title" (also known as the full name of the user) of the current user. We will need a small function to check if an array contains an object as well. Using that function, we then check if the name of the current user is included in the array. If it is, then we don't need to do anything. If the name of the user is not in the array, then we redirect the user to a different page, in my case a custom redirect page.
And that's all there is to it! Quite simple, now that I look back at it.

You'll need to have the jQuery library for SharePoint Web Services in order to get the current user, you can find it here.
Also make sure you added a reference to your script in your master page, like so:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink9" 
 runat="server" OnDemand="false" Localizable="false" 
 name="~sitecollection/Style Library/Scripts/custom-redirect.js" />-->

You'll also need to add the following line of code directly in your master page:
<script type="text/javascript">
 window.onpaint = redirectMe();
</script>

Make sure your master page and your script are checked in. Now you can freely continue working on your site without anyone seeing that it is not yet finished! Better yet, those who you have granted full access to your site can contribute as well!

If you have any questions, feel free to ask. ;)

How to prevent the page from going to "AllItems.aspx" after deleting a list item or a document in SharePoint 2013

Abouth a month ago on the 19th of November, I noticed something while I was cleaning up my site. I discovered that when I deleted a list item or a document from a list or library, the browser would send me to the "AllItems.aspx" of that list or library.

This was something I didn't really pay attention to at first, but then it came to my mind that end users might find this confusing since they would suddenly end up on a different page whenever they deleted something. Off course I understand that you probably wouldn't find this confusing at all, but imagine that a user with no knowledge of anything related to IT or computers would be in that situation. If such a user would end up on a page that no longer has the trusted design with a title and a paragraph of text, and a list or library that is suddenly showing way more columns than what he/she is used to see, that user would probably end up sending me an e-mail asking me for help because he/she is stuck. Yeah.

To avoid any confusion, no matter how small the problem, I wanted to make sure users would never end up on the "AllItems.aspx" page whenever they deleted something. So, not knowing where to start and not finding any relevant results on Google, I asked the question on Stack Exchange. I had thought that something like this wouldn't be so hard to figure out, but was dissappointed when my first and only "answer" was just a comment saying that I could take pointers. After a few weeks with still no solution, I gave up hope and started a bounty on the question.
Suddenly many more answers appeared, yet none were offering me an example in JavaScript. Most answers required me to use C# and seemed way to complicated for a problem this small, and after a while I thought that maybe my question wasn't clear enough.

Until someone answered with three options, one of which was to use cookies. When I started my search, I discovered that there was a successor to cookies. Named HTML5 Web Storage.

The logic behind the idea

First of all, we notice that when you want to delete something (be it a list item or a document, I'll just call it "item" from now on), you first need to click on the edit button for that item before you can choose to delete it. When I edit an item, it opens in a modal dialog. As you may or may not know, the URL of a modal dialog contains "EditForm.aspx".
If I then choose to delete the item, it directs me to the "AllItems.aspx" page. Based on this, we now know that we can write a piece of code that will only run when the user ends up on the "AllItems.aspx" page, having the "EditForm.aspx" page as its referrer.

Next, we just write a piece of code that will take us back to the previous page. But pay attention: technically, the previous page is the modal dialog. We need to go back to the page before we launched the modal dialog, so the page on which you clicked on a list item from a list or a document from a library.

To save the URL of that page somewhere, I used HTML5 Web Storage.
At this point, I just want to share the code with you. After all, there's really not much left to explain.

The actual code

var ref = document.referrer;  // Stores URL of previous page.
var url = window.location.pathname; // Stores URL of current page.

// The following code will run if the user edits a list item or properties of 
// a document.
if (url.indexOf("EditForm.aspx") > -1) {
 sessionStorage.setItem("page", ref);
}

if (url.indexOf("AllItems.aspx") > -1 
    && ref.indexOf("EditForm.aspx") > -1) {
 window.location = sessionStorage.getItem("page");
}

There you have it! It can't be easier, really!
If you follow the code, you'll see that if the URL in your browser window contains "EditForm.aspx", it will store the URL of the "previous" page as a session object. As soon as the URL in your browser window contains "AllItems.aspx" AND if the referrer (aka URL of the previous page) contains "EditForm.aspx", it will set the window location to the URL we stored as a session object earlier. The user won't even see that he/she has been redirected back to that page.

To make things work, you'll need to make sure that you've put a reference to your script in your master page. I had written the code in a new JavaScript file named "no-all-items.js" and added a reference to it in the master page:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink10" 
 runat="server" OnDemand="false" Localizable="false" 
 name="~sitecollection/Style Library/Scripts/no-all-items.js" />-->

And there you have it. Make sure your script is checked in, as well as your master page, and you're ready to go!
Enjoy!