A jQuery plugin for a fixed table of contents

John Avis by | October 12, 2018 | Web Development

Some long web pages have a handy feature where a table of contents are shown beside the content which remains fixed.
A jQuery plugin for a fixed table of contents

Some long web pages have a handy feature where a table of contents are shown beside the content which remains fixed.

A good example of this was the Bootstrap 3.x documentation pages. Once you scroll down past a certain point the table of contents position changes to fixed so it is always visible.

There is a problem with this particular implementation though. If the table of contents list is long and exceeds the height of the browser window, then it is impossible to see the bottom part of the table of contents.

In Bootstrap 4.x (example) they changed the way tables of contents were displayed to avoid this problem. They are now always fixed position and if the content cannot fit in the vertical space they show a secondary scroll bar just for the table of contents.

This works well enough, but I was looking for a more automatic method closer to the way it was done on the Bootstrap 3.x pages for my Bootstrap Customizer website, but with the ability to be able to scroll to see any content that wouldn't fit in the browser window.

This is quite a challenging task as the requirements are a little complex:

1) The TOC (table of contents) displays and scrolls as normal until you scroll down to the end of the TOC. At this point the TOC displays as fixed with the bottom of TOC at the bottom of the browser window.

2) When scrolled down to the end of the page, the TOC should not be displayed over the footer, and the end of the TOC should display at the end of it's container.

3) When you scroll up the TOC should also scroll up. This should only happen up to the point where the top most item in the TOC is at the top of the screen, or has reached it's original natural position.

Based on these requirements, I've been working on a prototype.

(function ($) {
$.fn.scrollfixed = function (options) {
var settings = $.extend({
breakpoint: 0
}, options);

var obj = this;
var isFixed = false;
var atBottom = false;
var lastScrollTop = -1;
var contents_top = obj.offset().top;

function handleScroll() {

if (window.matchMedia('(min-width: ' + options.breakpoint + 'px)').matches) {
var container_top = $('#' + options.container).offset().top;
var container_height = $('#' + options.container).height();
var contents_end = contents_top + obj.height();
var offset = $(window).height() + $(window).scrollTop();
var corr = parseInt(obj.css("top"));

if (isFixed === true && ((offset < contents_end && isNaN(corr)) || (!isNaN(corr) && $(window).scrollTop() < contents_top))) {
isFixed = false;
atBottom = false;
obj.css("top", "");
obj.css("left", "");
obj.css("position", "");
}
else if (offset >= contents_end) {
if (isFixed === false) {
obj.css("left", obj.offset().left);
obj.css("position", "fixed");
}

if (isFixed && $(window).scrollTop() < lastScrollTop) {
var top = parseInt(obj.css("top")) + (lastScrollTop - $(window).scrollTop());

if (top > 0) {
top = 0;
}

obj.css("top", top);
}
else
if (offset >= (container_top + container_height)) {
atBottom = true;
obj.css("top", ($(window).height() - obj.height()) + ((container_top + container_height) - offset));
}
else if ((atBottom == true || isFixed === false) && offset < (container_top + container_height)) {
atBottom = false;
obj.css("top", $(window).height() - obj.height());
}
else if (parseInt(obj.css("top")) != $(window).height() - obj.height()) {
var top = parseInt(obj.css("top")) + (lastScrollTop - $(window).scrollTop());

if (top < $(window).height() - obj.height()) {
top = $(window).height() - obj.height();
}

obj.css("top", top);
}

isFixed = true;
}

lastScrollTop = $(window).scrollTop();
}
else if (isFixed === true) {
isFixed = false;
atBottom = false;
obj.css("top", "");
obj.css("left", "");
obj.css("position", "");
}
}

$(window).scroll(function () {

clearTimeout(handleScroll._tId);
handleScroll._tId = setTimeout(function () {
handleScroll();
}, 10);
});

handleScroll();

return this;
};

}(jQuery));


This is done as a jQuery plugin, and can be called with parameters "breakpoint" (the pixel min-width at which the positional changes should take effect, so you can disable this functionality on small screens if needed) and "container" (the ID of the container object that should be used to calculate minimum and maximum positions - this would usually be somethign like a Bootstrap column).

$("#myTOC").scrollfixed({ breakpoint: 768, container: 'myTOCContainer' });


This achieves all of the objectives but there's still a few minor issues with this code which I need to improve.

Related Posts

Web Development

How to add AJAX suggestions to yairEO's Tagify tag input component

by John Avis | May 25, 2018

yairEO's Tagify is a great implementation of a tag component, being very lightweight and using in-built browser behaviour where possible. Here's how I get get suggestions via AJAX rather than a fixed whitelist.


Web Development

10 things a web developer should be careful of

by John Avis | April 23, 2018

Here's ten things every web developer should be aware of and avoid.


Web Development

Why Microsoft's ASP/ASP.NET may be the safe choice for development

by John Avis | February 15, 2018

Some reasons why developing using Microsoft's ASP/ASP.NET has been a good choice over the years.

Comments

There are no comments yet. Be the first to leave a comment!

Leave a Comment
Tags
ASP.NET Html Forms ASP.NET MVC ASP.NET Web Forms ASP.NET Web Pages Bootstrap C# Classic ASP Cool Websites Databases eBay and PayPal Electrical Repairs General Hardware HTML/CSS Jquery/Javascript Media Center Mobile Phones Responsive Web Design SEO and Social Networking Web Design Web Development Web Security web+db Website Hosting Windows XP Youtube

About me

...mostly about web development and programming, with a little bit of anything else related to the Internet, computers and technology.

Subscribe

Get the latest posts delivered to your inbox.