/** * Main Navigation JavaScript. * * @author Matthias Kittsteiner * @package rh-60 * @version 1.0.1 */ // variable mobileWidth will be declared via customizer var mobileWidthSmallScreens = 840; var isMobile = ! window.matchMedia( '(min-width: ' + mobileWidth + 'px)' ).matches; // document loaded document.addEventListener( 'DOMContentLoaded', () => { var adminBar = document.getElementById( 'wpadminbar' ); var adminBarOffsetHeight = adminBar ? adminBar.offsetHeight : 0; var content = document.getElementById( 'page' ) || document.getElementById( 'primary' ); // #primary as fallback if there is no #page var dropDownToggles = document.querySelectorAll( '#site-navigation .dropdown-toggle' ); var header = document.getElementById( 'masthead' ); var html = document.querySelector( 'html' ); var isAdminBarFixed = ! window.matchMedia( '(max-width: 600px)' ).matches; var isMobileWidthSmallScreens = ! window.matchMedia( '(min-width: ' + mobileWidthSmallScreens + 'px)' ).matches; var nav = document.getElementById( 'site-navigation' ); var navBlocker = createNewElement( 'div', 'nav-blocker', document.body, 'id' ); var navItems = document.querySelectorAll('.main-navigation a'); var navToggleBtn = document.getElementById( 'mobile-menu-toggle' ); var navHeight = isMobile && navToggleBtn ? navToggleBtn.offsetHeight : ( nav ? nav.offsetHeight : 0 ); // don't resort var navPosition = header.offsetHeight + ( isAdminBarFixed ? 0 : adminBarOffsetHeight ) - navHeight; var navToggleBtnAlt = document.getElementById( 'mobile-menu-toggle-inside' ); var navWrapper = document.getElementById( 'nav-wrapper' ); var isAlongside = ! ! navWrapper; // don't resort var page = document.body; var siteHeader = document.querySelector( '.site-header' ); var isBefore = siteHeader.classList.contains( 'nav-before' ); // don't resort var topLine = document.getElementById( 'top-line' ); if ( isMobile ) { // mobile initEvents(); } for ( const dropDownToggle of dropDownToggles ) { dropDownToggle.removeEventListener( 'click', toggleMobileSubNavigation ); dropDownToggle.addEventListener( 'click', toggleMobileSubNavigation ); } setTimeout( function() { gapForAnchorLinks(); }, 50 ); // if navigation is fixed fixedNav(); if ( nav && nav.classList.contains( 'nav-fixed' ) ) { if ( navWrapper && ! isMobile ) { var inner_content = document.querySelector( '.no-keyvisual #content' ); if ( inner_content ) { inner_content.style.marginTop = header.offsetHeight + 'px'; } } } // scroll event window.addEventListener( 'scroll', () => fixedNav() ); // resize event window.addEventListener( 'resize', () => { // recalculate variables adminBarOffsetHeight = adminBar ? adminBar.offsetHeight : 0; isAdminBarFixed = ! window.matchMedia( '(max-width: 600px)' ).matches; isMobile = ! window.matchMedia( '(min-width: ' + mobileWidth + 'px)' ).matches; isMobileWidthSmallScreens = ! window.matchMedia( '(min-width: ' + mobileWidthSmallScreens + 'px)' ).matches navHeight = isMobile && navToggleBtn ? navToggleBtn.offsetHeight : ( nav ? nav.offsetHeight : 0 ); navPosition = header.offsetHeight + ( isAdminBarFixed ? 0 : adminBarOffsetHeight ) - navHeight if ( nav ) nav.removeAttribute( 'style' ); if ( navToggleBtn ) navToggleBtn.removeAttribute( 'style' ); if ( isMobile ) { // mobile initEvents(); if ( navWrapper ) { var inner_content = document.querySelector( '.no-keyvisual #content' ); if ( inner_content ) { inner_content.style.removeProperty( 'margin-top' ); } } } else { for ( const link of nav.querySelectorAll( 'a' ) ) { link.removeAttribute( 'tabindex' ); } } // if navigation is fixed fixedNav(); if ( ! isMobile ) { // force closing on non-mobile devices _closeMenu(); if ( navWrapper && nav && ( nav.classList.contains( 'nav-fixed' ) || header.classList.contains( 'nav-fixed-wrapper' ) ) ) { var inner_content = document.querySelector( '.no-keyvisual #content' ); if ( inner_content ) { inner_content.style.marginTop = header.offsetHeight + 'px'; } } } }, true ); // click event on menu items if ( navItems && navItems.length ) { for ( var i = 0; i < navItems.length; i++ ) { navItems[ i ].addEventListener( 'click', function( event ) { var actualTarget = event.target; var isEmpty = actualTarget.href === '' || actualTarget.href === '#'; var isSvgElement = actualTarget.tagName === 'use' || actualTarget.tagName === 'svg'; if ( ! actualTarget.classList.contains( 'dropdown-toggle' ) && ! isEmpty && ! isSvgElement ) { _closeMenu(); } if ( ( isEmpty ) && actualTarget.querySelector( '.dropdown-toggle' ) && actualTarget.parentNode.classList.contains( 'menu-item-has-children' ) ) { toggleMobileSubNavigation( event, actualTarget.nextElementSibling ); } } ); } } /** * Fixing the navigation via CSS classes. */ function fixedNav() { // page template blank has no header if ( ! siteHeader ) { return; } // if mobile and navigation is open if ( isMobile && html.classList.contains( 'nav-open' ) ) { return; } // reset values if ( ! isMobileWidthSmallScreens && navWrapper ) { navWrapper.removeAttribute( 'style' ); } // if navigation should not be fixed if ( nav && ! nav.classList.contains( 'nav-fixed' ) ) { return; } // scroll away top line if available if ( topLine ) { if ( window.pageYOffset < 0 ) { topLine.style.removeProperty( 'margin-top' ); } else if ( window.pageYOffset <= topLine.offsetHeight ) { topLine.style.marginTop = window.pageYOffset * -1 + 'px'; } else { topLine.style.marginTop = topLine.offsetHeight * -1 + 'px'; } } if ( isBefore ) { // nav before logo if ( navToggleBtn && navToggleBtn.offsetHeight && window.pageYOffset > navToggleBtn.offsetTop - adminBarOffsetHeight ) { content.classList.add( 'fixed' ); header.classList.add( 'fixed' ); if ( isMobile ) { header.style.paddingTop = navToggleBtn ? navToggleBtn.offsetHeight + 'px' : 0; } } else if ( nav && nav.offsetWidth && window.pageYOffset > nav.offsetTop - adminBarOffsetHeight ) { content.classList.add( 'fixed' ); header.classList.add( 'fixed' ); header.style.paddingTop = nav ? nav.offsetHeight + 'px' : 0; } } else if ( isAlongside ) { // nav alongside logo if ( ( window.pageYOffset > header.offsetTop - adminBarOffsetHeight && window.pageYOffset > 0 ) ) { content.classList.add( 'fixed' ); header.classList.add( 'fixed' ); if ( ! isMobileWidthSmallScreens ) { content.classList.add( 'fixed' ); header.classList.add( 'fixed' ); } } else { content.classList.remove( 'fixed' ); header.classList.remove( 'fixed' ); header.removeAttribute( 'style' ); } } else { // nav after logo if ( window.pageYOffset >= navPosition ) { content.classList.add( 'fixed' ); header.classList.add( 'fixed' ); if ( isMobile ) { header.style.paddingTop = navToggleBtn ? navToggleBtn.offsetHeight + 'px' : 0; } else { header.style.paddingTop = nav ? nav.offsetHeight + 'px' : 0; } } else { content.classList.remove( 'fixed' ); header.classList.remove( 'fixed' ); header.removeAttribute( 'style' ); } } } /** * Add a gap by scrolling the window so that an anchor is still visible * with fixed navigation. */ function gapForAnchorLinks() { // check if a hash is available and valid var hashRegex = new RegExp( '^#[A-Za-z0-9_\-]+$' ); if ( ! window.location.hash || ! hashRegex.test( window.location.hash ) ) return; if ( ! document.querySelector( window.location.hash ) ) return; var adminBar = document.getElementById( 'wpadminbar' ); var adminBarOffset = adminBar ? adminBar.offsetHeight : 0; var hashElement = document.querySelector( window.location.hash ); var hashElementPos = getCoords( hashElement ); var headerHeight = 0; var isAlongside = ! ! navWrapper; var scrollPos = window.pageYOffset || document.documentElement.scrollTop; if ( isAlongside && ! isMobileWidthSmallScreens ) { headerHeight = header.offsetHeight; } else if ( isMobile ) { headerHeight = navToggleBtn.offsetHeight; } else { headerHeight = nav ? nav.offsetHeight : 0; } headerHeight += adminBarOffset; if ( scrollPos > hashElementPos.top - headerHeight ) { window.scrollTo( 0, scrollPos - headerHeight ); } } /** * Initialize events. */ function initEvents() { // remove any previous event listener if ( navToggleBtn ) navToggleBtn.removeEventListener( 'click', toggleMobileNavigationState ); if ( navToggleBtnAlt ) navToggleBtnAlt.removeEventListener( 'click', toggleMobileNavigationState ); if ( navBlocker ) navBlocker.removeEventListener( 'click', toggleMobileNavigationState ); // add new event listener if ( navToggleBtn ) navToggleBtn.addEventListener( 'click', toggleMobileNavigationState ); if ( navToggleBtnAlt ) navToggleBtnAlt.addEventListener( 'click', toggleMobileNavigationState ); if ( navBlocker ) navBlocker.addEventListener( 'click', toggleMobileNavigationState ); for ( const link of nav.querySelectorAll( 'a, button' ) ) { link.setAttribute( 'tabindex', '-1' ); } if ( isMobile ) { getAnchorLinks(); } } /** * Opens or closes the main navigation depending on * its current state. * @param event */ function toggleMobileNavigationState( event ) { if ( nav && html.classList.contains( 'nav-open' ) ) { _closeMenu(); } else { _openMenu(); } } /** * Opens or closes a sub navigation. * @param {Event} event * @param {Element} subMenu */ function toggleMobileSubNavigation ( event, subMenu ) { event.preventDefault(); var currentTarget = subMenu ? event.currentTarget.querySelector( '.dropdown-toggle' ) : event.currentTarget; var svgHref = currentTarget.querySelector( 'use' ); var thisSubMenu = subMenu || currentTarget.parentElement.querySelector( '.sub-menu' ); if ( ! isHidden( thisSubMenu ) ) { if ( svgHref ) { svgHref.setAttribute( 'href', '#angle-down' ); } else { currentTarget.classList.add( 'bottom' ); currentTarget.classList.add( 'closed' ); currentTarget.classList.remove( 'open' ); currentTarget.classList.remove( 'top' ); } thisSubMenu.classList.add( 'sub-menu-closed' ); thisSubMenu.classList.remove( 'sub-menu-open' ); thisSubMenu.setAttribute( 'aria-expanded', 'false' ); } else { if ( svgHref ) { svgHref.setAttribute( 'href', '#angle-up' ); } else { currentTarget.classList.add( 'open' ); currentTarget.classList.add( 'top' ); currentTarget.classList.remove( 'bottom' ); currentTarget.classList.remove( 'closed' ); } thisSubMenu.classList.add( 'sub-menu-open' ); thisSubMenu.classList.remove( 'sub-menu-closed' ); thisSubMenu.setAttribute( 'aria-expanded', 'true' ); } } /** * Close the menu. * @private */ function _closeMenu() { html.classList.remove( 'nav-open' ); page.style.removeProperty( 'top' ); page.removeAttribute( 'scroll-position' ); let selector = 'a, button'; if ( ! isMobile ) { selector = 'button'; } for ( const link of nav.querySelectorAll( selector ) ) { link.setAttribute( 'tabindex', '-1' ); } fixedNav(); } /** * Open the menu. * @private */ function _openMenu() { html.classList.add( 'nav-open' ); for ( const link of nav.querySelectorAll( 'a, button' ) ) { link.removeAttribute( 'tabindex' ); } fixedNav(); } } ); /** * Create a new HTML element. * * @param {string} name * @param {string} newSelector * @param {Element} parent * @param {string} idOrClass * @returns {Element} */ function createNewElement( name, newSelector, parent, idOrClass ) { var element = document.createElement( name ); if ( newSelector.length ) { if ( idOrClass === 'class' ) { element.classList.add( newSelector ); } else { element.setAttribute( 'id', newSelector ); } } parent.appendChild( element ); return element; } /** * Get all anchor links and scroll to the target element. */ function getAnchorLinks() { const anchors = document.querySelectorAll( 'a[href^="#"]' ); for ( const anchor of anchors ) { if ( ! anchor.href || ! document.getElementById( anchor.href.split( '#' ).pop() ) ) { continue; } anchor.addEventListener( 'click', function ( event ) { event.preventDefault(); const behavior = document.querySelector( 'html' ).classList.contains( 'has-smooth-scrolling' ) && ! window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ? 'smooth' : 'auto'; const id = event.currentTarget.href.split( '#' ).pop(); const targetElement = document.getElementById( id ); if ( event.currentTarget.closest( '.main-navigation' ) ) { document.getElementById( 'mobile-menu-toggle-inside').click(); } setTimeout( () => { history.pushState( null, null, '#' + id ); scrollToElement( targetElement, behavior ); }, 50 ); } ); } } /** * Cross-browser version to get element’s coordinates. * * @see https://stackoverflow.com/a/26230989/3461955 * @param {Element} elem * @return {object} */ function getCoords( elem ) { if ( ! elem ) { return { top: 0, left: 0 }; } var box = elem.getBoundingClientRect(); var body = document.body; var docEl = document.documentElement; var scrollTop = window.scrollY || docEl.scrollTop || body.scrollTop; var scrollLeft = window.scrollX || docEl.scrollLeft || body.scrollLeft; var clientTop = docEl.clientTop || body.clientTop || 0; var clientLeft = docEl.clientLeft || body.clientLeft || 0; var top = box.top + scrollTop - clientTop; var left = box.left + scrollLeft - clientLeft; return { top: Math.round( top ), left: Math.round( left ) }; } /** * Check if element is hidden. * @param {Element} element * @returns {boolean} */ function isHidden( element ) { return ( element.offsetParent === null ); } /** * Scroll to an element. * * @param {Element} element The element to scroll to * @param {String } behavior The scroll behavior */ function scrollToElement( element, behavior ) { const adminBar = document.getElementById( 'wpadminbar' ); const isAdminBarFixed = ! window.matchMedia( '(max-width: 600px)' ).matches; const fixedHeader = document.querySelector( window.matchMedia( '(min-width: 840px)' ).matches ? rhNavigation.smoothScrollingElementDesktop : rhNavigation.smoothScrollingElement ); let top = element.getBoundingClientRect().top + window.scrollY; if ( fixedHeader ) { top -= fixedHeader.offsetHeight; } if ( isAdminBarFixed && adminBar ) { top -= adminBar.offsetHeight; } window.scrollTo( { behavior: behavior, top: top, } ); }