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