curver/js/nav.js

320 lines
8.6 KiB
JavaScript
Raw Normal View History

2020-08-16 13:53:37 +02:00
/**
* Polyfill for IE11 - adds NodeList.foreach().
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach
*/
if ( window.NodeList && ! NodeList.prototype.forEach ) {
NodeList.prototype.forEach = function( callback, thisArg ) {
thisArg = thisArg || window;
for ( var i = 0; i < this.length; i++ ) { // eslint-disable-line vars-on-top
callback.call( thisArg, this[ i ], i, this );
}
};
}
window.alxMediaMenu = {
/**
*
* @param {Object} args - The arguments.
* @param {string} args.selector - The navigation selector.
* @param {int} args.breakpoint - The breakpoint in pixels.
*/
init: function( args ) {
var self = this,
navs = document.querySelectorAll( args.selector );
if ( ! navs.length ) {
return;
}
navs.forEach( function( nav ) {
var menuToggler = nav.querySelector( '.menu-toggle' );
// Hide menu toggle button if menu is empty and return early.
if ( ! nav.querySelector( 'ul' ) && nav.querySelector( '.menu-toggle' ) ) {
nav.querySelector( '.menu-toggle' ).style.display = 'none';
}
// Add nav-menu class.
if ( ! nav.classList.contains( 'nav-menu' ) ) {
nav.classList.add( 'nav-menu' );
}
// Toggle the hover event listeners.
self.toggleHoverEventListeners( nav );
// Toggle focus classes on links.
nav.querySelectorAll( 'a,button' ).forEach( function( link ) {
link.addEventListener( 'focus', window.alxMediaMenu.toggleFocus, true );
link.addEventListener( 'blur', window.alxMediaMenu.toggleFocus, true );
});
menuToggler.addEventListener( 'click', function() {
if ( nav.classList.contains( 'toggled' ) ) {
menuToggler.setAttribute( 'aria-expanded', 'false' );
nav.classList.remove( 'toggled' );
} else {
menuToggler.setAttribute( 'aria-expanded', 'true' );
nav.classList.add( 'toggled' );
}
});
// If on mobile nav, close it when clicking outside.
// If on desktop, close expanded submenus when clicking outside.
document.addEventListener( 'click', function( event ) {
if ( ! nav.contains( event.target ) ) {
// Mobile.
nav.classList.remove( 'toggled' );
// Desktop.
nav.querySelectorAll( 'button.active,.sub-menu.active' ).forEach( function( el ) {
el.classList.remove( 'active' );
});
menuToggler.setAttribute( 'aria-expanded', 'false' );
}
});
});
// Toggle mobile classes on initial load.
window.alxMediaMenu.toggleMobile( args.selector, args.breakpoint );
// Toggle mobile classes on resize.
window.addEventListener( 'resize', function() {
// If timer is null, reset it to our bounceDelay and run, otherwise wait until timer is cleared.
if ( ! window.resizeDebouncedTimeout ) {
window.resizeDebouncedTimeout = setTimeout( function() {
window.resizeDebouncedTimeout = null;
window.alxMediaMenu.toggleMobile( args.selector, args.breakpoint );
}, 250 );
}
});
// Toggle focus classes to allow submenu access on tables.
document.querySelectorAll( args.selector ).forEach( function( el ) {
window.alxMediaMenu.toggleFocusTouch( el );
});
},
/**
* Expand a menu item.
*
* @param {Element} - The menu item (DOM element).
* @return {void}
*/
toggleItem: function( el ) {
var parentLi = this.helper.firstAncestorMatch( el, 'li' ),
parentUl = this.helper.firstAncestorMatch( el, 'ul' ),
ul = parentLi.querySelector( 'ul.sub-menu' );
parentLi.classList.remove( 'hover' );
ul.setAttribute( 'tabindex', '-1' );
this.helper.toggleClass( ul, 'active' );
this.helper.toggleClass( el, 'active' );
// Go one level up in the list, and close other items that are already open.
parentUl.querySelectorAll( 'ul.sub-menu' ).forEach( function( subMenu ) {
var subMenuButton;
if ( ! parentLi.contains( subMenu ) ) {
subMenu.classList.remove( 'active' );
subMenuButton = subMenu.parentNode.querySelector( 'button.active' );
if ( subMenuButton ) {
subMenuButton.classList.remove( 'active' );
}
}
});
},
/**
* Toggles a mobile class to elements matching our selector,
* depending on the defined breakpoint.
*
* @param {string} selector - The elements where we want to toggle our mobile class.
* @param {string} className - The class-name we want to toggle.
* @param {int} breakpoint - The breakpoint.
* @return {void}
*/
toggleMobile: function( selector, breakpoint ) {
var self = this,
screenWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
navs = document.body.querySelectorAll( selector ),
isMobile;
breakpoint = breakpoint || 720;
isMobile = breakpoint > screenWidth;
if ( isMobile ) {
navs.forEach( function( nav ) {
if ( ! nav.classList.contains( 'mobile' ) ) {
nav.classList.add( 'mobile' );
self.toggleHoverEventListeners( nav );
}
});
} else {
navs.forEach( function( nav ) {
if ( nav.classList.contains( 'mobile' ) ) {
nav.classList.remove( 'mobile' );
self.toggleHoverEventListeners( nav );
}
});
}
},
/**
* Add a "hover" class.
*
* @return {void}
*/
liMouseEnterEvent: function() {
this.classList.add( 'hover' );
},
/**
* Remove the "hover" class.
*
* @return {void}
*/
liMouseLeaveEvent: function() {
this.classList.remove( 'hover' );
},
/**
*
* @param {Element} nav - The nav element.
* @return {void}
*/
toggleHoverEventListeners: function( nav ) {
if ( nav.classList.contains( 'mobile' ) ) {
this.removeHoverEventListeners( nav );
} else {
this.addHoverEventListeners( nav );
}
},
/**
* Add event-listeners for hover events.
*
* @param {Element} nav - The nav element.
* @return {void}
*/
addHoverEventListeners: function( nav ) {
nav.querySelectorAll( 'li' ).forEach( function( li ) {
li.addEventListener( 'mouseenter', window.alxMediaMenu.liMouseEnterEvent );
li.addEventListener( 'mouseleave', window.alxMediaMenu.liMouseLeaveEvent );
});
},
/**
* Remove event-listeners for hover events.
*
* @param {Element} nav - The nav element.
* @return {void}
*/
removeHoverEventListeners: function( nav ) {
nav.querySelectorAll( 'li' ).forEach( function( li ) {
li.removeEventListener( 'mouseenter', window.alxMediaMenu.liMouseEnterEvent );
li.removeEventListener( 'mouseleave', window.alxMediaMenu.liMouseLeaveEvent );
});
},
/**
* Sets or removes .focus class on an element.
*
* @return {void}
*/
toggleFocus: function() {
var self = this;
// Move up through the ancestors of the current link until we hit .nav-menu.
while ( -1 === self.className.indexOf( 'nav-menu' ) ) {
// On li elements toggle the class .focus.
if ( 'li' === self.tagName.toLowerCase() ) {
if ( -1 !== self.className.indexOf( 'focus' ) ) {
self.className = self.className.replace( ' focus', '' );
} else {
self.className += ' focus';
}
}
self = self.parentElement;
}
},
/**
* Toggle focus classes to allow submenu access on tables.
*
* @param {Element} el - The menu element.
* @return {void}
*/
toggleFocusTouch: function( el ) {
var touchStartFn,
parentLinks = el.querySelectorAll( '.menu-item-has-children > a, .page_item_has_children > a' );
if ( 'ontouchstart' in window ) {
touchStartFn = function( e ) {
var menuItem = this.parentNode;
if ( ! menuItem.classList.contains( 'focus' ) ) {
e.preventDefault();
menuItem.parentNode.children.forEach( function( child ) {
if ( menuItem !== child ) {
child.classList.remove( 'focus' );
}
});
menuItem.classList.add( 'focus' );
} else {
menuItem.classList.remove( 'focus' );
}
};
parentLinks.forEach( function( parentLink ) {
parentLink.addEventListener( 'touchstart', touchStartFn, false );
});
}
},
/**
* Helper methods.
*/
helper: {
/**
* Toggle a class to an element.
*
* @param {Element} el - The element.
* @param {string} className - The class we want to toggle.
* @return {void}
*/
toggleClass: function( el, className ) {
if ( el.classList.contains( className ) ) {
el.classList.remove( className );
} else {
el.classList.add( className );
}
},
/**
* Get the 1st ancestor of an element that matches our selector.
*
* @param {Element} el - The element.
* @param {string} selector - The class we want to toggle.
* @return {Element}
*/
firstAncestorMatch: function( el, selector ) {
if ( el.parentNode.matches( selector ) ) {
return el.parentNode;
}
return this.firstAncestorMatch( el.parentNode, selector );
}
}
};
window.alxMediaMenu.init({
selector: '.main-navigation.nav-menu',
breakpoint: 720
});