/** * Extend jquery with a scrollspy plugin. * This watches the window scroll and fires events when elements are scrolled into viewport. * * throttle() and getTime() taken from Underscore.js * https://github.com/jashkenas/underscore * * @author Copyright 2013 John Smart * @license https://raw.github.com/thesmart/jquery-scrollspy/master/LICENSE * @see https://github.com/thesmart * @version 0.1.2 */ (function($) { var jWindow = $(window); var elements = []; var elementsInView = []; var isSpying = false; var ticks = 0; var offset = { top : 0, right : 0, bottom : 0, left : 0 }; /** * Find elements that are within the boundary * @param {number} top * @param {number} right * @param {number} bottom * @param {number} left * @return {jQuery} A collection of elements */ function findElements(top, right, bottom, left) { var hits = $(); $.each(elements, function(i, element) { var elTop = element.offset().top, elLeft = element.offset().left, elRight = elLeft + element.width(), elBottom = elTop + element.height(); var isIntersect = !(elLeft > right || elRight < left || elTop > bottom || elBottom < top); if (isIntersect) { hits.push(element); } }); return hits; } /** * Called when the user scrolls the window */ function onScroll() { // unique tick id ++ticks; // viewport rectangle var top = jWindow.scrollTop(), left = jWindow.scrollLeft(), right = left + jWindow.width(), bottom = top + jWindow.height(); // determine which elements are in view var intersections = findElements(top+offset.top, right+offset.right, bottom+offset.bottom, left+offset.left); $.each(intersections, function(i, element) { var lastTick = element.data('scrollSpy:ticks'); if (typeof lastTick != 'number') { // entered into view element.triggerHandler('scrollSpy:enter'); } // update tick id element.data('scrollSpy:ticks', ticks); }); // determine which elements are no longer in view $.each(elementsInView, function(i, element) { var lastTick = element.data('scrollSpy:ticks'); if (typeof lastTick == 'number' && lastTick !== ticks) { // exited from view element.triggerHandler('scrollSpy:exit'); element.data('scrollSpy:ticks', null); } }); // remember elements in view for next tick elementsInView = intersections; } /** * Called when window is resized */ function onWinSize() { jWindow.trigger('scrollSpy:winSize'); } /** * Get time in ms * @license https://raw.github.com/jashkenas/underscore/master/LICENSE * @type {function} * @return {number} */ var getTime = (Date.now || function () { return new Date().getTime(); }); /** * Returns a function, that, when invoked, will only be triggered at most once * during a given window of time. Normally, the throttled function will run * as much as it can, without ever going more than once per `wait` duration; * but if you'd like to disable the execution on the leading edge, pass * `{leading: false}`. To disable execution on the trailing edge, ditto. * @license https://raw.github.com/jashkenas/underscore/master/LICENSE * @param {function} func * @param {number} wait * @param {Object=} options * @returns {Function} */ function throttle(func, wait, options) { var context, args, result; var timeout = null; var previous = 0; options || (options = {}); var later = function () { previous = options.leading === false ? 0 : getTime(); timeout = null; result = func.apply(context, args); context = args = null; }; return function () { var now = getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; } /** * Enables ScrollSpy using a selector * @param {jQuery|string} selector The elements collection, or a selector * @param {Object=} options Optional. throttle : number -> scrollspy throttling. Default: 100 ms offsetTop : number -> offset from top. Default: 0 offsetRight : number -> offset from right. Default: 0 offsetBottom : number -> offset from bottom. Default: 0 offsetLeft : number -> offset from left. Default: 0 * @returns {jQuery} */ $.scrollSpy = function(selector, options) { selector = $(selector); selector.each(function(i, element) { elements.push($(element)); }); options = options || { throttle: 100 }; offset.top = options.offsetTop || 0; offset.right = options.offsetRight || 0; offset.bottom = options.offsetBottom || 0; offset.left = options.offsetLeft || 0; var throttledScroll = throttle(onScroll, options.throttle || 100); var readyScroll = function(){ $(document).ready(throttledScroll); }; if (!isSpying) { jWindow.on('scroll', readyScroll); jWindow.on('resize', readyScroll); isSpying = true; } // perform a scan once, after current execution context, and after dom is ready setTimeout(readyScroll, 0); return selector; }; /** * Listen for window resize events * @param {Object=} options Optional. Set { throttle: number } to change throttling. Default: 100 ms * @returns {jQuery} $(window) */ $.winSizeSpy = function(options) { $.winSizeSpy = function() { return jWindow; }; // lock from multiple calls options = options || { throttle: 100 }; return jWindow.on('resize', throttle(onWinSize, options.throttle || 100)); }; /** * Enables ScrollSpy on a collection of elements * e.g. $('.scrollSpy').scrollSpy() * @param {Object=} options Optional. throttle : number -> scrollspy throttling. Default: 100 ms offsetTop : number -> offset from top. Default: 0 offsetRight : number -> offset from right. Default: 0 offsetBottom : number -> offset from bottom. Default: 0 offsetLeft : number -> offset from left. Default: 0 * @returns {jQuery} */ $.fn.scrollSpy = function(options) { return $.scrollSpy($(this), options); }; })(jQuery);