Fun with Online TOCs!: #4, Single Page Scrolling Navigation Using XHTML Book and Bootstrap Scrollspy

My manager asked me to build a prototype single page Flare output that implements a right-hand navigation that updates with scrolling. The example I was given was the documentation for the Bootstrap library. The documentation for Bootstrap uses Bootstrap. We used Bootstrap too. Here is how the prototype looks. The link after this image is a video clip of the scrolling behavior and the link after that is a live copy of the output.

See It

single-page-scroling

Video: Single Page Scrolling Example

Output: Single Page Example

Try It Yourself

You can download a sample project from BootstrapExample.flprjzip.

To change the content, you can adjust the topics in the Content.fltoc TOC. The XHTML Book target (SinglePage.fltar) is configured to build an output for another TOC (Master.fltoc) that contains Content.fltoc. If you want to make other kinds of adjustments, read the next section for more details about how this example works.

When you publish this output, you must manually copy some of the JavaScript files to the appropriate relative location. You may want to adjust the the paths in the example to suit your needs to reuse the script for multiple outputs.

There is a comments section or you can email info@tregner.com if you need more guidance.

Build One Yourself

Before jumping into the JavaScript part, let’s firstly create a single page output from Flare. If we decide to manage all our content from a single topic, there is no problem using any of the outputs to generate a single HTML file. But that defeats many of the benefits of using Flare and topic-based authoring. Rather than manage content in a single topic, let’s use a target type that will define a single page output from a TOC of many topics.

That target type is called XHTML Book. But we won’t be using the book viewer. You might say we are breathing new life into this target type.

We are going to use the generated XHTML file and CSS and omit the TOC that can be generated at the top of the content. Let’s work up to that point and then do our scripting.

Create a project. Use the Empty template. In Flare 10, we can’t select XHTML Book as the default target type from the New Project Wizard.

Create some topics in the project. Let’s throw in some different heading levels for good measure.

We’ll create a new TOC and add those topics to the TOC.

We’ll reference the new TOC in the default TOC. The reason is we want to bookend the content TOC with some other utility topics but keep those from clouding the authored content. We can reuse those utility topics for other single page outputs.

Now we’ll add an XHTML Book target.

And we’ll explicitly reference the default (outer TOC) from the target. This is just to ensure that one is always used.

We’ll select Modern.css as the Master Stylesheet.

We’ll deselect the option to generate a TOC proxy and the other proxies.

Now we have the basis for our single page output.

Let’s build the target to see what we get. But we won’t view the output with the book viewer. Instead, we’ll open the output folder and look at the *.htm file.

Let’s download Bootstrap and place it in our project in the Content\Resources folder.

http://getbootstrap.com/

Next we’ll create those two bookend utility topics mentioned earlier. We’ll call those Scripts0.htm and Scripts1.htm. In the outer TOC, we’ll put Scripts0.htm before the wrapped TOC and Scripts1.htm after the wrapped TOC. For most online outputs we could use a Master Page. But there is no Master Page option for XHTML Book targets. This is a “book” target. It uses Page Layouts. The outer TOC will serve as our “Master Page.”

Now we’ll remove the headings from Scripts0.htm and Scripts1.htm.

In Scripts0.htm, we’ll create script elements to load jQuery from a CDN (Content Delivery Network) and to load Bootstrap from the copy in the project. jQuery is a dependency for Bootstrap so that one goes first.

In Scripts1.htm, we’ll add this markup to form the outer elements for our navigation and to load the script (single-page.js) we are about to create.

The menu that updates with scrolling will appear in the div element in the preceding markup. The script adjusts the markup after it is loaded. In particular, we are clearing out name attributes used for bookmarks and replacing those with id attributes. Then we are building the navigation with JavaScript and Bootstrap.

We’ll also place this script in the Content\Resources folder in the project. In the example a subfolder is used. Much of this script adjusts the markup of the XHTML Book *.htm file to suit our needs. Many of the changes are to links and destinations. The script converts some of the cross reference information to true links. This involves sanitizing the strings for characters that are not valid in URL encoding.

You may run into issues with some types of links in your content. You’ll have to adjust the script to handle those. This script covers the scenario where there is an absolute link with some of the more common schema names. The script leaves those alone.

There are only two levels in the navigation. When the script builds the menu items, it places links in either the first level or as a child of a first level entry. First level entries are li elements with a link and a ul for children. Second level entries are li elements with links placed in a first level entry’s ul. The script places links to h1 elements in the first level and links to h2-h6 as second level entries. But the script can be adjusted if the division should be be somewhere else. To accomplish that, edit the condition in the inner if statement to include other tags. Look for the comments to find that section of the script. Right now the condition is as follows.

$(this).is('h1')

But to place h2 elements in the first level, the condition could be changed to this.

$(this).is('h1') || $(this).is('h2')

Scrollspy is started with the following line.

$('body').scrollspy({ target: '.navbar-example', offset: 0 });

Some content may run into issues where the navigation tracks up an entry when a link in the menu is clicked. The overcome that, adjust the offset.

$('body').scrollspy({ target: '.navbar-example', offset: 5 });

Here is the entire script.

//For existing links that are absolute, don't alter the value of href.
//Handle a reasonable set of scheme names.
//For those that aren't absolute, replace unfriendly characters with an underscore.
$('a[href]').each((function () {
    var hasSchemeName = false;
    var schemeEnd = $(this).attr('href').indexOf(":");
    var schemeName = $(this).attr('href').substring(0, schemeEnd);
    switch (schemeName) {
        case "http":
        case "https":
        case "shttp":
        case "ftp":
        case "file":
        case "gopher":
        case "news":
        case "chrome":
            hasSchemeName = true;
            break;
        default:
            hasSchemeName = false;
    }
    if (hasSchemeName != true) {
        var safeId = $(this).attr('href');
        safeId = safeId.replace(/[^a-zA-Z0-9-_#]/g, "_");
        $(this).attr('href', safeId);
    }
}));

//Replace madcap:xref elements with a elements.
$('madcap\\:xref').each(function () {
    $(this).replaceWith($("<a href=" + $(this).attr('href') + " class= " + $(this).attr('class') + ">" + this.innerHTML + "</a>"));
});

//For each heading element, create a string based on madcap:xreftargetname
//but replace unfriendly characters with an underscore.
//Set the value of the id attribute to that string.
$('h1, h2, h3, h4, h5, h6').filter(function () { return $(this).attr('madcap:xreftargetname'); }).each(function () {
    var safeId = $(this).attr('madcap:xreftargetname');
    safeId = safeId.replace(/[^a-zA-Z0-9-_]/g, "_");
    $(this).attr('id', safeId);
});

//For any heading element with an id attribute,
//create an item in the navigation menu.
//Make entries for h1 elements first level
//and all other entries second level.
//Adjust this section if a different selection
//of heading elements as first level is desired.
$('*[id]').each((function () {
    if ($(this).is('h1') || $(this).is('h2') || $(this).is('h3') || $(this).is('h4') || $(this).is('h5') || $(this).is('h6')) {
        if ($(this).is('h1')) {
            $('#sidebar').append("<li><a href='#" +
			$(this).attr('id') + "'>" +
			$(this).text() + "</a><ul></ul></li>");
        }
        else {
            $('#sidebar li ul').last().append("<li><a href='#" +
                $(this).attr('id') + "'>" +
                $(this).text() + "</a></li>");
        }
    }
}));

//For the destination bookmarks,
//create an id attribute and give it a
//value that uses a safe version of
//the name value.
$('a[name]').each((function () {
    var safeId = $(this).attr('name');
    safeId = safeId.replace(/[^a-zA-Z0-9-_]/g, "_");
    $(this).attr("id", safeId);
}));

//Remove those name attributes.
$('*[name]').each((function () {
    $(this).removeAttr('name');
}));

//Use scrollspy to track.
$('body').scrollspy({ target: '.navbar-example', offset: 0 });

$('[data-spy="scroll"]').each(function () {
    var $spy = (this).scrollspy('refresh');
});

//Use affix for the nav location.
$('#sidebar').affix({
    offset: { top: 0 }
});

//For shorter sections, especially in full page
//with shorter content for the page,
//clicking doesn't cause a scroll and the
//nav doesn't update. This helps with that.
$('#sidebar li').each((function () {
    $(this).click(function () {
        $('#sidebar li').each((function () {
			$(this).removeClass("active");
        }));
		$(this).addClass("active");
    });
}));

We’ll adjust the body element rules in Modern.css. This is so scrollspy plays well with the styling.

body
{
    font-family: Arial;
    font-size: 13px;
    line-height: 1.7em;
	width: 70%;
	height: 100%;
}

Then we’ll add this to Modern.css. We’ll append the following to define how the navigation appears. Notice the left borders are used to indicate the active parts.

#sidebar.affix-top
{
	position: fixed;
	top: 70px;
}

#sidebar.affix
{
	position: fixed;
	top: 70px;
}

#sidebar li
{
	border: 0 transparent solid;
	border-left-width: 4px;
	list-style-type: none;
	margin-left: 8px;
	padding-left: 4px;
}

#sidebar li ul
{
	display: none;
}

#sidebar li.active
{
	border: 0 #eee solid;
	border-left-width: 4px;
	list-style-type: none;
	margin-left: 8px;
	padding-left: 4px;
}

#sidebar li.active ul
{
	display: block;
}

.navbar-example
{
	position: fixed;
	top: 70px;
	right: 30%;
}

A note about publishing. The script locations resolve locally. But we must manually copy the scripts for Bootstrap and our single-page.js file from the project and post those at the correct relative location. Scripts are not included in the output for XHTML Book targets. One way around that is to adjust the example to point to a CDN for Bootstrap and to place the JavaScript from single-page.js in the script tag in the utility topic.

There it is. We also used a smooth scrolling library in a version of our prototype to animate scrolling when links are clicked. That may be a short post at a later date.

BootstrapExample.flprjzip

1 comment

  1. Hello,
    Are these actually separate topics loading on a single page? or just one page with different h1s or h2s that appear in the sidebar toc?

    thanks.

Leave a comment

Your email address will not be published.

HTML tags are not allowed.

250,717 Spambots Blocked by Simple Comments