1/* jshint esversion: 6 */ 2var wpex = {}; 3 4( function( l10n ) { 5 6 'use strict'; 7 8 wpex = { 9 10 /** 11 * Main init function. 12 */ 13 init: function() { 14 this.config(); 15 this.replaceNoJsClass(); 16 this.bindEvents(); 17 }, 18 19 /** 20 * Define vars for caching. 21 */ 22 config: function() { 23 this.config = { 24 localScrollSections: [] 25 }; 26 }, 27 28 /** 29 * Replaces the "wpex-no-js" body class with "wpex-js". 30 */ 31 replaceNoJsClass: function() { 32 let bodyClass = document.body.className; 33 bodyClass = bodyClass.replace(/wpex-no-js/, 'wpex-js'); 34 document.body.className = bodyClass; 35 }, 36 37 /** 38 * Bind Events. 39 */ 40 bindEvents: function() { 41 const self = this; 42 43 self.setScrollBarWidth(); 44 45 /*** Fires as soon as the document is loaded ***/ 46 self.domReady( () => { 47 document.body.classList.add( 'wpex-docready' ); 48 49 if ( self.retinaCheck() ) { 50 document.body.classList.add( 'wpex-is-retina' ); 51 } 52 53 if ( self.mobileCheck() ) { 54 document.body.classList.add( 'wpex-is-mobile-device' ); 55 } 56 57 self.localScrollSections(); 58 self.megaMenuAddClasses(); 59 self.dropdownMenuOnclick(); 60 self.dropdownMenuTouch(); 61 self.inlineHeaderLogo(); 62 self.menuSearch(); 63 self.skipToContent(); 64 self.backTopLink(); 65 self.backTopButton(); 66 self.goBackButton(); 67 self.smoothCommentScroll(); 68 self.dialogs(); 69 self.toggleElements(); 70 self.localScrollLinks(); 71 self.customSelects(); 72 self.hoverStyles(); 73 self.overlaysMobileSupport(); 74 self.accessibility(); 75 self.wpbakery(); 76 } ); 77 78 /*** Fires once the window has been loaded ***/ 79 window.addEventListener( 'load', () => { 80 document.body.classList.add( 'wpex-window-loaded' ); 81 82 // Main. 83 self.megaMenusWidth(); 84 self.megaMenusTop(); 85 self.stickyTopBar(); 86 self.headerOverlayOffset(); // Add before sticky header ( important ). 87 self.equalHeights(); 88 self.localScrollHighlight(); 89 90 // Sticky functions. 91 self.stickyHeaderMenu(); // must go before stickyHeader to prevent issues with dyn-styles 92 self.stickyHeader(); 93 self.stickyElements(); 94 95 // Run methods after sticky header. 96 self.footerReveal(); // Footer Reveal => Must run before fixed footer!!! 97 self.fixedFooter(); // Fixed Footer => Must run after footerReveal!!! 98 99 // Scroll to hash (must be last). 100 if ( l10n.scrollToHash ) { 101 window.setTimeout( function() { 102 self.scrollToHash( self ); 103 }
103, parseInt( l10n.scrollToHashTimeout ) ); 104 } 105 } ); 106 107 /*** Fires on window resize ***/ 108 window.addEventListener( 'resize', () => { 109 self.setScrollBarWidth(); 110 } ); 111 }, 112 113 /** 114 * Set scrollbar width variable to fix issues with 100vw 115 */ 116 setScrollBarWidth: function() { 117 const self = this; 118 const elements = document.querySelectorAll( '.wpex-vc-full-width-row:not(.wpex-vc-full-width-row--centered)' ); 119 if ( ! elements ) { 120 return; 121 } 122 const scrollbarWidth = self.getScrollBarWidth(); 123 elements.forEach( ( element ) => { 124 element.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`); 125 } ); 126 }, 127 128 /** 129 * Onclick dropdown menu. 130 */ 131 dropdownMenuOnclick: function() { 132 133 /* 134 // @todo should we implement this? 135 document.querySelectorAll( '.wpex-dropdown-menu--onclick .menu-item-has-children > a' ).forEach( function( element ) { 136 if ( element.closest( '.megamenu__inner-ul' ) ) { 137 return; 138 } 139 element.setAttribute( 'aria-expanded', 'false' ); 140 if ( l10n.dropdownMenuAriaLabel ) { 141 var ariaLabel = l10n.dropdownMenuAriaLabel; 142 element.setAttribute( 'aria-label', ariaLabel.replace( '%s', element.textContent.trim() ) ); 143 } 144 } );*/ 145 146 document.addEventListener( 'click', ( event ) => { 147 const target = event.target; 148 149 if ( ! target.closest( '.wpex-dropdown-menu--onclick .menu-item-has-children > a' ) ) { 150 document.querySelectorAll( '.wpex-dropdown-menu--onclick .menu-item-has-children' ).forEach( ( element ) => { 151 element.classList.remove( 'wpex-active' ); 152 //element.querySelector( 'a' ).setAttribute( 'aria-expanded', 'false' ); 153 } ); 154 return; 155 } 156 157 document.querySelectorAll( '.wpex-dropdown-menu--onclick .menu-item-has-children' ).forEach( ( element ) => { 158 if ( ! element.contains( target ) ) { 159 element.classList.remove( 'wpex-active' ); 160 //element.querySelector( 'a' ).setAttribute( 'aria-expanded', 'false' ); 161 } 162 } ); 163 164 const li = target.closest( '.menu-item-has-children' ); 165 const a = target.closest( 'a' ); 166 167 if ( li.classList.contains( 'wpex-active' ) ) { 168 li.classList.remove( 'wpex-active' ); 169 //a.setAttribute( 'aria-expanded', 'false' ); 170 if ( '#' === a.getAttribute( 'href' ) ) { 171 event.preventDefault(); 172 } 173 } else { 174 li.classList.add( 'wpex-active' ); 175 //a.setAttribute( 'aria-expanded', 'true' ); 176 event.preventDefault(); 177 } 178 179 } ); 180 181 document.addEventListener( 'keydown', ( event ) => { 182 const target = event.target.closest( '.wpex-dropdown-menu--onclick .menu-item-has-children.wpex-active' ); 183 if ( 'Escape' !== event.key || ! target ) { 184 return; 185 } 186 target.classList.remove( 'wpex-active' ); 187 //target.querySelector( 'a' ).setAttribute( 'aria-expanded', 'false' ); 188 } ); 189 190 document.querySelectorAll( '.wpex-dropdown-menu--onclick .sub-menu' ).forEach( ( dropdown ) => { 191 dropdown.addEventListener( 'keydown', ( event ) => { 192 if ( 'Escape' === event.key ) { 193 const closestParent = event.target.closest( '.menu-item-has-children.wpex-active' ); 194 if ( closestParent ) { 195 closestParent.classList.remove( 'wpex-active' ); 196 const closestParentA = closestParent.querySelector( 'a' ); 197 //closestParentA.setAttribute( 'aria-expanded', 'false' ); 198 closestParentA.focus(); 199 event.stopPropagation(); 200 } 201 } 202 } ); 203 } ); 204 }, 205 206 /* 207 * Dropdown menu touch support. 208 */ 209 dropdownMenuTouch: function() { 210 const self = this; 211 let dragging = false; 212 213 document.querySelectorAll( '.wpex-dropdown-menu--onhover .menu-item-has-children > a' ).forEach( ( element ) => { 214 element.addEventListener( 'touchend', ( event ) => { 215 if ( dragging ) { 216 // hide dropdowns here? 217 return; 218 } 219 220 const parent = element.closest( '.menu-item-has-children' ); 221 222 if ( parent.classList.contains( 'wpex-touched' ) ) { 223 return; // already open allow clicking. 224 } 225 226 event.preventDefault(); 227 228 parent.classList.add( 'wpex-touched' ); 229 } ); 230 231 element.addEventListener( 'touchmove', ( event ) => { 232 dragging = true; 233 }, { passive: true } ); 234 235 element.addEventListener( 'touchstart', ( event ) => { 236 dragging = false;
237 }, { passive: true } ); 238 } ); 239 240 const clickingOutside = ( event ) => { 241 const target = event.target; 242 // not this code will only run if menus are open so it's efficient. 243 document.querySelectorAll( '.menu-item-has-children.wpex-touched' ).forEach( ( element ) => { 244 if ( element.contains( target ) ) { 245 return; 246 } 247 element.classList.remove( 'wpex-touched' ); 248 } ); 249 }; 250 251 document.addEventListener( 'touchstart', clickingOutside, { passive: true } ); 252 document.addEventListener( 'touchmove', clickingOutside, { passive: true } ); 253 }, 254 255 /** 256 * Add "megamenu__inner-ul" classname to megamenu inner UL elements. 257 */ 258 megaMenuAddClasses: function() { 259 document.querySelectorAll( '.main-navigation-ul .megamenu > .sub-menu' ).forEach( ( element ) => { 260 element.querySelectorAll( '.sub-menu' ).forEach( ( element ) => { 261 element.classList.add( 'megamenu__inner-ul' ); 262 } ); 263 } ); 264 }, 265 266 /** 267 * MegaMenus Width. 268 * 269 * @todo Instead of listening on resize perhaps we should run when resize ends. 270 * since usually dropdowns won't be open while resizing. 271 */ 272 megaMenusWidth: function() { 273 const self = this; 274 275 const init = () => { 276 const navWrap = document.querySelector( '#site-navigation-wrap.wpex-stretch-megamenus' ); 277 let navLeft; 278 let headerInnerLeft; 279 280 if ( ! self.isVisible( navWrap ) ) { 281 return; 282 } 283 284 const megaMenus = navWrap.querySelectorAll( '.megamenu:not(.no-stretch) > ul' ); 285 286 if ( ! megaMenus.length ) { 287 return; 288 } 289 290 const headerInner = document.querySelector( '#site-header-inner.container' ); 291 292 if ( ! headerInner ) { 293 return; 294 } 295 296 const addNegativeMargin = navWrap.classList.contains( 'navbar-style-one' ); 297 const headerInnerWidth = headerInner.getBoundingClientRect().width; 298 if ( addNegativeMargin ) { 299 headerInnerLeft = parseInt( headerInner.getBoundingClientRect().left ); 300 navLeft = navWrap.getBoundingClientRect().left; 301 } 302 303 megaMenus.forEach( ( element ) => { 304 element.style.width = headerInnerWidth + 'px'; 305 if ( addNegativeMargin ) { 306 element.style.marginLeft = -(navLeft-headerInnerLeft) + 'px'; 307 } 308 } ); 309 }; 310 311 // Add events. 312 init(); 313 window.addEventListener( 'resize', init ); 314 }, 315 316 /** 317 * MegaMenus Top Position. 318 */ 319 megaMenusTop: function() { 320 const self = this; 321 const header = document.querySelector( '#site-header.header-one' ); 322 323 if ( ! header ) { 324 return; 325 } 326 327 const navWrap = header.querySelector( '#site-navigation-wrap.wpex-stretch-megamenus:not(.wpex-flush-dropdowns)' ); 328 329 if ( ! this.isVisible( navWrap ) ) { 330 return; 331 } 332 333 const megaMenus = navWrap.querySelectorAll( '.megamenu > ul'); 334 335 if ( ! megaMenus ) { 336 return; 337 } 338 339 const setTop = () => { 340 if ( ! self.isVisible( navWrap ) ) { 341 return; 342 } 343 const navHeight = navWrap.getBoundingClientRect().height; 344 const topVal = ( ( header.getBoundingClientRect().height - navHeight ) / 2 ) + navHeight; 345 megaMenus.forEach( ( element ) => { 346 element.style.top = topVal + 'px'; 347 } ); 348 }; 349 350 // Add events. 351 setTop(); 352 window.addEventListener( 'scroll', setTop, { passive: true } ); 353 window.addEventListener( 'resize', setTop ); 354 navWrap.querySelectorAll( '.megamenu > a' ).forEach( ( element ) => { 355 element.addEventListener( 'mouseenter', setTop, false ); 356 } ); 357 }, 358 359 /** 360 * Parses Megamenu HTML for mobile. 361 */ 362 megaMenusMobile: function( menu ) { 363 if ( ! menu ) { 364 return; 365 } 366 367 const megaMenusWithoutHeadings = menu.classList.contains( 'sidr-class-dropdown-menu' ) ? '.sidr-class-megamenu.sidr-class-hide-headings' : '.megamenu.hide-headings'; 368 369 // Loop through mega menus with the hide-headings class to remove top level child ul elements. 370 menu.querySelectorAll( megaMenusWithoutHeadings ).forEach( ( megamenu ) => { 371 372 if ( megamenu.classList.contains( 'show-headings-mobile' ) || megamenu.classList.contains( 'sidr-class-show-headings-mobile' ) ) { 373 return; 374 } 375 376 // Loop through top level megamenu li elements (aka columns). 377 megamenu.querySelectorAll( ':scope > ul > li' ).forEach( ( headingLi ) => { 378 379 // Remove the heading link. 380 const headingA = headingLi.querySelector( 'a' ); 381 if ( headingA ) { 382 headingA.parentNode.removeChild( headingA ); 383 } 384 385 // Remove ul wrapper around direct heading li elements. 386 const headingUL = headingLi.querySelector( 'ul' ); 387 if ( headingUL ) { 388 headingUL.outerHTML = headingUL.innerHTML; 389 } 390 391 // Remove classes. 392 headingLi.classList.remove( 'sidr-class-menu-item-has-children' ); 393 headingLi.classList.remove( 'menu-item-has-children' ); 394 } ); 395 396 } ); 397 }, 398 399 /** 400 * Header Search. 401 */ 402 menuSearch: function() {
403 const searchWrap = document.querySelector( '.header-searchform-wrap' ); 404 405 if ( ! searchWrap ) { 406 return; 407 } 408 409 const searchInput = searchWrap.querySelector( 'input[type="search"]' ); 410 411 if ( ! searchInput ) { 412 return; 413 } 414 415 if ( searchWrap ) { 416 if ( searchWrap.dataset.placeholder ) { 417 searchInput.setAttribute( 'placeholder', searchWrap.dataset.placeholder ); 418 } 419 if ( searchWrap.dataset.disableAutocomplete ) { 420 searchInput.setAttribute( 'autocomplete', 'off' ); 421 } 422 } 423 424 this.menuSearchDropdown(); 425 this.menuSearchOverlay(); 426 this.menuSearchHeaderReplace(); 427 }, 428 429 /** 430 * Dropdown search. 431 */ 432 menuSearchDropdown: function() { 433 const self = this; 434 const searchDropdownForm = document.querySelector( '#searchform-dropdown' ); 435 436 if ( ! searchDropdownForm ) { 437 return; 438 } 439 440 let isOpen = false; 441 let onCloseFocus = false; 442 let toggleBtn = null; 443 const searchInput = searchDropdownForm.querySelector( 'input[type="search"]' ); 444 const targets = 'a.search-dropdown-toggle, a.mobile-menu-search, .wpex-header-search-icon button'; 445 446 const show = () => { 447 document.body.classList.add( 'header-search-is-open' ); 448 searchDropdownForm.classList.add( 'show' ); 449 searchDropdownForm.classList.remove( 'wpex-invisible', 'wpex-opacity-0' ); 450 document.querySelectorAll( targets ).forEach( ( element ) => { 451 element.setAttribute( 'aria-expanded', 'true' ); 452 var elementLi = element.closest( 'li' ); 453 if ( elementLi ) { 454 elementLi.classList.add( 'active' ); 455 } 456 } ); 457 458 searchInput.value = ''; 459 460 if ( 'function' === typeof jQuery ) { 461 jQuery( document ).trigger( 'show.wpex.menuSearch' ); 462 } 463 464 const focus = ( event ) => { 465 self.focusOnElement( searchDropdownForm, searchInput ); 466 searchDropdownForm.removeEventListener( 'transitionend', focus ); // remove event as it keeps triggering. 467 }; 468 469 // Focus on the search when opening after transition is complete to prevent issues. 470 searchDropdownForm.addEventListener( 'transitionend', focus ); 471 472 isOpen = true; 473 onCloseFocus = false; 474 }; 475 476 const hide = () => { 477 document.body.classList.remove( 'header-search-is-open' ); 478 searchDropdownForm.classList.remove( 'show' ); 479 searchDropdownForm.classList.add( 'wpex-invisible', 'wpex-opacity-0' ); 480 document.querySelectorAll( targets ).forEach( ( element ) => { 481 element.setAttribute( 'aria-expanded', 'false' ); 482 var elementLi = element.closest( 'li' ); 483 if ( elementLi ) { 484 elementLi.classList.remove( 'active' ); 485 } 486 } ); 487 if ( toggleBtn && onCloseFocus ) { 488 toggleBtn.focus(); 489 } 490 isOpen = false; 491 }; 492 493 const onDocClick = ( event ) => { 494 toggleBtn = event.target.closest( targets ); 495 if ( ! toggleBtn ) { 496 if ( ! event.target.closest( '#searchform-dropdown' ) && isOpen ) { 497 hide(); 498 } 499 return; 500 } 501 event.preventDefault(); 502 if ( isOpen ) { 503 hide(); 504 } else { 505 show(); 506 } 507 }; 508 509 const onKeyDown = ( event ) => { 510 const key = event.key; 511 if ( 'Escape' === key && isOpen ) { 512 onCloseFocus = true; 513 hide(); 514 } 515 }; 516 517 // Add events. 518 document.addEventListener( 'click', onDocClick ); 519 searchDropdownForm.addEventListener( 'keydown', onKeyDown ); 520 }, 521 522 /** 523 * Overlay search. 524 */ 525 menuSearchOverlay: function() { 526 const self = this; 527 const searchOverlay = document.querySelector( '#wpex-searchform-overlay' ); 528 529 if ( ! searchOverlay ) { 530 return; 531 } 532 533 let isOpen = false; 534 let toggleBtn = null; 535 let onCloseFocus = false; 536 const searchInput = searchOverlay.querySelector( 'input[type="search"]' ); 537 const targets = 'a.search-overlay-toggle, a.mobile-menu-search, li.search-overlay-toggle > a, .wpex-header-search-icon button'; 538 539 const setFocus = () => { 540 const focus = ( event ) => { 541 self.focusOnElement( searchOverlay, searchInput ); 542 searchOverlay.removeEventListener( 'transitionend', focus ); // remove event as it keeps triggering. 543 }; 544 545 // Focus on the search when opening after transition is complete to prevent issues. 546 searchOverlay.addEventListener( 'transitionend', focus ); 547 }; 548 549 const returnFocus = () => { 550 if ( toggleBtn && onCloseFocus ) { 551 toggleBtn.focus(); 552 } 553 }; 554 555 const show = () => { 556 isOpen = true; 557 onCloseFocus = false;
558 document.body.classList.add( 'header-search-is-open' ); 559 searchOverlay.classList.add( 'active' ); 560 self.htmlNoScroll( 'add' ); 561 562 document.querySelectorAll( targets ).forEach( ( element ) => { 563 element.setAttribute( 'aria-expanded', 'true' ); 564 const elementLi = element.closest( 'li' ); 565 if ( elementLi ) { 566 elementLi.classList.add( 'active' ); 567 } 568 } ); 569 570 searchInput.value = ''; // reset value on re-open. 571 572 if ( 'function' === typeof jQuery ) { 573 jQuery( document ).trigger( 'show.wpex.menuSearch' ); 574 } 575 576 setFocus(); 577 }; 578 579 const hide = ( event ) => { 580 isOpen = false; 581 document.body.classList.remove( 'header-search-is-open' ); 582 searchOverlay.classList.remove( 'active' ); 583 document.querySelectorAll( targets ).forEach( ( element ) => { 584 element.setAttribute( 'aria-expanded', 'false' ); 585 const elementLi = element.closest( 'li' ); 586 if ( elementLi ) { 587 elementLi.classList.remove( 'active' ); 588 } 589 } ); 590 returnFocus(); 591 // after animation has finished. 592 setTimeout( () => { 593 self.htmlNoScroll( 'remove' ); 594 }, self.getElTransitionDuration( searchOverlay ) ); 595 }; 596 597 const onDocClick = ( event ) => { 598 const target = event.target.closest( targets ); 599 if ( ! target ) { 600 if ( isOpen && event.target.closest( '#wpex-searchform-overlay .wpex-close' ) ) { 601 hide(); 602 } 603 return; 604 } 605 toggleBtn = target; // store toggle button for future focus. 606 event.preventDefault(); 607 if ( isOpen ) { 608 hide(); 609 } else { 610 show(); 611 } 612 }; 613 614 const onKeyDown = ( event ) => { 615 const key = event.key; 616 if ( 'Escape' === key && isOpen ) { 617 onCloseFocus = true; 618 return hide(); 619 } else if ( event.target.closest( '#wpex-searchform-overlay .wpex-close' ) && ( 'Space' === key || 'Enter' === key ) ) { 620 onCloseFocus = true; 621 } 622 }; 623 624 // Add events. 625 document.addEventListener( 'click', onDocClick ); 626 document.addEventListener( 'keydown', onKeyDown ); 627 }, 628 629 /** 630 * Header Replace Search. 631 */ 632 menuSearchHeaderReplace: function() { 633 const self = this; 634 const headerReplace = document.querySelector( '#searchform-header-replace' ); 635 636 if ( ! headerReplace ) { 637 return; 638 } 639 640 let isOpen = false; 641 let onCloseFocus = false; 642 let toggleBtn = null; 643 const headerInner = document.querySelector( '#site-header-inner' ); 644 const slideDown = headerReplace.classList.contains( '-wpex-translate-y-50' ); 645 const searchInput = headerReplace.querySelector( 'input[type="search"]' ); 646 const targets = 'a.search-header-replace-toggle, a.mobile-menu-search, .wpex-header-search-icon button'; 647 648 const show = () => { 649 document.body.classList.add( 'header-search-is-open' ); 650 headerReplace.classList.add( 'show' ); 651 headerReplace.classList.remove( 'wpex-invisible', 'wpex-opacity-0' ); 652 if ( headerInner ) { 653 headerInner.classList.add( 'wpex-overflow-hidden' ); 654 // headerInner.classList.add( 'wpex-invisible' ); // will break Poppy demo, don't do this - would require extra option. 655 } 656 if ( slideDown ) { 657 headerReplace.classList.remove( '-wpex-translate-y-50' ); 658 } 659 document.querySelectorAll( targets ).forEach( ( element ) => { 660 element.setAttribute( 'aria-expanded', 'true' ); 661 const elementLi = element.closest( 'li' ); 662 if ( elementLi ) { 663 elementLi.classList.add( 'active' ); 664 } 665 } ); 666 667 searchInput.value = ''; 668 669 if ( 'function' === typeof jQuery ) { 670 jQuery( document ).trigger( 'show.wpex.menuSearch' ); 671 } 672 673 const focus = ( event ) => { 674 self.focusOnElement( headerReplace, searchInput ); 675 headerReplace.removeEventListener( 'transitionend', focus ); // remove event as it keeps triggering. 676 }; 677 678 // Focus on the search when opening after transition is complete to prevent issues. 679 headerReplace.addEventListener( 'transitionend', focus ); 680 681 isOpen = true; 682 onCloseFocus = false; 683 }; 684 685 const hide = () => { 686 document.body.classList.remove( 'header-search-is-open' ); 687 headerReplace.classList.remove( 'show' ); 688 headerReplace.classList.add( 'wpex-invisible', 'wpex-opacity-0' ); 689 if ( headerInner ) { 690 headerInner.classList.remove( 'wpex-overflow-hidden' ); 691 // headerInner.classList.remove( 'wpex-invisible' ); 692 } 693 if ( slideDown ) { 694 headerReplace.classList.add( '-wpex-translate-y-50' ); 695 } 696 697 document.querySelectorAll( targets ).forEach( ( element ) => { 698 element.setAttribute( 'aria-expanded', 'false' ); 699 const elementLi = element.closest( 'li' ); 700 if ( elementLi ) { 701 elementLi.classList.remove( 'active' ); 702 } 703 } ); 704 if ( toggleBtn && onCloseFocus ) { 705 toggleBtn.focus(); 706 } 707 isOpen = false;
708 }; 709 710 const onDocClick = ( event ) => { 711 const target = event.target.closest( targets ); 712 if ( ! target ) { 713 if ( ! event.target.closest( '#searchform-header-replace .searchform' ) && isOpen ) { 714 hide(); 715 } 716 return; 717 } 718 toggleBtn = target; 719 event.preventDefault(); 720 if ( isOpen ) { 721 hide(); 722 } else { 723 show(); 724 } 725 }; 726 727 const onKeyDown = ( event ) => { 728 if ( ! isOpen ) { 729 return; 730 } 731 const key = event.key; 732 if ( 'Escape' === key ) { 733 onCloseFocus = true; 734 hide(); 735 } else if ( event.target.closest( '#searchform-header-replace-close' ) && ( 'Space' === key || 'Enter' === key ) ) { 736 onCloseFocus = true; 737 } 738 }; 739 740 // Add events. 741 document.addEventListener( 'click', onDocClick ); 742 headerReplace.addEventListener( 'keydown', onKeyDown ); 743 }, 744 745 /** 746 * Automatically add padding to row to offset header. 747 */ 748 headerOverlayOffset: function() { 749 const header = document.querySelector( '#site-header' ); 750 751 if ( ! header || ! header.classList.contains( 'overlay-header' ) || header.classList.contains( 'header-fixed-height' ) ) { 752 return; 753 } 754 755 const setHeight = () => { 756 const headerHeight = header.getBoundingClientRect().height; 757 document.querySelectorAll( '.overlay-header-offset-div' ).forEach( ( el ) => { 758 el.style.height = headerHeight + 'px'; 759 } ); 760 }; 761 762 // Add events. 763 setHeight(); 764 window.addEventListener( 'resize', setHeight ); 765 }, 766 767 /** 768 * Header 5 - Inline Logo. 769 * 770 * @todo move to it's own file. 771 */ 772 inlineHeaderLogo: function() { 773 const self = this; 774 const header = document.querySelector( '#site-header' ); 775 776 if ( ! header || ! header.classList.contains( 'header-five' ) ) { 777 return; 778 } 779 780 const logo = document.querySelector( '#site-header.header-five #site-header-inner > .header-five-logo' ); 781 const nav = document.querySelector( '#site-header.header-five .navbar-style-five' ); 782 let menuLogoLi = null; 783 784 if ( ! logo || ! nav ) { 785 return; 786 } 787 788 const getLogoBeforeEl = () => { 789 const navLiElements = document.querySelectorAll( '.navbar-style-five .main-navigation-ul > li' ); 790 let navLiElementsVisible = []; 791 792 for ( let i=0; i < navLiElements.length; i++ ) { 793 if ( self.isVisible( navLiElements[i] ) ) { 794 navLiElementsVisible.push( navLiElements[i] ); 795 } 796 } 797 798 const navLiCount = navLiElementsVisible.length; 799 const insertLogoAfterIndex = Math.round( navLiCount / 2 ) - parseInt( l10n.headerFiveSplitOffset ); 800 801 return navLiElementsVisible[insertLogoAfterIndex]; 802 }; 803 804 // Insert Logo into Menu. 805 const insertLogo = () => { 806 const insertLogoBeforeEl = getLogoBeforeEl(); 807 if ( ! insertLogoBeforeEl ) { 808 return; 809 } 810 if ( self.viewportWidth() >
810 l10n.mobileMenuBreakpoint ) { 811 if ( ! menuLogoLi ) { 812 menuLogoLi = document.createElement( 'li' ); 813 menuLogoLi.className = 'menu-item-logo wpex-px-40'; 814 } 815 menuLogoLi.appendChild( logo ); 816 insertLogoBeforeEl.parentNode.insertBefore( menuLogoLi, insertLogoBeforeEl.nextSibling ); 817 logo.classList.add( 'display' ); 818 } 819 }; 820 821 const onResize = () => { 822 const inlineLogo = document.querySelector( '.menu-item-logo .header-five-logo' ); 823 if ( self.viewportWidth() <= l10n.mobileMenuBreakpoint ) { 824 if ( inlineLogo ) { 825 const headerInner = document.querySelector( '#site-header-inner' ); 826 if ( headerInner ) { 827 headerInner.insertBefore( inlineLogo, headerInner.firstChild ); 828 } 829 if ( menuLogoLi ) { 830 menuLogoLi.parentNode.removeChild( menuLogoLi ); 831 } 832 } 833 } else if ( ! inlineLogo ) { 834 insertLogo(); // Insert logo to menu. 835 } 836 }; 837 838 // Add Events. 839 insertLogo(); 840 window.addEventListener( 'resize', onResize ); 841 }, 842 843 /** 844 * Handles the skip to content link. 845 */ 846 skipToContent: function() { 847 const self = this; 848 849 const onDocClick = ( event ) => { 850 if ( ! event.target.classList.contains( 'skip-to-content' ) ) { 851 return; 852 } 853 const target = document.querySelector( event.target.getAttribute( 'href' ) ); 854 if ( target ) { 855 target.setAttribute( 'tabIndex', '-1' ); 856 self.scrollTo( target ); 857 } 858 event.preventDefault(); 859 event.stopPropagation(); 860 }; 861 862 document.addEventListener( 'click', onDocClick ); 863 }, 864 865 /** 866 * Back to top link. 867 */ 868 backTopLink: function() { 869 const self = this; 870 871 const onDocClick = ( event ) => { 872 let target = event.target; 873 874 if ( ! target.closest( 'a#site-scroll-top, a.wpex-scroll-top, .wpex-scroll-top a' ) ) { 875 return; 876 } 877 878 const mainLink = target.closest( '#site-scroll-top' ); 879 880 if ( mainLink ) { 881 target = mainLink; 882 } 883 884 if ( 0 !== window.scrollY ) { 885 const topSpan = document.querySelector( '[data-ls_id="#site_top"]' ); 886 if ( topSpan ) { 887 topSpan.focus( { 888 preventScroll: true, 889 focusVisible: false 890 } ); 891 } 892 const speed = parseInt( target.dataset.scrollSpeed || parseInt( l
89210n.localScrollSpeed ) ); 893 const easing = self.getEasing( target.dataset.scrollEasing ); 894 if ( easing && 'function' === typeof jQuery ) { 895 jQuery( 'html, body' ).stop( true, true ).animate( { 896 scrollTop: 0 897 }, speed, easing ); 898 } else { 899 window.scrollTo( { 900 top: 0, 901 behavior: self.getScrollToBehavior() 902 } ); 903 } 904 } 905 906 event.preventDefault(); 907 event.stopPropagation(); 908 }; 909 910 // Add events. 911 document.addEventListener( 'click', onDocClick ); 912 }, 913 914 /** 915 * Back to top link. 916 */ 917 backTopButton: function() { 918 const self = this; 919 const backTopButton = document.querySelector( '#site-scroll-top' ); 920 921 if ( ! backTopButton ) { 922 return; 923 } 924 925 const offset = parseInt( backTopButton.dataset.scrollOffset || 100 ); 926 927 if ( 0 === offset ) { 928 return; 929 } 930 931 const showHide = ( event ) => { 932 if ( window.pageYOffset > offset ) { 933 backTopButton.classList.add( 'show'); 934 backTopButton.classList.remove( 'wpex-invisible', 'wpex-opacity-0' ); 935 } else { 936 backTopButton.classList.remove( 'show' ); 937 backTopButton.classList.add( 'wpex-invisible', 'wpex-opacity-0' ); 938 } 939 }; 940 941 // Add events. 942 window.addEventListener( 'scroll', showHide, { passive: true } ); 943 }, 944 945 /** 946 * Go back button. 947 */ 948 goBackButton: function() { 949 document.addEventListener( 'click', ( event ) => { 950 if ( event.target.closest( '.wpex-go-back' ) ) { 951 event.preventDefault(); 952 history.back(); 953 } 954 } ); 955 }, 956 957 /** 958 * Smooth Comment Scroll. 959 */ 960 smoothCommentScroll: function() { 961 const self = this; 962 963 const onDocClick = ( event ) => { 964 if ( ! event.target.closest( '.comments-link' ) ) { 965 return; 966 } 967 const target = document.querySelector( '#comments' ); 968 if ( ! target ) { 969 return; 970 } 971 self.scrollTo( target, -20 ); 972 event.preventDefault(); 973 event.stopPropagation(); 974 }; 975 976 // Add events. 977 document.addEventListener( 'click', onDocClick ); 978 }, 979 980 /** 981 * Dialogs. 982 */ 983 dialogs: function() { 984 const self = this; 985 986 const dialogOnClick = ( event ) => { 987 const target = event.target; 988 if ( ! target.closest( '.wpex-modal__inner' ) ) { 989 const dialog = target.closest( 'dialog' ); 990 if ( dialog ) { 991 closeDialog( dialog ); 992 } 993 } 994 }; 995 996 const openDialog = ( modalId ) => { 997 if ( ! modalId ) { 998 return; 999 } 1000 1001 const dialog = document.querySelector( `#${modalId.replace('#', '')}` ); 1002 1003 if ( ! dialog ) { 1004 return; 1005 } 1006 1007 if ( dialog.classList.contains( 'wpex-modal' ) ) { 1008 dialog.showModal(); 1009 const inner = dialog.querySelector( '.wpex-modal__inner' ); 1010 if ( inner ) { 1011 dialog.addEventListener( 'click', dialogOnClick ); 1012 }
1013 document.body.classList.add( 'modal-is-open' ); 1014 if ( ! document.body.classList.contains( 'vc_editor' ) ) { 1015 self.htmlNoScroll( 'add' ); 1016 } 1017 } else { 1018 dialog.show(); 1019 } 1020 }; 1021 1022 const closeDialog = ( dialog ) => { 1023 if ( dialog.classList.contains( 'wpex-modal' ) ) { 1024 dialog.removeEventListener( 'click', dialogOnClick ); 1025 } 1026 dialog.close(); 1027 }; 1028 1029 const onDocClick = ( event ) => { 1030 const openButton = event.target.closest( '.wpex-open-modal,.wpex-open-dialog' ); 1031 const closeButton = event.target.closest( '.wpex-close-modal' ); 1032 1033 if ( ! openButton && ! closeButton ) { 1034 return; 1035 } 1036 1037 event.preventDefault(); 1038 1039 if ( openButton ) { 1040 const dialog = openButton.getAttribute( 'aria-controls' ); 1041 if ( dialog ) { 1042 return openDialog( dialog ); 1043 } 1044 } 1045 1046 if ( closeButton ) { 1047 const dialog = event.target.closest( 'dialog' ); 1048 if ( dialog ) { 1049 return closeDialog( dialog ); 1050 } 1051 } 1052 }; 1053 1054 const onModalClose = ( event ) => { 1055 document.body.classList.remove( 'modal-is-open' ); 1056 self.htmlNoScroll( 'remove' ); 1057 }; 1058 1059 const addOnCloseEvents = () => { 1060 document.querySelectorAll( '.wpex-modal' ).forEach( dialog => { 1061 dialog.addEventListener( 'close', onModalClose ); 1062 } ); 1063 }; 1064 1065 // Register event listeners. 1066 addOnCloseEvents(); // note: dialog close event does not bubble. 1067 document.addEventListener( 'click', onDocClick ); 1068 window.addEventListener( 'totaltheme_loadmore_posts', addOnCloseEvents ); 1069 window.addEventListener( 'vcexAfterAjaxSuccess', addOnCloseEvents ); 1070 }, 1071 1072 /** 1073 * Toggle Elements. 1074 */ 1075 toggleElements: function() { 1076 const self = this; 1077 // const hasToggleElements = document.querySelector( '.wpex-toggle-element' ); 1078 1079 const getTargetElement = ( trigger ) => { 1080 const target = trigger.getAttribute( 'aria-controls' ); 1081 if ( target ) { 1082 return document.querySelector( `#${target.replace('#', '')}` ); 1083 } 1084 }; 1085 1086 const enableTrigger = ( trigger ) => { 1087 trigger.setAttribute( 'aria-expanded', 'true' ); 1088 if ( trigger.classList.contains( 'vcex-button' ) || trigger.classList.contains( 'theme-button' ) ) { 1089 trigger.classList.add( 'active' ); 1090 } 1091 const closeText = trigger.getAttribute( 'data-close-text' ); 1092 const openText = trigger.getAttribute( 'data-open-text' ); 1093 const textEl = trigger.querySelector( '.theme-button-text' ) || trigger; 1094 if ( openText && closeText && textEl ) { 1095 textEl.textContent = closeText; 1096 } 1097 }; 1098 1099 const disableTrigger = ( trigger ) => { 1100 trigger.setAttribute( 'aria-expanded', 'false' ); 1101 if ( trigger.classList.contains( 'vcex-button' ) || trigger.classList.contains( 'theme-button' ) ) { 1102 trigger.classList.remove( 'active' ); 1103 } 1104 const closeText = trigger.getAttribute( 'data-close-text' ); 1105 const openText = trigger.getAttribute( 'data-open-text' ); 1106 const textEl = trigger.querySelector( '.theme-button-text' ) || trigger; 1107 if ( openText && closeText && textEl ) { 1108 textEl.textContent = openText; 1109 } 1110 }; 1111 1112 const showToggleEl = ( toggleEl, container ) => { 1113 container = container || document; 1114 const toggleElId = toggleEl.getAttribute( 'id' ); 1115 if ( ! toggleElId ) { 1116 return; 1117 } 1118 1119 toggleEl.classList.add( 'wpex-toggle-element--visible' ); 1120 1121 const elParent = toggleEl.parentNode; 1122 1123 // Fix issues with toggle elements inside toggle elements (@see vcex_icon_box). 1124 if ( elParent.classList.contains( 'wpex-toggle-element' ) ) { 1125 elParent.classList.add( 'wpex-toggle-element--visible' ); 1126 elParent.setAttribute( 'tabIndex', '-1' ); 1127 elParent.focus( { 1128 preventScroll: true, 1129 focusVisible: false 1130 } ); 1131 } else { 1132 toggleEl.setAttribute( 'tabIndex', '-1' ); 1133 toggleEl.focus( { 1134 preventScroll: true, 1135 focusVisible: false 1136 } ); 1137 } 1138 1139 container.querySelectorAll( `.wpex-toggle-element-trigger[aria-controls="${toggleElId}"]` ).forEach( ( trigger ) => { 1140 enableTrigger( trigger ); 1141 } ); 1142 1143 window.dispatchEvent( new Event( 'resize' ) ); 1144 toggleEl.dispatchEvent( new CustomEvent( 'wpex_toggle_element_open' ) ); 1145 }; 1146 1147 const closeToggleEl = ( toggleEl, container ) => { 1148 container = container || document; 1149 const toggleElId = toggleEl.getAttribute( 'id' ); 1150 if ( ! toggleElId ) { 1151 return; 1152 } 1153 toggleEl.classList.remove( 'wpex-toggle-element--visible' ); 1154 toggleEl.dispatchEvent( new CustomEvent( 'wpex_toggle_element_close' ) ); 1155 container.querySelectorAll( `.wpex-toggle-element-trigger[aria-controls="${toggleElId}"]` ).forEach( ( trigger ) => { 1156 disableTrigger( trigger ); 1157 } ); 1158 }; 1159 1160 /* 1161 const onLoad = () => { 1162 const hash = location.hash; 1163 if ( ! hash || 'undefined' === hash || '#' === hash || ! self.isSelectorValid( hash ) ) { 1164 return; 1165 } 1166 const element = document.querySelector( hash ); 1167 if ( ! element || ! element.classList.contains( 'wpex-toggle-element' ) ) { 1168 return; 1169 } 1170 showToggleEl( element, null ); 1171 window.setTimeout( function() { 1172 self.scrollTo( element, -20 ); 1173 }
1173, parseInt( l10n.scrollToHashTimeout ) ); 1174 }; 1175 */ 1176 1177 const onDocClick = ( event ) => { 1178 const button = event.target.closest( 'a.wpex-toggle-element-trigger,button.wpex-toggle-element-trigger' ); 1179 1180 if ( ! button ) { 1181 return; 1182 } 1183 1184 const toggleEl = getTargetElement( button ); 1185 1186 if ( ! toggleEl || ! toggleEl.classList.contains( 'wpex-toggle-element' ) ) { 1187 return; 1188 } 1189 1190 event.preventDefault(); 1191 1192 const toggleElClass = toggleEl.classList; 1193 const triggerParent = button.closest( '.vc_section' ) || button.closest( '.vc_row' ); 1194 let isToggleElementContained = false; 1195 1196 if ( triggerParent && triggerParent.contains( toggleEl ) ) { 1197 isToggleElementContained = true; 1198 } 1199 1200 const targetSection = isToggleElementContained ? triggerParent : document; 1201 1202 targetSection.querySelectorAll( '.wpex-toggle-element--visible' ).forEach( ( element ) => { 1203 if ( ! element.isSameNode( toggleEl ) ) { 1204 closeToggleEl( element, targetSection ); 1205 } 1206 } ); 1207 1208 if ( toggleElClass.contains( 'wpex-toggle-element--visible' ) ) { 1209 if ( ! toggleElClass.contains( 'wpex-toggle-element--persist' ) ) { 1210 closeToggleEl( toggleEl, targetSection ); 1211 } 1212 } else { 1213 showToggleEl( toggleEl, targetSection ); 1214 if ( 'true' === button.getAttribute( 'data-wpex-toggle-localscroll' ) && ! button.classList.contains( 'local-scroll-link' ) ) { 1215 self.scrollTo( toggleEl ); 1216 } 1217 } 1218 }; 1219 1220 // Focus on toggle when clicking escape inside a toggle element. 1221 const onKeyDown = ( event ) => { 1222 if ( 'Escape' !== event.key ) { 1223 return; 1224 } 1225 const toggleEl = event.target.closest( '.wpex-toggle-element--visible' ); 1226 if ( ! toggleEl ) { 1227 return; 1228 } 1229 const toggleElId = toggleEl.getAttribute( 'id' ); 1230 if ( ! toggleElId ) { 1231 return; 1232 } 1233 let container = toggleEl.closest( '.vc_row' ) || toggleEl.closest( '.vc_section' ); 1234 let trigger = container.querySelector( `.wpex-toggle-element-trigger[aria-controls="${toggleElId}"]` ); 1235 if ( ! trigger ) { 1236 trigger = document.querySelector( `.wpex-toggle-element-trigger[aria-controls="${toggleElId}"]` ); 1237 } 1238 if ( trigger ) { 1239 trigger.focus(); 1240 } 1241 }; 1242 1243 // Register event listeners. 1244 document.addEventListener( 'click', onDocClick ); 1245 document.addEventListener( 'keydown', onKeyDown ); 1246 /*if ( hasToggleElements ) { 1247 window.addEventListener( 'load', onLoad ); 1248 }*/ 1249 }, 1250 1251 /** 1252 * Local Scroll Offset. 1253 */ 1254 getLocalScrollOffset: function( instance ) { 1255 if ( l10n.localScrollOffset ) { 1256 return parseInt( l10n.localScrollOffset ); // custom user defined offset in pixels. 1257 } 1258 1259 const self = this; 1260 let offset = 0; 1261 1262 // Adds extra offset via filter. 1263 if ( l10n.localScrollExtraOffset ) { 1264 offset = parseInt( offset ) + parseInt( l10n.localScrollExtraOffset ); 1265 } 1266 1267 // Fixed header. 1268 // !!!important!!! must target the #site-header to prevent issues with the Transparent header which 1269 // will return 0 for the sticky-wrapper height. 1270 const stickyHeader = document.querySelector( '#site-header-sticky-wrapper.wpex-can-sticky #site-header' ); 1271 if ( stickyHeader ) { 1272 1273 // Return 0 for small screens if mobile fixed header is disabled. 1274 if ( ! l10n.hasStickyMobileHeader && self.viewportWidth() < l10n.stickyHeaderBreakPoint ) { 1275 offset = parseInt( offset ) + 0; 1276 } 1277 1278 // Return header height. 1279 else { 1280 1281 // Shrink header. 1282 if ( stickyHeader.classList.contains( 'shrink-sticky-header' ) ) { 1283 // @todo can we remove the instance and use a different check? 1284 // Maybe we can add a data attribute to the header like data-sticky-init="false" 1285 // that we can check. 1286 if ( 'init' === instance || self.isVisible( stickyHeader ) ) { 1287 offset = parseInt( offset ) + parseInt( l10n.shrinkHeaderHeight ); 1288 } 1289 } 1290 1291 // Standard header. 1292 else { 1293 offset = parseInt( offset ) + stickyHeader.getBoundingClientRect().height; 1294 } 1295 1296 } 1297 1298 } 1299 1300 // Loop through extra items. 1301 // @todo update elements to have the "wpex-ls-offset classname where possible". 1302 document.querySelectorAll( '.wpex-ls-offset,#wpadminbar,#top-bar-wrap-sticky-wrapper.wpex-can-sticky,#site-navigation-sticky-wrapper.wpex-can-sticky' ).forEach( ( element ) => { 1303 if ( self.isVisible( element ) ) { 1304 offset = parseInt( offset ) + element.getBoundingClientRect().height; 1305 } 1306 } ); 1307 1308 /** 1309 * Remove 1 pixel to prevent cross browser rounding issues (firefox). 1310 * 1311 * @todo anyway to fix this? 1312 */ 1313 offset = offset ? offset - 1 : 0; 1314 1315 return offset; 1316 }, 1317 1318 /** 1319 * Scroll to function. 1320 * 1321 * @note: Important never change this function since it's used by 3rd party scripts. 1322 */ 1323 scrollTo: function( hash, addedOffset = 0, callback ) { 1324 const self = this; 1325 1326 if ( ! hash ) { 1327 return; 1328 } 1329 1330 let target = null; 1331 let isLsDataLink = false;
1332 let localSection = null; 1333 let offset = 0; 1334 const lsOffset = self.getLocalScrollOffset(); 1335 const lsSpeed = parseInt( l10n.localScrollSpeed ); 1336 const sections = document.querySelectorAll( '[data-ls_id]' ); 1337 const easing = self.getEasing(); 1338 1339 // Use standard for loop since forEach doesn't support breaks. 1340 for ( let i=0; i < sections.length; i++ ) { 1341 if ( hash === sections[i].dataset.ls_id ) { 1342 localSection = sections[i]; 1343 break; 1344 } 1345 } 1346 1347 if ( localSection ) { 1348 target = localSection; 1349 isLsDataLink = true; 1350 } else if ( 'string' === typeof hash ) { 1351 if ( self.isSelectorValid( hash ) ) { 1352 target = document.querySelector( hash ); 1353 } 1354 } else if ( hash.nodeType ) { 1355 target = hash; 1356 } 1357 1358 if ( ! target ) { 1359 return; // bail early since there isn't any target. 1360 } 1361 1362 const run = () => { 1363 if ( target && target.nodeType ) { 1364 target.focus( { 1365 preventScroll: true, 1366 focusVisible: false 1367 } ); 1368 } 1369 if ( easing && 'function' === typeof jQuery ) { 1370 jQuery( 'html, body' ).stop( true, true ).animate( { 1371 scrollTop: offset 1372 }, lsSpeed, easing ); 1373 } else { 1374 window.scrollTo( { 1375 top: offset, 1376 behavior: self.getScrollToBehavior() 1377 } ); 1378 } 1379 }; 1380 1381 // Change the target to parent tabs when linking to tab item and open the tab. 1382 if ( target.classList.contains( 'vc_tta-panel' ) ) { 1383 const tabs = target.closest( '.vc_tta-tabs' ); 1384 if ( tabs ) { 1385 if ( target.id ) { 1386 const tabLink = tabs.querySelector(`.vc_tta-tab:not(.vc_active) a[href="#${target.id}"]`); 1387 if ( tabLink ) { 1388 tabLink.click(); 1389 } 1390 } 1391 target = tabs; 1392 addedOffset = -20; 1393 } 1394 } 1395 1396 // Set offset if 0. 1397 if ( ! offset ) { 1398 offset = self.offset( target ).top - lsOffset + addedOffset; 1399 } 1400 1401 // Update hash. 1402 if ( l10n.localScrollUpdateHash && 'string' === typeof hash && isLsDataLink ) { 1403 window.location.hash = hash; 1404 } 1405 1406 /** 1407 * Mobile toggle Menu needs it's own code so it closes before the event fires 1408 * to make sure we end up in the right place. 1409 * 1410 * @todo move into the toggle.js file, so we listen to events there and then run the scrollTo class after the element is closed. 1411 * we simply need to add an extra check to localScrollLinks > onDocClick to exclude the links int his menu. 1412 */ 1413 const mobileToggleNav = document.querySelector( '.mobile-toggle-nav' ); 1414 if ( mobileToggleNav && mobileToggleNav.classList.contains( 'visible' ) ) { 1415 1416 // Make sure the nav height is removed from the offset height, but only 1417 // when its not absolutely positioned. 1418 if ( 'absolute' !== window.getComputedStyle( mobileToggleNav ).position ) { 1419 offset = offset - mobileToggleNav.getBoundingClientRect().height; 1420 } 1421 1422 document.querySelectorAll( 'a.mobile-menu-toggle, button.mobile-menu-toggle, li.mobile-menu-toggle > a' ).forEach( ( element ) => { 1423 element.classList.remove( 'wpex-active' ); 1424 element.setAttribute( 'aria-expanded', 'false' ); 1425 } ); 1426 1427 self.setHamburgerIconState(); 1428 1429 if ( mobileToggleNav.classList.contains( 'mobile-toggle-nav--animate' ) ) { 1430 self.slideUp( mobileToggleNav, 300, function() { 1431 mobileToggleNav.classList.remove( 'visible' ); 1432 document.body.classList.remove( 'mobile-toggle-nav-open' ); 1433 run(); 1434 } ); 1435 } else { 1436 mobileToggleNav.classList.remove( 'visible' ); 1437 document.body.classList.remove( 'mobile-toggle-nav-open' ); 1438 run(); 1439 } 1440 } 1441 // Scroll to section. 1442 else { 1443 run(); 1444 } 1445 }, 1446 1447 /** 1448 * Scroll to Hash. 1449 */ 1450 scrollToHash: function( self ) { 1451 let target; 1452 let hash = location.hash; 1453 1454 if ( '' === hash || '#' === hash || 'undefined' === hash ) { 1455 return; 1456 } 1457 1458 // Scroll to comments. 1459 if ( '#view_comments' === hash || '#comments_reply' === hash ) { 1460 target = document.querySelector( '#comments' ); 1461 if ( target ) { 1462 return self.scrollTo( target, -20 ); 1463 } 1464 } 1465 1466 // Scroll to specific comment, fix for sticky header. 1467 if ( -1 !== hash.indexOf( 'comment-' ) && document.querySelector( '#site-header.fixed-scroll' ) ) { 1468 target = document.querySelector( hash ); 1469 if ( target ) { 1470 return self.scrollTo( target, -20 ); 1471 } 1472 } 1473 1474 // Custom local scroll using #localscroll-{ID} for targeting elements on the page. 1475 if ( -1 !== hash.indexOf( 'localscroll-' ) ) { 1476 hash = hash.replace( 'localscroll-', '' ); 1477 } 1478 1479 try { 1480 const element = document.querySelector( `[data-ls_id="${hash}"], ${hash}` ); 1481 if ( element 1482 && 'dialog' !== element.getAttribute( 'roles' ) 1483 && ! element.classList.contains( 'vcex-toggle__content' ) 1484 && ! element.classList.contains( 'wpex-toggle-element' ) 1485 && ! element.classList.contains( 'wpex-card-modal' ) 1486 ) { 1487 self.scrollTo( hash ); 1488 } 1489 } catch ( error ) { 1490 return false;
1491 } 1492 }, 1493 1494 /** 1495 * Local scroll links array. 1496 */ 1497 localScrollSections: function() { 1498 const self = this; 1499 const links = document.querySelectorAll( l10n.localScrollTargets ); 1500 const array = []; 1501 1502 // Loop through links. 1503 links.forEach( ( link ) => { 1504 const href = link.getAttribute( 'href' ); 1505 const hash = href ? `#${href.replace( /^.*?(#|$)/, '' )}` : null; 1506 1507 if ( ! hash || '#' === hash ) { 1508 return; 1509 } 1510 1511 if ( ! link.hasAttribute( 'data-ls_linkto' ) ) { 1512 link.setAttribute( 'data-ls_linkto', hash ); 1513 } 1514 1515 let section = document.querySelector( `[data-ls_id="${hash}"]` ); 1516 if ( ! section && 'string' === typeof hash && self.isSelectorValid( hash ) ) { 1517 section = document.querySelector( hash ); 1518 } 1519 1520 if ( section && -1 === array.indexOf( hash ) ) { 1521 array.push( hash ); 1522 } 1523 1524 } ); 1525 1526 self.config.localScrollSections = array; 1527 1528 return self.config.localScrollSections; 1529 }, 1530 1531 /** 1532 * Local Scroll link. 1533 */ 1534 localScrollLinks: function() { 1535 const self = this; 1536 1537 const findLinks = () => { 1538 document.querySelectorAll( 'a[href^="#"]:not([href="#"]):not(.local-scroll-link):not([target]):not([aria-selected]):not([aria-pressed]):not([aria-expanded]):not([data-vc-tabs]):not(.skip-to-content):not(#site-scroll-top):not([data-vc-accordion]):not([role="button"]):not(.wpex-lightbox):not(.wpex-lightbox-group-item)' ).forEach( ( link ) => { 1539 if ( link.closest( '.local-scroll, .local-scroll-link, [aria-controls]' ) ) { 1540 return; 1541 } 1542 let href = link.getAttribute( 'href' ); 1543 try { 1544 if ( document.querySelector( `[data-ls_id="${href}"], ${href}` ) ) { 1545 link.classList.add( 'local-scroll-link' ); 1546 } 1547 } catch ( error ) { 1548 return false; 1549 } 1550 } ); 1551 }; 1552 1553 const onDocClick = ( event ) => { 1554 const target = event.target; 1555 1556 if ( target.closest( '.wpex-open-submenu' ) ) { 1557 return; 1558 } 1559 1560 const link = target.closest( l10n.localScrollTargets ); 1561 1562 if ( ! link ) { 1563 return; 1564 } 1565 1566 let hash = link.dataset.ls_linkto || link.hash; // this.hash needed as fallback. 1567 let linkExists = false; 1568 1569 if ( self.config.localScrollSections && -1 !== self.config.localScrollSections.indexOf( hash ) ) { 1570 linkExists = true; 1571 } else { 1572 linkExists = ( self.isSelectorValid( hash ) ) ? document.querySelector( `[data-ls_id="${hash}"], ${hash}` ) : false; 1573 } 1574 1575 if ( ! linkExists ) { 1576 return; 1577 } 1578 1579 if ( link.closest( '.sfHover' ) ) { 1580 link.parentNode.classList.remove( 'sfHover' ); 1581 } 1582 1583 if ( link.classList.contains( 'wpex-toggle-element-trigger' ) && 'false' === link.getAttribute( 'aria-expanded' ) ) { 1584 return; 1585 } 1586 1587 self.scrollTo( hash ); 1588 1589 if ( link.closest( '.full-screen-overlay-nav-menu .local-scroll > a' ) ) { 1590 return; 1591 } 1592 1593 event.preventDefault(); 1594 event.stopPropagation(); 1595 }; 1596 1597 // Add events. 1598 if ( l10n.localScrollFindLinks ) { 1599 findLinks(); 1600 } 1601 document.addEventListener( 'click', onDocClick ); 1602 }, 1603 1604 /** 1605 * Local Scroll Highlight on scroll. 1606 * 1607 * @todo can this be optimized somehow? Maybe using Observer API perhaps 1608 * by checking if the element is visible on the screen (at least 50%) and excluding the 1609 * self.getLocalScrollOffset() in the calculation as this is whitespace. 1610 */ 1611 localScrollHighlight: function() { 1612 const self = this; 1613 const localScrollSections = self.config.localScrollSections; 1614 1615 if ( ! l10n.localScrollHighlight || ! localScrollSections.length ) { 1616 return; 1617 } 1618 1619 // Highlight active items. 1620 const highlightSection = ( section ) => { 1621 const targetDiv = document.querySelector( `[data-ls_id="${section}"], ${section}` ); 1622 1623 if ( ! targetDiv ) { 1624 return; 1625 } 1626 1627 let highlightSection = false; 1628 const winTop = self.winScrollTop(); 1629 const lsOffset = self.getLocalScrollOffset(); 1630 const divPos = self.offset( targetDiv ).top - lsOffset - 1; 1631 const divHeight = targetDiv.offsetHeight; 1632 const highlightLinks = document.querySelectorAll( `[data-ls_linkto="${section}"] `); 1633 1634 if ( winTop >= divPos && winTop < ( divPos + divHeight ) ) { 1635 highlightSection = true; 1636 } else { 1637 highlightSection = false;
1638 } 1639 1640 if ( highlightSection ) { 1641 targetDiv.classList.add( 'wpex-ls-inview' ); 1642 document.querySelectorAll( '.local-scroll.menu-item' ).forEach( ( element ) => { 1643 element.classList.remove( 'current-menu-item' ); 1644 } ); 1645 } else { 1646 targetDiv.classList.remove( 'wpex-ls-inview' ); 1647 } 1648 1649 highlightLinks.forEach( ( element ) => { 1650 if ( highlightSection ) { 1651 element.classList.add( 'active' ); 1652 } else { 1653 element.classList.remove( 'active' ); 1654 } 1655 const li = element.closest( 'li' ); 1656 if ( li ) { 1657 if ( highlightSection ) { 1658 li.classList.add( 'current-menu-item' ); 1659 } else { 1660 li.classList.remove( 'current-menu-item' ); 1661 } 1662 } 1663 } ); 1664 }; 1665 1666 const highlightSections = () => { 1667 for ( var i=0; i < localScrollSections.length; i++ ) { 1668 highlightSection( localScrollSections[i] ); 1669 } 1670 }; 1671 1672 window.addEventListener( 'scroll', highlightSections, { passive: true } ); 1673 }, 1674 1675 /** 1676 * Equal heights function. 1677 */ 1678 equalHeights: function( context ) { 1679 if ( 'function' !== typeof window.wpexEqualHeights ) { 1680 return; 1681 } 1682 wpexEqualHeights( '.match-height-grid', '.match-height-content', context ); 1683 wpexEqualHeights( '.match-height-row', '.match-height-content', context ); 1684 wpexEqualHeights( '.vc_row', '.equal-height-column', context ); 1685 wpexEqualHeights( '.vc_row', '.equal-height-content', context ); 1686 }, 1687 1688 /** 1689 * Footer Reveal Display on Load. 1690 */ 1691 footerReveal: function() { 1692 const self = this; 1693 const footerReveal = document.querySelector( '#footer-reveal' ); 1694 const wrap = document.querySelector( '#wrap' ); 1695 const main = document.querySelector( '#main' ); 1696 1697 if ( ! footerReveal || ! wrap || ! main ) { 1698 return; 1699 } 1700 1701 const showHide = () => { 1702 if ( self.viewportWidth() < 960 ) { 1703 if ( footerReveal.classList.contains( 'footer-reveal' ) ) { 1704 footerReveal.classList.remove( 'footer-reveal' ); 1705 footerReveal.classList.add( 'footer-reveal-visible' ); 1706 wrap.style.removeProperty( 'margin-bottom' ); 1707 } 1708 return; 1709 } 1710 1711 let heightCheck = 0; 1712 let revealHeight = footerReveal.offsetHeight; 1713 const windowHeight = window.innerHeight; 1714 1715 if ( footerReveal.classList.contains( 'footer-reveal' ) ) { 1716 heightCheck = wrap.offsetHeight + self.getLocalScrollOffset(); 1717 } else { 1718 heightCheck = wrap.offsetHeight + self.getLocalScrollOffset() - revealHeight; 1719 } 1720 1721 // Hide the footer. 1722 if ( ( windowHeight > revealHeight ) && ( heightCheck > windowHeight ) ) { 1723 if ( footerReveal.classList.contains( 'footer-reveal-visible' ) ) { 1724 wrap.style.marginBottom = revealHeight + 'px'; 1725 footerReveal.classList.remove( 'footer-reveal-visible' ); 1726 footerReveal.classList.add( 'footer-reveal' ); 1727 } 1728 } 1729 1730 // Display the footer. 1731 else { 1732 if ( footerReveal.classList.contains( 'footer-reveal' ) ) { 1733 wrap.style.removeProperty( 'margin-bottom' ); 1734 footerReveal.classList.remove( 'footer-reveal' ); 1735 footerReveal.classList.remove( 'wpex-visible' ); 1736 footerReveal.classList.add( 'footer-reveal-visible' ); 1737 } 1738 } 1739 1740 }; 1741 1742 const reveal = () => { 1743 if ( ! footerReveal.classList.contains( 'footer-reveal' ) ) { 1744 return; 1745 } 1746 if ( self.scrolledToBottom( main ) ) { 1747 footerReveal.classList.add( 'wpex-visible' ); 1748 } else { 1749 footerReveal.classList.remove( 'wpex-visible' ); 1750 } 1751 }; 1752 1753 // Fire right away. 1754 showHide(); 1755 reveal(); 1756 1757 // Fire on events. 1758 window.addEventListener( 'scroll', reveal, { passive: true } ); 1759 window.addEventListener( 'resize', showHide ); 1760 }, 1761 1762 /** 1763 * Set min height on main container to prevent issue with extra space below footer. 1764 */ 1765 fixedFooter: function() { 1766 if ( ! document.body.classList.contains( 'wpex-has-fixed-footer' ) ) { 1767 return; 1768 } 1769 1770 const main = document.querySelector( '#main' ); 1771 1772 if ( ! main ) { 1773 return; 1774 } 1775 1776 const run = () => { 1777 main.style.minHeight = ( main.offsetHeight + ( window.innerHeight - document.documentElement.offsetHeight ) ) + 'px'; 1778 }; 1779 1780 // Add Events 1781 run(); 1782 window.addEventListener( 'resize', run ); 1783 }, 1784 1785 /** 1786 * Custom Selects. 1787 */ 1788 customSelects: function( context ) { 1789 if ( ! l10n.customSelects ) { 1790 return; 1791 } 1792 1793 const self = this; 1794 1795 if ( ! context || ! context.childNodes ) { 1796 context = document; 1797 } 1798 1799 context.querySelectorAll( l10n.customSelects ).forEach( ( element ) => { 1800 const parent = element.parentNode; 1801 if ( parent.classList.contains( 'wpex-select-wrap' ) || parent.classList.contains( 'wpex-multiselect-wrap' ) ) { 1802 return; 1803 }
1804 const elementId = element.id; 1805 let addIcon = false; 1806 if ( self.isVisible( element ) ) { // @todo is this really needed? 1807 // Wrap the select 1808 const div = document.createElement( 'div' ); 1809 if ( element.hasAttribute( 'multiple' ) ) { 1810 if ( elementId ) { 1811 div.className = `wpex-multiselect-wrap wpex-multiselect-wrap--${elementId} wpex-${elementId}`; 1812 } else { 1813 div.className = `wpex-multiselect-wrap`; 1814 } 1815 } else { 1816 if ( elementId ) { 1817 div.className = `wpex-select-wrap wpex-select-wrap--${elementId} wpex-${elementId}`; 1818 } else { 1819 div.className = `wpex-select-wrap`; 1820 } 1821 addIcon = true; 1822 } 1823 element = self.wrap( element, div ); 1824 // Add icon to wrapper 1825 if ( addIcon && l10n.selectArrowIcon ) { 1826 const arrow = document.createElement( 'span' ); 1827 arrow.className = 'wpex-select-arrow'; 1828 const icon = document.createDocumentFragment(); 1829 const temp = document.createElement( 'div' ); 1830 temp.innerHTML = l10n.selectArrowIcon; 1831 icon.appendChild(temp.firstChild); 1832 arrow.appendChild( icon ); 1833 div.appendChild( arrow ); 1834 } 1835 } 1836 } ); 1837 }, 1838 1839 /** 1840 * Inline hover styles. 1841 * 1842 * @todo switch to using inline style var() where possible. 1843 */ 1844 hoverStyles: function() { 1845 let oldStyle, 1846 items, 1847 itemsLength, 1848 headCSS = '', 1849 cssObj = {}, 1850 style, 1851 head; 1852 1853 oldStyle = document.querySelector( '.wpex-hover-data' ); // querySelector will return null if nothing is found. 1854 if ( oldStyle ) { 1855 oldStyle.remove(); // prevent dups in VC front end. 1856 } 1857 1858 items = document.querySelectorAll( '[data-wpex-hover]' ); 1859 itemsLength = items.length; 1860 1861 // No need to do anything if we don't have any items to style. 1862 if ( ! itemsLength ) { 1863 return; 1864 } 1865 1866 const parseItem = ( index ) => { 1867 let element, 1868 data, 1869 uniqueClass, 1870 classList, 1871 hoverCSS = '', 1872 target = '', 1873 parent; 1874 1875 element = items[index]; 1876 data = element.dataset.wpexHover; 1877 1878 if ( ! data ) { 1879 return; 1880 } 1881 1882 data = JSON.parse( data ); 1883 1884 // Remove any wpex-dhover-{int} classname that may have been previously added. 1885 // This is a fix for AJAX functions and front-end editor edits. 1886 classList = element.classList; 1887 for ( let i = 0; i < classList.length; i++ ) { 1888 if ( -1 !== classList[i].indexOf( 'wpex-dhover-' ) ) { 1889 element.classList.remove( classList[i] ); 1890 } 1891 } 1892 1893 // New unique classname based on index. 1894 uniqueClass = `wpex-dhover-${index}`; 1895 1896 if ( data.parent ) { 1897 parent = element.closest( data.parent ); 1898 if ( parent ) { 1899 parent.classList.add( uniqueClass + '-p' ); 1900 element.classList.add( uniqueClass ); 1901 target = `.${uniqueClass}-p:hover .${uniqueClass}`; 1902 } 1903 } else { 1904 element.classList.add( uniqueClass ); 1905 target = `.${uniqueClass}:hover`; 1906 } 1907 1908 for ( let key in data ) { 1909 if ( data.hasOwnProperty( key ) ) { 1910 if ( 'target' === key || 'parent' === key ) { 1911 continue; 1912 } 1913 hoverCSS += `${key}:${data[key]}!important;`; 1914 } 1915 } 1916 1917 if ( hoverCSS ) { 1918 if ( hoverCSS in cssObj ) { 1919 cssObj[hoverCSS] = `${cssObj[hoverCSS]},${target}`; 1920 } else { 1921 cssObj[hoverCSS] = target; 1922 } 1923 } 1924 }; 1925 1926 for (let i = 0; i < itemsLength; i++) { 1927 parseItem( i ); 1928 } 1929 1930 if ( cssObj ) { 1931 for ( let css in cssObj ) { 1932 if ( cssObj.hasOwnProperty( css ) ) { 1933 headCSS += `${cssObj[css]}{${css}}`; 1934 } 1935 } 1936 } 1937 1938 if ( headCSS ) { 1939 style = document.createElement( 'style' ); 1940 style.classList.add( 'wpex-hover-data' ); 1941 style.appendChild( document.createTextNode( headCSS ) ); 1942 head = document.head || document.getElementsByTagName( 'head' )[0]; 1943 head.appendChild( style ); 1944 } 1945 }, 1946 1947 /** 1948 * Overlay Mobile Support. 1949 */ 1950 overlaysMobileSupport: function() { 1951 const self = this; 1952 const supportsTouch = (window.matchMedia("(any-pointer: coarse)").matches); 1953 1954 if ( ! supportsTouch ) { 1955 return; 1956 } 1957 1958 let dragging = false; 1959 1960 // Remove overlays completely if mobile support is disabled. 1961 document.querySelectorAll( '.overlay-parent.overlay-hh' ).forEach( ( element ) => { 1962 if ( ! element.classList.contains( 'overlay-ms' ) ) {
1963 const overlay = element.querySelector( '.theme-overlay' ); 1964 if ( overlay ) { 1965 element.parentNode.removeChild( element ); 1966 } 1967 } 1968 } ); 1969 1970 const hideAllOverlays = () => { 1971 document.querySelectorAll( '.wpex-touched' ).forEach( ( element ) => { 1972 element.classList.remove( 'wpex-touched' ); 1973 } ); 1974 }; 1975 1976 const touchEnd = ( event ) => { 1977 const target = event.target; 1978 let overlay = target.closest( '.overlay-parent.overlay-ms.overlay-h,.vcex-image-swap,.wpex-card-touch-support' ); 1979 1980 if ( ! overlay ) { 1981 return; 1982 } 1983 1984 if ( dragging ) { 1985 hideAllOverlays(); 1986 return; 1987 } 1988 1989 if ( overlay.classList.contains( 'wpex-touched' ) ) { 1990 overlay.classList.remove( 'wpex-touched' ); 1991 return; // overlay is open, allow clicking inside. 1992 } 1993 1994 event.preventDefault(); 1995 1996 hideAllOverlays(); 1997 overlay.classList.add( 'wpex-touched' ); 1998 }; 1999 2000 const touchMove = ( event ) => { 2001 const target = event.target; 2002 if ( ! target.closest( '.wpex-touched' ) ) { 2003 hideAllOverlays(); 2004 } 2005 const overlay = target.closest( '.overlay-parent.overlay-ms.overlay-h,.vcex-image-swap,.wpex-card-touch-support' ); 2006 if ( overlay ) { 2007 dragging = true; 2008 } 2009 }; 2010 2011 const touchStart = ( event ) => { 2012 const target = event.target; 2013 if ( ! target.closest( '.wpex-touched' ) ) { 2014 hideAllOverlays(); 2015 } 2016 const overlay = target.closest( '.overlay-parent.overlay-ms.overlay-h,.vcex-image-swap,.wpex-card-touch-support' ); 2017 if ( overlay ) { 2018 dragging = false; 2019 } 2020 }; 2021 2022 // Add events. 2023 document.addEventListener( 'touchend', touchEnd, false ); // must have false event listeners. 2024 document.addEventListener( 'touchmove', touchMove, { passive: true } ); 2025 document.addEventListener( 'touchstart', touchStart, { passive: true } ); 2026 }, 2027 2028 /** 2029 * Adds CSS variables to the body tag based on element heights. 2030 * 2031 * @todo enable this functionality. It's disabled because curently it causes severe lag 2032 * on the animated shrink header. 2033 * 2034 stickyElCssVar: function( el, varName ) { 2035 const setHeight = ( height ) => { 2036 document.body.style.setProperty(varName, `${height}px`); 2037 }; 2038 let prevHeight = 0; 2039 const resize_ob = new ResizeObserver( function( entries ) { 2040 let rect = entries[0].contentRect; 2041 let height = rect.height; 2042 if (typeof height === 'number' && height !== prevHeight) { 2043 prevHeight = height; 2044 setHeight( height ); 2045 } 2046 }); 2047 resize_ob.observe( el ); // @note the observer API will fire on init. 2048 }, 2049 */ 2050 2051 /** 2052 * Sticky Topbar. 2053 */ 2054 stickyTopBar: function() { 2055 const self = this; 2056 let isSticky = false; 2057 let offset = 0; 2058 const stickyTopbar = document.querySelector( '#top-bar-wrap.wpex-top-bar-sticky' ); 2059 const brkPoint = l10n.stickyTopBarBreakPoint; 2060 2061 if ( ! stickyTopbar ) { 2062 return; 2063 } 2064 2065 // self.stickyElCssVar( stickyTopbar, '--wpex-topbar-height' ); 2066 2067 // Add wrapper. 2068 const stickyWrap = document.createElement( 'div' ); 2069 stickyWrap.id = 'top-bar-wrap-sticky-wrapper'; 2070 stickyWrap.className = 'wpex-sticky-top-bar-holder not-sticky wpex-print-hidden'; 2071 2072 self.wrap( stickyTopbar, stickyWrap ); 2073 2074 // Get offset. 2075 const getOffset = () => { 2076 offset = 0; // Reset offset for resize. 2077 2078 document.querySelectorAll( '.wpex-sticky-el-offset,#wpadminbar' ).forEach( ( element ) => { 2079 const elPosition = window.getComputedStyle( element ).position; 2080 if ( self.isBeforeElement( element, stickyWrap ) && self.isVisible( element ) && ( 'fixed' === elPosition || 'sticky' === elPosition ) ) { 2081 offset += element.getBoundingClientRect().height; 2082 } 2083 } ); 2084 2085 return offset; 2086 }; 2087 2088 // Stick the TopBar. 2089 const setSticky = () => { 2090 if ( isSticky ) { 2091 return; 2092 } 2093 2094 // Tweak wrap class. 2095 stickyWrap.style.height = stickyTopbar.getBoundingClientRect().height + 'px'; 2096 stickyWrap.classList.remove( 'not-sticky' ); 2097 stickyWrap.classList.add( 'is-sticky' ); 2098 2099 // Tweak topbar class. 2100 stickyTopbar.classList.remove( 'wpex-z-99' ); 2101 stickyTopbar.classList.add( 'wpex-z-999' ); 2102 2103 // Add CSS to topbar. 2104 stickyTopbar.style.top = getOffset() + 'px'; 2105 stickyTopbar.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2106 stickyTopbar.style.position = 'fixed'; // don't use CSS class to prevent possible custom CSS issues. 2107 2108 // Set sticky to true. 2109 isSticky = true; 2110 }; 2111 2112 // Unstick the TopBar. 2113 const destroySticky = () => { 2114 if ( ! isSticky ) { 2115 return; 2116 } 2117 2118 // Tweak wrap class. 2119 stickyWrap.style.height = ''; 2120 stickyWrap.classList.remove( 'is-sticky' ); 2121 stickyWrap.classList.add( 'not-sticky' ); 2122 2123 // Tweak topbar classes. 2124 stickyTopbar.classList.add( 'wpex-z-99' ); 2125 stickyTopbar.classList.remove( 'wpex-z-999' ); 2126 2127 // Remove topbar css. 2128 stickyTopbar.style.width = ''; 2129 stickyTopbar.style.top = ''; 2130 stickyTopbar.style.position = ''; 2131
2132 // Set sticky to false. 2133 isSticky = false; 2134 }; 2135 2136 // Runs on load and resize. 2137 const initSticky = () => { 2138 if ( ! l10n.hasStickyTopBarMobile && ( self.viewportWidth() < brkPoint ) ) { 2139 stickyWrap.classList.remove( 'wpex-can-sticky' ); 2140 destroySticky(); 2141 return; 2142 } 2143 2144 const windowTop = self.winScrollTop(); 2145 2146 stickyWrap.classList.add( 'wpex-can-sticky' ); 2147 2148 if ( isSticky ) { 2149 2150 // Update sticky wrapper height incase it changed on resize. 2151 stickyWrap.style.height = stickyTopbar.getBoundingClientRect().height + 'px'; 2152 2153 // Update topbar top position and width incase it changed on resize. 2154 stickyTopbar.style.top = getOffset() + 'px'; 2155 stickyTopbar.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2156 2157 } else { 2158 2159 // Set sticky based on original offset. 2160 offset = self.offset( stickyWrap ).top - getOffset(); 2161 2162 // Set or destroy sticky. 2163 if ( 0 !== windowTop && windowTop > offset ) { 2164 setSticky(); 2165 } else { 2166 destroySticky(); 2167 } 2168 2169 } 2170 }; 2171 2172 // On scroll actions for sticky topbar. 2173 const onScroll = () => { 2174 if ( ! stickyWrap || ! stickyWrap.classList.contains( 'wpex-can-sticky' ) ) { 2175 return; 2176 } 2177 2178 const windowTop = self.winScrollTop(); 2179 2180 // Set or destroy sticky based on offset. 2181 if ( ( 0 !== windowTop ) && ( windowTop >= ( self.offset( stickyWrap ).top - getOffset() ) ) ) { 2182 setSticky(); 2183 } else { 2184 destroySticky(); 2185 } 2186 }; 2187 2188 // Add events. 2189 initSticky(); 2190 window.addEventListener( 'scroll', onScroll, { passive: true } ); 2191 window.addEventListener( 'resize', initSticky ); 2192 }, 2193 2194 /** 2195 * Sticky Header Menu. 2196 */ 2197 stickyHeaderMenu: function() { 2198 const self = this; 2199 const stickyNav = document.querySelector( '#site-navigation-wrap.fixed-nav' ); 2200 2201 if ( ! stickyNav ) { 2202 return; 2203 } 2204 2205 // self.stickyElCssVar( stickyNav, '--wpex-header-menu-height' ); 2206 2207 let isSticky = false; 2208 const header = document.querySelector( '#site-header' ); 2209 2210 const stickyWrap = document.createElement( 'div' ); 2211 stickyWrap.id = 'site-navigation-sticky-wrapper'; 2212 stickyWrap.className = 'wpex-sticky-navigation-holder not-sticky'; 2213 2214 self.wrap( stickyNav, stickyWrap ); 2215 2216 const getOffset = () => { 2217 let offset = 0; 2218 2219 document.querySelectorAll( '.wpex-sticky-el-offset,#wpadminbar,.wpex-top-bar-sticky' ).forEach( ( element ) => { 2220 const elPosition = window.getComputedStyle( element ).position; 2221 if ( self.isVisible( element ) && self.isBeforeElement( element, stickyWrap ) && ( 'fixed' === elPosition || 'sticky' === elPosition ) ) { 2222 offset += element.getBoundingClientRect().height; 2223 } 2224 } ); 2225 2226 if ( l10n.addStickyHeaderOffset ) { 2227 offset += l10n.addStickyHeaderOffset; 2228 } 2229 2230 return offset; 2231 }; 2232 2233 // Add offsets. 2234 const stickyWrapTop = self.offset( stickyWrap ).top; 2235 const setStickyPos = stickyWrapTop - getOffset(); 2236 2237 const setSticky = () => { 2238 if ( isSticky ) { 2239 return; 2240 } 2241 2242 // Add wrap class and toggle sticky class. 2243 stickyWrap.style.height = stickyNav.getBoundingClientRect().height + 'px'; 2244 stickyWrap.classList.remove( 'not-sticky' ); 2245 stickyWrap.classList.add( 'is-sticky' ); 2246 2247 // Add CSS to topbar. 2248 stickyNav.style.top = getOffset() + 'px'; 2249 stickyNav.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2250 2251 // Remove header dynamic styles. 2252 if ( header ) { 2253 header.classList.remove( 'dyn-styles' ); 2254 } 2255 2256 // Update shrunk var. 2257 isSticky = true; 2258 }; 2259 2260 const destroySticky = () => { 2261 if ( ! isSticky ) { 2262 return; 2263 } 2264 2265 // Remove sticky wrap height and toggle sticky class. 2266 stickyWrap.style.height = ''; 2267 stickyWrap.classList.remove( 'is-sticky' ); 2268 stickyWrap.classList.add( 'not-sticky' ); 2269 2270 // Remove navbar width. 2271 stickyNav.style.top = ''; 2272 stickyNav.style.width = ''; 2273 2274 // Re-add dynamic header styles. 2275 if ( header ) { 2276 header.classList.add( 'dyn-styles' ); 2277 } 2278 2279 // Update shrunk var. 2280 isSticky = false; 2281 }; 2282 2283 const initResizeSetSticky = () => { 2284 if ( self.viewportWidth() <= l10n.stickyNavbarBreakPoint ) { 2285 destroySticky(); 2286 stickyWrap.classList.remove( 'wpex-can-sticky' ); 2287 return; 2288 } 2289 2290 const windowTop = self.winScrollTop(); 2291 2292 stickyWrap.classList.add( 'wpex-can-sticky' ); 2293 2294 if ( isSticky ) { 2295 // Already sticky, lets update height, width and offsets. 2296 stickyWrap.style.height = stickyNav.getBoundingClientRect().height + 'px'; 2297 stickyNav.style.top = getOffset() + 'px'; 2298 stickyNav.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2299 } else { 2300 if ( windowTop >= setStickyPos && 0 !== windowTop ) { 2301 setSticky(); 2302 } else { 2303 destroySticky(); 2304 } 2305 2306 } 2307 }; 2308 2309 const onScroll = () => { 2310 if ( ! stickyWrap.classList.contains( 'wpex-can-sticky' ) ) { 2311 return; 2312 } 2313 2314 const windowTop = self.winScrollTop(); 2315 2316 // Sticky menu. 2317 if ( 0 !== windowTop && windowTop >= setStickyPos ) { 2318 setSticky(); 2319 } else { 2320 destroySticky(); 2321 } 2322 }; 2323 2324 const onOrientationChange = () => { 2325 destroySticky(); 2326 initResizeSetSticky(); 2327 }; 2328 2329 // Add events. 2330 initResizeSetSticky(); 2331 window.addEventListener( 'scroll', onScroll, { passive: true } ); 2332 window.addEventListener( 'resize', initResizeSetSticky ); 2333 window.addEventListener( 'orientationchange', onOrientationChange ); 2334 }, 2335 2336 /** 2337 * Sticky Header. 2338 */ 2339 stickyHeader: function() { 2340 const self = this; 2341 let stickyStyle = l10n.stickyHeaderStyle; 2342 if ( 'standard' !== stickyStyle && 'shrink' !== stickyStyle && 'shrink_animated' !== stickyStyle ) { 2343 return; 2344 } 2345 2346 const header = document.querySelector( '#site-header.fixed-scroll' ); 2347 2348 if ( ! header ) { 2349 return; 2350 } 2351 2352 // self.stickyElCssVar( header, '--wpex-header-height' ); 2353 2354 const mobileOnly = header.classList.contains( 'fixed-scroll--mobile-only' ); 2355 2356 const startPoint = () => { 2357 var startPosition = l10n.stickyHeaderStartPosition; 2358 if ( startPosition && ! isNaN( startPosition ) ) { 2359 return startPosition; 2360 } 2361 var el = document.querySelector( startPosition ); 2362 if ( el ) { 2363 return this.offset( el ).top; 2364 } 2365 return 0; 2366 }; 2367 2368 let isSticky = false;
2369 let isShrunk = false; 2370 2371 // Add sticky wrap. 2372 const stickyWrap = document.createElement( 'div' ); 2373 stickyWrap.id = 'site-header-sticky-wrapper'; 2374 stickyWrap.className = 'wpex-sticky-header-holder not-sticky wpex-print-hidden'; 2375 2376 self.wrap( header, stickyWrap ); 2377 2378 const getOffset = () => { 2379 let offset = 0; 2380 2381 document.querySelectorAll( '.wpex-sticky-el-offset,.wpex-top-bar-sticky,#wpadminbar' ).forEach( ( element ) => { 2382 const elPosition = window.getComputedStyle( element ).position; 2383 if ( self.isVisible( element ) && self.isBeforeElement( element, stickyWrap ) && ( 'fixed' === elPosition || 'sticky' === elPosition ) ) { 2384 offset += element.getBoundingClientRect().height; 2385 } 2386 } ); 2387 2388 if ( l10n.addStickyHeaderOffset ) { 2389 offset += l10n.addStickyHeaderOffset; 2390 } 2391 2392 return offset; 2393 }; 2394 2395 const getBreakpoint = () => { 2396 let brkPoint = parseInt( l10n.stickyHeaderBreakPoint ); 2397 return mobileOnly ? brkPoint -1 : brkPoint; 2398 }; 2399 2400 const getBreakpointComparisonOperator = () => { 2401 return mobileOnly ? '>' : '<'; 2402 }; 2403 2404 // Define main vars for sticky function. 2405 const brkPoint = getBreakpoint(); 2406 const mobileSupport = l10n.hasStickyMobileHeader; 2407 const customStart = startPoint(); 2408 2409 // Check if we are on mobile size. 2410 const pastBreakPoint = () => { 2411 switch ( getBreakpointComparisonOperator() ) { 2412 case '<': 2413 return ( self.viewportWidth() < brkPoint ); 2414 case '>': 2415 return ( self.viewportWidth() > brkPoint ); 2416 } 2417 }; 2418 2419 let pastHeaderBottomCheck = 0; 2420 const overlayHeader = document.querySelector( '#site-header.overlay-header' ); 2421 if ( overlayHeader && 'absolute' === window.getComputedStyle( overlayHeader ).position ) { 2422 pastHeaderBottomCheck = self.offset( header ).top + header.getBoundingClientRect().height; 2423 } else { 2424 pastHeaderBottomCheck = self.offset( stickyWrap ).top + stickyWrap.getBoundingClientRect().height; 2425 } 2426 2427 // Check if we are past the header. 2428 function pastheader() { 2429 if ( self.winScrollTop() > pastHeaderBottomCheck ) { 2430 return true; 2431 } 2432 return false; 2433 } 2434 2435 // Check start position. 2436 const start_position = () => { 2437 let startPosition = customStart || self.offset( stickyWrap ).top; 2438 return startPosition - getOffset(); 2439 }; 2440 2441 const getShrunkDuration = () => { 2442 let elDuration = window.getComputedStyle( header ).transitionDuration; 2443 let duration = ''; 2444 if ( ! elDuration || '0s' !== elDuration ) { 2445 duration = parseFloat( elDuration ) * ( elDuration.indexOf( 'ms' ) >- 1 ? 1 : 1000 ); 2446 } 2447 2448 if ( ! duration ) { 2449 duration = 300; 2450 } 2451 return duration; 2452 }; 2453 2454 // Transform. 2455 const transformPrepare = () => { 2456 var windowTop = self.winScrollTop(); 2457 if ( isSticky ) { 2458 header.classList.add( 'transform-go' ); // prevent issues when scrolling. 2459 } 2460 if ( windowTop <= 0 ) { 2461 header.classList.remove( 'transform-prepare' ); 2462 } else if ( pastheader() ) { 2463 header.classList.add( 'transform-prepare' ); 2464 } else { 2465 header.classList.remove( 'transform-prepare' ); 2466 } 2467 }; 2468 2469 // Set sticky. 2470 const setSticky = () => { 2471 if ( isSticky ) { 2472 return; 2473 } 2474 2475 // Set wrapper height before toggling sticky classes. 2476 stickyWrap.style.height = header.getBoundingClientRect().height + 'px'; 2477 2478 // Toggle sticky classes. 2479 stickyWrap.classList.remove( 'not-sticky' ); 2480 stickyWrap.classList.add( 'is-sticky' ); 2481 header.classList.remove( 'dyn-styles' ); 2482 2483 // Tweak header styles. 2484 header.style.top = getOffset() + 'px'; 2485 header.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2486 2487 // Add transform go class. 2488 if ( header.classList.contains( 'transform-prepare' ) ) { 2489 header.classList.add( 'transform-go' ); 2490 } 2491 2492 // Set sticky to true. 2493 isSticky = true; 2494 }; 2495 2496 // Shrink/unshrink header. 2497 const shrink = () => { 2498 let check = true; // we already check if it's enabled before running this function. 2499 2500 if ( mobileOnly ) { 2501 check = true; 2502 } else if ( pastBreakPoint() ) { 2503 if ( mobileSupport && l10n.hasStickyMobileHeaderShrink ) { 2504 check = true; 2505 } else { 2506 check = false;
2507 } 2508 } 2509 2510 if ( check && pastheader() ) { 2511 if ( ! isShrunk && isSticky ) { 2512 header.classList.add( 'sticky-header-shrunk' ); 2513 isShrunk = true; 2514 } 2515 } else { 2516 header.classList.remove( 'sticky-header-shrunk' ); 2517 isShrunk = false; 2518 } 2519 }; 2520 2521 // Destroy actions. 2522 const destroyActions = () => { 2523 2524 // Remove sticky wrap height and toggle sticky class. 2525 stickyWrap.classList.remove( 'is-sticky' ); 2526 stickyWrap.classList.add( 'not-sticky' ); 2527 2528 if ( header.classList.contains( 'shrink-sticky-header' ) ) { 2529 let duration = getShrunkDuration(); 2530 setTimeout( function() { 2531 if ( ! isSticky ) { 2532 stickyWrap.style.height = ''; 2533 } 2534 }, duration ); 2535 } else { 2536 stickyWrap.style.height = ''; //@todo remove for shrink as well? 2537 } 2538 2539 // Reset header. 2540 header.classList.add( 'dyn-styles' ); 2541 header.style.width = ''; 2542 header.style.top = ''; 2543 header.classList.remove( 'transform-go' ); 2544 2545 // Set sticky to false. 2546 isSticky = false; 2547 2548 // Make sure shrink header is removed. 2549 header.classList.remove( 'sticky-header-shrunk' ); // Fixes some bugs with really fast scrolling. 2550 isShrunk = false; 2551 2552 // Reset sticky wrapper (causes weird animations with shrink sticky)...@todo enable? 2553 //stickyWrap.style.height = ''; 2554 }; 2555 2556 // Destroy sticky. 2557 function destroySticky() { 2558 if ( ! isSticky ) { 2559 return; 2560 } 2561 2562 if ( customStart ) { 2563 header.classList.remove( 'transform-go' ); 2564 if ( isShrunk ) { 2565 header.classList.remove( 'sticky-header-shrunk' ); 2566 isShrunk = false; 2567 } 2568 } else { 2569 header.classList.remove( 'transform-prepare' ); 2570 } 2571 2572 destroyActions(); 2573 } 2574 2575 // On load check. 2576 const initResizeSetSticky = () => { 2577 let windowTop = self.winScrollTop(); 2578 2579 if ( ( mobileOnly || ! mobileSupport ) && pastBreakPoint() ) { 2580 destroySticky(); 2581 stickyWrap.classList.remove( 'wpex-can-sticky' ); 2582 header.classList.remove( 'transform-prepare' ); 2583 return; 2584 } 2585 2586 //header.classList.add( 'transform-go' ); 2587 stickyWrap.classList.add( 'wpex-can-sticky' ); 2588 2589 if ( isSticky ) { 2590 2591 // Update header height on resize incase it's changed height. 2592 // Can't update shrink as it may cause issues. 2593 if ( ! header.classList.contains( 'shrink-sticky-header' ) ) { 2594 stickyWrap.style.height = header.getBoundingClientRect().height + 'px'; 2595 } 2596 2597 header.style.top = getOffset() + 'px'; 2598 header.style.width = stickyWrap.getBoundingClientRect().width + 'px'; 2599 2600 } else { 2601 2602 if ( 0 !== windowTop && windowTop > start_position() ) { 2603 setSticky(); 2604 } else { 2605 destroySticky(); 2606 } 2607 2608 } 2609 2610 if ( l10n.hasStickyHeaderShrink ) { 2611 shrink(); 2612 } 2613 }; 2614 2615 // On scroll function. 2616 const onScroll = () => { 2617 let windowTop = self.winScrollTop(); 2618 2619 if ( ! stickyWrap.classList.contains( 'wpex-can-sticky' ) ) { 2620 return; 2621 } 2622 2623 // Animate scroll with custom start. 2624 if ( customStart ) { 2625 transformPrepare(); 2626 } 2627 2628 // Set or destroy sticky. 2629 if ( 0 != windowTop && windowTop >= start_position() ) { 2630 setSticky(); 2631 } else { 2632 destroySticky(); 2633 } 2634 2635 // Shrink. 2636 if ( l10n.hasStickyHeaderShrink ) { 2637 shrink(); 2638 } 2639 }; 2640 2641 // Add Events. 2642 initResizeSetSticky(); 2643 window.addEventListener( 'scroll', onScroll, { passive: true } ); 2644 window.addEventListener( 'resize', initResizeSetSticky ); 2645 window.addEventListener( 'orientationchange', () => { 2646 destroySticky(); 2647 initResizeSetSticky(); 2648 } ); 2649 }, 2650 2651 /** 2652 * Custom Sticky elements. 2653 */ 2654 stickyElements: function() { 2655 const bodyClass = document.body.classList; 2656 if ( bodyClass.contains( 'compose-mode' ) || bodyClass.contains( 'wp-admin' ) ) { 2657 return; 2658 } 2659 2660 const stickyElements = document.querySelectorAll( '.wpex-js-sticky' ); 2661 2662 if ( ! stickyElements ) { 2663 return; 2664 } 2665 2666 const self = this; 2667 2668 const addInnerWrap = ( el ) => { 2669 const wrapper = document.createElement( 'div' ); 2670 let wrapClassname = 'wpex-js-sticky-wrap'; 2671 const lsOffset = el.dataset.wpexStickyLsOffset || true; 2672 if ( 'true' == lsOffset || '1' == lsOffset ) { 2673 wrapClassname += ' wpex-ls-offset'; 2674 } 2675 wrapper.className = wrapClassname; 2676 if ( el.nextSibling ) { 2677 el.parentNode.insertBefore( wrapper, el.nextSibling ); 2678 } else { 2679 el.parentNode.appendChild( wrapper ); 2680 } 2681 wrapper.appendChild( el ); 2682 return wrapper; 2683 }; 2684 2685 const addHolder = ( el ) => { 2686 const wrapper = document.createElement( 'div' ); 2687 wrapper.className = 'wpex-js-sticky-holder'; 2688 if ( el.nextSibling ) { 2689 el.parentNode.insertBefore( wrapper, el.nextSibling ); 2690 } else { 2691 el.parentNode.appendChild( wrapper ); 2692 } 2693 wrapper.appendChild( el ); 2694 return wrapper; 2695 }; 2696
2697 stickyElements.forEach( ( stickyEl ) => { 2698 let isSticky = false; 2699 let stickyEndPoint = stickyEl.dataset.wpexStickyEndpoint || null; 2700 const stickyBreakpoint = parseInt( stickyEl.dataset.wpexStickyBreakpoint ); 2701 2702 if ( stickyEndPoint && isNaN( stickyEndPoint ) ) { 2703 stickyEndPoint = document.querySelector( stickyEndPoint ); 2704 } 2705 2706 const stickyWrapper = addInnerWrap( stickyEl ); 2707 const stickyHolder = addHolder( stickyWrapper ); 2708 2709 const getStickyOffset = () => { 2710 let offset = stickyEl.dataset.wpexStickyOffset ? parseInt( stickyEl.dataset.wpexStickyOffset ) : 0; 2711 document.querySelectorAll( '#top-bar-wrap-sticky-wrapper.wpex-can-sticky #top-bar-wrap,#site-header-sticky-wrapper.wpex-can-sticky #site-header,#site-navigation-sticky-wrapper.wpex-can-sticky #site-navigation-wrap,#wpadminbar,.wpex-sticky-el-offset,.wpex-js-sticky-wrap' ).forEach( ( item ) => { 2712 const itemPosition = window.getComputedStyle( item ).position; 2713 if ( ! stickyEl.isEqualNode( item ) && self.isVisible( item ) && self.isBeforeElement( item, stickyEl ) && ( 'fixed' === itemPosition || 'sticky' === itemPosition ) ) { 2714 offset += self.elHeight( item ); 2715 } 2716 } ); 2717 return offset; 2718 }; 2719 2720 const stickyCheck = () => { 2721 if ( stickyBreakpoint && self.viewportWidth() <= stickyBreakpoint ) { 2722 return destroySticky(); 2723 } 2724 2725 const windowTop = self.winScrollTop(); 2726 const stickyOffset = getStickyOffset(); 2727 const stickyWrapTop = self.offset( stickyHolder ).top; 2728 const setStickyPos = stickyWrapTop - stickyOffset; 2729 2730 if ( windowTop > setStickyPos && 0 !== windowTop ) { 2731 setSticky( stickyOffset ); 2732 if ( stickyEndPoint && self.isVisible( stickyEndPoint ) ) { 2733 if ( windowTop > ( self.offset( stickyEndPoint ).top - stickyOffset - self.elHeight( stickyEl ) ) ) { 2734 stickyWrapper.style.position = 'relative'; 2735 } else { 2736 stickyWrapper.style.position = 'fixed'; 2737 } 2738 } 2739 } else { 2740 destroySticky(); 2741 } 2742 }; 2743 2744 const setWidth = () => { 2745 const wrapWidth = stickyHolder.getBoundingClientRect().width; 2746 if ( wrapWidth ) { 2747 stickyWrapper.style.setProperty( 'width', wrapWidth + 'px' ); 2748 } 2749 }; 2750 2751 const setSticky = ( offset ) => { 2752 if ( ! self.isVisible( stickyEl ) ) { 2753 destroySticky(); 2754 return; 2755 } 2756 2757 if ( isSticky ) { 2758 stickyWrapper.style.top = getStickyOffset() + 'px'; // fix for items that may have changed sized like shrink header. 2759 return; 2760 } 2761 2762 stickyHolder.style.height = self.elHeight( stickyEl ) + 'px'; 2763 2764 stickyWrapper.classList.add( 'wpex-z-99' ); 2765 stickyWrapper.style.position = 'fixed'; 2766 stickyWrapper.style.top = offset + 'px'; 2767 2768 stickyEl.classList.add( 'wpex-js-sticky--stuck' ); 2769 2770 setWidth(); 2771 2772 isSticky = true; 2773 }; 2774 2775 // Unsticks element. 2776 const destroySticky = () => { 2777 if ( ! isSticky ) { 2778 return; 2779 } 2780 stickyHolder.style.height = ''; 2781 stickyWrapper.classList.remove( 'wpex-z-99' ); 2782 stickyWrapper.style.position = ''; 2783 stickyWrapper.style.top = ''; 2784 stickyWrapper.style.width = ''; 2785 2786 stickyEl.classList.remove( 'wpex-js-sticky--stuck' ); 2787 2788 isSticky = false; 2789 }; 2790 2791 const onResize = () => { 2792 stickyCheck(); 2793 if ( ! isSticky ) { 2794 return; 2795 } 2796 if ( ! self.isVisible( stickyEl ) ) { 2797 destroySticky(); 2798 } 2799 stickyHolder.style.height = self.elHeight( stickyEl ) + 'px'; 2800 stickyWrapper.style.top = getStickyOffset() + 'px'; 2801 setWidth( stickyEl ); 2802 }; 2803 2804 const onFlip = () => { 2805 destroySticky(); 2806 stickyCheck(); 2807 }; 2808 2809 // Run functions and add event listeners. 2810 stickyCheck(); 2811 window.addEventListener( 'scroll', stickyCheck, { passive: true } ); 2812 window.addEventListener( 'resize', onResize ); 2813 window.addEventListener( 'orientationchange', onFlip ); 2814 } ); 2815 }, 2816 2817 /** 2818 * Accessability fixes/enhancements. 2819 */ 2820 accessibility: function() { 2821
2822 // Add tabindex -1 to nav-no-click links. 2823 document.querySelectorAll( '#site-navigation li.nav-no-click:not(.menu-item-has-children) > a' ).forEach( ( element ) => { 2824 element.setAttribute( 'tabIndex', '-1' ); 2825 } ); 2826 2827 // Add tabindex -1 to megamenu headings that are using "#" for the href. 2828 document.querySelectorAll( '#site-navigation li.megamenu > ul.sub-menu > li.menu-item.menu-item-has-children > a' ).forEach( ( megaMenuHeading ) => { 2829 if ( '#' === megaMenuHeading.getAttribute( 'href' ) ) { 2830 megaMenuHeading.setAttribute( 'tabIndex', '-1' ); 2831 } 2832 } ); 2833 2834 // Allow for opening WPBakery FAQ elements with the enter button. 2835 document.querySelectorAll( '.vc_toggle .vc_toggle_title' ).forEach( ( element ) => { 2836 element.setAttribute( 'tabIndex', 0 ); 2837 element.addEventListener( 'keydown', ( event ) => { 2838 if ( 'Enter' === event.key ) { 2839 event.target.click(); 2840 } 2841 } ); 2842 } ); 2843 2844 }, 2845 2846 /** 2847 * WPBakery tweaks. 2848 */ 2849 wpbakery: function() { 2850 // Disable tab animation. 2851 if ( l10n.disable_vc_tta_animation 2852 && document.querySelector( '.vc_tta' ) 2853 && 'function' === typeof jQuery 2854 && 'function' === typeof jQuery.fn.vcAccordion 2855 ) { 2856 jQuery.fn.vcAccordion.Constructor.prototype.isAnimated = function() { 2857 return 0; 2858 } 2859 } 2860 }, 2861 2862 /* Helpers. 2863 ------------------------------------------------------------------------------ */ 2864 2865 /** 2866 * Is the DOM ready? 2867 */ 2868 domReady: function( fn ) { 2869 if ( typeof fn !== 'function' || 'undefined' === typeof document ) { 2870 return; 2871 } 2872 2873 const readyState = document.readyState; 2874 2875 // If document is already loaded, run method. 2876 if ( readyState === 'interactive' || readyState === 'complete' ) { 2877 return setTimeout( fn ); // Timeout prevents issues with dependencies when using async. 2878 } 2879 2880 // Otherwise, wait until document is loaded. 2881 document.addEventListener( 'DOMContentLoaded', fn, false ); 2882 }, 2883 2884 /** 2885 * Retina Check. 2886 */ 2887 retinaCheck: function() { 2888 const mediaQuery = '(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)'; 2889 if ( window.devicePixelRatio > 1 ) { 2890 return true; 2891 } 2892 if ( window.matchMedia && window.matchMedia( mediaQuery ).matches ) { 2893 return true; 2894 } 2895 return false; 2896 }, 2897 2898 /** 2899 * Mobile Check. 2900 */ 2901 mobileCheck: function() { 2902 if ( /Android|webOS|iPhone|iPad|iPod|iPad Simulator|iPhone Simulator|iPod Simulator|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ) ) { 2903 return true; 2904 } 2905 }, 2906 2907 /** 2908 * Get easing value. 2909 */ 2910 getEasing: function( customEasing ) { 2911 if ( ! l10n.localScrollEasing || 'function' !== typeof jQuery || 'undefined' === typeof jQuery.easing || 'function' !== typeof jQuery.easing.jswing ) { 2912 return; 2913 } 2914 let easing = customEasing || l10n.localScrollEasing; 2915 if ( ! jQuery.easing.hasOwnProperty( easing ) ) { 2916 easing = 'swing'; 2917 } 2918 return easing; 2919 }, 2920 2921 /** 2922 * Viewport width. 2923 */ 2924 viewportWidth: function() { 2925 let a = 'inner'; 2926 let e = window; 2927 if ( ! ( 'innerWidth' in window ) ) { 2928 a = 'client'; 2929 e = document.documentElement || document.body; 2930 } 2931 return e[ a+'Width' ]; 2932 }, 2933 2934 /** 2935 * Check if a given selector is valid. 2936 */ 2937 isSelectorValid: function( selector ) { 2938 const queryCheck = ( s ) => { 2939 document.createDocumentFragment().querySelector( s ); 2940 }; 2941 try { 2942 queryCheck( selector ); 2943 } catch( error ) { 2944 return false; 2945 } 2946 return true; 2947 }, 2948 2949 /** 2950 * SlideUp element. 2951 */ 2952 slideUp: function( target, duration, callback ) { 2953 if ( ! target ) { 2954 return; 2955 } 2956 2957 const display = window.getComputedStyle( target ).display; 2958 2959 // Element already closed. 2960 if ( 'none' === display ) { 2961 return; 2962 } 2963 2964 // Allow for CSS defined transition duration. 2965 const elDuration = window.getComputedStyle( target ).transitionDuration; 2966 2967 if ( ! elDuration || '0s' !== elDuration ) { 2968 duration = parseFloat( elDuration ) * ( elDuration.indexOf( 'ms' ) >- 1 ? 1 : 1000 ); 2969 } 2970 2971 if ( ! duration ) { 2972 duration = 300; 2973 } 2974 2975 // Add classname that we can check to prevent from doing other things during transition. 2976 target.classList.add( 'wpex-transitioning' ); 2977 2978 // Set transition duration for animation. 2979 target.style.transitionProperty = 'height, margin, padding'; 2980 target.style.transitionDuration = duration + 'ms'; 2981 2982 // Set element height. 2983 target.style.height = target.offsetHeight + 'px'; 2984 target.offsetHeight; // get height so that browser re-paints element. 2985 target.style.overflow = 'hidden'; 2986 2987 // Reset element height. 2988 target.style.height = 0; 2989 target.style.paddingTop = 0; 2990 target.style.paddingBottom = 0; 2991 target.style.marginTop = 0; 2992 target.style.marginBottom = 0; 2993 2994 // Remove properties after animation has finished 2995 setTimeout( () => { 2996 target.style.display = 'none'; 2997 target.style.removeProperty( 'height' ); 2998 target.style.removeProperty( 'padding-top' ); 2999 target.style.removeProperty( 'padding-bottom' ); 3000 target.style.removeProperty( 'margin-top' ); 3001 target.style.removeProperty( 'margin-bottom' ); 3002 target.style.removeProperty( 'overflow' ); 3003 target.style.removeProperty( 'transition-duration' ); 3004 target.style.removeProperty( 'transition-property' ); 3005 target.classList.remove( 'wpex-transitioning' ); 3006 if ( callback ) { 3007 callback(); 3008 } 3009 }, duration ); 3010 }, 3011 3012 /** 3013 * SlideDown element. 3014 */ 3015 slideDown: function( target, duration, callback ) { 3016 if ( ! target ) { 3017 return; 3018 } 3019 3020 let display = window.getComputedStyle( target ).display; 3021 3022 // Already open. 3023 if ( 'block' === display ) { 3024 return; 3025 } 3026 3027 // Allow for CSS defined transition duration.
3028 const elDuration = window.getComputedStyle( target ).transitionDuration; 3029 3030 if ( ! elDuration || '0s' !== elDuration ) { 3031 duration = parseFloat( elDuration ) * ( elDuration.indexOf( 'ms' ) >- 1 ? 1 : 1000 ); 3032 } 3033 3034 if ( ! duration ) { 3035 duration = 300; 3036 } 3037 3038 // Add classname that we can check to prevent from doing other things during transition. 3039 target.classList.add( 'wpex-transitioning' ); 3040 3041 // Remove inline display if it had previously been toggled. 3042 target.style.removeProperty( 'display' ); 3043 3044 // Element needs to be visible to calculate height and animate. 3045 if ( 'none' === display ) { 3046 display = 'block'; 3047 } 3048 3049 target.style.display = display; 3050 target.style.transitionProperty = 'none'; // prevent possible animation when calculating height. 3051 3052 const height = target.offsetHeight; 3053 3054 // Reset height so we can animate it. 3055 target.style.overflow = 'hidden'; 3056 target.style.height = 0; 3057 target.style.paddingTop = 0; 3058 target.style.paddingBottom = 0; 3059 target.style.marginTop = 0; 3060 target.style.marginBottom = 0; 3061 target.offsetHeight; // get height so that browser re-paints element. 3062 target.style.boxSizing = 'border-box'; 3063 3064 // Add transition duration for animation. 3065 target.style.transitionProperty = 'height, margin, padding'; 3066 target.style.transitionDuration = duration + 'ms'; 3067 3068 // Set element height using a timeout otherwise animation won't work. 3069 target.style.height = height + 'px'; 3070 target.style.removeProperty( 'padding-top' ); 3071 target.style.removeProperty( 'padding-bottom' ); 3072 target.style.removeProperty( 'margin-top' ); 3073 target.style.removeProperty( 'margin-bottom' ); 3074 3075 // Remove properties after animation has finished. 3076 setTimeout( () => { 3077 target.style.removeProperty( 'height' ); 3078 target.style.removeProperty( 'overflow' ); 3079 target.style.removeProperty( 'transition-duration' ); 3080 target.style.removeProperty( 'transition-property' ); 3081 target.classList.remove( 'wpex-transitioning' ); 3082 if ( callback ) { 3083 callback(); 3084 } 3085 }, duration ); 3086 }, 3087 3088 /** 3089 * Set correct focus states for custom elements. 3090 * 3091 * @param {HTMLElement} el 3092 */ 3093 focusOnElement: function( element, initialFocus ) { 3094 const self = this; 3095 const focusElements = element.querySelectorAll( 'button, [href], input, select, textarea, a,[tabindex]:not([tabindex="-1"])' ); 3096 3097 if ( ! focusElements.length ) { 3098 return; 3099 } 3100 3101 const focus = []; 3102 3103 for ( let i=0; i < focusElements.length; i++ ) { 3104 if ( self.isVisible( focusElements[i] ) ){ 3105 focus.push( focusElements[i] ); 3106 } 3107 } 3108 3109 if ( ! focus.length ) { 3110 return; 3111 } 3112 3113 const firstFocus = focus[0]; 3114 const lastFocus = focus[focus.length - 1]; 3115 3116 // Add initial focus. 3117 if ( initialFocus ) { 3118 initialFocus.focus(); 3119 } else { 3120 firstFocus.focus(); 3121 } 3122 3123 // Redirect last tab to first input. 3124 lastFocus.addEventListener( 'keydown', ( event ) => { 3125 if ( 'Tab' === event.key && ! event.shiftKey ) { 3126 event.preventDefault(); 3127 firstFocus.focus(); 3128 } 3129 } ); 3130 3131 // Redirect first shift+tab to last input. 3132 firstFocus.addEventListener( 'keydown', ( event ) => { 3133 if ( 'Tab' === event.key && event.shiftKey ) { 3134 event.preventDefault(); 3135 lastFocus.focus(); 3136 } 3137 } ); 3138 }, 3139 3140 /** 3141 * Wrap an element. 3142 */ 3143 wrap: function( element, wrapper ) { 3144 if ( ! element.childNodes ) { 3145 element = [element]; 3146 } 3147 if ( element.nextSibling ) { 3148 element.parentNode.insertBefore( wrapper, element.nextSibling ); 3149 } else { 3150 element.parentNode.appendChild( wrapper ); 3151 } 3152 wrapper.appendChild( element ); 3153 }, 3154 3155 /** 3156 * Insert element after another. 3157 */ 3158 insertAfter: function( newNode, referenceNode ) { 3159 referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); 3160 }, 3161 3162 /** 3163 * Get element offset. 3164 */ 3165 offset: function( element ) { 3166 const rect = element.getBoundingClientRect(); 3167 return { 3168 top: rect.top + this.winScrollTop(), 3169 left: rect.left + this.winScrollTop(), 3170 bottom: rect.bottom + this.winScrollTop(), 3171 }; 3172 }, 3173 3174 /** 3175 * Get element height. 3176 */ 3177 elHeight: function( element ) { 3178 return element.getBoundingClientRect().height; 3179 }, 3180 3181 /** 3182 * Check if element is visible. 3183 */ 3184 isVisible: function( element ) { 3185 if ( ! element ) { 3186 return false;
3187 } 3188 return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length ); 3189 }, 3190 3191 /** 3192 * Check if element is empty 3193 */ 3194 isEmpty: function( element ) { 3195 return ! element || '' === element.innerHTML; 3196 }, 3197 3198 /** 3199 * Check if an element is before another one. 3200 */ 3201 isBeforeElement: function( firstEl, secondEl ) { 3202 if ( 'wpadminbar' === firstEl.id ) { 3203 return true; // incase some plugin moved it's position. 3204 } 3205 if ( ! firstEl || ! secondEl ) { 3206 return; 3207 } 3208 const position = firstEl.compareDocumentPosition( secondEl ); 3209 return ( 4 === position ); 3210 }, 3211 3212 /** 3213 * Grabs content and inserts into another element. 3214 */ 3215 insertExtras: function( element, target, method ) { 3216 if ( ! element || ! target ) { 3217 return; 3218 } 3219 switch ( method ) { 3220 case 'append': 3221 target.appendChild( element ); 3222 break; 3223 case 'prepend': 3224 target.insertBefore( element, target.firstChild ); 3225 break; 3226 } 3227 element.classList.remove( 'wpex-hidden' ); 3228 }, 3229 3230 /** 3231 * Returns the window scrollTop position. 3232 */ 3233 winScrollTop: function() { 3234 let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 3235 if ( scrollTop < 0 ) { 3236 scrollTop = 0; // return 0 if negative to prevent issues with elastic scrolling in Safari. 3237 } 3238 return scrollTop; 3239 }, 3240 3241 /** 3242 * Check if window has scrolled to bottom of element. 3243 */ 3244 scrolledToBottom: function( element ) { 3245 return this.winScrollTop() >= element.offsetTop + element.offsetHeight - window.innerHeight; 3246 }, 3247 3248 /** 3249 * Remove class prefix. 3250 */ 3251 removeClassPrefix: function( elements, regex, prefix ) { 3252 elements.forEach( ( element ) => { 3253 const classes = element.classList; 3254 for ( let j=0; j < classes.length; j++ ) { 3255 if ( regex.test( classes[j] ) ) { 3256 const newclass = classes[j].replace( prefix, '' ); 3257 element.classList.replace( classes[j], newclass ); 3258 } 3259 } 3260 } ); 3261 }, 3262 3263 /** 3264 * Creates accordion menu. 3265 */ 3266 menuAccordion: function( menu ) { 3267 const self = this; 3268 3269 if ( ! menu ) { 3270 return; 3271 } 3272 3273 let isAnimating = false; 3274 const isMobileMenu = ( menu.classList.contains( 'mobile-toggle-nav' ) || menu.classList.contains( 'sidr-class-dropdown-menu' ) ); 3275 3276 // Add toggle buttons. 3277 menu.querySelectorAll( '.menu-item-has-children, .sidr-class-menu-item-has-children' ).forEach( ( menuItem ) => { 3278 const link = menuItem.querySelector( 'a' ); 3279 if ( ! link ) { 3280 return; 3281 } 3282 3283 const wrap = link.closest( 'span' ); 3284 const toggleBtn = document.createElement( 'button' ); 3285 3286 if ( isMobileMenu ) { 3287 toggleBtn.className = 'wpex-open-submenu wpex-unstyled-button wpex-flex wpex-items-center wpex-justify-end wpex-absolute wpex-top-0 wpex-right-0 wpex-h-100 wpex-cursor-pointer wpex-opacity-80 wpex-overflow-hidden'; 3288 } else { 3289 toggleBtn.className = 'wpex-open-submenu wpex-unstyled-button wpex-block'; 3290 } 3291 3292 toggleBtn.setAttribute( 'aria-haspopup', 'true' ); 3293 toggleBtn.setAttribute( 'aria-expanded', 'false' ); 3294 toggleBtn.setAttribute( 'role', 'button' ); 3295 toggleBtn.setAttribute( 'aria-label', l10n.i18n.openSubmenu.replace( '%s', link.textContent.trim() ) ); 3296 3297 if ( isMobileMenu ) { 3298 if ( l10n.mobileMenuOpenSubmenuIcon ) { 3299 const temp = document.createElement( 'div' ); 3300 temp.innerHTML = l10n.mobileMenuOpenSubmenuIcon; 3301 toggleBtn.appendChild(temp.firstChild); 3302 } 3303 if ( l10n.mobileMenuOpenSubmenuIconActive ) { 3304 const temp = document.createElement( 'div' ); 3305 temp.innerHTML = l10n.mobileMenuOpenSubmenuIconActive; 3306 toggleBtn.appendChild(temp.firstChild); 3307 } 3308 } else { 3309 const toggleBtnIcon = document.createElement( 'span' ); 3310 toggleBtnIcon.className = 'ticon ticon-angle-down'; 3311 toggleBtnIcon.setAttribute( 'aria-hidden', 'true' ); 3312 toggleBtn.appendChild( toggleBtnIcon ); 3313 } 3314 if ( wrap ) { 3315 wrap.appendChild( toggleBtn ); 3316 } else { 3317 link.appendChild( toggleBtn ); 3318 } 3319 } ); 3320 3321 const closeSubmenu = ( submenu ) => { 3322 const li = submenu.closest( 'li.active' ); 3323 li.classList.remove( 'active' ); 3324 const link = li.querySelector( 'a' ); 3325 const toggle = li.querySelector( '.wpex-open-submenu' ); 3326 toggle.setAttribute( 'aria-expanded', 'false' ); 3327 toggle.setAttribute( 'aria-label', l10n.i18n.openSubmenu.replace( '%s', link.textContent.trim() ) ); 3328 isAnimating = true; 3329 const toggleIcon = toggle.querySelector( '.wpex-open-submenu__icon' ); 3330 if ( toggleIcon ) { 3331 const toggleIconOpen = toggle.querySelector( '.wpex-open-submenu__icon--open' ); 3332 if ( toggleIconOpen ) { 3333 toggleIcon.classList.remove( 'wpex-hidden' ); 3334 toggleIconOpen.classList.add( 'wpex-hidden' ); 3335 } else { 3336 toggleIcon.classList.remove( 'wpex-rotate-180' ); 3337 } 3338 } 3339 self.slideUp( submenu, null, function() { 3340 isAnimating = false;
3341 } ); 3342 }; 3343 3344 const onDocClick = ( event ) => { 3345 const button = event.target.closest( '.wpex-open-submenu' ); 3346 if ( ! button || ! menu.contains( button ) ) { 3347 return; 3348 } 3349 3350 const li = button.closest( 'li' ); 3351 3352 if ( ! li ) { 3353 return; 3354 } 3355 3356 const ul = li.querySelector( 'ul' ); 3357 3358 if ( ! ul ) { 3359 return; // no dropdowns. 3360 } 3361 3362 const link = li.querySelector( 'a' ); 3363 3364 if ( ! link ) { 3365 return; // no link. 3366 } 3367 3368 event.preventDefault(); 3369 event.stopPropagation(); // !!important!! needed since button is inside link. 3370 3371 if ( isAnimating ) { 3372 return; // prevent click spam. 3373 } 3374 3375 // Closing. 3376 if ( li.classList.contains( 'active' ) ) { 3377 3378 // Close child submenus. 3379 li.querySelectorAll( 'li.active > ul' ).forEach( ( submenu ) => { 3380 closeSubmenu( submenu ); 3381 } ); 3382 3383 // Close self. 3384 li.classList.remove( 'active' ); 3385 button.setAttribute( 'aria-expanded', 'false' ); 3386 button.setAttribute( 'aria-label', l10n.i18n.openSubmenu.replace( '%s', link.textContent.trim() ) ); 3387 isAnimating = true; 3388 self.slideUp( ul, null, function() { 3389 isAnimating = false; 3390 } ); 3391 } 3392 3393 // Opening. 3394 else { 3395 button.setAttribute( 'aria-expanded', 'true' ); 3396 button.setAttribute( 'aria-label', l10n.i18n.closeSubmenu.replace( '%s', link.textContent.trim() ) ); 3397 3398 const toggleIcon = button.querySelector( '.wpex-open-submenu__icon' ); 3399 if ( toggleIcon ) { 3400 const toggleIconOpen = button.querySelector( '.wpex-open-submenu__icon--open' ); 3401 if ( toggleIconOpen ) { 3402 toggleIcon.classList.add( 'wpex-hidden' ); 3403 toggleIconOpen.classList.remove( 'wpex-hidden' ); 3404 } else { 3405 toggleIcon.classList.add( 'wpex-rotate-180' ); 3406 } 3407 } 3408 3409 // Close all open submenus that arent parents of this submenu. 3410 menu.querySelectorAll( 'li.active > ul' ).forEach( ( submenu ) => { 3411 if ( ! submenu.contains( ul ) ) { 3412 closeSubmenu( submenu ); 3413 } 3414 } ); 3415 3416 // Open self. 3417 isAnimating = true; 3418 self.slideDown( ul, null, function() { 3419 isAnimating = false; 3420 } ); 3421 li.classList.add( 'active' ); 3422 } 3423 }; 3424 3425 // Add events - @todo instead of adding event to document we should add it to menu? 3426 document.addEventListener( 'click', onDocClick ); 3427 }, 3428 3429 /** 3430 * Set hamburger icon state. 3431 */ 3432 setHamburgerIconState: function( el ) { 3433 if ( ! el ) { 3434 const toggleBtn = document.querySelector( 'a.mobile-menu-toggle, button.mobile-menu-toggle, li.mobile-menu-toggle > a' ); 3435 el = toggleBtn ? toggleBtn.querySelector( '.wpex-hamburger-icon' ) : null; 3436 } 3437 if ( ! el ) { 3438 return; 3439 } 3440 if ( el.classList.contains( 'wpex-hamburger-icon--active' ) ) { 3441 el.classList.remove( 'wpex-hamburger-icon--active' ); 3442 el.classList.add( 'wpex-hamburger-icon--inactive' ); 3443 } else if ( el.classList.contains( 'wpex-hamburger-icon--inactive' ) ) { 3444 el.classList.remove( 'wpex-hamburger-icon--inactive' ); 3445 el.classList.add( 'wpex-hamburger-icon--active' ); 3446 } 3447 }, 3448 3449 /** 3450 * Returns scrollbar width. 3451 */ 3452 getScrollBarWidth: function() { 3453 return window.innerWidth - document.body.clientWidth; 3454 }, 3455 3456 /** 3457 * Returns scrollbar width. 3458 */ 3459 getElTransitionDuration: function( el ) { 3460 let duration = window.getComputedStyle( el ).transitionDuration; 3461 3462 if ( ! duration || '0s' !== duration ) { 3463 duration = parseFloat( duration ) * ( duration.indexOf( 'ms' ) >- 1 ? 1 : 1000 ); 3464 } 3465 3466 return parseFloat( duration ) || 0; 3467 }, 3468 3469 /** 3470 * Set HTML no scroll. 3471 */ 3472 htmlNoScroll: function( action, duration ) { 3473 const html = document.querySelector( 'html' ); 3474 switch ( action ) { 3475 case 'add': 3476 const scrollbarWidth = this.getScrollBarWidth(); 3477 html.style.setProperty( '--html-noscroll-padding', `${scrollbarWidth}px` ); 3478 html.classList.add( 'html-noscroll' ); 3479 break; 3480 case 'remove': 3481 html.classList.remove( 'html-noscroll' ); 3482 html.style.setProperty( '--html-noscroll-padding', '' ); 3483 break; 3484 } 3485 }, 3486 3487 /** 3488 * Debounce function. 3489 */ 3490 debounce: function(callback, wait = 10) { 3491 let timeoutId = null; 3492 return (...args) => { 3493 window.clearTimeout(timeoutId); 3494 timeoutId = window.setTimeout(() => {
3495 callback.apply(null, args); 3496 }, wait); 3497 }; 3498 }, 3499 3500 /** 3501 * Returns scrollTo behavior. 3502 */ 3503 getScrollToBehavior: function() { 3504 return l10n.scrollToBehavior || 'smooth'; 3505 }, 3506 3507 }; 3508 3509 // Start things up 3510 wpex.init(); 3511 3512} ) ( wpex_theme_params );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.