1/** 2 * $us.scroll 3 * 4 * ScrollSpy, Smooth scroll links and hash-based scrolling all-in-one 5 * 6 * no_smooth_scroll - The presence of this class disables smooth scrolling 7 * for the selected element or for all elements inside the block. 8 */ 9! function( $, _undefined ) { 10 "use strict"; 11 12 const _document = document; 13 const _location = location; 14 15 const ceil = Math.ceil; 16 17 window.$ush = window.$ush || {}; 18 window.$us = window.$us || {}; 19 20 function USScroll( opts ) { 21 const self = this; 22 23 const defaultOpts = { 24 25 // Selectors for attach to smooth scroll 26 attachOnInit: [ 27 '.menu-item a[href*="#"]', 28 '.menu-item[href*="#"]', 29 '.post_custom_field a[href*="#"]', 30 '.post_title a[href*="#"]', 31 '.w-ibanner a[href*="#"]', 32 '.vc_custom_heading a[href*="#"]', 33 '.vc_icon_element a[href*="#"]', 34 '.w-comments-title a[href*="#"]', 35 '.w-iconbox a[href*="#"]', 36 '.w-image a[href*="#"]:not([onclick])', 37 '.w-text a[href*="#"]', 38 '.w-toplink', 39 'a.smooth-scroll[href*="#"]', 40 'a.w-btn[href*="#"]:not([onclick]):not(.w-skip-btn)', 41 'a.w-grid-item-anchor[href*="#"]' 42 ].join(), 43 44 // Classname that will be toggled on relevant buttons 45 buttonActiveClass: 'active', 46 47 // Classname that will be toggled on relevant menu items 48 menuItemActiveClass: 'current-menu-item', 49 50 // Classname that will be toggled on relevant menu ancestors 51 menuItemAncestorActiveClass: 'current-menu-ancestor', 52 53 // Duration of scroll animation 54 animationDuration: ( $us.canvasOptions || {} ).scrollDuration || 0, 55 56 // Easing for scroll animation 57 animationEasing: $us.getAnimationName( 'easeInOutExpo' ), 58 59 // End easing for scroll animation 60 endAnimationEasing: $us.getAnimationName( 'easeOutExpo' ) 61 }; 62 63 self.opts = $.extend( {}, defaultOpts, opts || {} ); 64 65 // Hash blocks with targets and activity indicators. 66 self.blocks = {}; 67 68 // Is scrolling to some specific block at the moment? 69 self.isScrolling = false; 70 71 // Boundable events 72 self._events = { 73 onAnchorClick: self.onAnchorClick.bind( self ), 74 onCancel: self.onCancel.bind( self ), 75 onScroll: self.onScroll.bind( self ), 76 onResize: self.onResize.bind( self ) 77 }; 78 79 $us.$window.on( 'resize load', $ush.debounce( self._events.onResize, 1 ) ); 80 $ush.timeout( self._events.onResize, 75 ); 81 82 $us.$window.on( 'scroll.noPreventDefault', self._events.onScroll ); 83 $ush.timeout( self._events.onScroll, 75 ); 84 85 if ( self.opts.attachOnInit ) { 86 self.attach( self.opts.attachOnInit ); 87 } 88 89 // Recount scroll positions on any content changes. 90 $us.$canvas.on( 'contentChange', self._countAllPositions.bind( self ) ); 91 92 // Handling initial document hash 93 if ( _location.hash && _location.hash.indexOf( '#!' ) == - 1 ) { 94 var hash = _location.hash, 95 scrollPlace = ( self.blocks[ hash ] !== _undefined ) 96 ? hash 97 : _undefined; 98 99 if ( scrollPlace === _undefined ) { 100 try { 101 const $target = $( hash ); 102 if ( $target.length != 0 ) { 103 scrollPlace = $target; 104 } 105 } 106 catch ( error ) { 107 // No action is required here since scrollPlace is undefined. 108 } 109 110 } 111 if ( scrollPlace !== _undefined ) { 112 113 // While page loads, its content changes, and we'll keep the proper scroll on each sufficient content 114 // change until the page finishes loading or user scrolls the page manually. 115 var keepScrollPositionTimer = setInterval( () => { 116 self.scrollTo( scrollPlace ); 117 // Additionally, let's check the states to avoid an infinite call. 118 if ( _document.readyState !== 'loading' ) { 119 clearInterval( keepScrollPositionTimer ); 120 } 121 }, 100 ); 122 const clearHashEvents = () => { 123 $us.$window.off( 'load mousewheel.noPreventDefault DOMMouseScroll touchstart.noPreventDefault', clearHashEvents ); 124 // Content size still may change via other script right after page load 125 $ush.timeout( () => { 126 $us.canvas.resize(); 127 self._countAllPositions(); 128 // The size of the content can be changed using another script, so we recount the waypoints. 129 if ( $us.hasOwnProperty( 'waypoints' ) ) {
130 $us.waypoints._countAll(); 131 } 132 self.scrollTo( scrollPlace ); 133 }, 100 ); 134 }; 135 $us.$window.on( 'load mousewheel.noPreventDefault DOMMouseScroll touchstart.noPreventDefault', clearHashEvents ); 136 } 137 } 138 139 // Basic set of options that should be extended by scrollTo methods 140 self.animateOpts = { 141 duration: self.opts.animationDuration, 142 easing: self.opts.animationEasing, 143 start: () => { 144 self.isScrolling = true; 145 }, 146 complete: () => { 147 self.onCancel(); 148 }, 149 } 150 } 151 152 USScroll.prototype = { 153 154 /** 155 * Count hash's target position and store it properly 156 * 157 * @param {String} hash 158 */ 159 _countPosition: function( hash ) { 160 const self = this; 161 var $target = self.blocks[ hash ].$target, 162 offsetTop = $target.offset().top; 163 164 // Get the real height for sticky elements, 165 // since after sticking their height changes 166 if ( $target.hasClass( 'type_sticky' ) ) { 167 var key = 'realTop'; 168 if ( ! $target.hasClass( 'is_sticky' ) ) { 169 $target.removeData( key ); 170 } 171 if ( ! $target.data( key ) ) { 172 $target.data( key, offsetTop ); 173 } 174 offsetTop = $target.data( key ) || offsetTop; 175 } 176 177 // For support Footer Reveal 178 if ( $us.$body.hasClass( 'footer_reveal' ) && $target.closest( 'footer' ).length ) { 179 offsetTop = $us.$body.outerHeight( true ) + ( offsetTop - $us.$window.scrollTop() ); 180 } 181 182 self.blocks[ hash ].top = ceil( offsetTop - $us.canvas.getOffsetTop() ); 183 }, 184 185 /** 186 * Count all targets' positions for proper scrolling 187 */ 188 _countAllPositions: function() { 189 const self = this; 190 for ( const hash in self.blocks ) { 191 if ( self.blocks[ hash ] ) { 192 self._countPosition( hash ); 193 } 194 } 195 }, 196 197 /** 198 * Indicate scroll position by hash 199 * 200 * @param {String} activeHash 201 */ 202 indicatePosition: function( activeHash ) { 203 const self = this; 204 for ( const hash in self.blocks ) { 205 if ( ! self.blocks[ hash ] ) { 206 continue; 207 } 208 const block = self.blocks[ hash ]; 209 210 if ( ! $ush.isUndefined( block.buttons ) ) { 211 block.buttons.toggleClass( self.opts.buttonActiveClass, hash === activeHash ); 212 } 213 if ( ! $ush.isUndefined( block.menuItems ) ) { 214 block.menuItems.toggleClass( self.opts.menuItemActiveClass, hash === activeHash ); 215 } 216 // Removing active class for all Menu Ancestors first. 217 if ( ! $ush.isUndefined( block.menuAncestors ) ) { 218 block.menuAncestors.removeClass( self.opts.menuItemAncestorActiveClass ); 219 } 220 } 221 // Adding active class for activeHash Menu Ancestors after all Menu Ancestors active classes was removed in 222 // previous loop. This way there would be no case when we first added classes for needed Menu Ancestors and 223 // then removed those classes while checking sibling menu item's hash. 224 if ( ! $ush.isUndefined( self.blocks[ activeHash ] ) && ! $ush.isUndefined( self.blocks[ activeHash ].menuAncestors ) ) { 225 self.blocks[ activeHash ].menuAncestors.addClass( self.opts.menuItemAncestorActiveClass ); 226 } 227 }, 228 229 /** 230 * Attach anchors so their targets will be listened for possible scrolls 231 * 232 * @param {String|jQuery} anchors Selector or list of anchors to attach 233 */ 234 attach: function( anchors ) { 235 const self = this; 236 const $anchors = $( anchors ).not( '.no_smooth_scroll' ); 237 238 if ( $anchors.length == 0 ) { 239 return; 240 } 241 242 var // Decode pathname to compare non-latin letters. 243 _pathname = decodeURIComponent( _location.pathname ), 244 // Location pattern to check absolute URLs for current location. 245 patternPathname = new RegExp( '^' + _pathname.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ) + '#' ), 246 // Pattern for getting a record or checking a string for a record. 247 patternPageId = /^\/?(\?page_id=\d+).*?/; 248 249 $anchors.each( ( index, anchor ) => { 250 const $anchor = $( anchor ); 251 252 if ( $anchor.closest( '.no_smooth_scroll' ).length > 0 ) { 253 return; 254 } 255 256 // Link without a host 257 var href = $ush.toString( $anchor.attr( 'href' ) ).replace( _location.origin, '' ), 258 hash = $anchor.prop( 'hash' ), 259 hasProtocol = /^(https?:\/\/)/.test( href ), 260 hasPageId = patternPageId.test( href ); 261 262 if ( 263 // Ignoring ajax links 264 hash.indexOf( '#!' ) > -1 265 // Case when there is no hash-tag 266 || href.indexOf( '#' ) < 0 267 // Case where /#hash-tag is used (Allowed on homepage) 268 || ( href.substr( 0, 2 ) == '/#' && _location.search && _pathname == '/' ) 269 // Сase when the domain name of a third-party resource 270 || ( hasProtocol && href.indexOf( _location.origin ) !== 0 ) 271 // Case when the search of another page example: /?page_id={id}#hash-tag 272 || ( hasPageId && href.indexOf( ( _location.search.match( patternPageId ) || [] )[1] ) == -1 ) 273 // Case when the pathname of another page example: /postname/#hash-tag 274 || ( href.charAt( 0 ) == '/' && ! hasPageId && ! patternPathname.test( href ) ) 275 ) { 276 return; 277 } 278 279 // Do we have an actual target, for which we'll need to count geometry? 280 if ( hash != '' && hash != '#' ) { 281 // Attach target 282 if ( self.blocks[ hash ] === _undefined ) { 283 // NOTE: Page may contain multiple identifiers if âHide onâ is used. #5449 284 var $target = $( `[id="${hash.slice(1)}"]:visible` ), $originalTarget, type = ''; 285 // Don't attach anchors that actually have no target 286 if ( $target.length == 0 ) { 287 return; 288 } 289 290 // If it's the only row in a section, than use section instead. 291 if ( 292 $target.hasClass( 'g-cols' ) 293 && $target.hasClass( 'vc-row' ) 294 && $target.parent().children().length == 1 295 ) { 296 $target = $target.closest( '.l-section' ); 297 } 298 // If it's a tabs or tour item, then use it's tabs container 299 if ( $target.hasClass( 'w-tabs-section' ) ) { 300 const $newTarget = $target.closest( '.w-tabs' );
301 if ( ! $newTarget.hasClass( 'accordion' ) ) { 302 $originalTarget = $target; 303 $target = $newTarget; 304 } 305 type = 'tab-section'; 306 307 } else if ( $target.hasClass( 'w-tabs' ) ) { 308 type = 'tabs'; 309 } 310 self.blocks[ hash ] = { 311 type: type, $target: $target, $originalTarget: $originalTarget 312 }; 313 self._countPosition( hash ); 314 } 315 // Attach activity indicator 316 if ( $anchor.parent().length > 0 && $anchor.parent().hasClass( 'menu-item' ) ) { 317 var $menuIndicator = $anchor.closest( '.menu-item' ); 318 self.blocks[ hash ].menuItems = ( self.blocks[ hash ].menuItems || $() ).add( $menuIndicator ); 319 var $menuAncestors = $menuIndicator.parents( '.menu-item-has-children' ); 320 if ( $menuAncestors.length > 0 ) { 321 self.blocks[ hash ].menuAncestors = ( self.blocks[ hash ].menuAncestors || $() ).add( $menuAncestors ); 322 } 323 } else { 324 self.blocks[ hash ].buttons = ( self.blocks[ hash ].buttons || $() ).add( $anchor ); 325 } 326 } 327 328 $anchor.on( 'click', self._events.onAnchorClick ); 329 } ); 330 }, 331 332 /** 333 * Get the place position 334 * 335 * @param {*} place 336 * @return {{}} 337 */ 338 getPlacePosition: function( place ) { 339 const self = this; 340 const data = { newY: 0, type: '' }; 341 // Scroll to top 342 if ( place === '' || place === '#' ) { 343 data.newY = 0; 344 data.type = 'top'; 345 } 346 // Scroll by hash 347 else if ( self.blocks[ place ] !== _undefined ) { 348 // Position recalculation 349 self._countPosition( place ); 350 data.newY = self.blocks[ place ].top; 351 data.type = 'hash'; 352 place = self.blocks[ place ].$target; 353 354 // JQuery object handler 355 } else if ( place instanceof $ ) { 356 if ( place.hasClass( 'w-tabs-section' ) ) { 357 var newPlace = place.closest( '.w-tabs' ); 358 if ( ! newPlace.hasClass( 'accordion' ) ) { 359 place = newPlace; 360 } 361 } 362 // Get the Y position, taking into account the height of the header, adminbar and sticky elements. 363 data.newY = place.offset().top; 364 data.type = 'element'; 365 } else { 366 // Get the Y position, taking into account the height of the header, adminbar and sticky elements. 367 data.newY = place; 368 } 369 370 // If the page has a sticky section, then consider the height of the sticky section. 371 if ( 372 $us.canvas.isStickySection() 373 && $us.canvas.hasPositionStickySections() 374 && ! $( place ).hasClass( 'type_sticky' ) 375 && $us.canvas.isAfterStickySection( place ) 376 ) { 377 data.newY -= $us.canvas.getStickySectionHeight(); 378 } 379 380 return data; 381 }, 382 383 /** 384 * Scroll page to a certain position or hash 385 * 386 * @param {Number|String|jQuery} place 387 * @param {Boolean} animate Enables or disables scroll animation 388 */ 389 scrollTo: function( place, animate ) { 390 const self = this; 391 var $place = $( place ); 392 if ( $place.closest( '.w-popup-wrap' ).length ) { 393 self.scrollToPopupContent( place ); 394 return true; 395 } 396 397 var offset = self.getPlacePosition( place ), 398 indicateActive = () => { 399 if ( offset.type === 'hash' ) { 400 self.indicatePosition( place ); 401 } else { 402 self.onScroll(); 403 } 404 }; 405 406 if ( animate ) { 407 // Fix for iPads since scrollTop returns 0 all the time 408 if ( navigator.userAgent.match( /iPad/i ) != null && $( '.us_iframe' ).length && offset.type == 'hash' ) { 409 $place[0].scrollIntoView( { behavior: "smooth", block: "start" } ); 410 } 411 412 var scrollTop = $us.$window.scrollTop(), 413 // Determining the direction of scrolling - up or down 414 scrollDirections = scrollTop < offset.newY 415 ? 'down' 416 : 'up'; 417 418 if ( scrollTop === offset.newY ) { 419 return; 420 } 421 422 // Animate options 423 const animateOpts = $.extend( 424 {}, 425 self.animateOpts, 426 { 427 always: () => { 428 self.isScrolling = false; 429 indicateActive(); 430 } 431 } 432 ); 433 434 /** 435 * Get and applying new values during animation 436 * 437 * @param number now 438 * @param object fx 439 */ 440 animateOpts.step = ( now, fx ) => { 441 // Checking the position of the element, since the position may change if the leading elements 442 // were loaded with a lazy load 443 var newY = self.getPlacePosition( place ).newY; 444 // Since the header at the moment of scrolling the scroll can change the height,
445 // we will correct the position of the element 446 if ( $us.header.isHorizontal() && $us.header.stickyEnabled() ) { 447 newY -= $us.header.getCurrentHeight(); 448 } 449 450 // Since elements can change height, we update the endpoint 451 // of each integration thanks to object references. 452 fx.end = newY; 453 }; 454 455 // If the place has a css animation, then run it so that it executes and 456 // after that it is possible to get the correct endpoint 457 if ( $place.hasClass( 'us_animate_this' ) ) { 458 $place.trigger( 'us_startAnimate' ); 459 } 460 461 // Start animation 462 $us.$htmlBody 463 .stop( true, false ) 464 .animate( { scrollTop: offset.newY + 'px' }, animateOpts ); 465 466 // Allow user to stop scrolling manually 467 $us.$window 468 .on( 'keydown mousewheel.noPreventDefault DOMMouseScroll touchstart.noPreventDefault', self._events.onCancel ); 469 470 } else { 471 472 // If scrolling without animation, then we get the height of the header and change the position. 473 if ( $us.header.stickyEnabled() && $us.header.isHorizontal() ) { 474 offset.newY -= $us.header.getCurrentHeight( /* adminBar */true ); 475 } 476 477 // Stop all animations and scroll to the set position 478 $us.$htmlBody 479 .stop( true, false ) 480 .scrollTop( offset.newY ); 481 482 indicateActive(); 483 } 484 }, 485 486 /** 487 * Scroll popup content to a specific hash 488 * 489 * @param {String} place 490 */ 491 scrollToPopupContent: function( place ) { 492 const self = this; 493 const node = _document.getElementById( place.replace( '#', '' ) ); 494 495 // Animate options 496 const animateOpts = $.extend( 497 {}, 498 self.animateOpts, 499 { 500 always: () => { 501 self.isScrolling = false; 502 }, 503 } 504 ); 505 506 $( node ).closest( '.w-popup-wrap' ) 507 .stop( true, false ) 508 .animate( { scrollTop: node.offsetTop + 'px' }, animateOpts ); 509 510 $us.$window 511 .on( 'keydown mousewheel.noPreventDefault DOMMouseScroll touchstart.noPreventDefault', self._events.onCancel ); 512 }, 513 514 /** 515 * Called on anchor click. 516 * 517 * @param {Event} e 518 * @return {Boolean} 519 */ 520 onAnchorClick: function( e ) { 521 e.preventDefault(); 522 523 const self = this; 524 const $anchor = $( e.currentTarget ); 525 const hash = $anchor.prop( 'hash' ); 526 527 // Prevent scroll on mobile menu items that should show child sub-items on click by label 528 if ( 529 $anchor.hasClass( 'w-nav-anchor' ) 530 && $anchor.closest( '.menu-item' ).hasClass( 'menu-item-has-children' ) 531 && $anchor.closest( '.w-nav' ).hasClass( 'type_mobile' ) 532 ) { 533 var menuOptions = $anchor.closest( '.w-nav' ).find( '.w-nav-options:first' )[0].onclick() || {}, 534 dropByLabel = $anchor.parents( '.menu-item' ).hasClass( 'mobile-drop-by_label' ), 535 dropByArrow = $anchor.parents( '.menu-item' ).hasClass( 'mobile-drop-by_arrow' ); 536 if ( dropByLabel || ( menuOptions.mobileBehavior && ! dropByArrow ) ) { 537 return false; 538 } 539 } 540 541 // Prevent scrolling if the URL is just a hash inside the popup 542 if ( 543 $anchor.attr( 'href' ) === '#' 544 && $anchor.closest( '.w-popup-wrap' ).length 545 ) { 546 return false; 547 } 548 549 self.scrollTo( hash, true ); 550 551 // Keep focus on the page. 552 if ( hash === '#page-top' ) { 553 const active = document.activeElement; 554 if ( active && typeof active.blur === 'function' ) { 555 active.blur(); 556 } 557 if ( ! self.focusHolder ) { 558 self.focusHolder = document.createElement( 'div' ); 559 self.focusHolder.tabIndex = -1; 560 self.focusHolder.classList.add( 'focus-holder' ); 561 self.focusHolder.setAttribute( 'aria-hidden', 'true' ); 562 document.body.appendChild( self.focusHolder ); 563 } 564 requestAnimationFrame( () => { 565 self.focusHolder.focus( { preventScroll: true } ); 566 } ); 567 return; 568 } 569 570 self.indicatePosition( hash ); 571 572 if ( typeof self.blocks[ hash ] !== 'undefined' ) { 573 var block = self.blocks[ hash ]; 574 // When scrolling to an element, check for the presence of tabs, and if necessary, open the 575 // first section 576 if ( [ 'tabs', 'tab-section' ].includes( block.type ) ) { 577 var $linkedSection = $( `.w-tabs-section[id="${hash.substr(1)}"]`, block.$target ); 578 if ( block.type === 'tabs' ) { 579 // Selects the first section 580 $linkedSection = $( '.w-tabs-section:first', block.$target ); 581 } else if ( block.$target.hasClass( 'w-tabs-section' ) ) { 582 // The selected section 583 $linkedSection = block.$target; 584 } 585 if ( $linkedSection.length ) { 586 // Trigger a click event to open the first section. 587 $( '.w-tabs-section-header', $linkedSection ).trigger( 'click' ); 588 } 589 } else if ( 590 block.menuItems !== _undefined 591 && $us.currentStateIs( ['mobiles', 'tablets'] ) 592 && $us.$body.hasClass( 'header-show' ) 593 ) { 594 $us.$body.removeClass( 'header-show' ); 595 } 596 } 597 }, 598 599 /** 600 * Cancel scroll 601 */ 602 onCancel: function() { 603 $us.$htmlBody.stop( true, false ); 604 $us.$window.off( 'keydown mousewheel.noPreventDefault DOMMouseScroll touchstart', this._events.onCancel ); 605 this.isScrolling = false; 606 }, 607 608 /** 609 * Scroll handler 610 */ 611 onScroll: function() { 612 const self = this; 613 if ( self.isScrolling ) { 614 return; 615 } 616 617 var scrollTop = ceil( $us.header.getScrollTop() ), 618 activeHash; 619 620 // Fix the negative scroller issue in Safari. 621 scrollTop = ( scrollTop >= 0 ) 622 ? scrollTop 623 : 0; 624 625 for ( const hash in self.blocks ) { 626 const block = self.blocks[ hash ]; 627 if ( ! block || activeHash || $ush.isNodeInViewport( block ) ) { 628 continue; 629 } 630 var top = block.top; 631 if ( ! $us.header.isHorizontal() ) { 632 // The with a vertical header, subtract only the height of the admin bar, if any. 633 top -= $us.canvas.getOffsetTop(); 634 } else { 635 // Since the header at the moment of scrolling the scroll can change the height,
636 // we will correct the position of the element 637 if ( $us.header.stickyEnabled() ) { 638 top -= $us.header.getCurrentHeight( /* adminBar */true ); 639 } 640 // If the page has a sticky section, then consider the height of the sticky section 641 if ( $us.canvas.hasStickySection() ) { 642 top -= $us.canvas.getStickySectionHeight(); 643 } 644 } 645 top = $ush.parseInt( top.toFixed(0) ); 646 if ( scrollTop >= top && scrollTop <= ( /* block bottom */top + block.$target.outerHeight( false ) ) ) { 647 activeHash = hash; 648 } 649 // For tabs, select only the active section 650 if ( activeHash && block.type === 'tab-section' && block.$originalTarget.is( ':hidden' ) ) { 651 activeHash = _undefined; 652 } 653 } 654 655 $ush.debounce_fn_1ms( self.indicatePosition.bind( self, activeHash ) ); 656 }, 657 658 /** 659 * Resize handler 660 */ 661 onResize: function() { 662 const self = this; 663 // Delay the resize event to prevent glitches 664 $ush.timeout( () => { 665 self._countAllPositions(); 666 self.onScroll(); 667 }, 150 ); 668 self._countAllPositions(); 669 self.onScroll(); 670 } 671 }; 672 673 $( () => $us.scroll = new USScroll( $us.scrollOptions || {} ) ); 674 675}( jQuery );
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.