[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Fri Jun 27 08:20:01 2025 | Cross-referenced by PHPXref |