Friday, July 26, 2013

SharePoint 2013: Breadcrumb for list/library

Maybe we’ve noticed the issue in SharePoint - if a list view webpart is added to page and user navigate to different folders in the list view, there’s no way for users to know current folder hierarchy. So basically breadcrumb for the list view webpart missing. If there would be a way of showing users the exact location in the folder hierarchy the user is current in (as shown in the image below), wouldn’t be that great?
image
Image 1: List view webpart with folder-navigation breadcrumb


I’ll explain in the post how you can achieve this kind of folder navigation with minimum changes/customisation.

Deployment

Download the FolderNavigation.js file from MSDN Code Gallery. You can deploy the script either in Layouts folder (in case of full trust solutions) or in Master Page gallery (in case of SharePoint Online or full trust). I would recommend to deploy in Master Page Gallery so that even if you move to cloud, it works without modification. If you deploy in Master page gallery, you don’t need to make any changes, but if you deploy in layouts folder, you need to make small changes in the script which is described in section ‘Deploy JS Link file in Layouts folder’.

Option 1: Deploy in Master Page Gallery (Suggested)
If you are dealing with SharePoint Online, you don’t have the option to deploy in Layouts folder. In that case you need to deploy it in Master page gallery. Note, deploying the script in other libraries (like site assets, site library) will not work, you need to deploy in master page gallery. To deploy in master page gallery manually, please follow the steps:
  1. Download the js file from MSDN Code Gallery.
  2. Navigate to Root web => site settings => Master Pages (under group ‘Web Designer Galleries’).
  3. From the ‘New Document’ ribbon try adding ’JavaScript Display Template’ and then upload the FolderNavigation.js file and set properties as shown below:
    image
    Image 2: Upload JS file to Master Page Gallery
In the above image, we’ve specified the content type to ‘JavaScript Display Template’, ‘target control type’ to view to use the js file in list view. Also I’ve set target scope to ‘/’ which means all sites and subsites will be applied. If you have a site collection ‘/sites/HR’, then you need to use ‘/Sites/HR’ instead. You can also use List Template ID, if you need.

Option 2: Deploy JS Link file in Layouts Folder
If you are deploying the FolderNavigation.js file in Layouts folder, you need to make small changes in the downloaded script’s RegisterModuleInti method as shown below:
RegisterModuleInit('FolderNavigation.js', folderNavigation);
In this case the ‘RegisterModuleInit’ first parameter will be the path relative to Layouts folder. If you deploy your file in path ‘/_Layouts/folder1’, the then you need to modify code as shown below:
RegisterModuleInit('Folder1/FolderNavigation.js', folderNavigation);

If you are deploying in other subfolders in Layouts folder, you need to update the path accordingly. What I’ve found till now, you can only deploy in Layouts and Master page gallery. But if you find deploying in other folders works, please share.

Use The JS File in List View WebPart

Once you deploy the FolderNavigation.js file, you can start using it in list view webpart. Edit the list view web part properties and then under ‘Miscellaneous’ section put the file url for JS Link as shown below:
image
Image 3: JS Link for list view webpart

Few points to note for this JS Link:
  • if you have deployed the js file in Master Page Gallery, You can use ~site or ~SiteCollection token, which means current site or current site collection respectively.
  • If you have deployed in Layouts folder, you need to use corresponding path in the JS Link properties. For example if you are deploying the file in Layouts folder, then use ‘/_layouts/15/FolderNavigation.js’, if you are deploying in ‘Layouts/Folder1’ then, use ‘/_layouts/15/Folder1/FolderNavigation.js’.

How it works?

If you are interested how it works, you can carry on reading, either move to deployment section. Basically I’ve utilized the SharePoint 2013 Client Side Rendering (CSR) concept. If you are not familiar with this CSR, you can get the basic idea by Googling. Simply, CSR allows us to customize the rendering of SharePoint fields using JavaScript. I’ve developed a client side rendering JavaScript file which is shown below:

///Author: Sohel Rana
//Version 1.2
//Last Modified on 27-Oct-2013
//Version History:
//  1. Added
//  2. Fixed the bug 'Filtering by clicking on field title would result duplicate navigation link'
//  3. Fixed the bug 'breadcrumb title always lowercase'. Now breadcrumb title is as like the folder name in the library even with case (lower/upper)

//replace query string key with value
function replaceQueryStringAndGet(url, key, value) {
    var re = new RegExp("([?|&])" + key + "=.*?(&|$)", "i");
    separator = url.indexOf('?') !== -1 ? "&" : "?";
    if (url.match(re)) {
        return url.replace(re, '$1' + key + "=" + value + '$2');
    }
    else {
        return url + separator + key + "=" + value;
    }
}


function folderNavigation() {
    function onPostRender(renderCtx) {
        if (renderCtx.rootFolder) {
            var listUrl = decodeURIComponent(renderCtx.listUrlDir);
            var rootFolder = decodeURIComponent(renderCtx.rootFolder);
            if (renderCtx.rootFolder == '' || rootFolder.toLowerCase() == listUrl.toLowerCase())
                return;

            //get the folder path excluding list url. removing list url will give us path relative to current list url
            var folderPath = rootFolder.toLowerCase().indexOf(listUrl.toLowerCase()) == 0 ? rootFolder.substr(listUrl.length) : rootFolder;
            var pathArray = folderPath.split('/');
            var navigationItems = new Array();
            var currentFolderUrl = listUrl;

            var rootNavItem =
                {
                    title: 'Root',
                    url: replaceQueryStringAndGet(document.location.href, 'RootFolder', listUrl)
                };
            navigationItems.push(rootNavItem);

            for (var index = 0; index < pathArray.length; index++) {
                if (pathArray[index] == '')
                    continue;
                var lastItem = index == pathArray.length - 1;
                currentFolderUrl += '/' + pathArray[index];
                var item =
                    {
                        title: pathArray[index],
                        url: lastItem ? '' : replaceQueryStringAndGet(document.location.href, 'RootFolder', encodeURIComponent(currentFolderUrl))
                    };
                navigationItems.push(item);
            }
            RenderItems(renderCtx, navigationItems);
        }
    }


    //Add a div and then render navigation items inside span
    function RenderItems(renderCtx, navigationItems) {
        if (navigationItems.length == 0) return;
        var folderNavDivId = 'foldernav_' + renderCtx.wpq;
        var webpartDivId = 'WebPart' + renderCtx.wpq;


        //a div is added beneth the header to show folder navigation
        var folderNavDiv = document.getElementById(folderNavDivId);
        var webpartDiv = document.getElementById(webpartDivId);
        if(folderNavDiv!=null){
            folderNavDiv.parentNode.removeChild(folderNavDiv);
            folderNavDiv =null;
        }
        if (folderNavDiv == null) {
            var folderNavDiv = document.createElement('div');
            folderNavDiv.setAttribute('id', folderNavDivId)
            webpartDiv.parentNode.insertBefore(folderNavDiv, webpartDiv);
            folderNavDiv = document.getElementById(folderNavDivId);
        }


        for (var index = 0; index < navigationItems.length; index++) {
            if (navigationItems[index].url == '') {
                var span = document.createElement('span');
                span.innerHTML = navigationItems[index].title;
                folderNavDiv.appendChild(span);
            }
            else {
                var span = document.createElement('span');
                var anchor = document.createElement('a');
                anchor.setAttribute('href', navigationItems[index].url);
                anchor.innerHTML = navigationItems[index].title;
                span.appendChild(anchor);
                folderNavDiv.appendChild(span);
            }

            //add arrow (>) to separate navigation items, except the last one
            if (index != navigationItems.length - 1) {
                var span = document.createElement('span');
                span.innerHTML = '&nbsp;>&nbsp;';
                folderNavDiv.appendChild(span);
            }
        }
    }


    function _registerTemplate() {
        var viewContext = {};

        viewContext.Templates = {};
        viewContext.OnPostRender = onPostRender;
        SPClientTemplates.TemplateManager.RegisterTemplateOverrides(viewContext);
    }
    //delay the execution of the script until clienttempltes.js gets loaded
    ExecuteOrDelayUntilScriptLoaded(_registerTemplate, 'clienttemplates.js');
};

//RegisterModuleInit ensure folderNavigation() function get executed when Minimum Download Strategy is enabled.
//if you deploy the FolderNavigation.js file in '_layouts' folder use 'FolderNavigation.js' as first paramter.
//if you deploy the FolderNavigation.js file in '_layouts/folder/subfolder' folder, use 'folder/subfolder/FolderNavigation.js as first parameter'
//if you are deploying in master page gallery, use '/_catalogs/masterpage/FolderNavigation.js' as first parameter
RegisterModuleInit('/_catalogs/masterpage/FolderNavigation.js', folderNavigation);

//this function get executed in case when Minimum Download Strategy not enabled.
folderNavigation();


Code Snippet 1: Folder Navigation JavaScript

Let me explain the code briefly.

  • The method ‘replaceQueryStringAndGet’ is used to replace query string parameter with new value. For example if you have url ‘http://abc.com?key=value&name=sohel’  and you would like to replace the query string ‘key’ with value ‘New Value’, you can use the method like
    replaceQueryStringAndGet("http://abc.com?key=value&name=sohel","key","New Value")
  • The function folderNavigation has three methods. Function ‘onPostRender’ is bound to rendering context’s OnPostRender event. The method first checks if the list view’s root folder is not null  and root folder url is not list url (which means user is browsing list’s/library’s root). Then the method split the render context’s folder path and creates navigation items as shown below:
    var item =
        {
            title: title,
            url: lastItem ? '' : replaceQueryStringAndGet(document.location.href, 'RootFolder', encodeURIComponent(rootFolderUrl))
        };
As shown above, in case of last item (which means current folder user browsing), the url is empty as we’ll show a text instead of link for current folder.
  • Function ‘RenderItems’ renders the items in the page. I think this is the place of customisation you might be interested. Having all navigation items passed to this function, you can render your navigation items in your own way. renderContext.wpq is unique webpart id in the page. As shown below with the wpq value of ‘WPQ2’ the webpart is rendered in a div with id ‘WebPartWPQ2’. 

    image
    Image 4: How list view webpart is rendered

    In ‘RenderItems’ function I’ve added a div just before the webpart div ‘WebPartWPQ2’ to put the folder navigation as shown in the image 1.
     
  • In the method ‘_registerTemplate’, I’ve registered the template and bound the OnPostRender event.
  • The final piece is RegisterModuleInit. In some example you will find the function folderNavigation is executed immediately along with the declaration. However, there’s a problem with Client Side Rendering and Minimal Download Strategy (MDS) working together as described in here. To avoid this problem, we need to Register foldernavigation function with RegisterModuleInit to ensure the script get executed in case of MDS-enabled site. The last line ‘folderNavigation()’ will execute normally in case of MDS-disabled site.
I’ve upload the js file in MSDN code gallery. Please download it from here.

Download

The JavaScript file is available in MSDN gallery for download.

Conclusion

The solution works for both SharePoint Server as well as SharePoint Online. Though I’ve shown manual process of deploying and updating the JS Link in webpart properties, you can develop powershell script to deploy the FolderNavigation. Few references might be useful:

30 comments:

  1. If I create a new View in the Documents list and use the default (which I just added a JSLink definition to) to start with, it does not inherit the JSLink definition.
    How will this then work for Personal Views users create??

    ReplyDelete
  2. @Danny, Yes I've noticed the same problem now. The only solution I find (maybe not the best one) is to add the JS link in the personal view also.

    ReplyDelete
  3. Thanks a lot for this great guide! Just yesterday my GM asked me to figure out how to enable breadcrumbs for Document Libraries, so your timing was perfect.

    ReplyDelete
  4. Perfect! Thank you!

    Only the procedure and screenshots are slightly different (updated?) in SharePoint online (e.g. it was called Display Template Code instead of JavaScript Display Template’)

    ReplyDelete
  5. Hi All, a bug was reported - "Filtering by clicking on field title would result duplicate navigation link". I've fixed the issue and uploaded the latest js file in code gallery. Please take update..

    ReplyDelete
  6. I tried this in an O365 site but doesnt work, any ideas?

    ReplyDelete
    Replies
    1. Rather to be more specific, the breadcrumb is not showing full path when i navigate to subfolders

      Delete
    2. Hi, Where have you deployed the script? Master Page gallery or Layouts folder? Can you please provide more specific details - like your folder structure or possibly a screenshot to my personal email ranaictiu at hotmail dot com? If there's any bug I'll fix it.

      Delete
    3. Sorry for the late reply Sohel, but i found that the issue was occurring if i was visiting the document library folder from a web part in the home page of the site. If i went to the documents library it was working fine. So basically your code is perfect, thanks a lot :)

      Delete
  7. It works fine for me.
    Thank you a lot for this !

    ReplyDelete
  8. Hi Sohel

    Thank you very much for this script. It worked like a charm. Just one thing, Is it possible to make first letter of the Folder name to be uppercase? Instead of "folder 1", can it be "Folder 1"?

    Thanks

    ReplyDelete
  9. Hi, the script were showing folder name in lower case always as reported by Dipal Patel. I've fixed the bug, now the folder name appears as it is in the list. I've uploaded the updated script in the MSDN code gallery. Thanks for your feedback.

    ReplyDelete
  10. Hi Sohel

    Thank you very much for making this change. It really helps a lot.

    ReplyDelete
  11. Hi Sohel, I've tried your step by step procedure but it doesn't show up :( I am using sharepoint online, but i dont think that should be the problem?
    I have uploaded it in masterpage gallery (_catalogs/masterpage) as Javascript Display Template content type. Then edited the webpart of my document library to add the link of js script. I don't get that breadcrumb below the webpart title. Do I need to activate a feature of something? Or edit something on the script? Thanks for all your help!

    ReplyDelete
  12. @Karen, How have you referenced the file in js link? You need to use ~site or ~siteCollection token.

    ReplyDelete
  13. Sohel, this is excellent and works great. Thanks so much. Is there any where in your js that I could add a line break after the navigation? If "Display search box" is selected in the web part properties and the toolbar with "new document" is set to show, the folder navigation is positioned very tightly above "new document" and looks cluttered. Perhaps there is a better way to get a line break there. Any suggestions?

    ReplyDelete
  14. @Neil G, Currently it doesn't support styling, but I'll include some stying option later...

    ReplyDelete
  15. I created a similiar feature, but I did it using front end jQuery code and the "Navigate Up" breadcrumb snippet. Hopefully it's useful to someone else. Your solution worked for list views, but for some reason wouldn't work on document library pages. You can view my solution here:

    https://hardwiredmedia.com/creating-breadcrumbs-sharepoint-2013-document-library-pages/

    It's a bit of a hack, but it works!

    ReplyDelete
    Replies
    1. also, this solution works for list views and doc libraries!

      Delete
  16. Thanks for your code.
    If I'm understanding this correctly, every list, library, view needs to have this added. Any new document libraries and lists as well.

    Stephan Onisick

    ReplyDelete
  17. @Stephano, Yes if you need to use in in a single library, but one of my colleagues has put it in master page and the breadcrumb appeared globally, I've not tested though.

    ReplyDelete
  18. Using the exact instructions from the first method doesn't work for our 2013 on site installation :S. Do i need to use the list template ID? If so what list template to I gain the ID of and how???

    Thanks in advance

    Lawrence

    ReplyDelete
    Replies
    1. I've since solved this :) my mistake. Fat fingers :P

      Delete
  19. Hi, at first thanks for this nice feature! Secondly, we are facing issue with SP Online. Users with read permission don't see breadcrumb. Only people having at least Contribute permissions for JavaScript file. Any ideas how to overcome this issue, so that we can set JS file as Read?
    Thanks for advance!

    ReplyDelete
  20. @Aivovaurio, Please check the js file permission, if everyone has read permission. I think if you upload in the 'masterpage gallery', everyone will have read permission by default.

    ReplyDelete
    Replies
    1. Thank you for response. That is part of the problem, when file has read only permissions, some users who has only read permission on site itself, cannot see breadcrumb. Only if they are promoted as Contribute for that js file. Which is very bad approach. I should come over this and keel the file read only for all, and also make this visible for people only read right for the site.

      Delete
    2. Ok, now it is solved. It is good idea to publish js file when uploaded! If it is in draft mode, sure it won't appear to read only people :)

      Delete
  21. When I press the "New Document" button all that I get is a "Add a master page" dialog not matter what document type I select. Has anyone else ran into this issue? This is what happens on the root site but on all subsites the "New Document" button is grayed out completely.

    ReplyDelete
  22. @skeletank, Do you have right permission (site collection administrator, most probably)? Also if you are using any custom branding, that might break the functionality sometimes.

    ReplyDelete
    Replies
    1. I am a site collection administrator. I realize now that on the root site the 'Add a master page' dialog is normal and that I select a content type afterwards. However, I still don't understand why the "New Document" option is grayed out on subsites.

      Delete