5641d7e51a
ALSO: Remove fading on dropdown menu click. This is also a bug on the actual upstream, when the cursor leaves the screen the menu disappears but the fading doesn't. This causes things like the menu saying it's open when it's not and closed when open. I'm not dealing with an upstream bug.
484 lines
No EOL
11 KiB
JavaScript
Vendored
484 lines
No EOL
11 KiB
JavaScript
Vendored
/*!
|
|
* 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;
|
|
|
|
})); |