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!

Set a web template’s welcome page URL declaratively without using the publishing feature

In order to set the welcome page of a web template, the documented and most common way is to use the publishing feature in your onet.xml, which have a feature parameter for setting the welcome page. Sometimes you don’t want to use the publishing framework for various reasons. In my case I was creating a variant of the team site STS#0 web template, and in my opinion the publishing feature does not belong in a team site – especially if it’s just there for setting the welcome page.

It’s pretty easy to set the welcome page programmatically in a feature receiver, but in these no-server-side-code/cloud-compatible times we do our best to avoid that stuff, don’t we?

Surprisingly Google does not give any results on how to set the welcome page url declaratively (without resorting to the publishing feature), only programmatically. The solution is actually very simple, and was discovered by setting a custom welcome page in a web, exporting the web as template and investigating the modules inside the exported .wsp. The welcome page url property is persisted using the rootfolder’s property bag.

To set a custom welcome page in a web template using no-code and no-publishing, do the following

  1. Create a web-scoped feature, e.g. WebTemplatePropertyBagFeature
  2. Add an empty element, e.g. WelcomePageProperty, to the project and to the feature above. The Elements.xml for the WelcomePageProperty element should look similar to the following (replace SitePages/MyCustomFrontPage.aspx with the name of the page you want to use as frontpage for the web)
    <?xml version="1.0" encoding="utf-8"?>
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
      <PropertyBag Url="" ParentType="Folder" RootWebOnly="FALSE">
        <Property Name="vti_welcomepage" Value="SitePages/MyCustomFrontPage.aspx" Type="string" />
      </PropertyBag>
    </Elements>
  3. Find the WebTemplatePropertyBagFeature’s feature guid, and add it to the web template’s onet.xml WebFeatures.
  4. Deploy the solution and create a new site based on the webtemplate. The frontpage should now be your custom page.

"The option for the SharePoint 2013 workflow platform is not available because the workflow service is not configured on the server."

You have configured SharePoint 2013 and Workflow Manager. You have created a new Site Collection. You’re unable to create SharePoint 2013 workflows (but SharePoint 2010 workflows works).

Problem: The site collection was created using the blank template, which doesn’t include all necessary dependency features for creating workflows.

Fix: You need to enable the hidden feature WorkflowServiceStore using PowerShell.

Enable-SPFeature -Identity WorkflowServiceStore –Url $yourUrl

 

Credit goes to Anuja Bhojani (blog) for finding this nugget.

SharePoint Management Shell (PowerShell) stops working after Windows Update installs Windows Management Framework 3.0

I recently logged into a SharePoint 2010 App Server (Server 2008 R2 SP1) and started SharePoint Management Shell. I was greeted with the familiar “The local farm is not accessible. Cmdlets with FeatureDependencyId are not registered”, and were also unable to execute any SharePoint PowerShell commands. The ventured reader will recognize this error, it’s the one that is displayed when the logged in account does not have permissions. The fix for that is quite easy, add the user as Shell Admin using Add-SPShellAdmin (using an account with appropriate permissions, of course).

In this case, however, this did not make any sense. I had previously used the SharePoint 2010 Management Shell on this server and, just to confirm, one of the local IT Administrator were also granted this warning message and were unable to run any SharePoint PowerShell commands.

Further investigation showed that the server had been patched using Windows Update earlier the same day. One of the updates that had been installed was KB5206143, Windows Management Framework 3.0 for Windows 7 SP1 and Windows Server 2008 R2 SP1, which among other features installs PowerShell 3.0. The update was published (published 11. Nov. 2012), As you might know, SharePoint 2010 does not like PowerShell 3.0 very much, yet… This is actually an issue even in Server 2012.

A bug has been raised for PowerShell 3.0, but closed by Microsoft with the message: image

Perhaps a fix is coming in a cumulative update soon? Until then, the workaround is quite simple: Start PowerShell with the parameter “-Version 2.0” or just “-v 2” and then add the SharePoint Snap-In using “Add-PSSnapIn Microsoft.SharePoint.PowerShell”.

I haven’t tried to uninstall the Windows Management Framework 3.0, but I don’t see why that wouldn’t work as well.

If Google brought you here, I hope I have been of help.

Querying for current user’s items using the Content by Query Web Part’s QueryOverride property

We have all learned to love and adore the Content by Query Web Part (CQWP) for showing content from our site collection without having to put our hardcore developer gloves on. It’s great for easily showing content of a certain kind, from a certain site or belonging to a certain user. We’re usually configuring the web part using the three filter properties of the web part in order to show the content we’re looking for.  Unfortunately, sometimes three filter properties are not enough.

image

Say we have several lists of a certain content type with the three columns Status, Responsible and Assigned. We want to show items from this list where status is equal to ‘Open’, and the current user is either in the Responsible-column or in the Assigned-column. We are unable to use the web part’s properties to filter the content, because the three filter properties of the web part is not enough for us to formulate the query. We want items where Status=Open AND (Responsible=Current User OR Assigned=Current User). Unfortunately the filters will be interpreted as (Status=Open AND Responsible=Current User OR Assigned=Current User). This will give us all items where Status is Open and Responsible is Current User, but it will also give us all items where Assigned is Current User, independent on the Status! We don’t have any possibility to change which expression is being interpreted first. This leads us to the QueryOverride property. (Never mind that the MSDN documentation is awful and incorrect: “Gets or sets the list field name used to filter the set of list items.”?)

The QueryOverride property allows us to write good, old CAML. You can try writing the CAML-queries by hand, but I recommend checking out CAML Designer, SharePoint CAML Query Helper or some other tool to help you write those hairy CAML-queries.

Now to the point of this post. Using tools like the above or by looking up resources in various tech forums, the CAML used to filter items by the current user leads us to the following CAML:

<Where>
   <Eq>
      <FieldRef Name='Responsible' /> /*Some will add LookupId=’TRUE’ here. */
      <Value Type='Integer'>
         <UserID Type='Integer' />
      </Value>
   </Eq>
</Where>

This looks great, right? Well, it’s actually not working in the QueryOverride-property (the generated CAML is working in the CAML-tools and data queries). After a lot of experimenting, I found out that you cannot look up the Integer value of the user when filtering on the current user! Fortunately, only minor changes are necessary to get it to work. Change Type from Integer to User, and remove the type attribute from the UserID tag. The working CAML should look like the following:

<Where>
   <Eq>
      <FieldRef Name='Responsible' />
      <Value Type=’User’>
         <UserID />
      </Value>
   </Eq>
</Where>

And the full CAML to find all items with status ‘Open’ and with the current logged in user as assigned or responsible?

<Where> <And> <Eq> <FieldRef Name='Status' /> <Value Type='Choice'>Open</Value> </Eq> <Or> <Eq> <FieldRef Name='Responsible' /> <Value Type='User'> <UserID/> </Value> </Eq> <Eq> <FieldRef Name='Assigned' /> <Value Type='User'> <UserID/> </Value> </Eq> </Or>

</And> </Where>

A final note on how to use the QueryOverride property: In your .webpart file you should wrap the CAML in the CDATA tag, and you should remove spaces and line breaks. If you’re deploying your web part to a page through a module, you need to HTML-encode the CAML. The easiest way I have found to do this is to deploy the .webpart file with the Queryoverride property set, and then add the webpart to a page. You should then be able to export it, and the QueryOverride-property will have it’s content encoded, which you can reuse.