Table of contents widget for ServiceNow Portal

Since I started exploring ServiceNow I was always using the knowledge management feature that comes out of the box. After several hundred articles I definitely started to miss a functionality – a table of contents widget.

For each article, you can add a table of contents that is somewhat dynamically updated to each knowledge article. But for my purpose, I always have to remember to add this part. In addition to this, the table of contents will always be fixed to the top of the article, what if I need to scroll further down and then want to check comments again?

So I was eager to build a solution for this. There is already an idea that might get realized in the future to be out of the box, but I didn’t want to wait.

Building a table of contents widget

The idea from the community was a good starting point. I tried to leverage what Lucas pulled together as a quick mockup and expand it to my needs:

  1. Table of contents widget generates for h1-h3 headings of the article.
  2. Knowledge blocks should be recognized as well for headings.
  3. The widget is part of the sidebar.
  4. When scrolling the widget will stay fixed and scroll with the screen.

Be aware that the solution provided here should be tested in a non-productive environment!

First of all, we have to generate a new widget. If you don’t know how to do that refer to the ServiceNow documentation. After you set up your basic widget adjust the following areas:

HTML Template
The following code will be the main widget area and how it looks on the page you insert the image. Be aware that we fixate the table of contents widget to fulfill requirement #4. This could cause issues with your page layout.

<!-- Initiate the table of content widget area -->
<div class="tlgr-fixed-sidebar">
  <div ng-init="c.allSections()"  ng-if="c.data.noHeadings!=0" id="toc" ng-class="c.isNative ? 'kb-mobile-panel' : 'panel panel-default b '" class="panel panel-default b ">
    <div  class="panel-heading" ng-class="{'b-b': !c.options.color}">
      <h3 class="panel-title ng-binding" aria-label="Table of Content" >${Contents}</h3>
    </div>
	</div>
</div>

CSS – SCSS
This is styling the widget a little bit.

div#toc ul{
  list-style-type: square !important;
  padding-inline-start: 25px !important;
}

div#toc ul li{
	color: #074089;
}



div#toc .panel-heading{
 	margin-bottom: 10px; 
}

div#toc a{
	color: #074089;
}

div#toc a:hover{
  text-decoration:underline;
}

.tlgr-fixed-sidebar{
position:fixed;
  width:14.5%;
}

Client Script
Here we collect the data for the table of contents widget. The script basically goes through the article and collects all “h1”, “h2”, “h3” tags. There is a specialty to check for a “div” tag as well which is needed if we add content blocks to the article. This fulfills requirements #1 and #2.

api.controller = function() {
    /* widget controller */
    var c = this;

    //Get all section tags from portal page
    c.allSections = function() {

        setTimeout(function() {
            var allSections = document.getElementsByTagName("section");
			// use second section to generate table of content. Second section contains the article content.
            generateTOC(allSections[1]);
        }, 500);



    };

	// Generate a table of content for the first three headings (h1, h2, h3). Further headings are not meaningful and will be too much to display as a quick navigation overview.
    function generateTOC(textBody) {
        toc = document.getElementById('toc'); // The toc element must exist
        var i1 = 0,
            i2 = 0,
            i3 = 0;
        toc = toc.appendChild(document.createElement("ul"));
        for (var i = 0; i < textBody.childNodes.length; ++i) {

            var node = textBody.childNodes[i];
            var tagName = node.nodeName.toLowerCase();

			// This section is required in case we will use knowledge blocks in articles. We have to iterate through them as well to get the headings back to the table of content.
            if (tagName == "div") {
                for (var x = 0; x < textBody.childNodes[i].childNodes.length; ++x) {
                    node = textBody.childNodes[i].childNodes[x];
                    tagName = node.nodeName.toLowerCase();
                    if (tagName == "h3") {
                        ++i3;
                        if (i3 == 1) toc.lastChild.lastChild.lastChild.appendChild(document.createElement("ul"));
                        section = +"." + i2 + "." + i3;
                        node.id = "section" + section;
                        toc.lastChild.lastChild.lastChild.lastChild.appendChild(document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                    } else if (tagName == "h2") {
                        ++i2;
                        i3 = 0;
                        if (i2 == 1) toc.lastChild.appendChild(document.createElement("ul"));
                        section = i1 + "." + i2;
                        node.id = "section" + section;
                        toc.lastChild.lastChild.appendChild(document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                    } else if (tagName == "h1") {
                        ++i1;
                        i2 = 0;
                        i3 = 0;
                        section = i1;
                        node.id = "section" + section;
                        toc.appendChild(h2item = document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                    }
                }


            } else {
                if (tagName == "h3") {
                    ++i3;
                    if (i3 == 1) toc.lastChild.lastChild.lastChild.appendChild(document.createElement("ul"));
                    section = +"." + i2 + "." + i3;
                    node.id = "section" + section;
                    toc.lastChild.lastChild.lastChild.lastChild.appendChild(document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                } else if (tagName == "h2") {
                    ++i2;
                    i3 = 0;
                    if (i2 == 1) toc.lastChild.appendChild(document.createElement("ul"));
                    section = i1 + "." + i2;
                    node.id = "section" + section;
                    toc.lastChild.lastChild.appendChild(document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                } else if (tagName == "h1") {
                    ++i1;
                    i2 = 0;
                    i3 = 0;
                    section = i1;
                    node.id = "section" + section;
                    toc.appendChild(h2item = document.createElement("li")).appendChild(createLink("#section" + section, node.innerHTML));
                }
            }

			// Counter to check if we have to display the ToC at all
            c.data.noHeadings = i1 + i2 + i3;
        }

    }

	// Create a link for each heading
    function createLink(href, innerHTML) {
        var a = document.createElement("a");
        a.setAttribute("href", href);
        a.innerHTML = innerHTML;
        return a;
    }

	
};

After the table of contents widget is saved we now have to add it to the knowledge article page in Service Portal. For this, you can open the page in page designer and adjust the columns to add a new sidebar containing the widget. I had to create a new sidebar so the widget will stay fixed at scrolling because I had no other option except of rebuilding the whole page.

Your page should look at the end somehow like this:

Page Designer – Knowledge Article View

If you now open any article that has h1-h3 headings and no table of contents added to the article itself your widget should display the table of contents.

On Load of the knowledge article
After scrolling in the article

You can directly jump to the download section in case you don’t want to rebuild this on your own.

Leave a Comment