/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
var twentytwenty = twentytwenty || {};
// Set a default value for scrolled.
twentytwenty.scrolled = 0;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
if ( ! Element.prototype.closest ) {
Element.prototype.closest = function( s ) {
el = el.parentElement || el.parentNode;
} while ( el !== null && el.nodeType === 1 );
// https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
if ( window.NodeList && ! NodeList.prototype.forEach ) {
NodeList.prototype.forEach = function( callback, thisArg ) {
thisArg = thisArg || window;
for ( i = 0; i < len; i++ ) {
callback.call( thisArg, this[ i ], i, this );
twentytwenty.createEvent = function( eventName ) {
if ( typeof window.Event === 'function' ) {
event = new Event( eventName );
event = document.createEvent( 'Event' );
event.initEvent( eventName, true, false );
// https://developer.mozilla.org/es/docs/Web/API/Element/matches
if ( ! Element.prototype.matches ) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
var matches = ( this.document || this.ownerDocument ).querySelectorAll( s ),
while ( --i >= 0 && matches.item( i ) !== this ) {}
// Add a class to the body for when touch is enabled for browsers that don't support media queries
// for interaction media features. Adapted from <https://codepen.io/Ferie/pen/vQOMmO>.
twentytwenty.touchEnabled = {
var matchMedia = function() {
// Include the 'heartz' as a way to have a non-matching MQ to help terminate the join. See <https://git.io/vznFH>.
var prefixes = [ '-webkit-', '-moz-', '-o-', '-ms-' ];
var query = [ '(', prefixes.join( 'touch-enabled),(' ), 'heartz', ')' ].join( '' );
return window.matchMedia && window.matchMedia( query ).matches;
if ( ( 'ontouchstart' in window ) || ( window.DocumentTouch && document instanceof window.DocumentTouch ) || matchMedia() ) {
document.body.classList.add( 'touch-enabled' );
}; // twentytwenty.touchEnabled
/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
twentytwenty.coverModals = {
if ( document.querySelector( '.cover-modal' ) ) {
// Handle cover modals when they're toggled.
// When toggled, untoggle if visitor clicks on the wrapping element of the modal.
// Close on escape key press.
// Hide and show modals before and after their animations have played out.
this.hideAndShowModals();
// Handle cover modals when they're toggled.
document.querySelectorAll( '.cover-modal' ).forEach( function( element ) {
element.addEventListener( 'toggled', function( event ) {
var modal = event.target,
if ( modal.classList.contains( 'active' ) ) {
body.classList.add( 'showing-modal' );
body.classList.remove( 'showing-modal' );
body.classList.add( 'hiding-modal' );
// Remove the hiding class after a delay, when animations have been run.
body.classList.remove( 'hiding-modal' );
// Close modal on outside click.
outsideUntoggle: function() {
document.addEventListener( 'click', function( event ) {
var target = event.target;
var modal = document.querySelector( '.cover-modal.active' );
// if target onclick is <a> with # within the href attribute
if ( event.target.tagName.toLowerCase() === 'a' && event.target.hash.includes( '#' ) && modal !== null ) {
this.untoggleModal( modal );
// wait 550 and scroll to the anchor
var anchor = document.getElementById( event.target.hash.slice( 1 ) );
if ( target === modal ) {
this.untoggleModal( target );
// Close modal on escape key press.
closeOnEscape: function() {
document.addEventListener( 'keydown', function( event ) {
if ( event.keyCode === 27 ) {
document.querySelectorAll( '.cover-modal.active' ).forEach( function( element ) {
this.untoggleModal( element );
// Hide and show modals before and after their animations have played out.
hideAndShowModals: function() {
modals = _doc.querySelectorAll( '.cover-modal' ),
htmlStyle = _doc.documentElement.style,
adminBar = _doc.querySelector( '#wpadminbar' );
function getAdminBarHeight( negativeValue ) {
currentScroll = _win.pageYOffset;
height = currentScroll + adminBar.getBoundingClientRect().height;
return negativeValue ? -height : height;
return currentScroll === 0 ? 0 : -currentScroll;
var overflow = _win.innerHeight > _doc.documentElement.getBoundingClientRect().height;
'overflow-y': overflow ? 'hidden' : 'scroll',
top: getAdminBarHeight( true ) + 'px',
modals.forEach( function( modal ) {
modal.addEventListener( 'toggle-target-before-inactive', function( event ) {
var styles = htmlStyles(),
offsetY = _win.pageYOffset,
paddingTop = ( Math.abs( getAdminBarHeight() ) - offsetY ) + 'px',
mQuery = _win.matchMedia( '(max-width: 600px)' );
if ( event.target !== modal ) {
Object.keys( styles ).forEach( function( styleKey ) {
htmlStyle.setProperty( styleKey, styles[ styleKey ] );
_win.twentytwenty.scrolled = parseInt( styles.top, 10 );
_doc.body.style.setProperty( 'padding-top', paddingTop );
if ( offsetY >= getAdminBarHeight() ) {
modal.style.setProperty( 'top', 0 );
modal.style.setProperty( 'top', ( getAdminBarHeight() - offsetY ) + 'px' );
modal.classList.add( 'show-modal' );
// Hide the modal after a delay, so animations have time to play out.
modal.addEventListener( 'toggle-target-after-inactive', function( event ) {
if ( event.target !== modal ) {
var clickedEl = twentytwenty.toggles.clickedEl;
modal.classList.remove( 'show-modal' );
Object.keys( htmlStyles() ).forEach( function( styleKey ) {
htmlStyle.removeProperty( styleKey );
_doc.body.style.removeProperty( 'padding-top' );
modal.style.removeProperty( 'top' );
if ( clickedEl !== false ) {
_win.scrollTo( 0, Math.abs( _win.twentytwenty.scrolled + getAdminBarHeight() ) );
_win.twentytwenty.scrolled = 0;
untoggleModal: function( modal ) {
// If the modal has specified the string (ID or class) used by toggles to target it, untoggle the toggles with that target string.
// The modal-target-string must match the string toggles use to target the modal.
if ( modal.dataset.modalTargetString ) {
modalTargetClass = modal.dataset.modalTargetString;
modalToggle = document.querySelector( '*[data-toggle-target="' + modalTargetClass + '"]' );
// If a modal toggle exists, trigger it so all of the toggle options are included.
// If one doesn't exist, just hide the modal.
modal.classList.remove( 'active' );
}; // twentytwenty.coverModals
/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
twentytwenty.intrinsicRatioVideos = {
window.addEventListener( 'resize', function() {
document.querySelectorAll( 'iframe, object, video' ).forEach( function( video ) {
container = video.parentNode;
// Skip videos we want to ignore.
if ( video.classList.contains( 'intrinsic-ignore' ) || video.parentNode.classList.contains( 'intrinsic-ignore' ) ) {
if ( ! video.dataset.origwidth ) {
// Get the video element proportions.
video.setAttribute( 'data-origwidth', video.width );
video.setAttribute( 'data-origheight', video.height );
iTargetWidth = container.offsetWidth;
// Get ratio from proportions.
ratio = iTargetWidth / video.dataset.origwidth;
// Scale based on ratio, thus retaining proportions.
video.style.width = iTargetWidth + 'px';
video.style.height = ( video.dataset.origheight * ratio ) + 'px';
}; // twentytwenty.instrinsicRatioVideos
/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
twentytwenty.modalMenu = {
// If the current menu item is in a sub level, expand all the levels higher up on load.
expandLevel: function() {
var modalMenus = document.querySelectorAll( '.modal-menu' );
modalMenus.forEach( function( modalMenu ) {
var activeMenuItem = modalMenu.querySelector( '.current-menu-item' );
twentytwentyFindParents( activeMenuItem, 'li' ).forEach( function( element ) {
var subMenuToggle = element.querySelector( '.sub-menu-toggle' );
twentytwenty.toggles.performToggle( subMenuToggle, true );
keepFocusInModal: function() {
_doc.addEventListener( 'keydown', function( event ) {
var toggleTarget, modal, selectors, elements, menuType, bottomMenu, activeEl, lastEl, firstEl, tabKey, shiftKey,
clickedEl = twentytwenty.toggles.clickedEl;
if ( clickedEl && _doc.body.classList.contains( 'showing-modal' ) ) {
toggleTarget = clickedEl.dataset.toggleTarget;
selectors = 'input, a, button';
modal = _doc.querySelector( toggleTarget );
elements = modal.querySelectorAll( selectors );
elements = Array.prototype.slice.call( elements );
if ( '.menu-modal' === toggleTarget ) {
menuType = window.matchMedia( '(min-width: 1000px)' ).matches;
menuType = menuType ? '.expanded-menu' : '.mobile-menu';
elements = elements.filter( function( element ) {
return null !== element.closest( menuType ) && null !== element.offsetParent;
elements.unshift( _doc.querySelector( '.close-nav-toggle' ) );
bottomMenu = _doc.querySelector( '.menu-bottom > nav' );
bottomMenu.querySelectorAll( selectors ).forEach( function( element ) {
elements.push( element );
lastEl = elements[ elements.length - 1 ];
activeEl = _doc.activeElement;
tabKey = event.keyCode === 9;
shiftKey = event.shiftKey;
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
if ( shiftKey && tabKey && firstEl === activeEl ) {
}; // twentytwenty.modalMenu
/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
twentytwenty.primaryMenu = {
this.focusMenuWithChildren();
// The focusMenuWithChildren() function implements Keyboard Navigation in the Primary Menu
// by adding the '.focus' class to all 'li.menu-item-has-children' when the focus is on the 'a' element.
focusMenuWithChildren: function() {
// Get all the link elements within the primary menu.
menu = document.querySelector( '.primary-menu-wrapper' );
links = menu.getElementsByTagName( 'a' );
// Each time a menu link is focused or blurred, toggle focus.
for ( i = 0, len = links.length; i < len; i++ ) {
links[i].addEventListener( 'focus', toggleFocus, true );
links[i].addEventListener( 'blur', toggleFocus, true );
//Sets or removes the .focus class on an element.
// Move up through the ancestors of the current link until we hit .primary-menu.
while ( -1 === self.className.indexOf( 'primary-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', '' );
self.className += ' focus';
self = self.parentElement;
}; // twentytwenty.primaryMenu
/* -----------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------- */
// Check for toggle/untoggle on resize.
// Check for untoggle on escape key press.
this.untoggleOnEscapeKeyPress();
performToggle: function( element, instantly ) {
var target, timeOutTime, classToToggle,
targetString = toggle.dataset.toggleTarget,
// Elements to focus after modals are closed.
if ( ! _doc.querySelectorAll( '.show-modal' ).length ) {
self.clickedEl = _doc.activeElement;
if ( targetString === 'next' ) {