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

Search This Blog

Showing posts with label SPServices. Show all posts
Showing posts with label SPServices. Show all posts

Thursday, April 9, 2015

How to prevent users from deleting other users their attachments from items in SharePoint 2013

I have been inactive for quite a long time. It was a busy period at work, and I've been learning to program server-side as well. But recently, I wrote another piece of code in JavaScript, and I'd like to share this with you!

This script will do the following:
  • When the user edits an existing item, check if the item has attachments;
  • If the item has one or more attachments, get the author of each attachment;
  • Check for each attachment if the current user is the author. If the current user is not the author, disable the option to delete the attachment and put the text in grey.


Why is this useful? Because if you have multiple users working on one list item (in my case, a request form that requires multiple users to add an attachment), they won't accidentally delete each others attachments.

I added the script to an existing script, named "scripts.js" which is located in my "Style Library" folder. The "scripts.js" file is referenced in my master page, so that it will run on every page.

if ((document.referrer == "" || document.referrer == null) 
   && window.location.pathname.toLowerCase().indexOf("editform.aspx") > -1) {
   // These two lines are required, without it my code won't run.
   SP.SOD.executeFunc('SP.js', 'SP.ClientContext');
   SP.SOD.executeFunc('sp.runtime.js');
 
   var currentItemID;
   var attachmentAuthor = [];
   var itemArray = [];
 
   $(document).ready(function(){  
      // Get ID of the current item.
      currentItemID = window.location.href.toLowerCase();
      currentItemID = currentItemID.substring(currentItemID.toLowerCase().indexOf("?id=") + 4);
      // Remove the line below in case the URL of your item is 
      // not shown as a modal dialog.
      currentItemID = currentItemID.substring(0, currentItemID.toLowerCase().indexOf("&isdlg"));
  
      // Save the ID of the current item in the session 
      // (not necessary, but I prefer it this way)
      sessionStorage.setItem("SessionItemCurrentItemID", currentItemID);
 
      // Get attachments of current item.
      var url = "/_api/Web/Lists/getByTitle('" + "stbz" + "')/Items(" + currentItemID + ")/AttachmentFiles",
         qs = "?$select=ID,Author/Title,*&$expand=Author,AttachmentFiles",
         siteUrl = "https://path-to-your-site.com";
      $.ajax( {
         url : siteUrl + url + qs,
         type : 'GET',
         headers : {
            'accept' : 'application/json;odata=verbose',
            'content-type' : 'application/json;odata=verbose'
         },
         success : successHandler,
         fail : failHandler
      });  
 
      function successHandler(data) {
         if (data) {
         // If the item has attachments, then run this function.
            $.each(data.d.results, function() {
               getWebProperties(sessionStorage.getItem("SessionItemCurrentItemID"));
            });
         }
      }
 
      function failHandler(data, errCode, errMessage) { 
         console.log('Error: ' + errMessage); 
      }
   
      function getWebProperties(itemID) {
         var attachmentFiles;
         var ctx = new SP.ClientContext.get_current();
         var web = ctx.get_web();
         var attachmentFolder = web.getFolderByServerRelativeUrl('Lists/stbz/Attachments/' + itemID);
         attachmentFiles = attachmentFolder.get_files();
         ctx.load(attachmentFiles);
         ctx.executeQueryAsync(function(){
            // I can't remember what the $2_1 was again, but anyway...
            for (var j = 0; j < attachmentFiles["$2_1"].length; j++) {
               var author = attachmentFiles.itemAt(j).get_author(); 
               attachmentAuthor.push([attachmentFiles.itemAt(j).get_name(),author]);
   
               // You'll need to load the author along with the title parameter,
               // in order to be able to fetch the name of the author later.
               ctx.load(author, 'Title');
               ctx.executeQueryAsync(function(){}, function(err) {});
            }
            // Loop is not necesarrily required, but you will need to set a
            // timeout. Comes in handy when you have a lot of attachments.
            checkAttachmentsLoop();
         }, function(err) {});
      }
   
      function checkAttachmentsLoop() {
         setTimeout(function(){
            if (attachmentAuthor.length) {
               checkAttachments();
            }
            else {
               checkAttachmentsLoop();
            }
         },100);
      }
   
      function checkAttachments() {
         for (var h = 0; h < attachmentAuthor.length; h++) {
            // if you log attachmentAuthor[h][0] to the concole, you'll get 
            // the name of the attachment.
            // if you log attachmentAuthor[h][1].get_title()) to the console,
            // you'll get the name of the author.
    
            var currentAuthor = attachmentAuthor[h][1].get_title();
            // If the current user is not the author of the current attachment,
            // then we disable the ability to delete the attachment from the item.
            if (currentAuthor != sessionStorage.getItem("sessionItemUserName")) {
               var tr = document.getElementById("idAttachmentsTable").getElementsByTagName("tr");
               for (var i = 0; i < tr.length; i++) {
                  var currentTR = $(tr)[i];
                  var anchors = $(currentTR).find("a")[0];
                  var deletes = $(currentTR).find("a")[1];
                  if (anchors.innerHTML == attachmentAuthor[h][0]) {
                     $(currentTR).attr("disabled", "disabled");
                     $(anchors).css("color", "#b1b1b1");
                     $(anchors).removeAttr("href");
                     $(anchors).removeAttr("onclick");
                     $(deletes).css("text-decoration", "line-through");
                     $(deletes).css("color", "#b1b1b1");
                     $(deletes).removeAttr("href");
                     $(deletes).removeAttr("onclick");
                  }
               }
            }
         }
      }
   }
}
else {
   if (sessionStorage.getItem("SessionItemCurrentItemID") != null) {
      sessionStorage.removeItem("SessionItemCurrentItemID");
   }
}

And there you have it! I know, I didn't format the code that nicely... And I mix up jQuery and JavaScript quite often... But anyway, it is readable.

In case you would like to see an example of the code in action, here it is:

As you can see, the first attachment is one I added. I am the author of that attachment. And thus, I can also decide whether or not I'm going to delete it. The second attachment however, was not uploaded to the item by me. I am not its author. Therefore, I am not allowed to delete it.

If you have any questions, feel free to ask!

Wednesday, March 5, 2014

Live notifications from new items in lists/libraries to which current user is subscribed, using JavaScript in SharePoint 2013

Warning! Long post! 
Screenshots can be found at the end of the post. Take your time to read everything.
----------------

So the past six days I've been working on a notification script for our SharePoint intranet.
At first I didn't even know where to start, so I asked a question on Stack Exchange. After getting some inspiration from the answers I received (thanks you guys!) I decided I should just give it a try.

I wanted a script that would show a notification in the top right corner of the page whenever a new item was added to a certain list or library.
I also wanted to give users the ability to "subscribe" to a list or library, and hence let them see only notifications whenever a new item was added to a list or library to which they were subscribed, so I also made a script to handle the subscribing.

These are the two scripts I've written: notification.js and subscribe-to-list-or-library.js.
Both scripts are working in the latest versions of Google Chrome, Mozilla Firefox and Internet Explorer.

Since I don't want to put too much comments in the code, I just decided to explain both scripts first and then provide you with the code. So please take some time to read how these scripts work and what steps they contain, and then take a look at the code. It will be a lot easier to understand the code when you read the explanation about it first.

Script: subscribe-to-list-or-library.js

This script has the following features:
  • subscribe to a list or library by clicking a button named "subscribe"
  • unsubscribe from a list or library by clicking a button named "unsubscribe"
  • be subscribed to multiple lists or libraries
  • receive live notifications whenever a new item was added to one or more lists or libraries to which a user is subscribed
Do note that it's possible to subscribe to a list or library from any sub site. So for example, you can subscribe to list X on sub site A, and library Y on sub site B, and receive notifications from list X and library Y while browsing content on sub site C. I tried explaining this as easy as I could, so there you have it.
Also, regarding the buttons: there is no such thing as a button to subscribe and another button to unsubscribe. They are one and the same button. Actually it's not even a button. It's a div element disguised as a button. I just change the text inside the div based on whether or not a user is subscribed. If a user is, then the text in the div will be "unsubscribe". If a user is not, then it will be "subscribe". Simple as that.

Steps on how this script works

First of all, you need to provide a button (a div, actually, but I'll call it a button) on a page that has a web part of a list or library.  The button contains two other elements, one div that will hold either the text "subscribe" or "unsubscribe", and one hidden div that will hold the name of the list or library a user should be able to subscribe to. This name has to be correct and is case sensitive.
And now for the steps. Keep in mind that part of the script runs as soon as the page has loaded, and certain functions only run on the click of a button.

On page load:
  • Find all elements that have a class named "subscrButton" (these are the subscribe buttons), and for each element, store the name of the list/library in a variable, as well as the text from the first div (which contains either "subscribe" or "unsubsribe").
  • Still in the "for each" statement: get all list items from a list named "Subscribed users", and for each row in that list, check if the current user is in that row and if he/she is subscribed to a list/library with the same name as the one stored in a variable.
    • If true, set a boolean named "inList" to true.
    • If false, set a boolean named "inList" to false.
  • If the boolean named "inList" is false, then set the innerHTML of the first div to "subscribe".
  • If the boolean named "inList" is true, then set the innerHTML of the first div to "unsubscribe".
So that part of the code is only to check whether or not a user is already subscribed to a list or library, and set the correct text to the button.Next comes the actual subscribe/unsubscribe functionality.

On button click:
  • Store the name of the list or library linked to that button, in a variable.
  • Store the name of the page on which the user clicked the button, in a variable.
  • Store the path to the sub site in which the page with the button that the user clicked on is located, in a variable.
  • Store the text of the button ("subscribe" or "unsubscribe") in a variable.
  • If the text of that variable equals "unsubscribe":
    • Set the innerHTML of the button that was clicked to "subscribe".
    • Check the "Subscribed users" list and look for the row that contains both the name of the user and the name of the list/library from which the users wants to unsubscribe.
      • If the row has been found, set another variable named "listIDDel" to the ID of the current row that matches the name of the user and the name of the list/library.
    •  Update the "Subscribed users" list by deleting the row that has the id matching "listIDDel" (and thus, removing the user from the "Subscribed users" list).
    • After the user has successfully been deleted from the "Subscribed users" list, run some functions named "runInOtherFile" and "runDeleteInOtherFile" which are both located in notification.js. I'll explain the use of these later.
  • If the text of that variable equals "subscribe":
    • Set the innerHTML of the button that was clicked to "unsubscribe".
    • Update the "Subscribed users" list by making a new item with the following values: the name of the user, name of the list/library, url of the page, path to the sub site.
    • After the user has successfully been added to the "Subscribed users" list, run a function named "runInOtherFile" which is located in notification.js. I'll explain the use of this one later.

Script: notification.js

This script has the following features:
  • Send a notification whenever a new item was added to a list or library to which a user is subscribed

A minor down side: the script does not keep track of which items were added to certain lists or library since the last time a user logged on. So it's not possible to store notifications somewhere about new items and greet a user with a list of all the new items that were added since the last time he/she logged in. This script is for live notifications, nothing else.

It also won't show a globe with a number on top of it saying "you have x new notifications" like on Facebook, I'm not keeping track of that. While I might be able to add such functionality to this script, this will be for another time.

Steps on how this script works

The script will contain two types of code. One that will run only on page load, and one that will run every five seconds.

On page load:
  • Check the "Subscribed users" list and look for rows that contain both the name of the user and the name of any list/library to which a user is subscribed.
    • For each row in that list: check if the user is in the row. If he/she is, raise the value of a counter by 1 and push the current row to an array. We then push the content of that array to another array, which will act as a container array (multidimensional array, actually).
  • If the counter equals 0, meaning no rows matching the current user were found, nothing will happen and the notification script will stop.
  • If the counter is not 0, meaning one or more occurences of the current user were found in the "Subscribed users" list, we will do a loop. For each item in the multidimensional array:
    • Store a unique session in the session.
    • Set an interval of five seconds for a function named "repeatEvery5Seconds".
Every five seconds:
  • The function "repeatEvery5Seconds" will run for each item in the multidimensional array, storing the following values in variables: the name of the user, name of the list/library, url of the page, path to the sub site, name of the session item. 
  • The function "repeatEvery5Seconds" will trigger another function named "fetchCurrentListStatus". 
    • The function "fetchCurrentListStatus" will check each list or library to which a user is subscribed to, and push each row to an array.
      • If the length of the array is null or 0, the list will be considered empty and an empty session variable will be set.
      • If the length of the array is not null or not 0:
        • If there is a session item present for the current list and if that session item is not empty, then reset that session item to a new value matching the array and run a function named "itemChange".
        • Else if there is a session item present for the current list and if that session item is null or 0, then reset the session item to a new value matching the array and run a function named "itemChange". 
        • If the length of the array matches the length of the session item, then that indicates no changes were made.
    • The function "itemChange" will run when called.
      • If the length of the array is smaller than the length of the session item, then this means that an item was deleted from the list/library. The session item will then be updated, and will contain the same value as the array.
      • Else if the length of the array is larger than the length of the session item, then this means that a new item was added to the list/library.
        • We will then compare all elements in the array with all elements in the session item, and if we find the one that is not present in the session storage, we will search for that item in the list/library and fetch its name. 
          • We then create a div that will be our notification, and we will give it a text containing the name of the new item and a link to the page on where the user can find it. After five seconds, the notification will be removed.
        • We then check if the session item matches the array. If not, set the session item to match the value of the array. 
Only when called by "subscribe-to-list-or-library.js": 
  • The function "runInOtherFile":
    • Will run with a timeout of one second, and will replace the array that was made on page load and that holds the list of all the lists/libraries to which a user is subscribed. Since the user decided to unsubscribe from a list/library, this array needs to be updated. This function will do so be checking the "Subscribed users" list again.
  • Yhe function "runDeleteInOtherFile":
    • For each item (array) in the multidimensional array, check if there is an item that matches the name of the list from which the user wanted to subscribe. If there is, then find the corresponding session item and set its value to nothing. And then we will remove the item from the multidimensional array (by using the split function).

There you have it. That was just the explanation about the scripts. I tried to write it as short as possible. Nest step: setting up the necessary lists and buttons.

Prerequisites 

Before you can actually use the scripts, you'll need to set up some things first. Like a list where you will store your subscribed users, and buttons for each list/library you want to have your users subscribe to.

Create a list for your subscribers

You need to make a simple list at the top site level named "Subscribed users". If you want my code to work straight away, then I suggest you use that name.

Your will then need to make three new columns for your list: ListLibID, PageURL and SubsiteURL. I originally used to store the GUID of a list/library in the ListLibID column, but after several problems with that in different browsers I decided to just use the name of a list or library instead of the GUID. That was also the main reason on why I also needed a column to save the subsite in (required to call the list across sub sites).

Make sure your list named "Subscribed users" is allowed to be edited by everyone, meaning everyone should have edit permissions. You can change the settings of the view of the list so that it is only visible to you or not visible at all (use the filter for this, set some impossible filter so no item will ever be shown but will still be present in the list).That way you can avoid users sneaking around and trying to meddle with the list. I just used CSS to hide that particular list and the list itself from the site content, so nobody ever finds it.

Create buttons for your lists/libraries

Now that you have your list ready, it is time to make some buttons. You'll only need to make these for list or libraries of which you want users to be able to subscribe to.

On the page that holds the web part of the list/library of which users should be able to subscribe to, place the following code directly above the code from the web part:
<div class="buttonContainer">
   <div class="subscrButton">
      <div class="subscrButtonTitle"></div>
      <div class="innerSubscr" style="display: none;">
         List or library name here​​
      </div>
   </div>
</div>

In case you want it, here's the CSS of the button above (note: not identical to the style as seen in screenshots!):
.buttonContainer {
 position: relative;
}
.subscrButton {
 background-color: white;  
 border: 1px solid rgb(185, 184, 184);
 font-family: "Segoe UI Semilight","Segoe UI","Segoe", Tahoma,
               Helvetica,Arial,sans-serif;
 color: #0066CC;
 text-align: center;
 z-index: 100;
 position: absolute;
 right: 0;
 padding: 3px 10px 5px 10px;
 -webkit-border-radius: 5px;
 -moz-border-radius: 5px;
 border-radius: 5px;
 opacity: 0.8;
}

Add references to the scripts

I suggest you just put a reference to the scripts in your master page. For example:
<!--SPM:<sharepoint:scriptlink id="scriptLink12" language="javascript" 
 localizable="false"  ondemand="false" runat="server"
 name="~sitecollection/Style Library/Scripts/subscribe-to-list-or-library.js">
-->
<!--SPM:<sharepoint:scriptlink id="scriptLink13" language="javascript" 
 localizable="false" ondemand="false" runat="server"
 name="~sitecollection/Style Library/Scripts/notification.js">
-->

Make sure your master page and your scripts are checked in.

Now, for the most interesting part, the actual code.

The code

subscribe-to-list-or-library.js

SP.SOD.executeOrDelayUntilScriptLoaded( 'SP.UserProfiles.js', 
   "~sitecollection/Style%20Library/Scripts/jquery.SPServices-2013.01.min.js");
SP.SOD.executeFunc('SP.js', 'SP.ClientContext');

var firstDiv;
var inList = new Boolean(); 
inList = false;
var listIDDel;
var SURL = $().SPServices.SPGetCurrentSite();
var PURL = window.location.pathname;
var LLID;
var USER = $().SPServices.SPGetCurrentUser({ 
 webURL: "", 
 fieldName: "Title", 
 fieldNames: {}, 
 debug: false
});

$("#content").find($("div.subscrButton")).each(function(){
 LLID = this.childNodes[1].innerHTML.replace(/[\u200B]/g, ''); 
 firstDiv = this.childNodes[0].innerHTML.replace(/[\u200B]/g, '');
 $().SPServices({
  operation: "GetListItems",
  async: false,     
  webURL: 'https://your-site-here.com/',
     listName: 'Subscribed users',
  completefunc: function (xData, Status) {
   $(xData.responseXML).find("z\\:row, row").each(function() {
    if (($(this).attr("ows_Title") == USER) 
        && ($(this).attr("ows_ListLibID") == LLID)) {
     inList = true;
    }
   });
  }
 });

 if (inList == false) {
  this.childNodes[0].innerHTML = 'SUBSCRIBE';
 }
 else if (inList == true) {
  this.childNodes[0].innerHTML = 'UNSUBSCRIBE';
  inList = false;
 }
});

$("div.subscrButton").click(function(){ 
 LLID = this.childNodes[1].innerHTML.replace(/[\u200B]/g, ''); 
 firstDiv = this.childNodes[0].innerHTML.replace(/[\u200B]/g, '');
 console.log(firstDiv);
 if (firstDiv == "UNSUBSCRIBE") {
  this.childNodes[0].innerHTML = 'SUBSCRIBE';
  console.log('User "' + USER + '" wants to unsubscribe. ');
  
  $().SPServices({
   operation: "GetListItems",
   async: false,     
   webURL: 'https://your-site-here.com/',
      listName: 'Subscribed users',
   completefunc: function (xData, Status) {
    $(xData.responseXML).find("z\\:row, row").each(function() {
     if (($(this).attr("ows_Title") == USER) 
         && ($(this).attr("ows_ListLibID") == LLID)) {
      listIDDel = $(this).attr("ows_ID");
     }
    });
   }
  });
         
  $().SPServices({
   operation: 'UpdateListItems',
   webURL: 'https://your-site-here.com/',
   listName: 'Subscribed users',
   updates: '<batch onerror="Continue" precalc="True">' + 
              '<method cmd="Delete" id="1">' +
              '<field name="ID">'+ listIDDel +'</field>' +
              '<field name="Title">'+ USER +'</field>' +
              '<field name="ListLibID">'+ LLID +'</field>' +
              '<field name="PageURL">'+ PURL +'</field>' +
              '<field name="SubsiteURL">'+ SURL +'</field>' +
              '</method>' +
           '</Batch>',
   completefunc: function(xData, Status) {
      console.log('User "'+USER+'" is now removed from the subscribers list.'); 
   }
  });

  runInOtherFile();
  runDeleteInOtherFile(LLID);

 }
 else if (firstDiv == "SUBSCRIBE") {
  this.childNodes[0].innerHTML = 'UNSUBSCRIBE';
  console.log('User "'+USER+'" wants to subscribe. ' + 
              'Adding user to subscribers list now.');
  
  $().SPServices({
   operation: 'UpdateListItems',
   webURL: 'https://your-site-here.com/',
   listName: 'Subscribed users',
   updates: '<batch onerror="Continue" precalc="True">' + 
              '<method cmd="New" id="1">' +
              '<field name="Title">'+ USER +'</field>' +
              '<field name="ListLibID">'+ LLID +'</field>' +
              '<field name="PageURL">'+ PURL +'</field>' +
              '<field name="SubsiteURL">'+ SURL +'</field>' +
              '</method>' +
           '</batch>',
   completefunc: function(xData, Status) {
    console.log('User "'+USER+'" has been added to the subscribers list.');  
      }
  }); 
  runInOtherFile();
 }
});

notification.js

var USER = $().SPServices.SPGetCurrentUser({ 
 webURL: "", 
 fieldName: "Title", 
 fieldNames: {}, 
 debug: false
});

var currentViewUser;
var currentViewLLIB;
var currentViewPURL;
var currentViewSURL;
var currentViewCOUNT;
var counter = 0;
var tempArr;
var itemArray = new Array();
var itemArrayContainer = new Array();
var itemArrayReplaced = new Array();
var itemArrayContainerReplaced = new Array();


console.log('Notification script has been loaded! Starting script now...'
          + '\n--------------------------------------------------------');
runOnLoad();

function runOnLoad() {
 $().SPServices({
  operation: "GetListItems",
  async: false,        
  webURL: 'https://your-site-here.com/',
     listName: 'Subscribed users',
  completefunc: function (xData, Status) {
   $(xData.responseXML).SPFilterNode("z:row").each(function() {
    if ($(this).attr("ows_Title") == USER) {
     counter++;
     itemArray[counter] = new Array($(this).attr("ows_Title"), 
                          unescape($(this).attr("ows_ListLibID")), 
                          $(this).attr("ows_PageURL"), 
                          $(this).attr("ows_SubsiteURL"), counter);
     itemArrayContainer.push(itemArray[counter]);
    }
   });
  }
 });
}

if (counter != 0) {   
 for (var j = 0; j < itemArrayContainer.length; j++) {
  sessionItem = "sessionItem" + (j + 1);
  sessionStorage.setItem(sessionItem, "");
 }
 function repeatEvery5Seconds() {
  for (var i = 0; i < itemArrayContainer.length; i++) {
   currentViewUser = itemArrayContainer[i][0];
   currentViewLLIB = itemArrayContainer[i][1]; 
   currentViewPURL = itemArrayContainer[i][2];
   currentViewSURL = itemArrayContainer[i][3];
   currentViewCOUNT = "sessionItem" + itemArrayContainer[i][4];
   fetchCurrentListStatus(currentViewLLIB, currentViewCOUNT);
  }
 }
 var t = setInterval(repeatEvery5Seconds,5000);
}
else {
 console.log('User "'+currentViewUser+'" is not in the subscribers list. '
             + 'Notifications will not be shown.');
}

function fetchCurrentListStatus(currentViewLLIB, currentViewCOUNT) {
 var listItemArray = new Array();
 var listItemArrayBackup = new Array();

 $().SPServices({
  operation: "GetListItems",
  async: false,     
  crossDomain: true,
  webURL: currentViewSURL,
     listName: currentViewLLIB,
  completefunc: function (xData, Status) {
   $(xData.responseXML).find("z\\:row, row").each(function() {
    var rowData = $(this).attr("ows_ID");
    listItemArray.push(rowData);
    listItemArrayBackup.push(rowData); 
   });
  }
 });
        
 if (listItemArray.length == null || listItemArray.length == 0) {
  console.log('List is empty.');
  sessionStorage.setItem(currentViewCOUNT, listItemArray);
 }
 else if (listItemArray.length != null || listItemArray.length != 0) {
  console.log('Server list: \t\t"' + listItemArray + '"');
  if ( sessionStorage.getItem(currentViewCOUNT).length != 0 
    || sessionStorage.getItem(currentViewCOUNT) != 0 
    || sessionStorage.getItem(currentViewCOUNT) != "" ) {
   tempArr = sessionStorage.getItem(currentViewCOUNT).split(",");
   console.log('Session list: \t\t"' + tempArr + '"');
   itemChange();
  }
  else if (sessionStorage.getItem(currentViewCOUNT).length == 0 
        || sessionStorage.getItem(currentViewCOUNT) == 0 
        || sessionStorage.getItem(currentViewCOUNT) == "" ) {
   sessionStorage.setItem(currentViewCOUNT, listItemArray);
   tempArr = sessionStorage.getItem(currentViewCOUNT).split(",");
   console.log('Session list: \t\t"' + tempArr + '"');
   itemChange();
  }
  if (tempArr.length == listItemArray.length) {
   console.log('No new item was added in the past 5 seconds.'
             + '\n--------------------------------------------------------'); 
  }
 }

 function itemChange() {
  if (listItemArray.length == tempArr.length) {}
  else if (listItemArray.length < tempArr.length) {
   console.log('An item was deleted. Now updating session storage.');
   sessionStorage.setItem(currentViewCOUNT, listItemArray);  
  }
  else if (listItemArray.length > tempArr.length) {
   console.log('An item has been added. Now updating session storage.'); 
   var array1 = listItemArray;
   var array2 = tempArr;
   var index;
   for (var i=0; i<array2 .length="" i="" if="" index=""> -1) {
           array1.splice(index, 1);
       }
   }
   var currentItemName;
   var newListItemArray = new Array(); 

   for (var i = 0; i < array1.length; i++) {
       var currentItem = array1[i];
     $().SPServices({
        operation: "GetListItems",
        ID: currentItem,
        async: false,
     crossDomain: true,
     webURL: currentViewSURL,
        listName: currentViewLLIB,
        completefunc: function (xData, Status) {
          $(xData.responseXML).SPFilterNode("z:row").each(function() {
             if ($(this).attr("ows_ID") == currentItem) {
                currentItemName = $(this).attr("ows_LinkFilename"); 
             }
          newListItemArray.push(currentItemName);
          });
       }
    });
   }

   var div = document.createElement("div");
   div.style.width = "auto";
   div.style.height = "21px";
   div.style.background = "#F7F7F7";
   div.style.color = "#3C82C7";
   div.style.border = "1px solid #d7d6d8";
   div.style.float = "right";
   div.style.padding = "5px";
   div.style.borderBottomLeftRadius = "4px";
   div.innerHTML = 'A document named "' + currentItemName 
                  + '" was added to <a href="' + currentViewPURL + '">' 
                  + currentViewLLIB + '</a>.';
   div.className = "notification";
   document.getElementById("notificationArea").appendChild(div);
   $(document.getElementsByClassName("notification")).hide();
   $(document.getElementsByClassName("notification")).fadeIn(1500);
   setTimeout(function() { 
    $(document.getElementsByClassName("notification")).fadeOut(1500);
    document.getElementById("notificationArea").removeChild(div);
   }, 5000);
   
   if (tempArr != listItemArray) {
    sessionStorage.setItem(currentViewCOUNT, listItemArrayBackup);
   }
  }
 }
}

function runInOtherFile() {
 console.log('Now running "runInOtherFile()". ');
 counter = 0;
 setTimeout(function() { 
 itemArrayContainerReplaced.length = 0;
  $().SPServices({
   operation: "GetListItems",
   async: false,        
   webURL: 'https://your-site-here.com/',
      listName: 'Subscribed users',
   completefunc: function (xData, Status) {
    $(xData.responseXML).SPFilterNode("z:row").each(function() {
     if ($(this).attr("ows_Title") == USER) {
      counter++;
      itemArrayReplaced[counter] = new Array($(this).attr("ows_Title"), 
                                   unescape($(this).attr("ows_ListLibID")), 
                                   $(this).attr("ows_PageURL"), 
                                   $(this).attr("ows_SubsiteURL"), counter);
      itemArrayContainerReplaced.push(itemArrayReplaced[counter]);
     }
    });
   }
  });
  console.log('List of subscribed users has been updated.'
            + '\n--------------------------------------------------------'); 
  itemArrayContainer = itemArrayContainerReplaced;
 }, 1000);
} 

function runDeleteInOtherFile() {
 for (var k = 0; k < itemArrayContainer.length; k++) {
  if (itemArrayContainer[k][1] == LLID) {
   var tempCurrentViewCOUNT = "sessionItem" + itemArrayContainer[k][4];
   sessionStorage.setItem(tempCurrentViewCOUNT, "");  // 0);
   itemArrayContainer.splice(k, 1);
  } 
 }
 console.log('Session corresponding to the list/library from which user ' 
            +'has unsubscribed is now empty.');
}

That's all the code. All of it. Everything you need to get fancy notifications. You can always change the style of the notification, it may be in the script but you can just personalize it in whatever way you want to.

Some screenshots

Console - when subscribed to two lists/libraries, first five seconds:

Console - when subscribed to two lists/libraries, after more than fifteen seconds:

Console - when a new item has been added to a list/library to which the current user is subscribed to:

Browser - notification the current user sees when a new item was added to a list/library to which the current user is subscribed to (translation: "A document named 'mouse-pointer.jpg' was added to TESTER."):

Console - when an item gets deleted from a list/library to which the current user is subscribed to:

Console - when the current user unsubscribes from a list/library (it's the one with the long array of ID's):

Console - when the current user subscribes to a list/library (again, the one with the long array of ID's):

Browser - the buttons; there is only one button per list but I used an image editor to place these next to each other, just to demonstrate:

The end

That was it, the whole blog post. My longest one so far I believe. I hope I explained things clear enough, I did my best to tell you every bit of information I have about these scripts. I hope that one day I'll be able to rewrite this functionality in C#, to make it server-side. But until that day, I'm happy with this and I hope you are happy with it too. :)

Do let me know if you are having trouble or problems with one of the scripts and I'll do my best to help you out.

As an extra tip, I suggest you minify the scripts and get rid of all console logs. Just to make it less
heavy.

Enjoy!

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. ;)