window.StickyHeaders = (function ($) { var me = {}, body, config, contentContainer, currentHeaderRangeIndex, currentScrollOffset, elem, headerRanges, headers, isScrolling, lastScrollOffset, selectors, stickyHeaderContainer; function init() { config = { switchOnCollisionWith: 'top', copy: 'element' }; selectors = []; headers = []; headerRanges = []; currentHeaderRangeIndex = -1; currentScrollOffset = 0; lastScrollOffset = 0; isScrolling = false; createStickyHeader(); } function createStickyHeader() { elem = $('
').attr('id', 'sticky-header'); } function setHeaders() { var elements = [], stickyHeaderLineHeight = parseCssValue(elem.css('line-height')), stickyHeaderTopPadding; headers = []; $.each(selectors, function (_, selector) { contentContainer.find(selector).each(function (_, element) { var $element = $(element), fontSize = parseCssValue($element.css('font-size')), topOffset = ( element.offsetTop + parseFloat($element.css('padding-top'), 10) + (- ((stickyHeaderLineHeight - fontSize) / 2)) ), height = Math.round($element.height()), outerHeight = Math.round($element.outerHeight(true)), bottomOffset = topOffset + outerHeight; headers.push({ element: element, $element: $element, topOffset: topOffset, bottomOffset: bottomOffset }); }) }) } function setHeaderRanges() { var offsetProp = config.switchOnCollisionWith + 'Offset', start, end; headerRanges = []; for (var i = 0, len = headers.length; i < len; i++) { start = headers[i][offsetProp]; if (headers[i+1]) { end = headers[i+1][offsetProp]; } else { end = null; } headerRanges.push({ start: start, end: end, element: headers[i].element }); } //debugHeaderRanges(); } function debugHeaderRanges() { contentContainer.find('.header-range-debug').remove(); $.each(headerRanges, function (i, range) { var color = 'hsla('+(20*i)+', 100%, 50%, 0.15)', debug = $('
') .addClass('header-range-debug') .css({ width: '100%', position: 'absolute', top: range.start + 'px', height: (range.end === null ? '1px' : (range.end - range.start) + 'px'), backgroundColor: color, borderTop: '1px solid black' }) .appendTo(contentContainer) debug.append( $('
') .css({ position: 'absolute', top: 0, right: 0, height: '2em', lineHeight: '2em', width: '40em', fontSize: '13px', backgroundColor: 'black', color: 'white', padding: '0 5px' }) .text(headers[i].$element.text() + ' (#' + i + ')') ) }) } function setCurrentHeaderIndex() { var scrollTop = contentContainer.scrollTop(); for (var i = 0, len = headers.length; i < len; i++) { if (scrollTop < headers[i].bottomOffset) { break; } currentHeaderIndex = i; } } function render() { var clonedHeader; if (currentHeaderRangeIndex < 0 || currentHeaderRangeIndex > headerRanges.length-1) { elem.removeClass('show'); body.removeClass('has-sticky-header'); } else { var realHeader = $(headerRanges[currentHeaderRangeIndex].element); if (typeof config.fillHeadersWith === 'function') { elem.html(config.fillHeadersWith(realHeader)); } else if (config.fillHeadersWith === 'content') { elem.html(realHeader.clone().html()); } else { elem.html(realHeader.clone()); } elem.addClass('show'); body.addClass('has-sticky-header'); } return me; } function determineCurrentHeaderRangeIndex(startIndex, direction) { var index = startIndex; while (true) { currentHeaderRange = headerRanges[index]; if (!currentHeaderRange || isWithinRange(currentScrollOffset, currentHeaderRange)) { break; } else { index += direction; } } return index; } function onScroll() { if (!headerRanges.length) { return; } currentScrollOffset = contentContainer.scrollTop(); if (currentScrollOffset > headerRanges[0].start) { var newCurrentHeaderRangeIndex = currentHeaderRangeIndex; var currentHeaderRange = headerRanges[newCurrentHeaderRangeIndex]; if (newCurrentHeaderRangeIndex < 0) { newCurrentHeaderRangeIndex = 0; } if (currentScrollOffset < lastScrollOffset) { // scrolling up newCurrentHeaderRangeIndex = determineCurrentHeaderRangeIndex(newCurrentHeaderRangeIndex, -1); } else { // scrolling down newCurrentHeaderRangeIndex = determineCurrentHeaderRangeIndex(newCurrentHeaderRangeIndex, +1); } } else { newCurrentHeaderRangeIndex = -1; } // only re-render when necessary if (newCurrentHeaderRangeIndex !== undefined && currentHeaderRangeIndex !== newCurrentHeaderRangeIndex) { currentHeaderRangeIndex = newCurrentHeaderRangeIndex; render(); } lastScrollOffset = currentScrollOffset; } function listenToScroll(element, callback, options) { options = options || {}; if (options.every) { element.on('scroll', function () { isScrolling = true; }) setInterval(function () { if (isScrolling) { callback(); isScrolling = false; } }, options.every); } else { element.on('scroll', callback); } } function isWithinRange(number, range) { return ( number >= range.start && ( range.end === undefined || range.end === null || number <= range.end ) ); } function parseCssValue(value) { if (value === null || value === undefined) { return 0; } else { return parseInt(value, 10); } } me.setHeaders = setHeaders; me.setHeaderRanges = setHeaderRanges; me.set = function (/* key, value | config */) { if ($.isPlainObject(arguments[0])) { $.extend(config, arguments[0]); } else { config[arguments[0]] = arguments[1]; } return me; } me.add = function (/* selectors... */) { selectors.push.apply(selectors, arguments); return me; } me.activate = function () { body = $('body'); contentContainer = config.contentContainer ? $(config.contentContainer) : body; stickyHeaderContainer = config.stickyHeaderContainer ? $(config.stickyHeaderContainer) : contentContainer; stickyHeaderContainer.append(elem); setHeaders(); setHeaderRanges(); setCurrentHeaderIndex(); return me; } me.update = function () { setHeaders(); setHeaderRanges(); render(); listenToScroll(contentContainer, onScroll); } me.getHeaders = function () { return headers; } me.getHeaderRanges = function () { return headerRanges; } init(); return me; })(jQuery);