SharePoint 2013: Trigger Event With Ribbon Tab Changes

I needed to change the delete button on the ribbon on some specific App parts on SharePoint 2013, so looking around I found a post that shows how to trigger an event with each ribbon change by Dennis George which was of great help because it did exactly what I needed, to capture when a certain tab was selected. The issue? It doesn’t work on SharePoint 2013 because the CUI.Ribbon.prototype.$L is always undefined, so I started debugging the whole ribbon and found that to make the code work in SharePoint 2013 I just had to change $L with $1q_0 and that was it. So here’s the code to trigger an event each time there is a change on the SharePoint 2013 ribbon. Use it at will:


// Fires 'ribbontabselected' every time the ribbon selection changes

ExecuteOrDelayUntilScriptLoaded(function () {

debugger;

CUI.Ribbon.prototype.$1q_0_old = CUI.Ribbon.prototype.$1q_0;

CUI.Ribbon.prototype.$1q_0 = function () {

this.$1q_0_old();

$(document).trigger('ribbontabselected', [this.get_selectedTabCommand()]);

};

}, 'cui.js');

// Fires 'ribbontabselected' after the ribbon has been initialized after load

ExecuteOrDelayUntilScriptLoaded(function () {

debugger;

var pm = SP.Ribbon.PageManager.get_instance();

pm.add_ribbonInited(function () {

$(document).trigger('ribbontabselected', [SP.Ribbon.PageManager.get_instance().get_ribbon().get_selectedTabCommand()]);

});

}, 'sp.ribbon.js');

// Example code for binding to the event

$(document).on('ribbontabselected', function (e, selectedTabCommand) {

debugger;

if (selectedTabCommand != "ReadTab") {

alert(selectedTabCommand);

}

});

Advertisements

Fully Functional Likes in Search Results (SharePoint 2013)

I’ve been struggling with this way too long… But in the end and after a whole lot of googling I was able to make this work as magic (that is with JavaScript/jQuery). What you’ll need to do is create a new Display Template for your search, add some managed properties to it and put some magical JS code. I will assume you know how to create a new Display Template, but if not you can search on google for it or you can read this blog series by Microsoft’s own Bella Engen. So let’s get started.

Display Template

For your display template you’ll have to add two managed properties. The ones that you will be needing are the List ID and the List Item ID, you can add LikesCount as well but the main problem with it is that if you show that property it wouldn’t be updated until your next crawl which kind of ruins the whole thing since if you just unliked the item you might want that to show on other user’s searches as well, so retrieving that value in real-time with JavaScript would be your best bet. Back to business, in my case those properties where already there and mapped (I think they might be by default), but if in your case the managed properties are not created go ahead and create them. Here’s how those properties looked:

Managed Properties

Look at the ListID and ListItemID Managed Properties

So from there you have to add those two properties to the Display Template as follows:


<mso:ManagedPropertyMapping msdt:dt="string">'ListItemID':'ListItemID','ListID':'ListID','Title':'Title','Path':'Path','Description':'Description','EditorOWSUSER':'EditorOWSUSER','LastModifiedTime':'LastModifiedTime','CollapsingStatus':'CollapsingStatus','DocId':'DocId','HitHighlightedSummary':'HitHighlightedSummary','HitHighlightedProperties':'HitHighlightedProperties','FileExtension':'FileExtension','ViewsLifeTime':'ViewsLifeTime','ParentLink':'ParentLink','FileType':'FileType','IsContainer':'IsContainer','SecondaryFileExtension':'SecondaryFileExtension','DisplayAuthor':'DisplayAuthor','Likes Count'{Likes Count}:'LikesCount'</mso:ManagedPropertyMapping>

As you can see on that line I also left the LikesCount added, but you don’t really need it. After having that let’s leave the Display Template for a second and move on to…

The JavaScript

I’ve made this on a separate JS file which I’ve added to the Search Results page. Here I’ve made several functions to handle the likes. The first one would be the click for the “Like/Unlike” link, but you wouldn’t understand that function until you see the one that goes and checks if the user had already liked the item, for this I have to thank Simon Pedersen for his post where he has a similar function to the one I was working on (and yes, at this point I’m wondering if I’m a programmer or just a good googler). So anyway, here’s the function I’ve made:


function getUserLikedPage(listID, itemID) {
  var didUserLiked = false;
  var context = new SP.ClientContext(_spPageContextInfo.webServerRelativeUrl);
  var list = context.get_web().get_lists().getById(listID);
  var item = list.getItemById(itemID);
  var likes, success = false;

  context.load(item, "LikedBy", "ID", "LikesCount");
  context.executeQueryAsync(Function.createDelegate(this, function (success) {
    // Check if the user id of the current users is in the collection LikedBy.
    var usersLiked = item.get_item('LikedBy');
    likes = item.get_item('LikesCount');
    if (!SP.ScriptHelpers.isNullOrUndefined(usersLiked)) {
      for (var i = 0; i < usersLiked.length; i++) {
        var user = usersLiked[i];
        if (user.$1E_1 === _spPageContextInfo.userId) {
          didUserLiked = true;
        }
      }
    }
    var span = $('span.MyLikes[listid=' + listID + '][itemid=' + itemID +']');
    span.data('userliked',didUserLiked);
    if(didUserLiked)
      span.next().text("Unlike");
    else
      span.next().text("Like");
    span.text(likes);
  }),
    Function.createDelegate(this, function (sender, args) {
      //Custom error handling if needed
      console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  }));
}

At the end this function does a lot of things, some are self-explanatory, but some I need to explain you. What I’ve decided to do is to set a data attribute on the element that holds the number of likes so that I can get the value from there whenever and wherever I need it, that’s done by the

span.data('userliked',didUserLiked);

line, and then to get that value you just need to do the following

span.data('userliked');

and be done with it. The other thing I think I need to explain is that I’m changing the value of the Text for the like/unlike here instead of doing it from the Display Template because it needs to be done asynchronously and as the display template goes item by item it won’t wait until you get the result, so this will be updated via JavaScript on my end.

Now I can show you the other two functions which could be just one, but I just felt like splitting them…


function SetLike(sender, listID, itemID){
  var didUserLiked = $(sender).prev().data('userliked');
  setActualLike(listID, itemID, didUserLiked);
  return false;
}

function setActualLike(listID, itemID, didUserLiked) {
  var span = $('span.MyLikes[listid=' + listID + '][itemid=' + itemID +']');
  var countLikes = span.text();
  var aContextObject = new SP.ClientContext();
  EnsureScriptFunc('reputation.js', 'Microsoft.Office.Server.ReputationModel.Reputation', function() {
    Microsoft.Office.Server.ReputationModel.Reputation.setLike(aContextObject, listID.substring(1, 37), itemID, !didUserLiked);
    aContextObject.executeQueryAsync(function() {
      if(didUserLiked) {
        countLikes--;
        span.next().text("Like");
      } else {
        countLikes++;
        span.next().text("Unlike");
      }
      span.text(countLikes);
      span.data('userliked',!didUserLiked);
    }, function(sender, args) {
      // Custom error handling if needed

    });
  });
}

That concludes the JavaScript part. So…

Let’s go back to the Display Template

Now we need to do make this whole thing work… So we need to call the functions when needed and pass them the values of the ListID and ListItemID managed properties. So let’s start with the JavaScript part of the display template:


var listID = $getItemValue(ctx, "ListID").value;

var itemID = $getItemValue(ctx, "ListItemID").value;

if(listID != null && itemID != null)

getUserLikedPage(listID, itemID);

This will get you the ListID and ListItemID properties on two handy javascript variables and if they are not null they will be sent to the getUserLikedPage from the separate JS file. Then we need to change the HTML part so that you can display the things that you want displayed, here’s that code:


<div id="_#= $htmlEncode(itemId) =#_" name="Item" data-displaytemplate="My_DisplayItem" class="ms-srch-item" onmouseover="_#= ctx.currentItem_ShowHoverPanelCallback =#_" onmouseout="_#= ctx.currentItem_HideHoverPanelCallback =#_">
  _#=ctx.RenderBody(ctx)=#_
  <div>Likes: <span class="MyLikes" ListID="_#= listID =#_" ItemID="_#= itemID =#_"></span> <a onclick="SetLike('_#= listID =#_', '_#= itemID =#_')"></a></div>

  <div id="_#= $htmlEncode(hoverId) =#_" class="ms-srch-hover-outerContainer"></div>
</div>

As you can see I’ve also added the ListID and ListItemID properties to the span as attributes to be able to find that easily with my jQuery.

With this code you should be getting your likes on the search results and being able to Like or Unlike from those same results (if you have set the Display Template correctly and activated the Rating features of the list).

And here’s how the end result looks like on my end:

End Result

If you have any questions feel free to ask and I’ll try to answer them below.

Search Navigation Without Creating New Pages (JavaScript) – SharePoint 2013

In the project I’m working on they asked me to create a JS file that would change the Result Sources on the fly as the user clicked on the navigation links. So after googling a bit I’ve stumbled upon Elio Struyf’s post where he changes the Result Sources based on the query, so that pointed me in the right direction… At the beginning my idea was to make it so that it changed the Result Source based on a parameter in the query string, so I thought that Elio’s code would be a great base and that I just needed to make the changes so that it would pull the values from the QueryString instead of the query for the search. Right?

Wrong. That approach turned out to be a bit more difficult than expected. To start with I learned that the SharePoint site navigation didn’t account for query parameters so I decided that I would change the href of the links via the JavaScript file that I was writing, that was easy enough… But still the code was not executing, after a careful look at the situation I realized that the fillKeywordQuery function was not being called. The problem was that in the project we don’t use a default Search Box Web Part, we use a simple web part that fills the results onto a customized Search Results Web Part so the function was never being called. I added a default Search Box Web Part and noted that when I clicked on the search image the code was executing just fine.

In the end I decided to go with a different approach that doesn’t depends on the QueryString, it just changes the behavior of the fillKeywordQuery each time you click on the navigation links.

So the first thing that I had to do was to remove the href and the onclick from all the links so that they wouldn’t redirect me to the page that was assigned on the SharePoint side. To do that I used just some simple jQuery:

//Save the original fillKeywordQuery function
if (typeof Srch.U.fillKeywordQuery !== 'undefined') {
 var originalFillKeywordQuery = Srch.U.fillKeywordQuery;
}

var links = $('li.ms-srchnav-item > h2.ms-displayInline > a');
links.prop('onclick','');
links.prop('href','#'); //I'm leaving the href set so that the default SharePoint styles continue to work

After that I had to go through each link and assign the click function that would do the specific Result Source change:


links.each(function(i) {
   var item = $(this);
   if(item.attr('title').indexOf(' All') > -1){
      item.click(function(){
         Srch.U.fillKeywordQuery = function(query, dp) {
           //Set the Result Source
           dp.set_sourceID("a7b9a73f-c771-4237-a1b5-c8c44beb462a");
           //Continue processing the KeywordQuery with the default function
           originalFillKeywordQuery(query, dp);
           selectedClassLink(links, item);
        };
      });
   }
   else if(item.attr('title').indexOf(' Documents') > -1) {
      item.click(function(){
         Srch.U.fillKeywordQuery = function(query, dp) {
           //Set the Result Source
           dp.set_sourceID("24f2f75a-6c8d-4657-b9b0-181204e8b92e");
           //Continue processing the KeywordQuery with the default function
           originalFillKeywordQuery(query, dp);
           selectedClassLink(links, item);
        };
     });
   }
}

function selectedClassLink (links, sLink) {
   links.attr('class','ms-srchnav-link');
   sLink.removeClass('ms-srchnav-link');
   sLink.addClass('ms-srchnav-link-selected');
}

In my case I also needed to call the fillKeywordQuery myself since the web part that we’re using doesn’t call it. If that is your case you’ll also need to set a KeywordQuery and get the DataProvider and call the function on each of the clicks from the links.

And when you put this in you realize that the Results Web Part is not refreshing… So we need to make it refresh on each of the click functions of the links. And again come Elio Struyf to give the helping hand, he also has a post where he explains how to refresh the Results web part automatically so basing myself on that I was able to change a couple of things to make it work on my solution.

So here’s the final code:

var keywordQuery,datap;//To be used when manually calling the fillKeywordQuery function.
$(document).ready(function(){
	var clientContext = new SP.ClientContext(document.location.href);
	var contextSite = clientContext.get_site();
	datap = $getClientControl($('#DataProvider')[0]);
	EnsureScript("sp.search.js",TypeofFullName("Microsoft.SharePoint.Client.Search.Query.KeywordQuery"),datap.$$d_$54_3)
	keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(clientContext);
	changeNavLinks();
});

//Save the original fillKeywordQuery function
if (typeof Srch.U.fillKeywordQuery !== 'undefined') {
	var originalFillKeywordQuery = Srch.U.fillKeywordQuery;
}

//Change the way the Search Navigation Web Part links function
function changeNavLinks() {
	var links = $('li.ms-srchnav-item > h2.ms-displayInline > a');
	links.prop('onclick','');
        links.prop('href','#');
	links.each(function(i) {
		var item = $(this);
		if(item.attr('title').indexOf(' All') > -1){
			item.click(function(){
				overrideFKQ('6fa34605-8405-43c3-b484-a04508a80ebc');
				Srch.U.fillKeywordQuery(keywordQuery,datap);
				var queryState = new Srch.QueryState();
				var queryStateArgs = new Srch.QueryEventArgs(queryState);
				$getClientControl($('#Result')[0]).raiseQueryReadyEvent(queryStateArgs);
				selectedClassLink(links, item);
			});
		}
		else if(item.attr('title').indexOf(' Documents') > -1) {
			item.click(function(){
				overrideFKQ('a7b9a73f-c771-4237-a1b5-c8c44beb462a');
				Srch.U.fillKeywordQuery(keywordQuery,datap);
				var queryState = new Srch.QueryState();
				var queryStateArgs = new Srch.QueryEventArgs(queryState);
				$getClientControl($('#Result')[0]).raiseQueryReadyEvent(queryStateArgs);
				selectedClassLink(links, item);
			});
		}
	});
}

function overrideFKQ(sourceID) {
	Srch.U.fillKeywordQuery = function(query, dp) {
		//Set the Result Source
		dp.set_sourceID(sourceID);
		//Continue processing
		originalFillKeywordQuery(query, dp);
	};
}

function selectedClassLink (links, sLink) {
	links.attr('class','ms-srchnav-link');
	sLink.removeClass('ms-srchnav-link');
	sLink.addClass('ms-srchnav-link-selected');
}

There you have it. Hope this helps you and thanks again to Elio Struyf for his posts that helped me make this code.

If you have any questions or comments please leave them below and I’ll try to answer them.