/*!

* gumshoejs v5.1.1
* A simple, framework-agnostic scrollspy script.
* (c) 2019 Chris Ferdinandi
* MIT License
* http://github.com/cferdinandi/gumshoe
*/

(function (root, factory) {

if ( typeof define === 'function' && define.amd ) {
        define([], (function () {
                return factory(root);
        }));
} else if ( typeof exports === 'object' ) {
        module.exports = factory(root);
} else {
        root.Gumshoe = factory(root);
}

})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this, (function (window) {

'use strict';

//
// Defaults
//

var defaults = {

        // Active classes
        navClass: 'active',
        contentClass: 'active',

        // Nested navigation
        nested: false,
        nestedClass: 'active',

        // Offset & reflow
        offset: 0,
        reflow: false,

        // Event support
        events: true

};

//
// Methods
//

/**
 * Merge two or more objects together.
 * @param   {Object}   objects  The objects to merge together
 * @returns {Object}            Merged values of defaults and options
 */
var extend = function () {
        var merged = {};
        Array.prototype.forEach.call(arguments, (function (obj) {
                for (var key in obj) {
                        if (!obj.hasOwnProperty(key)) return;
                        merged[key] = obj[key];
                }
        }));
        return merged;
};

/**
 * Emit a custom event
 * @param  {String} type   The event type
 * @param  {Node}   elem   The element to attach the event to
 * @param  {Object} detail Any details to pass along with the event
 */
var emitEvent = function (type, elem, detail) {

        // Make sure events are enabled
        if (!detail.settings.events) return;

        // Create a new event
        var event = new CustomEvent(type, {
                bubbles: true,
                cancelable: true,
                detail: detail
        });

        // Dispatch the event
        elem.dispatchEvent(event);

};

/**
 * Get an element's distance from the top of the Document.
 * @param  {Node} elem The element
 * @return {Number}    Distance from the top in pixels
 */
var getOffsetTop = function (elem) {
        var location = 0;
        if (elem.offsetParent) {
                while (elem) {
                        location += elem.offsetTop;
                        elem = elem.offsetParent;
                }
        }
        return location >= 0 ? location : 0;
};

/**
 * Sort content from first to last in the DOM
 * @param  {Array} contents The content areas
 */
var sortContents = function (contents) {
        if(contents) {
                contents.sort((function (item1, item2) {
                        var offset1 = getOffsetTop(item1.content);
                        var offset2 = getOffsetTop(item2.content);
                        if (offset1 < offset2) return -1;
                        return 1;
                }));
        }
};

/**
 * Get the offset to use for calculating position
 * @param  {Object} settings The settings for this instantiation
 * @return {Float}           The number of pixels to offset the calculations
 */
var getOffset = function (settings) {

        // if the offset is a function run it
        if (typeof settings.offset === 'function') {
                return parseFloat(settings.offset());
        }

        // Otherwise, return it as-is
        return parseFloat(settings.offset);

};

/**
 * Get the document element's height
 * @private
 * @returns {Number}
 */
var getDocumentHeight = function () {
        return Math.max(
                document.body.scrollHeight, document.documentElement.scrollHeight,
                document.body.offsetHeight, document.documentElement.offsetHeight,
                document.body.clientHeight, document.documentElement.clientHeight
        );
};

/**
 * Determine if an element is in view
 * @param  {Node}    elem     The element
 * @param  {Object}  settings The settings for this instantiation
 * @param  {Boolean} bottom   If true, check if element is above bottom of viewport instead
 * @return {Boolean}          Returns true if element is in the viewport
 */
var isInView = function (elem, settings, bottom) {
        var bounds = elem.getBoundingClientRect();
        var offset = getOffset(settings);
        if (bottom) {
                return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight);
        }
        return parseInt(bounds.top, 10) <= offset;
};

/**
 * Check if at the bottom of the viewport
 * @return {Boolean} If true, page is at the bottom of the viewport
 */
var isAtBottom = function () {
        if (window.innerHeight + window.pageYOffset >= getDocumentHeight()) return true;
        return false;
};

/**
 * Check if the last item should be used (even if not at the top of the page)
 * @param  {Object} item     The last item
 * @param  {Object} settings The settings for this instantiation
 * @return {Boolean}         If true, use the last item
 */
var useLastItem = function (item, settings) {
        if (isAtBottom() && isInView(item.content, settings, true)) return true;
        return false;
};

/**
 * Get the active content
 * @param  {Array}  contents The content areas
 * @param  {Object} settings The settings for this instantiation
 * @return {Object}          The content area and matching navigation link
 */
var getActive = function (contents, settings) {
        var last = contents[contents.length-1];
        if (useLastItem(last, settings)) return last;
        for (var i = contents.length - 1; i >= 0; i--) {
                if (isInView(contents[i].content, settings)) return contents[i];
        }
};

/**
 * Deactivate parent navs in a nested navigation
 * @param  {Node}   nav      The starting navigation element
 * @param  {Object} settings The settings for this instantiation
 */
var deactivateNested = function (nav, settings) {

        // If nesting isn't activated, bail
        if (!settings.nested) return;

        // Get the parent navigation
        var li = nav.parentNode.closest('li');
        if (!li) return;

        // Remove the active class
        li.classList.remove(settings.nestedClass);

        // Apply recursively to any parent navigation elements
        deactivateNested(li, settings);

};

/**
 * Deactivate a nav and content area
 * @param  {Object} items    The nav item and content to deactivate
 * @param  {Object} settings The settings for this instantiation
 */
var deactivate = function (items, settings) {

        // Make sure their are items to deactivate
        if (!items) return;

        // Get the parent list item
        var li = items.nav.closest('li');
        if (!li) return;

        // Remove the active class from the nav and content
        li.classList.remove(settings.navClass);
        items.content.classList.remove(settings.contentClass);

        // Deactivate any parent navs in a nested navigation
        deactivateNested(li, settings);

        // Emit a custom event
        emitEvent('gumshoeDeactivate', li, {
                link: items.nav,
                content: items.content,
                settings: settings
        });

};

/**
 * Activate parent navs in a nested navigation
 * @param  {Node}   nav      The starting navigation element
 * @param  {Object} settings The settings for this instantiation
 */
var activateNested = function (nav, settings) {

        // If nesting isn't activated, bail
        if (!settings.nested) return;

        // Get the parent navigation
        var li = nav.parentNode.closest('li');
        if (!li) return;

        // Add the active class
        li.classList.add(settings.nestedClass);

        // Apply recursively to any parent navigation elements
        activateNested(li, settings);

};

/**
 * Activate a nav and content area
 * @param  {Object} items    The nav item and content to activate
 * @param  {Object} settings The settings for this instantiation
 */
var activate = function (items, settings) {

        // Make sure their are items to activate
        if (!items) return;

        // Get the parent list item
        var li = items.nav.closest('li');
        if (!li) return;

        // Add the active class to the nav and content
        li.classList.add(settings.navClass);
        items.content.classList.add(settings.contentClass);

        // Activate any parent navs in a nested navigation
        activateNested(li, settings);

        // Emit a custom event
        emitEvent('gumshoeActivate', li, {
                link: items.nav,
                content: items.content,
                settings: settings
        });

};

/**
 * Create the Constructor object
 * @param {String} selector The selector to use for navigation items
 * @param {Object} options  User options and settings
 */
var Constructor = function (selector, options) {

        //
        // Variables
        //

        var publicAPIs = {};
        var navItems, contents, current, timeout, settings;

        //
        // Methods
        //

        /**
         * Set variables from DOM elements
         */
        publicAPIs.setup = function () {

                // Get all nav items
                navItems = document.querySelectorAll(selector);

                // Create contents array
                contents = [];

                // Loop through each item, get it's matching content, and push to the array
                Array.prototype.forEach.call(navItems, (function (item) {

                        // Get the content for the nav item
                        var content = document.getElementById(decodeURIComponent(item.hash.substr(1)));
                        if (!content) return;

                        // Push to the contents array
                        contents.push({
                                nav: item,
                                content: content
                        });

                }));

                // Sort contents by the order they appear in the DOM
                sortContents(contents);

        };

        /**
         * Detect which content is currently active
         */
        publicAPIs.detect = function () {

                // Get the active content
                var active = getActive(contents, settings);

                // if there's no active content, deactivate and bail
                if (!active) {
                        if (current) {
                                deactivate(current, settings);
                                current = null;
                        }
                        return;
                }

                // If the active content is the one currently active, do nothing
                if (current && active.content === current.content) return;

                // Deactivate the current content and activate the new content
                deactivate(current, settings);
                activate(active, settings);

                // Update the currently active content
                current = active;

        };

        /**
         * Detect the active content on scroll
         * Debounced for performance
         */
        var scrollHandler = function (event) {

                // If there's a timer, cancel it
                if (timeout) {
                        window.cancelAnimationFrame(timeout);
                }

                // Setup debounce callback
                timeout = window.requestAnimationFrame(publicAPIs.detect);

        };

        /**
         * Update content sorting on resize
         * Debounced for performance
         */
        var resizeHandler = function (event) {

                // If there's a timer, cancel it
                if (timeout) {
                        window.cancelAnimationFrame(timeout);
                }

                // Setup debounce callback
                timeout = window.requestAnimationFrame((function () {
                        sortContents(contents);
                        publicAPIs.detect();
                }));

        };

        /**
         * Destroy the current instantiation
         */
        publicAPIs.destroy = function () {

                // Undo DOM changes
                if (current) {
                        deactivate(current, settings);
                }

                // Remove event listeners
                window.removeEventListener('scroll', scrollHandler, false);
                if (settings.reflow) {
                        window.removeEventListener('resize', resizeHandler, false);
                }

                // Reset variables
                contents = null;
                navItems = null;
                current = null;
                timeout = null;
                settings = null;

        };

        /**
         * Initialize the current instantiation
         */
        var init = function () {

                // Merge user options into defaults
                settings = extend(defaults, options || {});

                // Setup variables based on the current DOM
                publicAPIs.setup();

                // Find the currently active content
                publicAPIs.detect();

                // Setup event listeners
                window.addEventListener('scroll', scrollHandler, false);
                if (settings.reflow) {
                        window.addEventListener('resize', resizeHandler, false);
                }

        };

        //
        // Initialize and return the public APIs
        //

        init();
        return publicAPIs;

};

//
// Return the Constructor
//

return Constructor;

}));