priority-nav-scroller
Version:
A plugin for the priority+ navigation pattern. When the navigation items don’t fit on screen they are hidden in a horizontal scrollable container with controls.
214 lines (158 loc) • 6.45 kB
JavaScript
/**
Priority+ horizontal scrolling menu.
@param {Object} object - Container for all options.
@param {string || DOM node} selector - Element selector.
@param {string} navSelector - Nav element selector.
@param {string} contentSelector - Content element selector.
@param {string} itemSelector - Items selector.
@param {string} buttonLeftSelector - Left button selector.
@param {string} buttonRightSelector - Right button selector.
@param {integer || string} scrollStep - Amount to scroll on button click. 'average' gets the average link width.
*/
const PriorityNavScroller = function({
selector: selector = '.nav-scroller',
navSelector: navSelector = '.nav-scroller-nav',
contentSelector: contentSelector = '.nav-scroller-content',
itemSelector: itemSelector = '.nav-scroller-item',
buttonLeftSelector: buttonLeftSelector = '.nav-scroller-btn--left',
buttonRightSelector: buttonRightSelector = '.nav-scroller-btn--right',
scrollStep: scrollStep = 80
} = {}) {
const navScroller = typeof selector === 'string' ? document.querySelector(selector) : selector;
const validateScrollStep = () => {
return Number.isInteger(scrollStep) || scrollStep === 'average';
}
if (navScroller === undefined || navScroller === null || !validateScrollStep()) {
throw new Error('There is something wrong, check your options.');
}
const navScrollerNav = navScroller.querySelector(navSelector);
const navScrollerContent = navScroller.querySelector(contentSelector);
const navScrollerContentItems = navScrollerContent.querySelectorAll(itemSelector);
const navScrollerLeft = navScroller.querySelector(buttonLeftSelector);
const navScrollerRight = navScroller.querySelector(buttonRightSelector);
let scrolling = false;
let scrollAvailableLeft = 0;
let scrollAvailableRight = 0;
let scrollingDirection = '';
let scrollOverflow = '';
let timeout;
// Sets overflow and toggle buttons accordingly
const setOverflow = function() {
scrollOverflow = getOverflow();
toggleButtons(scrollOverflow);
calculateScrollStep();
}
// Debounce setting the overflow with requestAnimationFrame
const requestSetOverflow = function() {
if (timeout) window.cancelAnimationFrame(timeout);
timeout = window.requestAnimationFrame(() => {
setOverflow();
});
}
// Gets the overflow available on the nav scroller
const getOverflow = function() {
let scrollWidth = navScrollerNav.scrollWidth;
let scrollViewport = navScrollerNav.clientWidth;
let scrollLeft = navScrollerNav.scrollLeft;
scrollAvailableLeft = scrollLeft;
scrollAvailableRight = scrollWidth - (scrollViewport + scrollLeft);
// 1 instead of 0 to compensate for number rounding
let scrollLeftCondition = scrollAvailableLeft > 1;
let scrollRightCondition = scrollAvailableRight > 1;
// console.log(scrollWidth, scrollViewport, scrollAvailableLeft, scrollAvailableRight);
if (scrollLeftCondition && scrollRightCondition) {
return 'both';
}
else if (scrollLeftCondition) {
return 'left';
}
else if (scrollRightCondition) {
return 'right';
}
else {
return 'none';
}
}
// Calculates the scroll step based on the width of the scroller and the number of links
const calculateScrollStep = function() {
if (scrollStep === 'average') {
let scrollViewportNoPadding = navScrollerNav.scrollWidth - (parseInt(getComputedStyle(navScrollerContent).getPropertyValue('padding-left')) + parseInt(getComputedStyle(navScrollerContent).getPropertyValue('padding-right')));
let scrollStepAverage = Math.floor(scrollViewportNoPadding / navScrollerContentItems.length);
scrollStep = scrollStepAverage;
}
}
// Move the scroller with a transform
const moveScroller = function(direction) {
if (scrolling === true || (scrollOverflow !== direction && scrollOverflow !== 'both')) return;
let scrollDistance = scrollStep;
let scrollAvailable = direction === 'left' ? scrollAvailableLeft : scrollAvailableRight;
// If there will be less than 25% of the last step visible then scroll to the end
if (scrollAvailable < (scrollStep * 1.75)) {
scrollDistance = scrollAvailable;
}
if (direction === 'right') {
scrollDistance *= -1;
if (scrollAvailable < scrollStep) {
navScrollerContent.classList.add('snap-align-end');
}
}
navScrollerContent.classList.remove('no-transition');
navScrollerContent.style.transform = 'translateX(' + scrollDistance + 'px)';
scrollingDirection = direction;
scrolling = true;
}
// Set the scroller position and removes transform, called after moveScroller() in the transitionend event
const setScrollerPosition = function() {
var style = window.getComputedStyle(navScrollerContent, null);
var transform = style.getPropertyValue('transform');
var transformValue = Math.abs(parseInt(transform.split(',')[4]) || 0);
if (scrollingDirection === 'left') {
transformValue *= -1;
}
navScrollerContent.classList.add('no-transition');
navScrollerContent.style.transform = '';
navScrollerNav.scrollLeft = navScrollerNav.scrollLeft + transformValue;
navScrollerContent.classList.remove('no-transition', 'snap-align-end');
scrolling = false;
}
// Toggle buttons depending on overflow
const toggleButtons = function(overflow) {
if (overflow === 'both' || overflow === 'left') {
navScrollerLeft.classList.add('active');
}
else {
navScrollerLeft.classList.remove('active');
}
if (overflow === 'both' || overflow === 'right') {
navScrollerRight.classList.add('active');
}
else {
navScrollerRight.classList.remove('active');
}
}
const init = function() {
setOverflow();
window.addEventListener('resize', () => {
requestSetOverflow();
});
navScrollerNav.addEventListener('scroll', () => {
requestSetOverflow();
});
navScrollerContent.addEventListener('transitionend', () => {
setScrollerPosition();
});
navScrollerLeft.addEventListener('click', () => {
moveScroller('left');
});
navScrollerRight.addEventListener('click', () => {
moveScroller('right');
});
};
// Self init
init();
// Reveal API
return {
init
};
};
export default PriorityNavScroller;