/** * @file * Controls the visibility of desktop navigation. * * Shows and hides the desktop navigation based on scroll position and controls * the functionality of the button that shows/hides the navigation. */ /* eslint-disable no-inner-declarations */ ((Drupal) => { /** * Olives helper functions. * * @namespace */ Drupal.olives = {}; /** * Checks if the mobile navigation button is visible. * * @return {boolean} * True if navButtons is hidden, false if not. */ function isDesktopNav() { const navButtons = document.querySelector( '[data-drupal-selector="mobile-buttons"]', ); return navButtons ? window.getComputedStyle(navButtons).getPropertyValue('display') === 'none' : false; } Drupal.olives.isDesktopNav = isDesktopNav; const stickyHeaderToggleButton = document.querySelector( '[data-drupal-selector="sticky-header-toggle"]', ); const siteHeaderFixable = document.querySelector( '[data-drupal-selector="site-header-fixable"]', ); /** * Checks if the sticky header is enabled. * * @return {boolean} * True if sticky header is enabled, false if not. */ function stickyHeaderIsEnabled() { return stickyHeaderToggleButton.getAttribute('aria-checked') === 'true'; } /** * Save the current sticky header expanded state to localStorage, and set * it to expire after two weeks. * * @param {boolean} expandedState * Current state of the sticky header button. */ function setStickyHeaderStorage(expandedState) { const now = new Date(); const item = { value: expandedState, expiry: now.getTime() + 20160000, // 2 weeks from now. }; localStorage.setItem( 'Drupal.olives.stickyHeaderState', JSON.stringify(item), ); } /** * Toggle the state of the sticky header between always pinned and * only pinned when scrolled to the top of the viewport. * * @param {boolean} pinnedState * State to change the sticky header to. */ function toggleStickyHeaderState(pinnedState) { if (isDesktopNav()) { siteHeaderFixable.classList.toggle('is-expanded', pinnedState); stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState); setStickyHeaderStorage(pinnedState); } } /** * Return the sticky header's stored state from localStorage. * * @return {boolean} * Stored state of the sticky header. */ function getStickyHeaderStorage() { const stickyHeaderState = localStorage.getItem( 'Drupal.olives.stickyHeaderState', ); if (!stickyHeaderState) return false; const item = JSON.parse(stickyHeaderState); const now = new Date(); // Compare the expiry time of the item with the current time. if (now.getTime() > item.expiry) { // If the item is expired, delete the item from storage and return null. localStorage.removeItem('Drupal.olives.stickyHeaderState'); return false; } return item.value; } // Only enable scroll interactivity if the browser supports Intersection // Observer. // @see https://github.com/w3c/IntersectionObserver/blob/master/polyfill/intersection-observer.js#L19-L21 if ( 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype ) { const fixableElements = document.querySelectorAll( '[data-drupal-selector="site-header-fixable"], [data-drupal-selector="social-bar-inner"]', ); function toggleDesktopNavVisibility(entries) { if (!isDesktopNav()) return; entries.forEach((entry) => { // Firefox doesn't seem to support entry.isIntersecting properly, // so we check the intersectionRatio. fixableElements.forEach((el) => el.classList.toggle('is-fixed', entry.intersectionRatio < 1), ); }); } /** * Gets the root margin by checking for various toolbar classes. * * @return {string} * Root margin for the Intersection Observer options object. */ function getRootMargin() { let rootMarginTop = 72; const { body } = document; if (body.classList.contains('toolbar-fixed')) { rootMarginTop -= 39; } if ( body.classList.contains('toolbar-horizontal') && body.classList.contains('toolbar-tray-open') ) { rootMarginTop -= 40; } return `${rootMarginTop}px 0px 0px 0px`; } /** * Monitor the navigation position. */ function monitorNavPosition() { const primaryNav = document.querySelector( '[data-drupal-selector="site-header"]', ); const options = { rootMargin: getRootMargin(), threshold: [0.999, 1], }; const observer = new IntersectionObserver( toggleDesktopNavVisibility, options, ); if (primaryNav) { observer.observe(primaryNav); } } if (stickyHeaderToggleButton) { stickyHeaderToggleButton.addEventListener('click', () => { toggleStickyHeaderState(!stickyHeaderIsEnabled()); }); } // If header is pinned open and a header element gains focus, scroll to the // top of the page to ensure that the header elements can be seen. const siteHeaderInner = document.querySelector( '[data-drupal-selector="site-header-inner"]', ); if (siteHeaderInner) { siteHeaderInner.addEventListener('focusin', () => { if (isDesktopNav() && !stickyHeaderIsEnabled()) { const header = document.querySelector( '[data-drupal-selector="site-header"]', ); const headerNav = header.querySelector( '[data-drupal-selector="header-nav"]', ); const headerMargin = header.clientHeight - headerNav.clientHeight; if (window.scrollY > headerMargin) { window.scrollTo(0, headerMargin); } } }); } monitorNavPosition(); setStickyHeaderStorage(getStickyHeaderStorage()); toggleStickyHeaderState(getStickyHeaderStorage()); } })(Drupal);