import debounce from 'lodash/debounce';
import isVisible from '../utils/dom/isVisible';
import customEvent from '../utils/customEvent';
import delegateEvent from '../utils/delegateEvent';
import dynamicElement from '../utils/dynamicElement';
/**
* Sets up main menu navigation dropdowns
*
* @module MainMenu
* @prop {Element} $navBar - DOM reference to the navigation bar
* @prop {Element} $longformHeader
* - DOM reference to short header ( appears only on custom templates )
* @prop {Element} $toggles - DOM reference to all dropdown menu toggles
* @prop {Element} $close - DOM reference to the dropdown menu close button
* @prop {Element} $moreMenu - DOM reference to the more menu
* @prop {String} currentMenuId - DOM selector for active menu dropdown
* @prop {Element} $currentMenu - DOM reference to the current menu
* @prop {String} navClass - DOM selector to capture the navigation bar
* @prop {String} longformHeaderContrastClass
* - Modifier class applied to short header when menu opens
* @prop {String} longformHeaderPositionClass
* - Modifier class applied to short header when user scrolled past hero section
* @prop {String} navMenuOpenClass - Modifier class to apply when a menu is open
* @prop {String} navMenuStuckClass - Modifier class to apply when menu is stuck
* @prop {String} submenusCss - css class of element wrapping all submenus
* @prop {String} panelAboveNav - css class added to the panel when it's scrolls above the nav
* @prop {String} sidebarSelector - DOM selector for sidebar
* @prop {Array} menuIds - List of menu dropdown selectors
* @prop {String} focusableSelector - DOM selector to capture focusable elements
* @prop {String} disabledAttribute
* - data attribute for controlling element disabled behavior on menu open and close
* @prop {String} disabledFlagAlways
* - DOM selector to identify elements that should always be disabled when any menu is open
* @prop {String} disabledFlagSelf - DOM selector to capture focusable elements
* - DOM selector to identify elements that should be disabled when its own menu is open
*/
const MainMenu = {
$navBar: null,
$longformHeader: null,
$interactiveHeader: null,
$toggles: {},
$close: null,
$moreMenu: null,
currentMenuId: null,
$currentMenu: null,
heightResetTimeout: false,
navClass: 'l-navbar',
longformHeaderContrastClass: 'l-longform-header--active',
longformHeaderPositionClass: 'l-longform-header--narrow',
interactiveHeaderContrastClass: 'l-interactive-header--active',
interactiveHeaderPositionClass: 'l-interactive-header--narrow',
navMenuOpenClass: 'l-navWrapper--menuOpen',
navMenuStuckClass: 'l-navbar--stuck',
submenusCss: 'l-navbar__submenus',
panelAboveNav: 'l-panel--aboveNav',
sidebarSelector: '#sidebar',
menuIds: ['#menuMore', '#menuLocal', '#menuSearch', '#menuSocialShareNav', '#menuSocialShareArticle', '#menuComments', '#eventsCalendarNav', '#eventsTypesNav'],
focusableSelector: 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])',
disabledAttribute: 'data-disabled',
disabledFlagAlways: '[data-disabled="all"]',
disabledFlagSelf: '[data-disabled="self"]',
breakPointDesktop: 1312,
breakpointTabletPortrait: 768,
/**
* Binds event listeners to main menu toggles.
* @method init
*/
init() {
// define elements
this.$toggles = document.querySelectorAll( '[data-toggle-menu]' );
this.$hoverToggles = document.querySelectorAll( '[data-hover-toggle-menu]' );
this.$navBar = document.querySelector( '.l-navbar' );
this.$wrapper = document.querySelector( '.l-navWrapper' );
this.$longformHeader = document.querySelector( '.l-longform-header' );
this.$interactiveHeader = document.querySelector( '.l-interactive-header' );
this.$menuMore = document.querySelector( '#menuMore' );
// bind menu click listeners
[].forEach.call( this.$toggles, ( $toggle ) => {
$toggle.addEventListener( 'click', ( event ) => {
event.preventDefault();
event.stopPropagation();
const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
});
});
// watch for toggle buttons dynamically added
delegateEvent( dynamicElement.selector, 'click', '[data-toggle-menu]', ( event, $toggle ) => {
event.preventDefault();
event.stopPropagation();
const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
});
// bind menu hover state listeners
[].forEach.call( this.$hoverToggles, ( $toggle ) => {
$toggle.addEventListener( 'mouseenter', ( event ) => {
let mouseWithin = true;
// delay required so it fires after the click event
setTimeout( () => {
event.preventDefault();
event.stopPropagation();
// don't show menu if mouse has already left the toggle
$toggle.removeEventListener( 'mouseleave', this.boundCheckMouseLeave );
if ( false === mouseWithin ) {
return;
}
const selectedMenuId = $toggle.getAttribute( 'data-hover-toggle-menu' );
this.handleMenuToggle( $toggle, selectedMenuId, 'mouseenter' );
}, 200 );
// check if mouse leaves the toggle in less than 200ms
this.boundCheckMouseLeave = () => { mouseWithin = false; };
$toggle.addEventListener( 'mouseleave', this.boundCheckMouseLeave );
});
// on touch-enabled devices we need to be able to toggle the menu with a click
$toggle.addEventListener( 'click', ( event ) => {
event.preventDefault();
event.stopPropagation();
const selectedMenuId = $toggle.getAttribute( 'data-hover-toggle-menu' );
this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
});
});
// bind menu close buttons
const $closeButtons = document.querySelectorAll( '[data-close-menu]' );
[].forEach.call( $closeButtons, ( $closeButton ) => {
$closeButton.addEventListener( 'click', () => {
this.closeCurrentMenu();
});
});
// bind focus event to menu icon so we can reset nav if tabbing
const $menuIcon = document.querySelector( '[data-toggle-menu="#menuMore"]' );
if ( $menuIcon ) {
$menuIcon.addEventListener( 'focus', () => {
const $topicsNav = document.querySelector( '#menuMain-topics' );
if ( $topicsNav ) {
$topicsNav.scrollTop = 0;
}
});
}
window.addEventListener( customEvent.NAV_TITLE_AND_SOCIAL, ( event ) => {
if ( true === event.detail.in_view ) {
// Add SocialShare panel offset when nav transitions
// to menu to title and social share
this.setSocialShareMenuMargin();
} else {
// Add Local Menu panel offset when nav transitions
// from menu to title and social share
this.setLocalMenuMargin();
}
});
if ( this.$navBar ) {
// bind event to capture resize and orientation change
this.resizeHandler = debounce( () => {
// Add SocialShare panel offset when page is resized
this.setSocialShareMenuMargin();
// Add Local menu panel offset when page is resized
this.setLocalMenuMargin();
}, 200 );
window.addEventListener( 'orientationchange', this.resizeHandler );
window.addEventListener( 'resize', this.resizeHandler );
window.addEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );
this.resizeHandler();
}
},
initClickToggles( $toggles ) {
[].forEach.call( $toggles, ( $toggle ) => {
$toggle.addEventListener( 'click', ( event ) => {
event.preventDefault();
event.stopPropagation();
const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
});
});
},
/**
* Handles click events on main menu toggles.
*
* @method handleMenuToggle
* @param {Element} $toggle - DOM reference to the clicked toggle element
* @param {string} $eventType - Specifies whether menu is opened via click or mouseenter
*/
handleMenuToggle( $toggle, selectedMenuId, $eventType = 'click' ) {
// don't run this code if user is simply hovering back onto a mouseenter toggle
// while the menu is still open (e.g. hovering from local menu onto local)
if ( 'mouseenter' === $eventType && this.currentMenuId ) {
if ( selectedMenuId === this.currentMenuId ) {
return;
}
}
// also don't run this code if the menu is already open via hover
// and the user just clicked on the toggle (to avoid accidental closing by click)
if ( 'click' === $eventType && this.currentMenuId ) {
// Fix - inability to close the menu in iPad Mini (iOS 12)
// Bail on the first click after the menu is already opened,
// carry on with toggling the menu on subqequent clicks.
if ( selectedMenuId === this.currentMenuId && 'mouseenter' === this.eventType && 'true' !== this.$currentMenu.dataset.clicked ) {
this.$currentMenu.dataset.clicked = 'true';
return;
}
}
// Reset the 'clicked' flag for identifying subsequent clicks.
if ( this.$currentMenu ) {
this.$currentMenu.dataset.clicked = '';
}
// close any open menus, if the user is just closing the menu don't do anything else
const openMenuEvent = this.closeOpenMenus( $toggle );
if ( openMenuEvent ) {
this.currentMenuId = selectedMenuId;
this.$currentMenu = document.querySelector( this.currentMenuId );
if ( ! this.$currentMenu ) {
return;
}
this.eventType = $eventType;
// init current menu
this.$currentMenu.classList.add( 'l-panel--init' );
// fade in current menu
this.$currentMenu.classList.remove( 'is-slid-and-faded-out' );
this.$currentMenu.classList.add( 'is-slid-and-faded-in' );
const skipParent = $toggle.getAttribute( 'data-skip-parent' );
if ( skipParent ) {
this.$currentMenu.classList.add( 'l-panel--single' );
}
// toggle menu button (need to toggle all buttons tied to this menu)
const $menuToggles = document.querySelectorAll( `[data-toggle-menu="${this.currentMenuId}"], [data-hover-toggle-menu="${this.currentMenuId}"]` );
[].forEach.call( $menuToggles, ( $menuToggle ) => {
$menuToggle.classList.add( 'is-toggled' );
// add active modifier if the user is clicking on a c-nav__item toggle
if ( $menuToggle.classList.contains( 'c-nav__item' ) ) {
$menuToggle.classList.add( 'c-nav__item--active' );
}
// add active modifier if the user is clicking on a c-nav__link toggle
if ( $menuToggle.classList.contains( 'c-nav__link' ) ) {
$menuToggle.classList.add( 'c-nav__link--active' );
}
});
// set aria-expaneded
$toggle.setAttribute( 'aria-expanded', true );
// gray out other nav items and update focus
this.focusOnMenu( $toggle );
// set proper menu sizing and update on resize
this.menuSizing( $toggle, selectedMenuId );
// setup close handlers
this.menuClosing( $eventType, $toggle, selectedMenuId );
// add navbar modifier class (ensures dropdown appears over everything)
if ( ! this.isOrphan() ) {
this.$wrapper.classList.add( this.navMenuOpenClass );
// if the page has a short header, add contrast class to it when menu opens
if ( this.$longformHeader ) {
this.$longformHeader.classList.add( this.longformHeaderContrastClass );
}
if ( this.$interactiveHeader ) {
this.$interactiveHeader.classList.add( this.interactiveHeaderContrastClass );
}
}
}
},
/**
* Closes any open menus. Checks to see if user is attempting to open another menu.
*
* @method closeOpenMenus
* @param {Element} $toggle - DOM reference to the clicked toggle element
* @returns {bool} - True if the user is attempting to open another menu
*/
closeOpenMenus( $toggle ) {
// check if user is toggling off the menu
if ( $toggle.classList.contains( 'is-toggled' ) ) {
this.closeCurrentMenu();
return false;
}
// fade out any open menu
if ( this.currentMenuId ) {
this.closeCurrentMenu();
}
return true;
},
/**
* Grays out non-active menu items.
*
* @method focusOnMenu
* @param {Element} $toggle - DOM reference to the clicked toggle element
*/
focusOnMenu( $toggle ) {
// if menu is an orphan, or the local menu, don't bother graying out other menu elements
if ( ! this.isOrphan() || '#menuLocal' === this.currentMenuId ) {
// gray out all menu items with the data-disabled="all" flag
const $alwaysHide = document.querySelectorAll( this.disabledFlagAlways );
[].forEach.call( $alwaysHide, elem => elem.classList.add( 'is-disabled' ) );
}
// gray out menu items in the currently selected menu is data-disabled="self" flag is present
// also gray out if data-disabled="all" flag is present ( like comment button in social share )
const $siblings = $toggle.parentNode.children;
[].forEach.call( $siblings, ( elem ) => {
const disabledFlag = elem.getAttribute( this.disabledAttribute );
if ( 'self' === disabledFlag || 'all' === disabledFlag ) {
elem.classList.add( 'is-disabled' );
}
});
// setup tab focus and event handlers
this.updateTabFocus( this.$currentMenu );
},
/**
* Sets up handlers so that menu size is adjusted to fit screen height.
* This allows for vertical scroll on overflow, as well as proper margins for offset menus.
*
* @method menuSizing
* @param {Element} $toggle - DOM reference to the clicked toggle element
*/
menuSizing( $toggle, selectedMenuId ) {
// Bail when there's no associated nav bar
if ( ! this.$navBar ) {
return;
}
// set menu max height to allow scrolling
this.setMenuHeight( this.$currentMenu, true );
// set menu width on Comments menu
let menuId = selectedMenuId;
menuId = menuId.replace( '#', '' );
if ( 'menuComments' === menuId ) {
this.setMenuWidth( this.$currentMenu );
}
// bind event to capture resize and orientation change
this.resizeHandler = debounce( () => {
this.setMenuHeight( this.$currentMenu );
}, 200 );
window.addEventListener( 'orientationchange', this.resizeHandler );
window.addEventListener( 'resize', this.resizeHandler );
window.addEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );
},
/**
* Binds close events so that the menu dropdown will close if the user clicks outside the menu.
*
* @method menuClosing
*/
menuClosing( $eventType = 'click', $toggle, selectedMenuId ) {
if ( 'mouseenter' === $eventType ) {
// bind event to capture mouse leaving menu
const $menu = document.querySelector( selectedMenuId );
this.boundMouseLeaveMenu = e => this.handleMouseLeaveMenu( e, $toggle );
$menu.addEventListener( 'mouseleave', this.boundMouseLeaveMenu );
// bind event to capture mouse leaving toggle
this.boundMouseLeaveToggle = e => this.handleMouseLeaveMenu( e, $menu );
$toggle.addEventListener( 'mouseleave', this.boundMouseLeaveToggle );
} else {
// bind event to capture away clicks (and first ensure that "this" is correctly bound)
this.boundClickOutsideMenu = e => this.handleClickOutsideMenu( e );
document.addEventListener( $eventType, this.boundClickOutsideMenu );
}
},
/**
* Redirects tab focus to current active dropdown menu and binds keyboard event listeners.
*
* @method updateTabFocus
*/
updateTabFocus() {
// save current active element
this.menuTrigger = document.activeElement;
// get first and last focusable Elements in dropdown and filter out non-visible
const focusBounds = this.getFocusBounds();
if ( focusBounds && focusBounds.firstFocusable && focusBounds.lastFocusable ) {
this.firstFocusable = focusBounds.firstFocusable;
this.lastFocusable = focusBounds.lastFocusable;
this.boundKeydown = e => this.handleKeydownEvent( e );
document.addEventListener( 'keydown', this.boundKeydown );
}
},
/**
* Gets the first and last focusable elements of the menu.
*
* @method getFocusBounds
*/
getFocusBounds() {
let focusableEls = this.$currentMenu.querySelectorAll( this.focusableSelector );
focusableEls = [].filter.call( focusableEls, elem => isVisible( elem ) );
if ( focusableEls.length > 0 ) {
const { 0: firstFocusable, [focusableEls.length - 1]: lastFocusable } = focusableEls;
return { firstFocusable, lastFocusable };
}
return null;
},
/**
* Handles keydown event and redirects to the appropriate handler.
*
* @method handleKeydownEvent
* @param {object} event - Native JS event object
*/
handleKeydownEvent( event ) {
const keycodeTab = 9;
const keycodeEsc = 27;
if ( 'Tab' === event.key || keycodeTab === event.keyCode ) {
this.handleTabKey( event );
}
if ( keycodeEsc === event.keyCode ) {
this.handleEscKey();
}
},
/**
* Handles tabbing through dropdown menu.
* If user `tabs` on menu trigger after opening menu, it brings them to the first element.
* If user `shift` + `tabs` before the start of the menu.
* If user `tabs` past the end of the dropdown, close menu and redirect focus back to `$toggle`.
* close menu and redirect focus back to `$toggle`.
*
* @method handleTabKey
* @param {object} event - Native JS event object
*/
handleTabKey( event ) {
if ( ! event.shiftKey && document.activeElement === this.menuTrigger ) {
// tab into the first menu element
event.preventDefault();
this.firstFocusable.focus();
} else if ( event.shiftKey && document.activeElement === this.firstFocusable ) {
// close the menu as the user is tabbing back out of it
event.preventDefault();
this.closeCurrentMenu();
this.menuTrigger.focus();
} else if ( ! event.shiftKey && document.activeElement === this.lastFocusable ) {
// check if this is really the last focus element or if things have changed
if ( ! this.newLastFocusable() ) {
// close the menu as the user has tabbed through it
event.preventDefault();
this.closeCurrentMenu();
this.menuTrigger.focus();
}
}
},
/**
* Checks to see if the last focusable element has changed (for example after an ajax request)
*
* @method newLastFocusable
*/
newLastFocusable() {
const focusBounds = this.getFocusBounds();
if ( focusBounds.firstFocusable && focusBounds.lastFocusable ) {
if ( focusBounds.lastFocusable !== this.lastFocusable ) {
this.firstFocusable = focusBounds.firstFocusable;
this.lastFocusable = focusBounds.lastFocusable;
return true;
}
}
return false;
},
/**
* Closes menu if user presses the `esc` key. Also redirect focus back to `$toggle`.
*
* @method handleEscKey
*/
handleEscKey() {
this.closeCurrentMenu();
this.menuTrigger.focus();
},
/**
* Closes menu if user clicks outside the menu dropdown.
*
* @method handleClickOutsideMenu
* @param {object} event - Native JS event object
*/
handleClickOutsideMenu( event ) {
// Do not close current menu if user is interacting with date picker within the opened menu.
if ( event.target.classList.contains( 'ui-datepicker-next' )
|| event.target.classList.contains( 'ui-datepicker-prev' )
|| event.target.classList.contains( 'ui-datepicker-title' ) ) {
return;
}
// check if click is coming from outside of menu
if ( ! event.target.closest( this.currentMenuId ) ) {
this.closeCurrentMenu();
}
},
/**
* Closes menu if user's mouse moves outside the menu and not into the toggle
* or vice versa
*
* @method handleMouseLeaveMenu
* @param {object} event - Native JS event object
* @param {element} $alt - Toggle or menu div which the mouse may move into
*/
handleMouseLeaveMenu( event, $alt ) {
if ( ! this.isMouseWithinElem( event, $alt ) ) {
this.closeCurrentMenu();
}
},
/**
* Checks if mouse position is within element
* @param {object} event - Native JS event object
* @param {element} $elem - DOM element to check
* @return {bool} - True / False depending on whether mouse position is within element.
*/
isMouseWithinElem( event, $elem ) {
const bounds = $elem.getBoundingClientRect();
if ( event.pageX >= bounds.left + window.pageXOffset
&& event.pageX <= bounds.right + window.pageXOffset
&& event.pageY >= bounds.top + window.pageYOffset
&& event.pageY <= bounds.bottom + window.pageYOffset ) {
return true;
}
return false;
},
/**
* Fades out active dropdown menu, resets toggle states, and unbinds event listeners.
*
* @method closeCurrentMenu
*/
closeCurrentMenu() {
// close menu
const $currentMenu = document.querySelector( this.currentMenuId );
if ( ! $currentMenu ) {
return;
}
const menuId = this.currentMenuId.replace( '#', '' );
$currentMenu.classList.remove( 'is-slid-and-faded-in' );
$currentMenu.classList.add( 'is-slid-and-faded-out' );
// clear timeout for reseting submenu height
if ( this.heightResetTimeout ) {
clearTimeout( this.heightResetTimeout );
}
// Delay reseting the state till after being faded out
this.heightResetTimeout = setTimeout( () => {
$currentMenu.classList.remove( 'l-panel--single' );
$currentMenu.classList.remove( 'l-panel--init' );
// unset height of submenus wrapper
const $parent = $currentMenu.parentNode;
if ( $parent && $parent.classList.contains( this.submenusCss ) ) {
$parent.removeAttribute( 'style' );
if ( $parent.classList.contains( `${this.submenusCss}--${menuId}` ) ) {
$parent.classList.remove( `${this.submenusCss}--${menuId}` );
}
}
}, 500 );
// reset nav toggle state (there may be multiple toggles for a given menu)
const $menuToggles = document.querySelectorAll( `[data-toggle-menu="${this.currentMenuId}"], [data-hover-toggle-menu="${this.currentMenuId}"]` );
[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'is-toggled' ) );
[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'c-nav__item--active' ) );
[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'c-nav__link--active' ) );
// reset aria-expanded
[].forEach.call( $menuToggles, $menuToggle => $menuToggle.setAttribute( 'aria-expanded', false ) );
// turn off grayed out menu items
const $disabledMenuItems = document.querySelectorAll( `${this.disabledFlagAlways}, ${this.disabledFlagSelf}` );
[].forEach.call( $disabledMenuItems, elem => elem.classList.remove( 'is-disabled' ) );
this.currentMenuId = null;
this.$currentMenu = null;
// remove navbar modifier class
if ( this.$wrapper ) {
this.$wrapper.classList.remove( this.navMenuOpenClass );
}
// if the page has a short header, remove contrast class from it when menu closes
if ( this.$longformHeader ) {
if ( ! this.$longformHeader.classList.contains( this.longformHeaderPositionClass ) ) {
this.$longformHeader.classList.remove( this.longformHeaderContrastClass );
}
}
if ( this.$interactiveHeader ) {
if ( ! this.$interactiveHeader.classList.contains( this.interactiveHeaderPositionClass ) ) {
this.$interactiveHeader.classList.remove( this.interactiveHeaderContrastClass );
}
}
// unbind listeners
window.removeEventListener( 'orientationchange', this.resizeHandler );
window.removeEventListener( 'resize', this.resizeHandler );
window.removeEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );
document.removeEventListener( 'keydown', this.boundKeydown );
// remove click outside listener if it's a click toggle
if ( this.boundClickOutsideMenu ) {
document.removeEventListener( 'click', this.boundClickOutsideMenu );
}
// remove mouseleave listeners if it's a hover toggle
if ( this.boundMouseLeaveMenu ) {
$currentMenu.removeEventListener( 'mouseleave', this.boundMouseLeaveMenu );
[].forEach.call( $menuToggles, ( $menuToggle ) => {
$menuToggle.removeEventListener( 'mouseleave', this.boundMouseLeaveToggle );
});
}
},
/**
* Sets menu height to maximum available space in the `viewport`. Vertical scroll if it overflows.
*
* @method setMenuHeight
* @param {Element} $menu - DOM reference to the current active dropdown menu
* @param {Boolean} opening - Indicates whether to account for the y-translation when the menu
* is initially opened.
*/
setMenuHeight( $menu, opening = false ) {
if ( this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
$menu.classList.add( this.panelAboveNav );
} else {
$menu.classList.remove( this.panelAboveNav );
}
if ( this.isOrphan() && 'force' !== $menu.dataset.menuFullHeight ) {
return;
}
// clear timeout for reseting submenu height
if ( this.heightResetTimeout ) {
clearTimeout( this.heightResetTimeout );
}
const menuId = $menu.getAttribute( 'id' );
const $parent = $menu.parentNode;
const viewport = window.innerHeight;
const bounds = $menu.getBoundingClientRect();
const $menuInner = $menu.firstElementChild;
const boundsInner = $menuInner.getBoundingClientRect();
const paddingOffset = bounds.height - boundsInner.height;
let maxHeight = viewport - bounds.top - ( opening ? 20 : 0 );
// Comments panel should open up full height
// or below the header if it's visible
if ( 'menuComments' === menuId ) {
// let topOffset = 0;
maxHeight = viewport;
// Regular article template
if ( ! this.$interactiveHeader && ! this.$longformHeader ) {
const topOffset = window.innerWidth < this.breakpointTabletPortrait
? this.$navBar.getBoundingClientRect().top : 20;
if ( this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
$parent.style.top = `${topOffset}px`;
} else {
maxHeight -= document.querySelector( '.l-header' ).getBoundingClientRect().height;
$parent.style.top = 0;
}
} else if ( this.$longformHeader ) { // Longform
let topOffset = 0;
if ( window.innerWidth > this.breakpointTabletPortrait ) {
topOffset = this.$longformHeader.classList
.contains( this.longformHeaderPositionClass )
? 54 : 78;
}
$parent.style.top = `-${topOffset}px`;
} else if ( this.$interactiveHeader ) { // Interactive
let topOffset = 0;
if ( window.innerWidth > this.breakpointTabletPortrait ) {
topOffset = 80;
}
$parent.style.top = `-${topOffset}px`;
}
}
$menu.style.maxHeight = `${maxHeight}px`; // eslint-disable-line no-param-reassign
if ( 'true' === $menu.dataset.menuFullHeight || 'force' === $menu.dataset.menuFullHeight ) {
$menuInner.style.height = `${maxHeight - paddingOffset}px`;
}
if ( 'undefined' !== typeof ( $menu.style.webkitOverflowScrolling ) ) {
$menu.style.webkitOverflowScrolling = 'touch'; // eslint-disable-line no-param-reassign
}
// remove overflow scroll-y if menu fits in viewport to avoid grayed-out scrollbar
if ( bounds.bottom < viewport ) {
$menu.style.overflow = 'auto'; // eslint-disable-line no-param-reassign
} else {
$menu.style.overflow = ''; // eslint-disable-line no-param-reassign
}
// workaround for bottom-positioned mobile menus
if ( 'menuSocialShareNav' === menuId ) {
if ( ! this.$interactiveHeader && ! this.$longformHeader ) {
if ( window.innerWidth < this.breakpointTabletPortrait ) {
if ( ! this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
$parent.style.top = '-48px';
} else {
$parent.style.top = 0;
}
}
} else {
$parent.style.top = 0;
}
$parent.style.height = `${viewport}px`;
// set height of submenus wrapper, which sets overflow to hidden
// ensuring that submenus appear to slide in from behind main nav
} else if ( $parent && $parent.classList.contains( this.submenusCss ) ) {
$parent.style.height = `${maxHeight}px`;
}
},
/**
* Sets menu minimum width
* Calculate width of the comments menu so it always covers the sidebar
*
* @method setMenuWidth
* @param {Element} $menu - DOM reference to the current active dropdown menu
*/
setMenuWidth( $menu ) {
const $sidebar = document.querySelector( this.sidebarSelector );
// Longform and Issues templates does not have a sidebar
// Skip calculation - width is set with CSS
if ( ! $sidebar ) {
return;
}
const $thisMenu = $menu;
const menuBounds = $menu.getBoundingClientRect();
const sidebarBounds = $sidebar.getBoundingClientRect();
const commentsMenuWidth = menuBounds.right - sidebarBounds.left;
$thisMenu.style.width = `${commentsMenuWidth}px`;
},
/**
* Offsets the menu's margin so it aligns with its `$toggle` trigger.
*
* @method setMenuMargin
* @param {Element} $menu - DOM reference to the current active dropdown menu
* @param {Element} $toggle - DOM reference to the clicked toggle element
* @param {string} marginSide - The side of the menu to apply the offset
* @param {number} padding - Additional horizontal offset between the menu and its toggle
*/
setMenuMargin( $menu, $toggle, marginSide = 'left', padding = 16 ) {
const margin = $toggle.getBoundingClientRect()[marginSide];
const offset = this.$navBar.getBoundingClientRect()[marginSide];
if ( 'left' === marginSide ) {
$menu.style.marginLeft = `${margin - offset - padding}px`; // eslint-disable-line no-param-reassign
} else if ( 'right' === marginSide ) {
$menu.style.marginRight = `${offset - margin - padding}px`; // eslint-disable-line no-param-reassign
}
},
setLocalMenuMargin() {
if ( window.innerWidth > this.breakPointDesktop ) {
// Local Nav panel offset when nav transitions
// from menu to title and social share
const $localButton = document.querySelector( '[data-hover-toggle-menu="#menuLocal"]' );
const $localMenu = document.querySelector( '#menuLocal' );
if ( $localButton && $localMenu ) {
this.setMenuMargin( $localMenu, $localButton );
}
}
},
setSocialShareMenuMargin() {
const $socialButton = document.querySelector( '[data-toggle-menu="#menuSocialShareNav"]' );
const $socialMenu = document.querySelector( '#menuSocialShareNav' );
if ( $socialButton && $socialMenu ) {
this.setMenuMargin( $socialMenu, $socialButton, 'right', 0 );
}
},
/**
* Checks if menu is separate from the main nav
*
* @method isOrphan
* @return {Boolean} - True / False whether or not it is a child of the main nav
*/
isOrphan() {
return ( null === this.$currentMenu.closest( `.${this.navClass}` ) );
},
};
export default MainMenu;