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.