A jQuery plugin for a fixed table of contents
by johna | 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 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; var contentTop = 0; var contentTopOriginal = obj.offset().top; function handleScroll() { var contentHeight = obj.outerHeight(true); var containerTop = $('#' + options.container).offset().top; var containerHeight = $('#' + options.container).height(); var containerBottom = containerTop + containerHeight; var viewportHeight = $(window).height(); var scrollTop = $(window).scrollTop(); var scrollingUp = scrollTop < lastScrollTop; var scrollingDown = scrollTop > lastScrollTop; var nextIsFixed; var nextContentTop; if (window.matchMedia('(min-width: ' + options.breakpoint + 'px)').matches && containerHeight !== contentHeight) { if (scrollTop > contentTopOriginal) { nextIsFixed = true; if (contentHeight <= containerHeight && (containerBottom - scrollTop - contentHeight) < 0) { if ((containerBottom - scrollTop - contentHeight) < 0) { nextContentTop = (containerBottom - scrollTop - contentHeight); } else { nextContentTop = 0; } } else { nextContentTop = contentTop; if (scrollingDown) { if (scrollTop > containerBottom - (contentTop + contentHeight)) { nextContentTop += (lastScrollTop - scrollTop); } else if ((contentTop + contentHeight) > viewportHeight) { nextContentTop += (lastScrollTop - scrollTop); } if (nextContentTop < (viewportHeight - contentHeight)) { nextContentTop = (viewportHeight - contentHeight); } } else if (scrollingUp) { if (contentTop < 0) { nextContentTop -= (scrollTop - lastScrollTop); } if (nextContentTop > 0) { nextContentTop = 0; } } } } else { nextIsFixed = false; nextContentTop = 0; task = 'fits???'; } } else { nextIsFixed = false; nextContentTop = 0; } if (nextIsFixed !== isFixed) { obj.css("position", (nextIsFixed === true ? "fixed" : "")); obj.css("left", (nextIsFixed === true ? obj.offset().left + "px" : "")); } if (nextContentTop !== contentTop || nextIsFixed !== isFixed) { obj.css("top", (nextIsFixed === true ? nextContentTop + "px" : "")); } isFixed = nextIsFixed; contentTop = nextContentTop; lastScrollTop = scrollTop; } $(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 something like a Bootstrap column).
$("#myTOC").scrollfixed({ breakpoint: 768, container: 'myTOCContainer' });
Demo
This achieves all of the objectives but there's still a few minor issues with this code which I need to improve.
Change History
12-Dec-2020 - Code revised and improved, demo added
Related Posts
Converting dBase IV programs to run in the browser
by johna | September 13, 2024
Some pointless entertainment trying to get some old dBase programs running in the browser.
How to set up a debugging using the Turnkey Linux LAMP stack and VS Code
by johna | December 19, 2023
The second part in my guide to setting up a website and database using the Turnkey Linux LAMP stack.
How to set up a website and database using the Turnkey Linux LAMP stack
by johna | November 18, 2023
If you need to host your own website for the purposes of web development, Turnkey Linux LAMP Stack is an easy to install all-in-one solution that you can set up on a spare computer or a VM (Virtual Machine).
Comments
There are no comments yet. Be the first to leave a comment!