Client-side auto-generated table of contents for text-heavy pages in SharePoint

toc

In the project I’m currently working on we are creating a solution in SharePoint 2013 for content that previously has been in word documents. The content is very text-focused, and consists almost entirely of headings and text blocks. We are using enterprise wiki as the new home for the content in SharePoint.

One of the “key features” of the documents (and Word-documents in general) are the table of contents, where you get quick links to the sections you are interested in. This was a feature we wanted to have in SharePoint as well, and what’s better than having the table of contents auto-generated to avoid ToC’s that needs to be manually updated and often go stale.

The solution is pretty simple. In the page layout we’re using, I’ve added a header and an empty unordered list.

<h1 class="pzl-toc-header">Table of Contents</h1>
<ul class="pzl-toc-content"></ul>

The Javascript

When the DOM is ready, we’re calling a nice little function that iterates over all the h1 and h2 tags in the document, adds anchor bookmarks to the headings and adds the headings to the table of contents. To make it somewhat more reusable, the function takes the selector of the table of contents list element and the selector of the section we are looking for headers in.

var Pzl = Pzl || {};
Pzl.Wiki = Pzl.Wiki || {};

Pzl.Wiki.GenerateTableOfContents = function (jQueryTocListSelector, jQueryContentSectionSelector) {
    var tocId = 0;
    jQuery(jQueryContentSectionSelector + ' h1,' + jQueryContentSectionSelector + ' h2').each(function () {
        var heading = jQuery(this);
        var headingText = heading.text();
        //Replace any non-alphanumeric characters
        var headingId = headingText.replace(/\W/g, '') + '-' + tocId++;
        var cssLevelClass = heading.is('h1') ? "level1" : "level2";
        
        //Add the id to the header-element
        heading.attr('id', headingId);
        //Add the list item to the ToC
        jQuery(jQueryTocListSelector).append('<li class="' + cssLevelClass + '"><a href="#' + headingId + '">' + headingText + '</a></li>');
    });
};

The code should be pretty self-explanatory, but there’s a couple of things to note.

  • I’m removing all non-alphanumeric characters (line 7) because the element’s id’s needs to be on a valid format.
  • I’m also adding a level1 or level2 css class (line 8) in order to style the ToC afterwards (I obviously want the level 2 items to have more left-padding than level  1 items).

In my page layout where I want the table of contents to be generated I’m calling this function in the following way:

<script type="text/javascript">
    jQuery(document).ready(function () {
        Pzl.Wiki.GenerateTableOfContents('ul.pzl-toc-content', '.pzl-radviser .page-content .ms-rtestate-field');
    });
</script>

The looks of it

The CSS for the table of contents is super-simple and looks like this

ul.pzl-toc-content {
    list-style: none;
    font-size: 16px;
    padding: 0;
    margin: 0 0 20px 0;
}

Which creates a table of contents that looks similar to the image in the top of this post (there are some other css styles defined which are not relevant).

Back to the top

When the user gets to the bottom of the page, he most likely would love to go the top again. Hence I’m adding a simple bookmark-link to go to the top of the page after the content section.

<a href="#top">To the top</a>

That’s basically it! The solution would work fine as it is now.

Bonus: Smoothscrolling

Wouldn’t it be great if the browser scrolled smoothly down to the sections we cared about instead of skipping down instantly? This way it’s more clear to the user what happens at the click of the button.

Smoothscrolling in SharePoint apparently wasn’t that easy . I’m going to spare you the details, but the final solution I ended up with was the one discussed and improved over at the technet forums. I did some minor modifications to the script discussed there, and came up with the following function which I’m calling after generating the Table of Contents.

Pzl.Wiki.ApplySmoothScrollingOfTOC = function() {
    jQuery('#s4-workspace .page-content a[href^="#"]').on('click', function (e) {
        var elemEvt, elemDst, hashVal;
        e.preventDefault();

        elemEvt = jQuery(this);
        hashVal = elemEvt.attr('href').match('^#+(.*)')[1];

        // Look for id, if not found then a[name]
        elemDst = jQuery('#' + hashVal).eq(0);

        // Still not found, either implicit 'top' or don't scroll at all
        if (elemDst.size() == 0) {
            elemDst = hashVal == 'top' ? 0 : null;
        }

        if (elemDst !== null) {
            jQuery("#s4-workspace").scrollTo(elemDst, {
                axis: 'y',
                duration: 300,
                onAfter: function () {
                    window.location.hash = hashVal;
                }
            });
        }
    });
};

There are a couple of things to note here

  • I’m using the scrollTo jQuery plugin which needs to be references somewhere on the page.
  • After the scrolling has happened I still want the #bookmark in the URL, in order to allow directly linking to sections as well as back and forth functionality in the browser. That’s why I’m setting window.location.hash = hashVal.

Good luck!