Pi2-FGABreja/FGABrejaWeb

View on GitHub
FGABreja/static/js/scrollspy.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * 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 unique_id = 1;
    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) {
            if (element.height() > 0) {
                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
//        + 60 accounts for fixed nav
        var intersections = findElements(top+offset.top + 200, 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) {
        var visible = [];
        selector = $(selector);
        selector.each(function(i, element) {
            elements.push($(element));
            $(element).data("scrollSpy:id", i);
            // Smooth scroll to section
          $('a[href=#' + $(element).attr('id') + ']').click(function(e) {
            e.preventDefault();
            var offset = $(this.hash).offset().top + 1;

//          offset - 200 allows elements near bottom of page to scroll
            
            $('html, body').animate({ scrollTop: offset - 200 }, {duration: 400, queue: false, easing: 'easeOutCubic'});
            
          });
        });
        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);


        selector.on('scrollSpy:enter', function() {
            visible = $.grep(visible, function(value) {
          return value.height() != 0;
        });

            var $this = $(this);

            if (visible[0]) {
                $('a[href=#' + visible[0].attr('id') + ']').removeClass('active');
                if ($this.data('scrollSpy:id') < visible[0].data('scrollSpy:id')) {
                    visible.unshift($(this));
                }
                else {
                    visible.push($(this));
                }
            }
            else {
                visible.push($(this));
            }


            $('a[href=#' + visible[0].attr('id') + ']').addClass('active');
        });
        selector.on('scrollSpy:exit', function() {
            visible = $.grep(visible, function(value) {
          return value.height() != 0;
        });

            if (visible[0]) {
                $('a[href=#' + visible[0].attr('id') + ']').removeClass('active');
                var $this = $(this);
                visible = $.grep(visible, function(value) {
            return value.attr('id') != $this.attr('id');
          });
          if (visible[0]) { // Check if empty
                    $('a[href=#' + visible[0].attr('id') + ']').addClass('active');
          }
            }
        });

        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);