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