
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
Rate this post:
Comments
There are no comments yet. Be the first to leave a comment!