暂无描述

index.js 25KB

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