navbar
Version:
A tiny library to create nav elements that smart update on scroll to keep the correct item active.
124 lines (94 loc) • 3.29 kB
JavaScript
const selectedClass = 'navbar-active';
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
supportsPassive = true;
return true;
}
});
window.addEventListener('test', null, opts);
} catch { /* nothing */ }
function createAndAppendListItems(navList, elementList, makeNavListItem) {
const pairs = [];
// Create list elements
for (const element of elementList) {
const li = makeNavListItem(element);
navList.appendChild(li);
pairs.push({ element, navElement: li });
}
return pairs;
}
function makeHandleScroll(pairs, debounceTime) {
function handleScroll() {
/** @type {HTMLElement|null} */
let frontRunner = null;
let closestDist = Infinity;
for (const pair of pairs) {
const absDist = Math.abs(pair.element.getBoundingClientRect().top);
// If this element is not the front runner for top, deactivate it.
if (absDist > closestDist) {
pair.navElement.classList.remove(selectedClass);
continue;
}
// If this is a new front runner, deactivate the previous front runner.
if (frontRunner) {
frontRunner.classList.remove(selectedClass);
}
frontRunner = pair.navElement;
closestDist = absDist;
}
// All other elements have been deactivated, and now the top element is known and can be set
// as active.
frontRunner.classList.add(selectedClass);
}
// The default behaviour is no debounce.
if (typeof debounceTime !== 'number' || isNaN(debounceTime)) {
return handleScroll;
}
let timeout;
function nullifyTimeout() {
timeout = null;
}
return function debouncedHandleScroll() {
if (timeout) {
return;
}
// Immediately use handleScroll to calculate.
handleScroll();
// No further calls to handleScroll until debounceTime has elapsed.
timeout = setTimeout(nullifyTimeout, debounceTime);
};
}
function addScrollListener(target, handleScroll) {
function scrollHandleWrapper(evt) {
if (evt.target === target) {
handleScroll();
}
}
if (target.addEventListener) {
target.addEventListener('scroll', scrollHandleWrapper, supportsPassive ? { passive: true } : false);
} else if (target.attachEvent) {
target.attachEvent('onscroll', scrollHandleWrapper);
} else {
throw new Error('This browser does not support addEventListener or attachEvent.');
}
// To calculate the initial active list element.
handleScroll();
}
export default function makeNav(options) {
if (!options || !options.elementList || !options.makeNavListItem) {
throw new Error('Options object with elementList and makeNavListItem must be provided.');
}
const nav = document.createElement(options.tagName || 'nav');
const navList = document.createElement('ul');
// The target defaults to window.
const target = options.target || document;
// Create list elements
const pairs = createAndAppendListItems(navList, options.elementList, options.makeNavListItem);
// Whenever the window is scrolled, recalculate the active list element. Compatible with older
// versions of IE.
addScrollListener(target, makeHandleScroll(pairs, options.debounceTime));
nav.appendChild(navList);
return nav;
}