Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2023/adding-reading-progress-indicator

Adding a reading-progress bar to blog posts

Easy stuff isn't always as easy as you expect

Published 04 December 2023

I'd had the idea that I should add a "reading progress" indicator to my blog posts for a while now, and I finally got around to adding it the other weekend. What I'd assumed would be a five minute job had an interesting issue I thought I should document for others...

The basics

I'd done a quick Google for some code I could reuse. (Thank you TheTomasJ, whoever you are!) That provided some CSS and JavaScript to add to the project, and a little markup to add to the page template.

Adding this in was pretty trivial. The CSS can be appended to the end of the "project custom CSS" file in my Statiq generator project. And the JS file can be added to the project as-is. And the markup needed was tiny. Just a single <div/> at the start of the page and the references to the CSS and JS. The one wrinkle here was that I only wanted this code added on blog posts, rather than on all the site pages.

The site has a single layout, so the code has a flag to say if it's rendering a post or not, so the div to display the read-progress and the <script/> itself can be added or not based on that flag:

<body>
    @if(isPost)
    {
        <div id="rp-bar"></div>
    }

    ... snip ...

    @if(isPost)
    {
        <script src="@Context.GetLink("/vendor/rp/rpbar-1.0.js")"></script>
    }
</body>

					

And the site CSS file is already being included in the <head/>, so there's nothing to change for that.

Testing that with a page worked, but I spotted two issues. First, I didn't like the default colours for the progress bar, so I tweaked the colours in the CSS to be friendlier to my theme. A red for the "read" state and grey for the "unread" bit:

#rp-read {
    background-color: #EB1F1F;
}

#rp-unread {
    background-color: #808080;
}

					

And second, I was seeing the progress bar get obscured behind some of the other HTML elements on the page:

Progress bar is obscured behind page elements

That's not right - because it's supposed to stay above everything. But that's a fairly easy fix - I just needed to force its z-order to the front by adding an extra line to the provided style for the bar:

#rp-bar {
    width: 100%;
    top: 0;
    right: 0;
    position: fixed;
    z-index: 99999;
}

					

But that's not right!

For my test page this all appeared to work fine. But after a bit of browsing about the rest of my site content to test I noticed that some of my blog post pages didn't work correctly:

The progress bar isn't at 100% but the page scrolled to the end

Sometimes, when you scrolled to the end of the page, the bar didn't make it to 100%...

I spent a chunk of time browsing about trying to work out which pages didn't work, and which did. Eventually I ruled out a basic maths error (as there were plenty of pages where the bar performed correctly) and I ruled out pages with images on them as being the cause.

But what I finally worked out was that the problem was Mermaid diagrams. When looking at a page which included the markup for a diagram, I would see the problem. But if I disabled the javascript which turns the markdown into a picture then the progress bar worked fine.

So the problem was to do with the different length of the page between the markup that's there initially, and the diagram that's there once Mermaid runs. In many cases the markup is taller than the resulting diagram.

So the next issue was to work out how to fix this...

The answer...

After a bit of thinking, it struck me that this was basically an execution order issue. Looking at the JavaScript for the progress bar, I could see that it does some sums when it's first initialised:

function initProgressBar() {
	document.getElementById("rp-bar").innerHTML = '<div id="rp-read"></div><div id="rp-unread"></div>';
	barHeight = 5;
	animArea = 30;
	barInc = animArea / barHeight;
	doc = document.documentElement;
	bar = document.getElementById("rp-bar");
	start = document.getElementById("rp-start");
	start = start == undefined ? 0 : start.offsetTop;
	end = document.getElementById("rp-end");
	end = end == undefined ? (document.height !== undefined ? document.height : document.body.offsetHeight) : end.offsetTop;
	end -= window.innerHeight;
	range = end - start;
	read = document.getElementById("rp-read");
	unread = document.getElementById("rp-unread");
	updateProgressBar();
}

					

So it's storing the "start" and "end" positions of the scroll range, so that each scroll update can calculate the correct percentage based on the current scroll position. And this init code gets called when the document is loaded:

document.addEventListener("DOMContentLoaded", function(event) { 
  initProgressBar();
});

					

So likely what was happening was that this code was executing before Mermaid had replaced the <pre/> element(s) full of markup to describe the diagram(s) with SVG drawing that they represent. Because those were different sizes the stored data from the initProgressBar() function here becomes outdated after Mermaid completes.

So my initial thought was "can Mermaid make a callback once it's finished?" but a certain amoount of digging about showed that the version of Mermaid I was using did not support this in a helpful way. (Must get around to updating to the latest version of this...) So I started thinking of alternatives.

My first guess was "can I just call the initProgressBar() method again after Mermaid's init call?" but that didn't work. And more debugging showed me that this was because Mermaid's drawing process ran asynchronously and took a little time to finish. And the easiest way of delaying the progress bar init seemed to be something like this:

<script type="text/javascript">
    var config = {
        logLevel: "fatal",
        startOnLoad: true,
        htmlLabels: true,
        theme: "neutral",
        flowchart: {
            useMaxWidth: false
        }
    };
    mermaid.initialize(config);

    setTimeout(function () {
        initProgressBar();
    }, 250);
</script>

					

If Mermaid is being configured (it's not added to pages without diagrams) then wait a quarter of a second after triggering the Mermaid init, and then trigger the progress bar init.

And bingo - that little hack sorted my issue.

The default init gets called on pages where there's no diagram, and on pages where there is a diagram it initialises a second time after Mermaid has completed. I also realised that the page needed another event handler added - so that the reading progress barg got re-initialised after the browser resized:

window.addEventListener("resize", function(event) {
    initProgressBar();
}, true);

					

Not the prettiest code to fix the Mermaid issue - and I do plan to come back to this later when I've managed to update that library - but it works. And hopefully that means you're seeing a working reading progress bar on all the post pages...

↑ Back to top