spectaql
Version:
A powerful library for autogenerating static GraphQL API documentation
133 lines (109 loc) • 3.69 kB
JavaScript
;function scrollSpy() {
var INIT_DELAY_MS = 300;
var SCROLL_DEBOUNCE_MS = 100;
var RESIZE_DEBOUNCE_MS = 500;
var PADDING = 5;
var htmlElement = document.querySelector('html');
if (htmlElement) {
var scrollPaddingTop = window.getComputedStyle(htmlElement).scrollPaddingTop;
if (
scrollPaddingTop &&
typeof scrollPaddingTop === 'string' &&
scrollPaddingTop !== 'auto' &&
scrollPaddingTop.endsWith('px'))
{
PADDING = PADDING + parseInt(scrollPaddingTop.split('px')[0]);
}
}
var ACTIVE_CLASS = 'nav-scroll-active';
var EXPAND_CLASS = 'nav-scroll-expand';
var EXPANDABLE_SELECTOR = '.nav-group-section';
var currentIndex = null;
var sections = [];
function init() {
findScrollPositions();
handleScroll();
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
}
function findScrollPositions() {
currentIndex = null;
var allScrollableItems = document.querySelectorAll('[data-traverse-target]');
Array.prototype.forEach.call(allScrollableItems, function (e) {
sections.push({ id: e.id, top: e.offsetTop });
});
}
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (
window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth));
}
var handleResize = debounce(function () {
findScrollPositions();
handleScroll();
}, RESIZE_DEBOUNCE_MS);
var handleScroll = debounce(function () {
var scrollPosition =
document.documentElement.scrollTop || document.body.scrollTop;
var index = getVisibleSectionIndex(scrollPosition);
if (index === currentIndex) {
return;
}
currentIndex = index;
var section = sections[index];
var activeEl = document.querySelector(`.${ACTIVE_CLASS}`);
var nextEl = section ?
document.querySelector('#nav a[href="#' + section.id + '"]') :
null;
var parentNextEl = getParentSection(nextEl);
var parentActiveEl = getParentSection(activeEl);
var isDifferentParent = parentActiveEl !== parentNextEl;
if (parentActiveEl && isDifferentParent) {
toggleSectionExpansion(parentActiveEl, false);
}
if (parentNextEl && isDifferentParent) {
toggleSectionExpansion(parentNextEl, true);
}
if (nextEl) {
nextEl.classList.add(ACTIVE_CLASS);
if (nextEl.scrollIntoViewIfNeeded) {
nextEl.scrollIntoViewIfNeeded();
} else if (nextEl.scrollIntoView && !isElementInViewport(nextEl)) {
nextEl.scrollIntoView({ block: 'center', inline: 'start' });
}
}
if (activeEl) {
activeEl.classList.remove(ACTIVE_CLASS);
}
}, SCROLL_DEBOUNCE_MS);
function toggleSectionExpansion(element, shouldExpand) {
const classListFunc = shouldExpand ? 'add' : 'remove';
while (element) {
element.classList[classListFunc](EXPAND_CLASS);
element = getParentSection(element.parentNode);
}
}
function getParentSection(el) {
if (!el || !el.closest) return null;
return el.closest(EXPANDABLE_SELECTOR);
}
function getVisibleSectionIndex(scrollPosition) {
var positionToCheck = scrollPosition + PADDING;
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
var nextSection = sections[i + 1];
if (
positionToCheck >= section.top && (
!nextSection || positionToCheck < nextSection.top))
{
return i;
}
}
return -1;
}
setTimeout(init, INIT_DELAY_MS);
}