SPServices Stories #22 : SPServices SharePoint Attachments in Internet Explorer 9

This entry is part 21 of 21 in the series SPServices Stories

Introduction

This is another story I ran across on Twitter and wanted to add to the SPServices Stories series. I wrote about my own experience getting attachment uploading to work in my post Uploading Attachments to SharePoint Lists Using SPServices. My cheat in that case was to change the DOCTYPE to HTML5 in a SharePoint 2010 master page. It was a cheat in the sense that it broke a lot of basic SharePoint functioning. Luckily I could get away with it in that particular case.

In this story, Jason Williams (@JayWll) explains how he got uploading to work with the fileReader polyfill. As much as all of us would like to be working with the latest and greatest technologies, in reality most organizations – especially larger ones – don’t update their tech very often. In Jason’s case he needed the upload capability to work with IE9, which doesn’t have a lot of the HTML5 spec implemented in it.

I’ve combined two of Jason’s posts here, SPServices addAttachment jquery example and SPServices SharePoint Attachments in Internet Explorer 9. Jason built on a post from Brendon Wilbore (@bjwildbore) to get things up and running. This is one of the great things about the SharePoint community: there are many people out there who are generous enough to post their work so that we can build upon it.

SPServices SharePoint Attachments in Internet Explorer 9

Brendon Wilbore:

If you’re having a few issues adding attachments via ajax and SPServices on SharePoint have a look over the code snippets below.

To upload a file to a list you need to make use of the fileReader javascript class, using the readAsDataURL method and stripping the first part off the dataurl to get the base64 component. Then submit this to SPServices.

I’ve been asked a few times to add the ability to upload attachments to SharePoint tools that I’ve created, and I’ve never been able to achieve it until I eventually came across this blog post last week.

If (like me) you’re developing in a front-end only way without any server-side programming then it seems like this is the way to upload files and attach them to SharePoint list items.

It relies on the javascript fileReader feature so your users will need a fairly modern browser… which is where I ran into trouble. The default browser deployed within my company is Internet Explorer 9, and that doesn’t have fileReader support.

With much work and even more googling I was able to get this technique to work in Internet Explorer 9. In the future I’ll write more about how I managed it, and how you can too!


There was a problem, though – the solution relies on the fileReader JavaScript feature which requires Internet Explorer 10, and the default browser deployed within my organization is Internet Explorer 9. What we need is a fileReader alternative for older browsers. Thankfully, such a thing exists. Today I’m going to post some example code that uses the fileReader polyfill and works in older browsers.

What You Need

The code has several pre-requisites. You’ll need jQuery, jQuery UI, SPServices, SWFObject and the JavaScript and flash file that form the fileReader polyfill.

For the purposes of my demo I created a simple SharePoint list called “File Attachment Test.” The list has a single field – title – and attachments to the list are enabled. Your list is probably named differently, so you’ll need to change the references in the code to reflect your list name.

The Code

<html>
<head>
  <meta charset="utf-8" />
  <title>File Attachment Test</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js"></script>
  <script type="text/javascript" src="js/jquery.FileReader.min.js"></script>
  <script type="text/javascript" src="js/jquery.SPServices-2013.01.min.js"></script>
  <script type="text/javascript">
    var selectedfile = false;

    $(document).ready(function() {
      $('input#itemfile').fileReader({filereader: 'js/filereader.swf'});

      $('input#itemfile').change(function(e) {
        selectedfile = e.target.files[0];

        $('span#filename').html(selectedfile.name);
        $('span#fileinput').hide();
      });

      $('input#createitem').click(function() {
        $().SPServices({
          operation: 'UpdateListItems',
          async: false,
          listName: 'File Attachment Test',
          batchCmd: 'New',
          webURL: '/demo',
          valuepairs: [
            ['Title', $('input#itemtitle').val()]
          ],
          completefunc: function(xData, Status) {
            if (Status == 'success' && $(xData.responseXML).find('ErrorCode').text() == '0x00000000') {
              currentitem = $(xData.responseXML).SPFilterNode("z:row").attr("ows_ID");
              alert('List item created with ID ' + currentitem);

              if (selectedfile) {
                filereader = new FileReader();
                filereader.filename = selectedfile.name;

                filereader.onload = function() {
                  data = filereader.result;
                  n = data.indexOf(';base64,') + 8;
                  data = data.substring(n);

                  $().SPServices({
                    operation: 'AddAttachment',
                    async: false,
                    listName: 'File Attachment Test',
                    listItemID: currentitem,
                    fileName: selectedfile.name,
                    attachment: data,
                    completefunc: function(xData, Status) {
                      alert('File uploaded');
                    }
                  });
                };

                filereader.onabort = function() {
                  alert('Upload aborted');
                };

                filereader.onerror = function() {
                  alert('Upload error');
                };

                filereader.readAsDataURL(selectedfile);
              }
            } else alert('List item creation failed');
          }
        })
      });
    });
  </script>
</head>
<body>
  <p>Title:<br><input type="text" id="itemtitle"></p>
  <p>File:<br><span id="fileinput"><input type="file" id="itemfile"></span><span id="filename"></span></p>
  <p><input type="button" id="createitem" value="Go!"></p>
</body>
</html>

Notes

The fileReader polyfill takes the file input box and puts the flash file on top of it, so that the file selection and upload is handled by flash instead of natively in the browser. I found that this fell apart if the file input box didn’t remain in the same place on the page. In other words, I had problems if I tried to use jQuery’s .show() and .hide() functions (or similar).

I solved this by putting the file selection form in a pop-up window. If the page you place your form on is static (i.e. nothing changes after the DOM is loaded) then you shouldn’t have this problem.

Enjoy!

SPServices Stories #21 – Redirect If User Clicked a Button Previously

This entry is part 22 of 21 in the series SPServices Stories

Introduction

teylynIngeborg Hawighorst (@IngeborgNZ) is a long-time SPServices user who has come up with any number of intriguing uses for the library. I’d recommend her blog anytime if you’d like to learn about interesting things you can do with SharePoint, but even more so if Excel is your bag. Ingeborg has been an Excel MVP for years running (see her profile on the MVP site). Some of the best solutions using SPServices come out of discussions in various SharePoint-oriented forums. In this case, Ingeborg spotted some suggestions from Eric Alexander (@ejaya2 aka PirateEric) and decided to build it out. Without further ado, here is Ingoborg’s article, reposted from her blog cheers, teylyn.

Redirect If User Clicked a Button Previously

I just came across this question in sharepoint.stackexchange.com. When a user visits a SharePoint site, they are presented with a splash screen and need to accept the policy before they can proceed. Upon subsequent visits, the splash screen does not show, because SharePoint will remember the  user. PirateEric outlined a possible solution: Use a SharePoint list to save the user name when the button is clicked. When the page loads, look up the user in the list. If they already exist, redirect the page, if not, show the splash page with the button to accept the policy. If the policy changes and users need to be made aware of that, simply remove all items in the list that tracks the users. All this can be done with jQuery and web services. That intrigued me and I had a go at actually building this, using Marc Anderson’s SPServices.

How to set it up

Create a SharePoint custom list with two fields, Title and UserName. The former is the out of the box field, the latter is a simple text field. Create two pages, the Splash page with the button and the page that is the desired destination page for all visitors. In my sample these are called Splash.aspx and MainContent.aspx On the Splash page the code that you can see below will be loaded before any other web part. If you use a Content Editor Web Part to load the code with a content link, make sure that it’s the first web part on the page. In many cases, JavaScript and jQuery will be placed in the last web part of the page and run after the DOM has loaded. But in this case this would mean that the Splash page appears briefly, even if it is followed by a redirect to a different page. The Splash page provides the policy (or terms and conditions) that the user must accept, and a link or a button that the user can click to accept. This link or button must then trigger a JavaScript function. That is very easy to do. I used a button and put the html straight into a CEWP like this:

<button onclick="PolicyButtonClick()" type="submit">
   I accept the policy
</button>

So the user clicks the button and the JavaScript function PolicyButtonClick() will run. This function can be found in the code below. First, the jQuery library and the SPServices library are loaded. Then the user name of the current user is retrieved with the SPServices call using SPGetCurrentUser. It returns a text string in the format DOMAIN\Account.  Next, the SPServices call uses the GetListItem. In the call, a CAML query is constructed that will return only list items where the column UserName equals the current user.  The items returned by that query are then counted. Since the user account is a unique value, we can safely assume that the query will either return one result or no result at all. If the query returned an item, this means that the user has previously accepted the policy and the script will redirect to the MainContent.aspx page.  If the query did not return anything, the current page will continue to be displayed. When the user clicks the button to accept the policy,  the user name is written into a variable. Then the SPServices operation to UpdateListItem is called and will create a new item in the list “PolicyAccepted”, storing the previously established account name in the column UserName. Then the MainContent.aspx page is loaded. The next time the user opens the Splash page, their account name will be found in the PolicyAccepted list and they will not see the Splash page again, unless the entry in the list is deleted. Here is the complete script:

<script type="text/javascript" src="/path/jquery-1.10.2.min.js" language="javascript"></script><script type="text/javascript" src="/path/jquery.SPServices-2013.02a.min.js" language="javascript"></script><script type="text/javascript">// <![CDATA[
// start the code even before the DOM is loaded, so not waiting for document ready
//$(document).ready( function() {
// get the user name
 var userName= getUserName();
// find the user name in the list
 var userAccepted = matchUserName(userName);
 if (userAccepted == 1 )
 {
 // redirecting page
 window.location.replace("http://path/Documents/MainContent.aspx");
 }
//});

function getUserName() {
 var thisUserAccount= $().SPServices.SPGetCurrentUser({
 fieldName: "Name",
 debug: false
 });
 return(thisUserAccount);
}

function createNewItem(theTitle, theUser) {
 $().SPServices({
 operation: "UpdateListItems",
 async: false,
 batchCmd: "New",
 listName: "PolicyAccepted",
 valuepairs: [["Title", theTitle], ["UserName", theUser]],
 completefunc: function(xData, Status) {
 }
 });
}

function matchUserName(userName) {
 var queryText = "<Query><Where><Eq><FieldRef Name='UserName'/><Value Type='Text'>" + userName + "</Value></Eq></Where></Query>";
 $().SPServices({
 operation: "GetListItems",
 listName: "PolicyAccepted",
 async: false,
 CAMLQuery: queryText,
 completefunc: function (xData, status) {
 itemCount = $(xData.responseXML.xml).find("rs\\:data, data").attr("ItemCount");
 }
 });
 return(itemCount);
}

function PolicyButtonClick() {
 var userName= getUserName();
 var theTitle= "Accepted";
 createNewItem(theTitle, userName);
 window.location.href = "http://path/Documents/MainContent.aspx";
}
// ]]></script>

SPServices Stories #20 – Modify User Profile Properties on SharePoint Online 2013 using SPServices

This entry is part 20 of 21 in the series SPServices Stories

Introduction

Sometimes people ask me why I’m still bothering with the crufty old SOAP Web Services in SPServices. After all, there are REST and CSOM to play with and Microsoft has decided to deprecate the SOAP Web Services.

Well in some cases, the Shiny New Toys don’t let you get the job done. In cases where you’re implementing on Office365 and simply can’t deploy server side code, SPServices can sometimes be just the right tool. It’s easy to use and it gets stuff done that you need. What more can I say?

I found this nice story from Gary Arora about updating User Profile data a few weeks back. In it, Gary shows us how to easily make those updates on Office365 using SPServices. Gary calls himself “your friendly neighborhood SharePointMan” and he is happy to be have his story be one of my stories.

The Use Case

SharePoint 2013 users need to modify (specific) user-profile-properties client-side without having to navigate away to their ‘MySite’ site and swift through rows of user properties.
(Following is an mock-up showing a simple interface to update a user profile property via CEWP)

Modify_User_Profile_Properties_SharePoint

Simple interface to update Fax number from a CEWP. (Basic demo)

The Usual Solution

In the SharePoint 2013 universe there are 2 ways to read/write data client-side. CSOM and REST. Unfortunately CSOM and REST are not fully there yet when it comes to matching the server side functionality.

In this specific case, one could use CSOM or REST to retrieve (read) User Profile Properties but there is no way to modify (update) these properties from client-side. Here’s Microsoft’s official position.

Not all functionality that you find in the Microsoft.Office.Server.UserProfiles assembly is available from client APIs. For example, you have to use the server object model to create or change user profiles because they’re read-only from client APIs (except the user profile picture)

Hence The Dirty Workaround

So our workaround is SOAP, the forgotten granddaddy of Web services. The User Profile Service web service, from the SharePoint 2007 days, has a method called ModifyUserPropertyByAccountName which functions exactly as it sounds. But since SOAP can be a bit intimidating & ugly to write, we’ll use SPServices, “a jQuery library which abstracts SharePoint’s Web Services and makes them easier to use”

So here’s how we’ll use SPServices to modify User Profile Properties. The method is applicable to SharePoint 2013 & 2010 (online & on-prem) versions.

1. Reference SPServices library

You have two options here. You can either download the SPService library and reference it locally or reference it from its CDN:

<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2013.01/jquery.SPServices-2013.01.min.js"></script>

2. Create updateUserProfile() Function

The following function simulates the ModifyUserPropertyByAccountName method on the User Profile Service web service.

function updateUserProfile(userId, propertyName, propertyValue) {

  var propertyData = "<PropertyData>" +
  "<IsPrivacyChanged>false</IsPrivacyChanged>" +
  "<IsValueChanged>true</IsValueChanged>" +
  "<Name>" + propertyName + "</Name>" +
  "<Privacy>NotSet</Privacy>" +
  "<Values><ValueData><Value xsi:type=\"xsd:string\">" + propertyValue + "</Value></ValueData></Values>" +
  "</PropertyData>";
  
  $().SPServices({
    operation: "ModifyUserPropertyByAccountName",
    async: false,
    webURL: "/",
    accountName: userId,
    newData: propertyData,
    completefunc: function (xData, Status) {
      var result = $(xData.responseXML);
    }
  });

}

3. Invoke updateUserProfile() Function

This function takes 3 parameters.

  • userId: Your userID. The format is “domain\userId” for on-prem and “i:0#.f|membership|<federated ID>” for SharePoint Online.
  • propertyName: The user profile property that needs to be changed
  • propertyValue: The new user profile property value

Example:

updateUserProfile(
  "i:0#.f|membership|garya@aroragary.onmicrosoft.com",
  "Fax", "555 555 5555");

Note: The above code works but notice that you are passing a hardcoded userId.

To pass the current userId dynamically, we can use CSOM’s get_currentUser(). But since that’s based on the successful execution of ClientContext query, we need to “defer” invoking “updateUserProfile()” until we have received current userId. Therefore we’ll create a Deferred object as follows:

function getUserLogin() {
  var userLogin = $.Deferred(function () {
    var clientContext = new SP.ClientContext.get_current();
    var user = clientContext.get_web().get_currentUser();
    clientContext.load(user);
    clientContext.executeQueryAsync(
      function () {
        userLogin.resolve(user.get_loginName());
      }
      ,
      function () {
        userLogin.reject(args.get_message());
      }
      );
  });
  return userLogin.promise();
}

Now when we invoke updateUserProfile(), it will execute getUserLogin() first, implying that the ClientContext query was successful :

getUserLogin().done(function (userId) {
  updateUserProfile(userId, "Fax", "555 555 5555");
});

4. Full working code

Steps to replicate the demo

  1. Copy the following code snippet and save it as a text file (.txt)
  2. Upload this text file anywhere on your SharePoint site (e.g. Site Assets). Remember its path.
  3. On the same SharePoint site, add/edit a page and insert a CEWP (Content Editor Web Part)
  4. On the web part properties of CEWP, add the path to the text file under “Content Link” section
  5. Click Ok, and save the page.
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2013.01/jquery.SPServices-2013.01.min.js"></script>
<script type="text/javascript">

//updateUserProfilePreFlight defers until getUserId() is "done"
//then it invokes updateUserProfile
function updateUserProfilePreFlight(){
  getUserId().done(function (userId) {
    var propertyName = "Fax"
    var propertyValue =  $("#Fax").val();
    updateUserProfile(userId, propertyName, propertyValue);
  });
}

//getUserLogin() uses CSOM to retrive current userId.
function getUserId() {
  var userLogin = $.Deferred(function () {
    var clientContext = new SP.ClientContext.get_current();
    var user = clientContext.get_web().get_currentUser();
    clientContext.load(user);
    clientContext.executeQueryAsync(
      function () {
        userLogin.resolve(user.get_loginName());
      }
      ,
      function () {
        userLogin.reject(args.get_message());
      }
      );
  });
  return userLogin.promise();
}

//updateUserProfile updates the userprofile property 
function updateUserProfile(userId, propertyName, propertyValue) {

  var propertyData = "<PropertyData>" +
  "<IsPrivacyChanged>false</IsPrivacyChanged>" +
  "<IsValueChanged>true</IsValueChanged>" +
  "<Name>" + propertyName + "</Name>" +
  "<Privacy>NotSet</Privacy>" +
  "<Values><ValueData><Value xsi:type=\"xsd:string\">" + propertyValue + "</Value></ValueData></Values>" +
  "</PropertyData>";

  $().SPServices({
    operation: "ModifyUserPropertyByAccountName",
    async: false,
    webURL: "/",
    accountName: userId,
    newData: propertyData,
    completefunc: function (xData, Status) {
      var result = $(xData.responseXML);
    }
  });

}


</script>

<input id="Fax" type="text" placeholder="Update Fax" />
<input onclick="updateUserProfilePreFlight()" type="button" value="Update" />

Note

  • You can only edit the user profile properties that are editable (unlocked) on your MySite. Certain fields like department are usually locked for editing as per company policy

SPServices Stories #19 – Folders in SharePoint are as necessary as evil. Make the best of it using jQuery and SPServices.

This entry is part 18 of 21 in the series SPServices Stories

Introduction

Ever on the hunt for good SPServices Stories, I spotted this cool one a few weeks back when Patrick Penn (@nfpenn) posted a screenshot of something he had done on Twitter. I encouraged him to do a post about it and this is the result: Folders in SharePoint are as necessary as evil. Make the best of it using jQuery and SPServices.

I liked this post for several reasons. First, it’s a great example of how we can improve the user experience (UX) with SPServices and client side scripting. Second, Patrick tells us in some depth what he was trying to accomplish from a non-technical perspective and how he made it work. Some have accused me of posting purely technical pieces in this series without enough story to them; this post has both.
A few notes from my end:

  • SPServices has a function to parse the query string called SPGetQueryString, so Patrick’s function getQueryStrings isn’t really needed.
  • Using the .find(“z\\:row, row”) syntax will cause issues in some browsers due to the z namespace. I’ve got a function in SPServices called SPFilterNode which you should use instead: .SPFilterNode(“z:row”). The function will work with any selectors in the XML (at least all that I’ve tried), but you should definitely use it for z:row and rs:data. In other words, use SPFilterNode anywhere you see namespacing in an XML node.

Patrick PennPatrick Penn (@nfpenn) is a SharePoint Architect at netflower (http://netflower.de) in Germany. He loves SharePoint but also knows its weak points. Based on this knowledge he gives advice to clients and builds custom-made solutions to improve efficiency and usability within SharePoint.

Folders in SharePoint are as necessary as evil. Make the best of it using jQuery and SPServices.

Let me say right upfront, this post is not about Folders vs Metadata. If you’re searching for that, you will find a rather good one here.

Hopefully you know about the benefits of SharePoint and its features like, enterprise keywords, taxonomy and metadata navigation. But sometimes you or your client need a good old folder hierarchy. If you’re a valuable consultant you will neither roll your eyes nor surrender but assure him with a smile that you will build a solution that will absolutely meet his needs.

Some Reasons Why Folders are Necessary

Some logical arguments for using folders in SharePoint are:

  • If you have many document types which need different permissions within a single document library. Sure, you can configure dedicated permissions for each single document, but this may be hard to maintain.
  • If your customer needs a quick solution to share documents without manually setting managed metadata for each single document, because they often upload documents in a bulk.
  • If you need to logically group different document types and provide a dynamically generated status based on documents or its metadata, which needs to be displayed on a higher hierarchy level to provide an consolidated overview about the content. Sounds complicated? Practically speaking it may be needed to show a completeness status about documents to deliver, which brought me to the solution dealt with in this post.
  • Another reason is to simply not to overstrain the users, if they’re new to SharePoint. They quite likely know the folder structure and you as a consultant have the possibility to provide a solution which include the best of both worlds. To work future-oriented, be creative, for example you’re able to automatically define metadata predefined by a documents name or its parent folder or even better by its content. There’s an outdated solution on Codeplex, that may give you an idea. Maybe I dedicate a post to this topic in the near future.

While you’re reading this, you may think: “Why the hell didn’t he use Document Sets?”.
We involved this feature in our planning, but there is no metadata navigation within Document Sets, which could have been an advantage. Furthermore the customer needed a multi-level hierarchical structure. As we had no significant arguments for it, the latter was a show stopper for Document Sets.

How to Pep Up Folders

As you can imagine, you have many possibilities to get more out of old and boring folders. For example JSLink may be your favorite approach, but I didn’t use it, because this is a migrated solution.

Something like the following is a simple approach to provide much more value to folders.

peppedup_folders_1-1The presumably simplest way may be consulting SharePoint Designer and add conditional formatting to change the presentation of a document library view.
But if you think further and consider a more professional deployment you may realize, that there must be a better way. Also using XSLT will not be fun to handle the content of multiple subfolders and I’m sure the result will not be a smooth experience either.

To keep things simple and manageable I decided to use jQuery and a powerful javascript library from SharePoint MVP Marc D. Anderson, author of SPServices.

I’m not allowed to publish the whole source code regarding this solution, due to the NDA with our client, but I will hopefully give you enough information to build a solution like this by yourself. Sorry for that!

Let’s Begin

I used the following javascript libraries for SharePoint 2013:
jQuery 1.10.2 and SPServices 2013.02a

Because I use jQuery and SPServices in multiple places I decided to place the script references within the custom masterpage. You can do it manually or use a more professional approach like this, by using a custom delegate control.

For testing purpose you can simply add a reference within the content editor webpart. But be sure to implement this before you call the functions.

<script src="/Style%20Library/scripts/jquery/jquery-1.10.2.min.js" type=text/javascript></script>
<script src="/Style%20Library/scripts/jquery/jquery.SPServices-201302a.js" type=text/javascript></script>

Congrats! Now you’re able to use the full power of jQuery and SPServices!

I want to keep things as simple as possible. So to implement the pepped up folder (PUF) functionality within a document library, I just added a content editor webpart (CEWP) below the list view, which is invisible for users.

webpart_placholders_1-1Then I added a reference (Content Link) for the PUF-script.

webpart_contentlink_1-1To load a .js file as Content Link you should put the whole code between these tags. The alternative is to use a simple .txt file.

<script type="text/javascript">
<!--
  //your code here
-->
</script>

I like the way using .js files, because of reusability outside a CEWP Content Link.

Logic

  • folder-2default folder: Containing files within the folder or in any of its subfolders. The user knows that there will be content and the click will not be for nothing
  • folder_empty-2empty folder: No files are contained within the folder and each of its subfolders. So the user doesn’t have to look for any content and knows that there is still something to deliver.
  • folder_not_reviewed-2not reviewed folder: No files are contained within the folder and each of its subfolders and the status is “not reviewed”.
  • folder_reviewed-2reviewed folder: The folder status is set to “reviewed” or all child folders are set to “reviewed”. Sometimes there are no files to deliver for a specific folder, so it can directly be marked as “reviewed”.

Optionally you can notify the user that pepping up begins and load the pepUpFolders function with a short delay.

$(document).ready(function(){
  var loadingNotifyId = SP.UI.Notify.addNotification('Pepping up folders ...', false);
  setTimeout('pepUpFolders();',1100);
});

It may happen, that folders are already updated before the user is able to read the notification, so the delay helps.
You can decide which approach is the best for your users. Our customer wanted to notify users about what’s going to happen.

Use this function to retrieve the document libraries root folder from the url parameter if you’re currently in any subfolder.

function getQueryStrings() {
  var assoc  = {};
  var decode = function (s) { return decodeURIComponent(s.replace(/\+/g, " ")); };
  var queryString = location.search.substring(1);
  var keyValues = queryString.split('&');

  for(var i in keyValues) {
    var key = keyValues[i].split('=');
    if (key.length > 1) {
      assoc[decode(key[0])] = decode(key[1]);
    }
  }

  return assoc;
}

Last but not least the more exciting part. This is where the magic happens.

As I mentioned before this function is a bit truncated, but for the result you’ll see a difference regarding folders with and without content. The nice part is, that the most functionality loads asynchronously so the user feels nearly no delay, when navigating through the folder structure.

function pepUpFolders() {
    $(document).ready(function() {
        var sitecollectionUrl = _spPageContextInfo.siteServerRelativeUrl;
        if (sitecollectionUrl == "/") {
            sitecollectionUrl = "";
        }
        var emptyFolderIconPath = sitecollectionUrl + "/Style Library/scripts/images/folder_empty.gif";
        var folderIconPath = sitecollectionUrl + "/_layouts/15/images/folder.gif?rev=23";
        var loaderIconPath = sitecollectionUrl + "/Style Library/scripts/images/loading.gif";
        var folderPrefix = "";

        /* You need this prefix for SharePoint 2010 to replace folder icons

        switch(_spPageContextInfo.currentLanguage)
        {
          case 1031:
          folderPrefix = "";//"Ordner: ";
          break;

          case 1033:
            folderPrefix = "";//"Folder: ";
            break;
        }*/

        var folderStatusReviewedString = "reviewed";

        var listName = $().SPServices.SPListNameFromUrl();
        var siteUrl = $().SPServices.SPGetCurrentSite();

        //Parsing the server relative url of the current web
        if (siteUrl.startsWith("http")) {
            var siteServerRelativeUrl = siteUrl.match(/\/[^\/]+(.+)?/)[1] + "/";
        } else {
            var siteServerRelativeUrl = siteUrl;
        }

        var currentFolderPath;
        var qs = getQueryStrings();
        var rootFolder = decodeURI(qs["RootFolder"]);

        //If the rootFolder is not set, then we are currently in the root folder
        if (rootFolder != null) {
            rootFolder = rootFolder.replace(siteServerRelativeUrl, "");
            currentFolderPath = rootFolder;
        } else {
            //The root folder is not set, then we need to get the rootFolder from list
            $().SPServices({
                operation: "GetList",
                async: false,
                listName: listName,
                completefunc: function(xData, Status) {
                    $(xData.responseXML).find("List").each(function() {
                        currentFolderPath = $(this).attr("RootFolder");
                    });
                }
            });
        }

        currentFolderPath = currentFolderPath.replace(siteServerRelativeUrl, "");
        parentFolderPath = currentFolderPath.substring(0, currentFolderPath.lastIndexOf('/'));
        parentFolderName = currentFolderPath.substring(currentFolderPath.lastIndexOf('/') + 1);

        //We request only a list of folders as we don't need to inspect files from current folder
        var query = "<Query><Where><Eq><FieldRef Name='FSObjType'></FieldRef><Value Type='Lookup'>1</Value></Eq></Where></Query>";
        var queryOptions = '<QueryOptions><Folder><![CDATA[' + currentFolderPath.substring(1) + ']]></Folder></QueryOptions>';
        var viewFields = "<ViewFields Properties='true'><FieldRef Name='Level' /></ViewFields>";

        var promFolders = [];
        promFolders[0] = $().SPServices({
            operation: "GetListItems",
            listName: listName,
            CAMLQuery: query,
            CAMLQueryOptions: queryOptions,
            CAMLViewFields: viewFields
        });

        var promSubFolders = [];
        $.when.apply($, promFolders).done(function() {
            $(promFolders[0].responseXML).SPFilterNode("z:row").each(function() {
                var subFolderPath = $(this).attr("ows_FileRef").split(";#")[1];
                subFolderPath = subFolderPath.replace(siteServerRelativeUrl.substring(1), "").substring(1);
                var subFolderName = $(this).attr("ows_FileLeafRef").split(";#")[1];
                $("[title='" + folderPrefix + subFolderName + "']").attr("src", loaderIconPath);

                //Search for Files in any subfolder and return 1 item per Folder max.
                var query = "<Query><Where><Eq><FieldRef Name='FSObjType'></FieldRef><Value Type='Lookup'>0</Value></Eq></Where></Query>";
                var queryOptions = "<QueryOptions><Folder><![CDATA[" + subFolderPath + "]]></Folder><ViewAttributes Scope='Recursive' /></QueryOptions>";

                $().SPServices({
                    operation: "GetListItems",
                    async: true,
                    listName: listName,
                    CAMLRowLimit: 1,
                    CAMLQuery: query,
                    CAMLQueryOptions: queryOptions,
                    completefunc: function(xData, Status) {
                        //Replace Folder Icon with empty Folder icon
                        if ($(xData.responseXML).find("z\\:row, row").length == 0) {
                            $("[title='" + folderPrefix + subFolderName + "']").attr("src", emptyFolderIconPath);
                        } else {
                            $("[title='" + folderPrefix + subFolderName + "']").attr("src", folderIconPath);
                        }
                    }
                });
            });
        });
    });
}

function getQueryStrings() {
    var assoc = {};
    var decode = function(s) {
        return decodeURIComponent(s.replace(/\+/g, " "));
    };
    var queryString = location.search.substring(1);
    var keyValues = queryString.split('&');

    for (var i in keyValues) {
        var key = keyValues[i].split('=');
        if (key.length > 1) {
            assoc[decode(key[0])] = decode(key[1]);
        }
    }
    return assoc;
}

I’m sure there is something to optimize. But the intention of this post was to show a solution to pep up boring folders and make them more valuable.

I was struggling with async calls somehow, so Marc D. Anderson recommended me to use promises instead of regular async calls, because of a better controllable program flow.

So, I updated my code and combined both approaches, because of depending calls. Now everything seems to work as expected and it’s clear what javascript promises are and how they can help to improve program flow.

I hope this post gives you some fresh ideas how to gain more value out of folders, because they are still necessary, albeit evil.

If you need some advice feel free to contact me.