[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-content/themes/twentytwenty/assets/js/ -> index.js (source)

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


Generated : Fri Jun 27 08:20:01 2025 Cross-referenced by PHPXref