media/js/base/protocol/protocol-navigation.js (166 lines of code) (raw):

/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Copied from Protocol, to be back-ported along with nav updates. (function () { 'use strict'; var MzpNavigation = {}; var _navElem; var _navItemsLists; var _options = { onNavOpen: null, onNavClose: null }; var _ticking = false; var _lastKnownScrollPosition = 0; var _animationFrameID = null; var _stickyScrollOffset = 300; var _wideBreakpoint = '768px'; var _tallBreakpoint = '600px'; var _mqLargeNav; var _viewport = document.getElementsByTagName('html')[0]; /** * Does the viewport meet the minimum width and height * requirements for sticky behavior? * @returns {Boolean} */ MzpNavigation.isLargeViewport = function () { return _mqLargeNav.matches; }; /** * Feature detect for sticky navigation * @returns {Boolean} */ MzpNavigation.supportsSticky = function () { if (typeof window.MzpSupports !== 'undefined') { return ( window.MzpSupports.matchMedia && window.MzpSupports.classList && window.MzpSupports.requestAnimationFrame && window.MzpSupports.cssFeatureQueries && CSS.supports('position', 'sticky') ); } else { return false; } }; /** * Scroll event listener. No computationally expensive * operations such as DOM modifications should happen * here. Instead we throttle using `requestAnimationFrame`. */ MzpNavigation.onScroll = function () { if (!_ticking) { _animationFrameID = window.requestAnimationFrame( MzpNavigation.checkScrollPosition ); _ticking = true; } }; /** * Create sticky state for the navigation. */ MzpNavigation.createSticky = function () { _viewport.classList.add('mzp-has-sticky-navigation'); _animationFrameID = window.requestAnimationFrame( MzpNavigation.checkScrollPosition ); window.addEventListener('scroll', MzpNavigation.onScroll, false); }; /** * Destroy sticky state for the navigation. */ MzpNavigation.destroySticky = function () { _viewport.classList.remove('mzp-has-sticky-navigation'); _navElem.classList.remove('mzp-is-scrolling'); _navElem.classList.remove('mzp-is-hidden'); _lastKnownScrollPosition = 0; if (_animationFrameID) { window.cancelAnimationFrame(_animationFrameID); } window.removeEventListener('scroll', MzpNavigation.onScroll, false); }; /** * Initialize sticky state for the navigation. * Uses `matchMedia` to determine if conditions * for sticky navigation are satisfied. */ MzpNavigation.initSticky = function () { _mqLargeNav = matchMedia( '(min-width: ' + _wideBreakpoint + ') and (min-height: ' + _tallBreakpoint + ')' ); function makeStickyNav(mq) { if (mq.matches) { MzpNavigation.createSticky(); } else { MzpNavigation.destroySticky(); } } if (window.matchMedia('all').addEventListener) { _mqLargeNav.addEventListener('change', makeStickyNav, false); } else if (window.matchMedia('all').addListener) { _mqLargeNav.addListener(makeStickyNav); } if (MzpNavigation.isLargeViewport()) { MzpNavigation.createSticky(); } }; /** * Implements sticky navigation behavior as * user scrolls up and down the viewport. */ MzpNavigation.checkScrollPosition = function () { // add styling for when scrolling the viewport if (window.scrollY > 0) { _navElem.classList.add('mzp-is-scrolling'); } else { _navElem.classList.remove('mzp-is-scrolling'); } // scrolling down if (window.scrollY > _lastKnownScrollPosition) { // hide the sticky nav shortly after scrolling down the viewport. if (window.scrollY > _stickyScrollOffset) { // if there's a menu currently open, close it. if (typeof window.MzpMenu !== 'undefined') { window.MzpMenu.close(); } _navElem.classList.add('mzp-is-hidden'); } } // scrolling up else { _navElem.classList.remove('mzp-is-hidden'); } _lastKnownScrollPosition = window.scrollY; _ticking = false; }; /** * Event handler for navigation menu button `click` events. */ MzpNavigation.onClick = function (e) { var thisNavItemList = e.target.parentNode.querySelector( '.c-navigation-items' ); e.preventDefault(); // Update button state e.target.classList.toggle('mzp-is-active'); // Update menu state thisNavItemList.classList.toggle('mzp-is-open'); // Update aria-expended state on menu. var expanded = thisNavItemList.classList.contains('mzp-is-open') ? true : false; thisNavItemList.setAttribute('aria-expanded', expanded); if (expanded) { if (typeof _options.onNavOpen === 'function') { _options.onNavOpen(thisNavItemList); } } else { if (typeof _options.onNavClose === 'function') { _options.onNavClose(thisNavItemList); } } }; /** * Set initial ARIA navigation states. */ MzpNavigation.setAria = function () { for (var i = 0; i < _navItemsLists.length; i++) { _navItemsLists[i].setAttribute('aria-expanded', false); } }; /** * Bind navigation event handlers. */ MzpNavigation.bindEvents = function () { _navItemsLists = document.querySelectorAll('.c-navigation-items'); if (_navItemsLists.length > 0) { var navButtons = document.querySelectorAll( '.c-navigation-menu-button' ); for (var i = 0; i < navButtons.length; i++) { navButtons[i].addEventListener( 'click', MzpNavigation.onClick, false ); } MzpNavigation.setAria(); } }; /** * Initialize menu. * @param {Object} options - configurable options. */ MzpNavigation.init = function (options) { if (typeof options === 'object') { for (var i in options) { if (options.hasOwnProperty.call(i)) { _options[i] = options[i]; } } } MzpNavigation.bindEvents(); /** * Init (optional) sticky navigation. * If there are multiple navigation organisms on a single page, * assume only the first (and hence top-most) instance can and * will be sticky. * * Do not init sticky navigation if user prefers reduced motion */ _navElem = document.querySelector('.c-navigation'); var _navIsSticky = _navElem && _navElem.classList.contains('mzp-is-sticky') && MzpNavigation.supportsSticky(); if (_navIsSticky && matchMedia('(prefers-reduced-motion)').matches) { _navElem.classList.remove('mzp-is-sticky'); } else if (_navIsSticky) { MzpNavigation.initSticky(); } }; window.MzpNavigation = MzpNavigation; })(window.Mzp);