PageSourceSearch

https://assets.viewstub.com/wp/wp-content/themes/twentytwenty/assets/js/index.js?ver=2.2

js viewstub.com collected 2026-09-24 09:25:34 UTC 26,111 bytes, 842 lines download raw bytes

1/*	-----------------------------------------------------------------------------------------------
2	Namespace
3--------------------------------------------------------------------------------------------------- */
4
5var twentytwenty = twentytwenty || {};
6
7// Set a default value for scrolled.
8twentytwenty.scrolled = 0;
9
10// polyfill closest
11// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
12if ( ! 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
30if ( 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"
44twentytwenty.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
57if ( ! 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>.
74twentytwenty.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
94twentytwenty.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
297twentytwenty.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--------------------------------------------------------------------------------------------------- */
339twentytwenty.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
422twentytwenty.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
471twentytwenty.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 */
653function 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
665twentytwentyDomReady( 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
680function twentytwentyToggleAttribute( element, attribute, trueVal, falseVal ) {
681	var toggles;
682
683	if ( ! element.hasAttribute( attribute ) ) {
684		return;
685	}
686
687	if ( trueVal === undefined ) {
688		trueVal = true;
689	}
690	if ( falseVal === undefined ) {
691		falseVal = false;
692	}
693
694	/*
695	 * Take into account multiple toggle elements that need their state to be
696	 * synced. For example: the Search toggle buttons for desktop and mobile.
697	 */
698	toggles = document.querySelectorAll( '[data-toggle-target="' + element.dataset.toggleTarget + '"]' );
699
700	toggles.forEach( function( toggle ) {
701		if ( ! toggle.hasAttribute( attribute ) ) {
702			return;
703		}
704
705		if ( toggle.getAttribute( attribute ) !== trueVal ) {
706			toggle.setAttribute( attribute, trueVal );
707		} else {
708			toggle.setAttribute( attribute, falseVal );
709		}
710	} );
711}
712
713/**
714 * Toggle a menu item on or off.
715 *
716 * @since Twenty Twenty 1.0
717 *
718 * @param {HTMLElement} target
719 * @param {number} duration
720 */
721function twentytwentyMenuToggle( target, duration ) {
722	var initialParentHeight, finalParentHeight, menu, menuItems, transitionListener,
723		initialPositions = [],
724		finalPositions = [];
725
726	if ( ! target ) {
727		return;
728	}
729
730	menu = target.closest( '.menu-wrapper' );
731
732	// Step 1: look at the initial positions of every menu item.
733	menuItems = menu.querySelectorAll( '.menu-item' );
734
735	menuItems.forEach( function( menuItem, index ) {
736		initialPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
737	} );
738	initialParentHeight = target.parentElement.offsetHeight;
739
740	target.classList.add( 'toggling-target' );
741
742	// Step 2: toggle target menu item and look at the final positions of every menu item.
743	target.classList.toggle( 'active' );
744
745	menuItems.forEach( function( menuItem, index ) {
746		finalPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
747	} );
748	finalParentHeight = target.parentElement.offsetHeight;
749
750	// Step 3: close target menu item again.
751	// The whole process happens without giving the browser a chance to render, so it's invisible.
752	target.classList.toggle( 'active' );
753
754	/*
755	 * Step 4: prepare animation.
756	 * Position all the items with absolute offsets, at the same starting position.
757	 * Shouldn't result in any visual changes if done right.
758	 */
759	menu.classList.add( 'is-toggling' );
760	target.classList.toggle( 'active' );
761	menuItems.forEach( function( menuItem, index ) {
762		var initialPosition = initialPositions[ index ];
763		if ( initialPosition.y === 0 && menuItem.parentElement === target ) {
764			initialPosition.y = initialParentHeight;
765		}
766		menuItem.style.transform = 'translate(' + initialPosition.x + 'px, ' + initialPosition.y + 'px)';
767	} );
768
769	/*
770	 * The double rAF is unfortunately needed, since we're toggling CSS classes, and
771	 * the only way to ensure layout completion here across browsers is to wait twice.
772	 * This just delays the start of the animation by 2 frames and is thus not an issue.
773	 */
774	requestAnimationFrame( function() {
775		requestAnimationFrame( function() {
776			/*
777			 * Step 5: start animation by moving everything to final position.
778			 * All the layout work has already happened, while we were preparing for the animation.
779			 * The animation now runs entirely in CSS, using cheap CSS properties (opacity and transform)
780			 * that don't trigger the layout or paint stages.
781			 */
782			menu.classList.add( 'is-animating' );
783			menuItems.forEach( function( menuItem, index ) {
784				var finalPosition = finalPositions[ index ];
785				if ( finalPosition.y === 0 && menuItem.parentElement === target ) {
786					finalPosition.y = finalParentHeight;
787				}
788				if ( duration !== undefined ) {
789					menuItem.style.transitionDuration = duration + 'ms';
790				}
791				menuItem.style.transform = 'translate(' + finalPosition.x + 'px, ' + finalPosition.y + 'px)';
792			} );
793			if ( duration !== undefined ) {
794				target.style.transitionDuration = duration + 'ms';
795			}
796		} );
797
798		// Step 6: finish toggling.
799		// Remove all transient classes when the animation ends.
800		transitionListener = function() {
801			menu.classList.remove( 'is-animating' );
802			menu.classList.remove( 'is-toggling' );
803			target.classList.remove( 'toggling-target' );
804			menuItems.forEach( function( menuItem ) {
805				menuItem.style.transform = '';
806				menuItem.style.transitionDuration = '';
807			} );
808			target.style.transitionDuration = '';
809			target.removeEventListener( 'transitionend', transitionListener );
810		};
811
812		target.addEventListener( 'transitionend', transitionListener );
813	} );
814}
815
816/**
817 * Traverses the DOM up to find elements matching the query.
818 *
819 * @since Twenty Twenty 1.0
820 *
821 * @param {HTMLElement} target
822 * @param {string} query
823 * @return {NodeList} parents matching query
824 */
825function twentytwentyFindParents( target, query ) {
826	var parents = [];
827
828	// Recursively go up the DOM adding matches to the parents array.
829	function traverse( item ) {
830		var parent = item.parentNode;
831		if ( parent instanceof HTMLElement ) {
832			if ( parent.matches( query ) ) {
833				parents.push( parent );
834			}
835			traverse( parent );
836		}
837	}
838
839	traverse( target );
840
841	return parents;
842}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.