ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

index.js (25738B)


      1 /*	-----------------------------------------------------------------------------------------------
      2 	Namespace
      3 --------------------------------------------------------------------------------------------------- */
      4 
      5 var twentytwenty = twentytwenty || {};
      6 
      7 // Set a default value for scrolled.
      8 twentytwenty.scrolled = 0;
      9 
     10 // polyfill closest
     11 // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
     12 if ( ! Element.prototype.closest ) {
     13 	Element.prototype.closest = function( s ) {
     14 		var el = this;
     15 
     16 		do {
     17 			if ( el.matches( s ) ) {
     18 				return el;
     19 			}
     20 
     21 			el = el.parentElement || el.parentNode;
     22 		} while ( el !== null && el.nodeType === 1 );
     23 
     24 		return null;
     25 	};
     26 }
     27 
     28 // polyfill forEach
     29 // https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
     30 if ( window.NodeList && ! NodeList.prototype.forEach ) {
     31 	NodeList.prototype.forEach = function( callback, thisArg ) {
     32 		var i;
     33 		var len = this.length;
     34 
     35 		thisArg = thisArg || window;
     36 
     37 		for ( i = 0; i < len; i++ ) {
     38 			callback.call( thisArg, this[ i ], i, this );
     39 		}
     40 	};
     41 }
     42 
     43 // event "polyfill"
     44 twentytwenty.createEvent = function( eventName ) {
     45 	var event;
     46 	if ( typeof window.Event === 'function' ) {
     47 		event = new Event( eventName );
     48 	} else {
     49 		event = document.createEvent( 'Event' );
     50 		event.initEvent( eventName, true, false );
     51 	}
     52 	return event;
     53 };
     54 
     55 // matches "polyfill"
     56 // https://developer.mozilla.org/es/docs/Web/API/Element/matches
     57 if ( ! Element.prototype.matches ) {
     58 	Element.prototype.matches =
     59 		Element.prototype.matchesSelector ||
     60 		Element.prototype.mozMatchesSelector ||
     61 		Element.prototype.msMatchesSelector ||
     62 		Element.prototype.oMatchesSelector ||
     63 		Element.prototype.webkitMatchesSelector ||
     64 		function( s ) {
     65 			var matches = ( this.document || this.ownerDocument ).querySelectorAll( s ),
     66 				i = matches.length;
     67 			while ( --i >= 0 && matches.item( i ) !== this ) {}
     68 			return i > -1;
     69 		};
     70 }
     71 
     72 // Add a class to the body for when touch is enabled for browsers that don't support media queries
     73 // for interaction media features. Adapted from <https://codepen.io/Ferie/pen/vQOMmO>.
     74 twentytwenty.touchEnabled = {
     75 
     76 	init: function() {
     77 		var matchMedia = function() {
     78 			// Include the 'heartz' as a way to have a non-matching MQ to help terminate the join. See <https://git.io/vznFH>.
     79 			var prefixes = [ '-webkit-', '-moz-', '-o-', '-ms-' ];
     80 			var query = [ '(', prefixes.join( 'touch-enabled),(' ), 'heartz', ')' ].join( '' );
     81 			return window.matchMedia && window.matchMedia( query ).matches;
     82 		};
     83 
     84 		if ( ( 'ontouchstart' in window ) || ( window.DocumentTouch && document instanceof window.DocumentTouch ) || matchMedia() ) {
     85 			document.body.classList.add( 'touch-enabled' );
     86 		}
     87 	}
     88 }; // twentytwenty.touchEnabled
     89 
     90 /*	-----------------------------------------------------------------------------------------------
     91 	Cover Modals
     92 --------------------------------------------------------------------------------------------------- */
     93 
     94 twentytwenty.coverModals = {
     95 
     96 	init: function() {
     97 		if ( document.querySelector( '.cover-modal' ) ) {
     98 			// Handle cover modals when they're toggled.
     99 			this.onToggle();
    100 
    101 			// When toggled, untoggle if visitor clicks on the wrapping element of the modal.
    102 			this.outsideUntoggle();
    103 
    104 			// Close on escape key press.
    105 			this.closeOnEscape();
    106 
    107 			// Hide and show modals before and after their animations have played out.
    108 			this.hideAndShowModals();
    109 		}
    110 	},
    111 
    112 	// Handle cover modals when they're toggled.
    113 	onToggle: function() {
    114 		document.querySelectorAll( '.cover-modal' ).forEach( function( element ) {
    115 			element.addEventListener( 'toggled', function( event ) {
    116 				var modal = event.target,
    117 					body = document.body;
    118 
    119 				if ( modal.classList.contains( 'active' ) ) {
    120 					body.classList.add( 'showing-modal' );
    121 				} else {
    122 					body.classList.remove( 'showing-modal' );
    123 					body.classList.add( 'hiding-modal' );
    124 
    125 					// Remove the hiding class after a delay, when animations have been run.
    126 					setTimeout( function() {
    127 						body.classList.remove( 'hiding-modal' );
    128 					}, 500 );
    129 				}
    130 			} );
    131 		} );
    132 	},
    133 
    134 	// Close modal on outside click.
    135 	outsideUntoggle: function() {
    136 		document.addEventListener( 'click', function( event ) {
    137 			var target = event.target;
    138 			var modal = document.querySelector( '.cover-modal.active' );
    139 
    140 			// if target onclick is <a> with # within the href attribute
    141 			if ( event.target.tagName.toLowerCase() === 'a' && event.target.hash.includes( '#' ) && modal !== null ) {
    142 				// untoggle the modal
    143 				this.untoggleModal( modal );
    144 				// wait 550 and scroll to the anchor
    145 				setTimeout( function() {
    146 					var anchor = document.getElementById( event.target.hash.slice( 1 ) );
    147 					anchor.scrollIntoView();
    148 				}, 550 );
    149 			}
    150 
    151 			if ( target === modal ) {
    152 				this.untoggleModal( target );
    153 			}
    154 		}.bind( this ) );
    155 	},
    156 
    157 	// Close modal on escape key press.
    158 	closeOnEscape: function() {
    159 		document.addEventListener( 'keydown', function( event ) {
    160 			if ( event.keyCode === 27 ) {
    161 				event.preventDefault();
    162 				document.querySelectorAll( '.cover-modal.active' ).forEach( function( element ) {
    163 					this.untoggleModal( element );
    164 				}.bind( this ) );
    165 			}
    166 		}.bind( this ) );
    167 	},
    168 
    169 	// Hide and show modals before and after their animations have played out.
    170 	hideAndShowModals: function() {
    171 		var _doc = document,
    172 			_win = window,
    173 			modals = _doc.querySelectorAll( '.cover-modal' ),
    174 			htmlStyle = _doc.documentElement.style,
    175 			adminBar = _doc.querySelector( '#wpadminbar' );
    176 
    177 		function getAdminBarHeight( negativeValue ) {
    178 			var height,
    179 				currentScroll = _win.pageYOffset;
    180 
    181 			if ( adminBar ) {
    182 				height = currentScroll + adminBar.getBoundingClientRect().height;
    183 
    184 				return negativeValue ? -height : height;
    185 			}
    186 
    187 			return currentScroll === 0 ? 0 : -currentScroll;
    188 		}
    189 
    190 		function htmlStyles() {
    191 			var overflow = _win.innerHeight > _doc.documentElement.getBoundingClientRect().height;
    192 
    193 			return {
    194 				'overflow-y': overflow ? 'hidden' : 'scroll',
    195 				position: 'fixed',
    196 				width: '100%',
    197 				top: getAdminBarHeight( true ) + 'px',
    198 				left: 0
    199 			};
    200 		}
    201 
    202 		// Show the modal.
    203 		modals.forEach( function( modal ) {
    204 			modal.addEventListener( 'toggle-target-before-inactive', function( event ) {
    205 				var styles = htmlStyles(),
    206 					offsetY = _win.pageYOffset,
    207 					paddingTop = ( Math.abs( getAdminBarHeight() ) - offsetY ) + 'px',
    208 					mQuery = _win.matchMedia( '(max-width: 600px)' );
    209 
    210 				if ( event.target !== modal ) {
    211 					return;
    212 				}
    213 
    214 				Object.keys( styles ).forEach( function( styleKey ) {
    215 					htmlStyle.setProperty( styleKey, styles[ styleKey ] );
    216 				} );
    217 
    218 				_win.twentytwenty.scrolled = parseInt( styles.top, 10 );
    219 
    220 				if ( adminBar ) {
    221 					_doc.body.style.setProperty( 'padding-top', paddingTop );
    222 
    223 					if ( mQuery.matches ) {
    224 						if ( offsetY >= getAdminBarHeight() ) {
    225 							modal.style.setProperty( 'top', 0 );
    226 						} else {
    227 							modal.style.setProperty( 'top', ( getAdminBarHeight() - offsetY ) + 'px' );
    228 						}
    229 					}
    230 				}
    231 
    232 				modal.classList.add( 'show-modal' );
    233 			} );
    234 
    235 			// Hide the modal after a delay, so animations have time to play out.
    236 			modal.addEventListener( 'toggle-target-after-inactive', function( event ) {
    237 				if ( event.target !== modal ) {
    238 					return;
    239 				}
    240 
    241 				setTimeout( function() {
    242 					var clickedEl = twentytwenty.toggles.clickedEl;
    243 
    244 					modal.classList.remove( 'show-modal' );
    245 
    246 					Object.keys( htmlStyles() ).forEach( function( styleKey ) {
    247 						htmlStyle.removeProperty( styleKey );
    248 					} );
    249 
    250 					if ( adminBar ) {
    251 						_doc.body.style.removeProperty( 'padding-top' );
    252 						modal.style.removeProperty( 'top' );
    253 					}
    254 
    255 					if ( clickedEl !== false ) {
    256 						clickedEl.focus();
    257 						clickedEl = false;
    258 					}
    259 
    260 					_win.scrollTo( 0, Math.abs( _win.twentytwenty.scrolled + getAdminBarHeight() ) );
    261 
    262 					_win.twentytwenty.scrolled = 0;
    263 				}, 500 );
    264 			} );
    265 		} );
    266 	},
    267 
    268 	// Untoggle a modal.
    269 	untoggleModal: function( modal ) {
    270 		var modalTargetClass,
    271 			modalToggle = false;
    272 
    273 		// If the modal has specified the string (ID or class) used by toggles to target it, untoggle the toggles with that target string.
    274 		// The modal-target-string must match the string toggles use to target the modal.
    275 		if ( modal.dataset.modalTargetString ) {
    276 			modalTargetClass = modal.dataset.modalTargetString;
    277 
    278 			modalToggle = document.querySelector( '*[data-toggle-target="' + modalTargetClass + '"]' );
    279 		}
    280 
    281 		// If a modal toggle exists, trigger it so all of the toggle options are included.
    282 		if ( modalToggle ) {
    283 			modalToggle.click();
    284 
    285 			// If one doesn't exist, just hide the modal.
    286 		} else {
    287 			modal.classList.remove( 'active' );
    288 		}
    289 	}
    290 
    291 }; // twentytwenty.coverModals
    292 
    293 /*	-----------------------------------------------------------------------------------------------
    294 	Intrinsic Ratio Embeds
    295 --------------------------------------------------------------------------------------------------- */
    296 
    297 twentytwenty.intrinsicRatioVideos = {
    298 
    299 	init: function() {
    300 		this.makeFit();
    301 
    302 		window.addEventListener( 'resize', function() {
    303 			this.makeFit();
    304 		}.bind( this ) );
    305 	},
    306 
    307 	makeFit: function() {
    308 		document.querySelectorAll( 'iframe, object, video' ).forEach( function( video ) {
    309 			var ratio, iTargetWidth,
    310 				container = video.parentNode;
    311 
    312 			// Skip videos we want to ignore.
    313 			if ( video.classList.contains( 'intrinsic-ignore' ) || video.parentNode.classList.contains( 'intrinsic-ignore' ) ) {
    314 				return true;
    315 			}
    316 
    317 			if ( ! video.dataset.origwidth ) {
    318 				// Get the video element proportions.
    319 				video.setAttribute( 'data-origwidth', video.width );
    320 				video.setAttribute( 'data-origheight', video.height );
    321 			}
    322 
    323 			iTargetWidth = container.offsetWidth;
    324 
    325 			// Get ratio from proportions.
    326 			ratio = iTargetWidth / video.dataset.origwidth;
    327 
    328 			// Scale based on ratio, thus retaining proportions.
    329 			video.style.width = iTargetWidth + 'px';
    330 			video.style.height = ( video.dataset.origheight * ratio ) + 'px';
    331 		} );
    332 	}
    333 
    334 }; // twentytwenty.instrinsicRatioVideos
    335 
    336 /*	-----------------------------------------------------------------------------------------------
    337 	Modal Menu
    338 --------------------------------------------------------------------------------------------------- */
    339 twentytwenty.modalMenu = {
    340 
    341 	init: function() {
    342 		// If the current menu item is in a sub level, expand all the levels higher up on load.
    343 		this.expandLevel();
    344 		this.keepFocusInModal();
    345 	},
    346 
    347 	expandLevel: function() {
    348 		var modalMenus = document.querySelectorAll( '.modal-menu' );
    349 
    350 		modalMenus.forEach( function( modalMenu ) {
    351 			var activeMenuItem = modalMenu.querySelector( '.current-menu-item' );
    352 
    353 			if ( activeMenuItem ) {
    354 				twentytwentyFindParents( activeMenuItem, 'li' ).forEach( function( element ) {
    355 					var subMenuToggle = element.querySelector( '.sub-menu-toggle' );
    356 					if ( subMenuToggle ) {
    357 						twentytwenty.toggles.performToggle( subMenuToggle, true );
    358 					}
    359 				} );
    360 			}
    361 		} );
    362 	},
    363 
    364 	keepFocusInModal: function() {
    365 		var _doc = document;
    366 
    367 		_doc.addEventListener( 'keydown', function( event ) {
    368 			var toggleTarget, modal, selectors, elements, menuType, bottomMenu, activeEl, lastEl, firstEl, tabKey, shiftKey,
    369 				clickedEl = twentytwenty.toggles.clickedEl;
    370 
    371 			if ( clickedEl && _doc.body.classList.contains( 'showing-modal' ) ) {
    372 				toggleTarget = clickedEl.dataset.toggleTarget;
    373 				selectors = 'input, a, button';
    374 				modal = _doc.querySelector( toggleTarget );
    375 
    376 				elements = modal.querySelectorAll( selectors );
    377 				elements = Array.prototype.slice.call( elements );
    378 
    379 				if ( '.menu-modal' === toggleTarget ) {
    380 					menuType = window.matchMedia( '(min-width: 1000px)' ).matches;
    381 					menuType = menuType ? '.expanded-menu' : '.mobile-menu';
    382 
    383 					elements = elements.filter( function( element ) {
    384 						return null !== element.closest( menuType ) && null !== element.offsetParent;
    385 					} );
    386 
    387 					elements.unshift( _doc.querySelector( '.close-nav-toggle' ) );
    388 
    389 					bottomMenu = _doc.querySelector( '.menu-bottom > nav' );
    390 
    391 					if ( bottomMenu ) {
    392 						bottomMenu.querySelectorAll( selectors ).forEach( function( element ) {
    393 							elements.push( element );
    394 						} );
    395 					}
    396 				}
    397 
    398 				lastEl = elements[ elements.length - 1 ];
    399 				firstEl = elements[0];
    400 				activeEl = _doc.activeElement;
    401 				tabKey = event.keyCode === 9;
    402 				shiftKey = event.shiftKey;
    403 
    404 				if ( ! shiftKey && tabKey && lastEl === activeEl ) {
    405 					event.preventDefault();
    406 					firstEl.focus();
    407 				}
    408 
    409 				if ( shiftKey && tabKey && firstEl === activeEl ) {
    410 					event.preventDefault();
    411 					lastEl.focus();
    412 				}
    413 			}
    414 		} );
    415 	}
    416 }; // twentytwenty.modalMenu
    417 
    418 /*	-----------------------------------------------------------------------------------------------
    419 	Primary Menu
    420 --------------------------------------------------------------------------------------------------- */
    421 
    422 twentytwenty.primaryMenu = {
    423 
    424 	init: function() {
    425 		this.focusMenuWithChildren();
    426 	},
    427 
    428 	// The focusMenuWithChildren() function implements Keyboard Navigation in the Primary Menu
    429 	// by adding the '.focus' class to all 'li.menu-item-has-children' when the focus is on the 'a' element.
    430 	focusMenuWithChildren: function() {
    431 		// Get all the link elements within the primary menu.
    432 		var links, i, len,
    433 			menu = document.querySelector( '.primary-menu-wrapper' );
    434 
    435 		if ( ! menu ) {
    436 			return false;
    437 		}
    438 
    439 		links = menu.getElementsByTagName( 'a' );
    440 
    441 		// Each time a menu link is focused or blurred, toggle focus.
    442 		for ( i = 0, len = links.length; i < len; i++ ) {
    443 			links[i].addEventListener( 'focus', toggleFocus, true );
    444 			links[i].addEventListener( 'blur', toggleFocus, true );
    445 		}
    446 
    447 		//Sets or removes the .focus class on an element.
    448 		function toggleFocus() {
    449 			var self = this;
    450 
    451 			// Move up through the ancestors of the current link until we hit .primary-menu.
    452 			while ( -1 === self.className.indexOf( 'primary-menu' ) ) {
    453 				// On li elements toggle the class .focus.
    454 				if ( 'li' === self.tagName.toLowerCase() ) {
    455 					if ( -1 !== self.className.indexOf( 'focus' ) ) {
    456 						self.className = self.className.replace( ' focus', '' );
    457 					} else {
    458 						self.className += ' focus';
    459 					}
    460 				}
    461 				self = self.parentElement;
    462 			}
    463 		}
    464 	}
    465 }; // twentytwenty.primaryMenu
    466 
    467 /*	-----------------------------------------------------------------------------------------------
    468 	Toggles
    469 --------------------------------------------------------------------------------------------------- */
    470 
    471 twentytwenty.toggles = {
    472 
    473 	clickedEl: false,
    474 
    475 	init: function() {
    476 		// Do the toggle.
    477 		this.toggle();
    478 
    479 		// Check for toggle/untoggle on resize.
    480 		this.resizeCheck();
    481 
    482 		// Check for untoggle on escape key press.
    483 		this.untoggleOnEscapeKeyPress();
    484 	},
    485 
    486 	performToggle: function( element, instantly ) {
    487 		var target, timeOutTime, classToToggle,
    488 			self = this,
    489 			_doc = document,
    490 			// Get our targets.
    491 			toggle = element,
    492 			targetString = toggle.dataset.toggleTarget,
    493 			activeClass = 'active';
    494 
    495 		// Elements to focus after modals are closed.
    496 		if ( ! _doc.querySelectorAll( '.show-modal' ).length ) {
    497 			self.clickedEl = _doc.activeElement;
    498 		}
    499 
    500 		if ( targetString === 'next' ) {
    501 			target = toggle.nextSibling;
    502 		} else {
    503 			target = _doc.querySelector( targetString );
    504 		}
    505 
    506 		// Trigger events on the toggle targets before they are toggled.
    507 		if ( target.classList.contains( activeClass ) ) {
    508 			target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-before-active' ) );
    509 		} else {
    510 			target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-before-inactive' ) );
    511 		}
    512 
    513 		// Get the class to toggle, if specified.
    514 		classToToggle = toggle.dataset.classToToggle ? toggle.dataset.classToToggle : activeClass;
    515 
    516 		// For cover modals, set a short timeout duration so the class animations have time to play out.
    517 		timeOutTime = 0;
    518 
    519 		if ( target.classList.contains( 'cover-modal' ) ) {
    520 			timeOutTime = 10;
    521 		}
    522 
    523 		setTimeout( function() {
    524 			var focusElement,
    525 				subMenued = target.classList.contains( 'sub-menu' ),
    526 				newTarget = subMenued ? toggle.closest( '.menu-item' ).querySelector( '.sub-menu' ) : target,
    527 				duration = toggle.dataset.toggleDuration;
    528 
    529 			// Toggle the target of the clicked toggle.
    530 			if ( toggle.dataset.toggleType === 'slidetoggle' && ! instantly && duration !== '0' ) {
    531 				twentytwentyMenuToggle( newTarget, duration );
    532 			} else {
    533 				newTarget.classList.toggle( classToToggle );
    534 			}
    535 
    536 			// If the toggle target is 'next', only give the clicked toggle the active class.
    537 			if ( targetString === 'next' ) {
    538 				toggle.classList.toggle( activeClass );
    539 			} else if ( target.classList.contains( 'sub-menu' ) ) {
    540 				toggle.classList.toggle( activeClass );
    541 			} else {
    542 				// If not, toggle all toggles with this toggle target.
    543 				_doc.querySelector( '*[data-toggle-target="' + targetString + '"]' ).classList.toggle( activeClass );
    544 			}
    545 
    546 			// Toggle aria-expanded on the toggle.
    547 			twentytwentyToggleAttribute( toggle, 'aria-expanded', 'true', 'false' );
    548 
    549 			if ( self.clickedEl && -1 !== toggle.getAttribute( 'class' ).indexOf( 'close-' ) ) {
    550 				twentytwentyToggleAttribute( self.clickedEl, 'aria-expanded', 'true', 'false' );
    551 			}
    552 
    553 			// Toggle body class.
    554 			if ( toggle.dataset.toggleBodyClass ) {
    555 				_doc.body.classList.toggle( toggle.dataset.toggleBodyClass );
    556 			}
    557 
    558 			// Check whether to set focus.
    559 			if ( toggle.dataset.setFocus ) {
    560 				focusElement = _doc.querySelector( toggle.dataset.setFocus );
    561 
    562 				if ( focusElement ) {
    563 					if ( target.classList.contains( activeClass ) ) {
    564 						focusElement.focus();
    565 					} else {
    566 						focusElement.blur();
    567 					}
    568 				}
    569 			}
    570 
    571 			// Trigger the toggled event on the toggle target.
    572 			target.dispatchEvent( twentytwenty.createEvent( 'toggled' ) );
    573 
    574 			// Trigger events on the toggle targets after they are toggled.
    575 			if ( target.classList.contains( activeClass ) ) {
    576 				target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-after-active' ) );
    577 			} else {
    578 				target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-after-inactive' ) );
    579 			}
    580 		}, timeOutTime );
    581 	},
    582 
    583 	// Do the toggle.
    584 	toggle: function() {
    585 		var self = this;
    586 
    587 		document.querySelectorAll( '*[data-toggle-target]' ).forEach( function( element ) {
    588 			element.addEventListener( 'click', function( event ) {
    589 				event.preventDefault();
    590 				self.performToggle( element );
    591 			} );
    592 		} );
    593 	},
    594 
    595 	// Check for toggle/untoggle on screen resize.
    596 	resizeCheck: function() {
    597 		if ( document.querySelectorAll( '*[data-untoggle-above], *[data-untoggle-below], *[data-toggle-above], *[data-toggle-below]' ).length ) {
    598 			window.addEventListener( 'resize', function() {
    599 				var winWidth = window.innerWidth,
    600 					toggles = document.querySelectorAll( '.toggle' );
    601 
    602 				toggles.forEach( function( toggle ) {
    603 					var unToggleAbove = toggle.dataset.untoggleAbove,
    604 						unToggleBelow = toggle.dataset.untoggleBelow,
    605 						toggleAbove = toggle.dataset.toggleAbove,
    606 						toggleBelow = toggle.dataset.toggleBelow;
    607 
    608 					// If no width comparison is set, continue.
    609 					if ( ! unToggleAbove && ! unToggleBelow && ! toggleAbove && ! toggleBelow ) {
    610 						return;
    611 					}
    612 
    613 					// If the toggle width comparison is true, toggle the toggle.
    614 					if (
    615 						( ( ( unToggleAbove && winWidth > unToggleAbove ) ||
    616 							( unToggleBelow && winWidth < unToggleBelow ) ) &&
    617 							toggle.classList.contains( 'active' ) ) ||
    618 						( ( ( toggleAbove && winWidth > toggleAbove ) ||
    619 							( toggleBelow && winWidth < toggleBelow ) ) &&
    620 							! toggle.classList.contains( 'active' ) )
    621 					) {
    622 						toggle.click();
    623 					}
    624 				} );
    625 			} );
    626 		}
    627 	},
    628 
    629 	// Close toggle on escape key press.
    630 	untoggleOnEscapeKeyPress: function() {
    631 		document.addEventListener( 'keyup', function( event ) {
    632 			if ( event.key === 'Escape' ) {
    633 				document.querySelectorAll( '*[data-untoggle-on-escape].active' ).forEach( function( element ) {
    634 					if ( element.classList.contains( 'active' ) ) {
    635 						element.click();
    636 					}
    637 				} );
    638 			}
    639 		} );
    640 	}
    641 
    642 }; // twentytwenty.toggles
    643 
    644 /**
    645  * Is the DOM ready?
    646  *
    647  * This implementation is coming from https://gomakethings.com/a-native-javascript-equivalent-of-jquerys-ready-method/
    648  *
    649  * @since Twenty Twenty 1.0
    650  *
    651  * @param {Function} fn Callback function to run.
    652  */
    653 function twentytwentyDomReady( fn ) {
    654 	if ( typeof fn !== 'function' ) {
    655 		return;
    656 	}
    657 
    658 	if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
    659 		return fn();
    660 	}
    661 
    662 	document.addEventListener( 'DOMContentLoaded', fn, false );
    663 }
    664 
    665 twentytwentyDomReady( function() {
    666 	twentytwenty.toggles.init();              // Handle toggles.
    667 	twentytwenty.coverModals.init();          // Handle cover modals.
    668 	twentytwenty.intrinsicRatioVideos.init(); // Retain aspect ratio of videos on window resize.
    669 	twentytwenty.modalMenu.init();            // Modal Menu.
    670 	twentytwenty.primaryMenu.init();          // Primary Menu.
    671 	twentytwenty.touchEnabled.init();         // Add class to body if device is touch-enabled.
    672 } );
    673 
    674 /*	-----------------------------------------------------------------------------------------------
    675 	Helper functions
    676 --------------------------------------------------------------------------------------------------- */
    677 
    678 /* Toggle an attribute ----------------------- */
    679 
    680 function twentytwentyToggleAttribute( element, attribute, trueVal, falseVal ) {
    681 	if ( element.classList.contains( 'close-search-toggle' ) ) {
    682 		return;
    683 	}
    684 	if ( trueVal === undefined ) {
    685 		trueVal = true;
    686 	}
    687 	if ( falseVal === undefined ) {
    688 		falseVal = false;
    689 	}
    690 	if ( element.getAttribute( attribute ) !== trueVal ) {
    691 		element.setAttribute( attribute, trueVal );
    692 	} else {
    693 		element.setAttribute( attribute, falseVal );
    694 	}
    695 }
    696 
    697 /**
    698  * Toggle a menu item on or off.
    699  *
    700  * @since Twenty Twenty 1.0
    701  *
    702  * @param {HTMLElement} target
    703  * @param {number} duration
    704  */
    705 function twentytwentyMenuToggle( target, duration ) {
    706 	var initialParentHeight, finalParentHeight, menu, menuItems, transitionListener,
    707 		initialPositions = [],
    708 		finalPositions = [];
    709 
    710 	if ( ! target ) {
    711 		return;
    712 	}
    713 
    714 	menu = target.closest( '.menu-wrapper' );
    715 
    716 	// Step 1: look at the initial positions of every menu item.
    717 	menuItems = menu.querySelectorAll( '.menu-item' );
    718 
    719 	menuItems.forEach( function( menuItem, index ) {
    720 		initialPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
    721 	} );
    722 	initialParentHeight = target.parentElement.offsetHeight;
    723 
    724 	target.classList.add( 'toggling-target' );
    725 
    726 	// Step 2: toggle target menu item and look at the final positions of every menu item.
    727 	target.classList.toggle( 'active' );
    728 
    729 	menuItems.forEach( function( menuItem, index ) {
    730 		finalPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
    731 	} );
    732 	finalParentHeight = target.parentElement.offsetHeight;
    733 
    734 	// Step 3: close target menu item again.
    735 	// The whole process happens without giving the browser a chance to render, so it's invisible.
    736 	target.classList.toggle( 'active' );
    737 
    738 	/*
    739 	 * Step 4: prepare animation.
    740 	 * Position all the items with absolute offsets, at the same starting position.
    741 	 * Shouldn't result in any visual changes if done right.
    742 	 */
    743 	menu.classList.add( 'is-toggling' );
    744 	target.classList.toggle( 'active' );
    745 	menuItems.forEach( function( menuItem, index ) {
    746 		var initialPosition = initialPositions[ index ];
    747 		if ( initialPosition.y === 0 && menuItem.parentElement === target ) {
    748 			initialPosition.y = initialParentHeight;
    749 		}
    750 		menuItem.style.transform = 'translate(' + initialPosition.x + 'px, ' + initialPosition.y + 'px)';
    751 	} );
    752 
    753 	/*
    754 	 * The double rAF is unfortunately needed, since we're toggling CSS classes, and
    755 	 * the only way to ensure layout completion here across browsers is to wait twice.
    756 	 * This just delays the start of the animation by 2 frames and is thus not an issue.
    757 	 */
    758 	requestAnimationFrame( function() {
    759 		requestAnimationFrame( function() {
    760 			/*
    761 			 * Step 5: start animation by moving everything to final position.
    762 			 * All the layout work has already happened, while we were preparing for the animation.
    763 			 * The animation now runs entirely in CSS, using cheap CSS properties (opacity and transform)
    764 			 * that don't trigger the layout or paint stages.
    765 			 */
    766 			menu.classList.add( 'is-animating' );
    767 			menuItems.forEach( function( menuItem, index ) {
    768 				var finalPosition = finalPositions[ index ];
    769 				if ( finalPosition.y === 0 && menuItem.parentElement === target ) {
    770 					finalPosition.y = finalParentHeight;
    771 				}
    772 				if ( duration !== undefined ) {
    773 					menuItem.style.transitionDuration = duration + 'ms';
    774 				}
    775 				menuItem.style.transform = 'translate(' + finalPosition.x + 'px, ' + finalPosition.y + 'px)';
    776 			} );
    777 			if ( duration !== undefined ) {
    778 				target.style.transitionDuration = duration + 'ms';
    779 			}
    780 		} );
    781 
    782 		// Step 6: finish toggling.
    783 		// Remove all transient classes when the animation ends.
    784 		transitionListener = function() {
    785 			menu.classList.remove( 'is-animating' );
    786 			menu.classList.remove( 'is-toggling' );
    787 			target.classList.remove( 'toggling-target' );
    788 			menuItems.forEach( function( menuItem ) {
    789 				menuItem.style.transform = '';
    790 				menuItem.style.transitionDuration = '';
    791 			} );
    792 			target.style.transitionDuration = '';
    793 			target.removeEventListener( 'transitionend', transitionListener );
    794 		};
    795 
    796 		target.addEventListener( 'transitionend', transitionListener );
    797 	} );
    798 }
    799 
    800 /**
    801  * Traverses the DOM up to find elements matching the query.
    802  *
    803  * @since Twenty Twenty 1.0
    804  *
    805  * @param {HTMLElement} target
    806  * @param {string} query
    807  * @return {NodeList} parents matching query
    808  */
    809 function twentytwentyFindParents( target, query ) {
    810 	var parents = [];
    811 
    812 	// Recursively go up the DOM adding matches to the parents array.
    813 	function traverse( item ) {
    814 		var parent = item.parentNode;
    815 		if ( parent instanceof HTMLElement ) {
    816 			if ( parent.matches( query ) ) {
    817 				parents.push( parent );
    818 			}
    819 			traverse( parent );
    820 		}
    821 	}
    822 
    823 	traverse( target );
    824 
    825 	return parents;
    826 }