PageSourceSearch

https://www.boweryboyshistory.com/wp-content/themes/twentytwenty/assets/js/index.js?ver=1.3.0

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

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.