1/** 2 * UpSolution Element: Tabs 3 * 4 * @requires $us.canvas 5 */ 6! function( $, _undefined ) { 7 "use strict"; 8 9 $us.WTabs = function( container, options ) { 10 const self = this; 11 12 const _defaults = { 13 easing: 'cubic-bezier(.78,.13,.15,.86)', 14 duration: 300 15 }; 16 17 self.options = $.extend( {}, _defaults, options ); 18 self.isRtl = $( '.l-body' ).hasClass( 'rtl' ); 19 20 // Elements 21 self.$container = $( container ); 22 self.$tabsList = $( '> .w-tabs-list:first', self.$container ); 23 self.$tabs = $( '.w-tabs-item', self.$tabsList ); 24 self.$sectionsWrapper = $( '> .w-tabs-sections:first', self.$container ); 25 self.$sections = $( '> .w-tabs-section', self.$sectionsWrapper ); 26 self.$headers = self.$sections.children( '.w-tabs-section-header' ); 27 self.$contents = self.$sections.children( '.w-tabs-section-content' ); 28 self.$tabsBar = $(); 29 30 // Overriding specific to Web Accessibility, it is not allowed to have several identical id and 31 // aria-content, aria-control. http://web-accessibility.carnegiemuseums.org/code/accordions/ 32 if ( self.$container.hasClass( 'accordion' ) ) { 33 self.$tabs = self.$headers; 34 } 35 36 // Private "Variables" 37 self.accordionAtWidth = self.$container.data( 'accordion-at-width' ); 38 self.align = self.$tabsList.usMod( 'align' ); 39 self.count = self.$tabs.length; 40 self.hasScrolling = self.$container.hasClass( 'has_scrolling' ) || false; 41 self.isAccordionAtWidth = $ush.parseInt( self.accordionAtWidth ) !== 0; 42 self.isScrolling = false; 43 self.isTogglable = self.$container.usMod( 'type' ) === 'togglable'; 44 self.minWidth = 0; // container width at which we should switch to accordion layout. 45 self.tabHeights = []; 46 self.tabLefts = []; 47 self.tabTops = []; 48 self.tabWidths = []; 49 self.width = 0; 50 51 // If there are no tabs, abort further execution. 52 if ( self.count === 0 ) { 53 return; 54 } 55 56 // Basic layout 57 if ( self.$container.hasClass( 'accordion' ) ) { 58 self.basicLayout = 'accordion'; 59 60 } else { 61 self.basicLayout = self.$container.usMod( 'layout' ) || 'hor'; 62 } 63 64 // Current active layout (may be switched to 'accordion'). 65 self.curLayout = self.basicLayout; 66 67 // Array of active tabs indexes. 68 self.active = []; 69 self.activeOnInit = []; 70 self.definedActive = []; 71 72 // Preparing arrays of jQuery objects for easier manipulating in future. 73 self.tabs = $.map( self.$tabs.toArray(), $ ); 74 self.sections = $.map( self.$sections.toArray(), $ ); 75 self.headers = $.map( self.$headers.toArray(), $ ); 76 self.contents = $.map( self.$contents.toArray(), $ ); 77 78 // Do nothing it there are no sections. 79 if ( ! self.sections.length ) { 80 return; 81 } 82 83 $.each( self.tabs, ( index ) => { 84 85 if ( self.sections[ index ].hasClass( 'content-empty' ) ) { 86 self.tabs[ index ].hide(); 87 self.sections[ index ].hide(); 88 } 89 90 if ( self.tabs[ index ].hasClass( 'active' ) ) { 91 self.active.push( index ); 92 self.activeOnInit.push( index ); 93 } 94 95 if ( self.tabs[ index ].hasClass( 'defined-active' ) ) { 96 self.definedActive.push( index ); 97 } 98 99 self.tabs[ index ] 100 .add( self.headers[ index ] ) 101 .on( 'click mouseover', ( e ) => { 102 103 var $link = self.tabs[ index ]; 104 if ( ! $link.is( 'a' ) ) { 105 $link = $( 'a', $link ); 106 } 107 108 if ( $link.length ) { 109 if ( e.type == 'click' && ! $ush.toString( $link.attr( 'href' ) ) ) { 110 e.preventDefault(); 111 } 112 } else { 113 e.preventDefault(); 114 } 115 116 if ( 117 e.type == 'mouseover' 118 && ( 119 self.$container.hasClass( 'accordion' ) 120 || ! self.$container.hasClass( 'switch_hover' ) 121 ) 122 ) { 123 return; 124 } 125 126 // Toggling accordion sections. 127 if ( self.curLayout === 'accordion' && self.isTogglable ) { 128 self.toggleSection( index ); // cannot toggle the only active item. 129 130 // Setup active tab 131 } else { 132 133 if ( index != self.active[0] ) { 134 self.headerClicked = true; 135 self.openSection( index ); 136 137 } else if ( self.curLayout === 'accordion' ) { 138 139 self.contents[ index ] 140 .css( 'display', 'block' ) 141 .slideUp( self.options.duration, self._events.contentChanged ); 142 self.tabs[ index ] 143 .attr( 'aria-expanded', 'true' ) 144 .removeClass( 'active' ); 145 self.sections[ index ] 146 .removeClass( 'active' ); 147 self.active[0] = _undefined; 148 } 149 } 150 } ); 151 } ); 152 153 // Bindable events 154 self._events = { 155 resize: self.resize.bind( self ), 156 hashchange: self.hashchange.bind( self ), 157 contentChanged: function() { 158 $.each( self.tabs, ( _, item ) => { 159 var $content = $( item ); 160 $content.attr( 'aria-expanded', $content.hasClass( 'active' ) ); 161 } ) 162 $us.$canvas.trigger( 'contentChange', { elm: self.$container } ); 163 }, 164 wheel: function() { 165 if ( self.isScrolling ) { 166 $us.$htmlBody.stop( true, false ); // stop animation when scrolling wheel 167 } 168 } 169 }; 170 171 // Starting everything. 172 self.switchLayout( self.curLayout ); 173 174 $us.$window 175 .on( 'resize', $ush.debounce( self._events.resize, 5 ) )
176 .on( 'hashchange', self._events.hashchange ) 177 .on( 'wheel.noPreventDefault', $ush.debounce( self._events.wheel.bind( self ), 5 ) ); 178 179 $us.$document.ready( () => { 180 self.resize(); 181 $ush.timeout( self._events.resize, 50 ); 182 $ush.timeout( () => { 183 // TODO: move to a class function for code reading improvement. 184 // Open tab on page load by hash. 185 if ( window.location.hash ) { 186 var hash = window.location.hash.substr( 1 ), 187 $linkedSection = $( `.w-tabs-section[id="${hash}"]`, self.$sectionsWrapper ); 188 if ( $linkedSection.length && ! $linkedSection.hasClass( 'active' ) ) { 189 $( '.w-tabs-section-header', $linkedSection ).trigger( 'click' ); 190 } 191 } 192 }, 150 ); 193 } ); 194 195 // Support for external links to tabs. 196 $.each( self.tabs, ( index ) => { 197 if ( self.headers.length && self.headers[ index ].attr( 'href' ) != _undefined ) { 198 var tabHref = self.headers[ index ].attr( 'href' ), 199 tabHeader = self.headers[ index ]; 200 $( `a[href="${tabHref}"]`, self.$container ).on( 'click', function( e ) { 201 e.preventDefault(); 202 if ( $( this ).hasClass( 'w-tabs-section-header', 'w-tabs-item' ) ) { 203 return; 204 } 205 if ( ! $( tabHeader ).parent( '.w-tabs-section' ).hasClass( 'active' ) ) { 206 tabHeader.trigger( 'click' ); 207 } 208 } ); 209 } 210 } ); 211 212 self.$container.addClass( 'initialized' ); 213 214 // Gets the height of the header after animation. 215 self.headerHeight = 0; 216 $us.header.on( 'transitionEnd', ( header ) => { 217 self.headerHeight = header.getCurrentHeight( /* adminBar */true ); 218 } ); 219 220 if ( $us.usbPreview() ) { 221 222 const usbContentChange = () => { 223 if ( ! self.isTrendy() || self.curLayout == 'accordion' ) { 224 return; 225 } 226 self.measure(); 227 self.setBarPosition( self.active[0] || 0 ); // set bar position for certain element index and current layout 228 }; 229 230 self.$container.on( 'usb.contentChange', $ush.debounce( usbContentChange, 1 ) ); 231 } 232 }; 233 234 $us.WTabs.prototype = { 235 236 /** 237 * Determines if trendy style (Material style) 238 * 239 * @return {boolean} True if trendy, False otherwise 240 */ 241 isTrendy: function() { 242 return this.$container.hasClass( 'style_trendy' ); 243 }, 244 245 hashchange: function() { 246 if ( window.location.hash ) { 247 var hash = window.location.hash.substr( 1 ), 248 $linkedSection = $( `.w-tabs-section[id="${hash}"]`, this.$sectionsWrapper ); 249 if ( $linkedSection.length && ! $linkedSection.hasClass( 'active' ) ) { 250 $( '.w-tabs-section-header', $linkedSection ).click(); 251 } 252 } 253 }, 254 255 switchLayout: function( to ) { 256 this.cleanUpLayout( this.curLayout ); 257 this.prepareLayout( to ); 258 this.curLayout = to; 259 }, 260 261 /** 262 * Clean up layout's special inline styles and/or dom elements. 263 * 264 * @param from 265 */ 266 cleanUpLayout: function( from ) { 267 this.$sections.resetInlineCSS( 'display' ); 268 269 if ( from === 'accordion' ) { 270 this.$container.removeClass( 'accordion' ); 271 this.$contents.resetInlineCSS( 'height', 'padding-top', 'padding-bottom', 'display', 'opacity' ); 272 } 273 274 if ( this.isTrendy() && [ 'hor', 'ver' ].includes( from ) ) { 275 this.$tabsBar.remove(); 276 } 277 }, 278 279 /** 280 * Apply layout's special inline styles and/or dom elements. 281 * 282 * @param to 283 */ 284 prepareLayout: function( to ) { 285 if ( to !== 'accordion' && this.active[0] === _undefined ) { 286 this.active[0] = this.activeOnInit[0]; 287 if ( this.active[ 0 ] !== _undefined ) { 288 this.tabs[ this.active[0] ] 289 .addClass( 'active' ); 290 this.sections[ this.active[0] ] 291 .addClass( 'active' ); 292 } 293 } 294 295 if ( to === 'accordion' ) { 296 this.$container.addClass( 'accordion' ); 297 this.$contents.hide(); 298 if ( this.curLayout !== 'accordion' && this.active[0] !== _undefined && this.active[0] !== this.definedActive[0] ) { 299 this.headers[ this.active[0] ] 300 .removeClass( 'active' ); 301 this.tabs[ this.active[0] ] 302 .removeClass( 'active' ); 303 this.sections[ this.active[0] ] 304 .removeClass( 'active' ); 305 this.active[0] = this.definedActive[0];
306 307 } 308 for ( var i = 0; i < this.active.length; i ++ ) { 309 if ( this.contents[ this.active[ i ] ] !== _undefined ) { 310 this.tabs[ this.active[ i ] ] 311 .attr( 'aria-expanded', 'true' ) 312 .addClass( 'active' ); 313 this.sections[ this.active[ i ] ] 314 .addClass( 'active' ); 315 this.contents[ this.active[ i ] ] 316 .show(); 317 } 318 } 319 320 } else if ( to === 'ver' ) { 321 this.$contents.hide(); 322 this.contents[ this.active[0] ] 323 .show(); 324 } 325 326 if ( this.isTrendy() && 'hor|ver'.indexOf( this.curLayout ) > - 1 ) { 327 this.$tabsBar = $( '<div class="w-tabs-list-bar"></div>' ) 328 .appendTo( this.$tabsList ); 329 } 330 }, 331 332 /** 333 * Measure needed sizes. 334 */ 335 measure: function() { 336 if ( this.basicLayout === 'ver' ) { 337 // Get the specified minimum width or determine automatically 338 if ( this.isAccordionAtWidth ) { 339 this.minWidth = this.accordionAtWidth; 340 } else { 341 var // Measuring minimum tabs width. 342 minTabWidth = this.$tabsList.outerWidth( true ), 343 // Static value fo min content width 344 minContentWidth = 300, 345 // Measuring minimum tabs width for percent-based sizes. 346 navWidth = this.$container.usMod( 'navwidth' ); 347 348 if ( navWidth !== 'auto' ) { 349 minTabWidth = Math.max( minTabWidth, minContentWidth * parseInt( navWidth ) / ( 100 - parseInt( navWidth ) ) ); 350 } 351 this.minWidth = Math.max( 480, minContentWidth + minTabWidth + 1 ) 352 } 353 354 if ( this.isTrendy() ) { 355 this.tabHeights = []; 356 this.tabTops = []; 357 for ( var index = 0; index < this.tabs.length; index ++ ) { 358 this.tabHeights.push( this.tabs[ index ].outerHeight( true ) ); 359 this.tabTops.push( 360 index 361 ? ( this.tabTops[ index - 1 ] + this.tabHeights[ index - 1 ] ) 362 : 0 363 ); 364 } 365 } 366 367 } else { 368 if ( this.basicLayout === 'hor' ) { 369 this.$container.addClass( 'measure' ); 370 // Get the specified minimum width or determine automatically 371 if ( this.isAccordionAtWidth ) { 372 this.minWidth = this.accordionAtWidth; 373 } else { 374 this.minWidth = 0; 375 for ( var index = 0; index < this.tabs.length; index ++ ) { 376 this.minWidth += this.tabs[ index ].outerWidth( true ); 377 } 378 } 379 this.$container.removeClass( 'measure' ); 380 } 381 382 if ( this.isTrendy() ) { 383 this.tabWidths = []; 384 this.tabLefts = []; 385 for ( var index = 0; index < this.tabs.length; index ++ ) { 386 this.tabWidths.push( this.tabs[ index ].outerWidth( true ) ); 387 this.tabLefts.push( index 388 ? ( this.tabLefts[ index - 1 ] + this.tabWidths[ index - 1 ] ) 389 : this.tabs[ index ].position().left 390 ); 391 } 392 // Offset correction for RTL version with Trendy enabled 393 if ( this.isRtl ) { 394 var 395 // Get the width of the first tab 396 firstTabWidth = this.tabWidths[0], 397 // Get X offsets 398 offset = ( 'none' == this.align ) 399 ? this.$tabsList.outerWidth( true ) 400 : this.tabWidths // Get the total width of all tabs 401 .reduce( ( a, b ) => { return a + b }, /* default */0 ); 402 // Calculate position based on offset 403 this.tabLefts = this.tabLefts 404 .map( ( left ) => Math.abs( left - offset + firstTabWidth ) ); 405 } 406 } 407 } 408 }, 409 410 /** 411 * Set bar position for certain element index and current layout 412 * 413 * @param {number} index The index element 414 * @param {boolean} animated Animating an element when updating css 415 */ 416 setBarPosition: function( index, animated ) { 417 if ( 418 index === _undefined 419 || ! this.isTrendy() 420 || 'hor|ver'.indexOf( this.curLayout ) == - 1 421 ) { 422 return; 423 } 424 // Add a bar to the document if it does not exist 425 if ( ! this.$tabsBar.length ) { 426 this.$tabsBar = $( '<div class="w-tabs-list-bar"></div>' ) 427 .appendTo( this.$tabsList ); 428 } 429 // Get bar position for certain element index and current layout 430 var css = {}; 431 if ( this.curLayout === 'hor' ) { 432 css = { width: this.tabWidths[ index ] }; 433 css[ this.isRtl ? 'right' : 'left' ] = this.tabLefts[ index ]; 434 } else if ( this.curLayout === 'ver' ) { 435 css = { 436 top: this.tabTops[ index ], 437 height: this.tabHeights[ index ] 438 }; 439 } 440 // Set css properties for a bar element 441 if ( ! animated ) { 442 this.$tabsBar.css( css ); 443 } else { 444 this.$tabsBar 445 .performCSSTransition( css, this.options.duration, null, this.options.easing ); 446 } 447 }, 448 449 /** 450 * Open tab section. 451 * 452 * @param index int 453 */ 454 openSection: function( index ) { 455 if ( this.sections[ index ] === _undefined ) { 456 return; 457 } 458 459 if ( this.curLayout === 'hor' ) { 460 this.$sections 461 .removeClass( 'active' ) 462 .css( 'display', 'none' ); 463 this.sections[ index ] 464 .stop( true, true ) 465 .fadeIn( this.options.duration, function() { 466 $( this ).addClass( 'active' ); 467 } ); 468 } else if ( this.curLayout === 'accordion' ) { 469 if ( this.contents[ this.active[0] ] !== _undefined ) { 470 this.contents[ this.active[0] ] 471 .css( 'display', 'block' ) 472 .stop( true, false ) 473 .slideUp( this.options.duration ); 474 } 475 this.contents[ index ] 476 .css( 'display', 'none' ) 477 .stop( true, false ) 478 .slideDown( this.options.duration, function() { 479 this._events.contentChanged.call( this ); 480 481 // Scrolling to the opened section 482 if ( this.hasScrolling && this.curLayout === 'accordion' && this.headerClicked == true ) { 483 var top = this.headers[ index ].offset().top; 484 485 if ( ! jQuery.isMobile ) { 486 top -= $us.$canvas.offset().top || 0; 487 } 488 // If there is a sticky section in front of the current section, 489 // then take into account the position this section. 490 var $prevStickySection = this.$container 491 .closest( '.l-section' ) 492 .prevAll( '.l-section.type_sticky' ); 493 494 if ( $prevStickySection.length ) { 495 top -= parseInt( $prevStickySection.outerHeight( true ) ); 496 } 497 498 // Animate options 499 var animateOptions = { 500 duration: $us.canvasOptions.scrollDuration, 501 easing: $us.getAnimationName( 'easeInOutExpo' ), 502 start: function() { 503 this.isScrolling = true; 504 }.bind( this ), 505 always: function() { 506 this.isScrolling = false; 507 }.bind( this ), 508 /** 509 * Get and applying new values during animation. 510 * 511 * @param number now 512 * @param object fx 513 */ 514 step: function( now, fx ) { 515 var newTop = top; 516 // Since the header at the moment of scrolling the scroll can change the height,
517 // we will correct the position of the element. 518 if ( 519 $us.header.isHorizontal() 520 && $us.header.stickyEnabled() 521 ) { 522 newTop -= this.headerHeight; 523 } 524 if ( fx.end !== newTop ) { 525 $us.$htmlBody 526 .stop( true, false ) 527 .animate( { scrollTop: newTop }, $.extend( animateOptions, { 528 easing: $us.getAnimationName( 'easeOutExpo' ) 529 } ) ); 530 } 531 }.bind( this ) 532 }; 533 $us.$htmlBody 534 .stop( true, false ) 535 .animate( { scrollTop: top }, animateOptions ); 536 this.headerClicked = false; 537 } 538 }.bind( this ) ); 539 this.$sections 540 .removeClass( 'active' ); 541 this.sections[ index ] 542 .addClass( 'active' ); 543 } else if ( this.curLayout === 'ver' ) { 544 if ( this.contents[ this.active[0] ] !== _undefined ) { 545 this.contents[ this.active[0] ] 546 .css( 'display', 'none' ); 547 } 548 this.contents[ index ] 549 .css( 'display', 'none' ) 550 .stop( true, true ) 551 .fadeIn( this.options.duration, this._events.contentChanged ); 552 this.$sections 553 .removeClass( 'active' ); 554 this.sections[ index ] 555 .addClass( 'active' ); 556 } 557 558 this._events.contentChanged(); 559 this.$tabs 560 .attr( 'aria-expanded', 'false' ) 561 .removeClass( 'active' ); 562 this.tabs[ index ] 563 .attr( 'aria-expanded', 'true' ) 564 .addClass( 'active' ); 565 this.active[ 0 ] = index; 566 567 // Set bar position for certain element index and current layout 568 this.setBarPosition( index, /* animated */true ); 569 }, 570 571 /** 572 * Toggle some togglable accordion section. 573 * 574 * @param index 575 */ 576 toggleSection: function( index ) { 577 // (!) Can only be used within accordion state 578 var indexPos = $.inArray( index, this.active ); 579 if ( indexPos != - 1 ) { 580 this.contents[ index ] 581 .css( 'display', 'block' ) 582 .slideUp( this.options.duration, this._events.contentChanged ); 583 this.tabs[ index ] 584 .attr( 'aria-expanded', 'true' ) 585 .removeClass( 'active' ); 586 this.sections[ index ] 587 .removeClass( 'active' ); 588 this.active.splice( indexPos, 1 ); 589 } else { 590 this.contents[ index ] 591 .css( 'display', 'none' ) 592 .slideDown( this.options.duration, this._events.contentChanged ); 593 this.tabs[ index ] 594 .attr( 'aria-expanded', 'false' ) 595 .addClass( 'active' ); 596 this.sections[ index ] 597 .addClass( 'active' ); 598 this.active.push( index ); 599 } 600 }, 601 602 /** 603 * Resize-driven logics 604 */ 605 resize: function() { 606 this.width = this.isAccordionAtWidth 607 ? $us.$window.outerWidth() 608 : this.$container.width(); 609 610 // Skip changing Tabs into Accordion inside header menu on Mobiles 611 if ( 612 this.curLayout !== 'accordion' 613 && ! this.width 614 && this.$container.closest( '.w-nav' ).length 615 && ! jQuery.isMobile 616 ) { 617 return; 618 } 619 620 var nextLayout = ( this.width <= this.minWidth ) 621 ? 'accordion' 622 : this.basicLayout; 623 if ( nextLayout !== this.curLayout ) { 624 this.switchLayout( nextLayout ); 625 } 626 if ( this.curLayout !== 'accordion' ) { 627 this.measure(); 628 } 629 630 this._events.contentChanged(); 631 // Set bar position for certain element index and current layout 632 this.setBarPosition( this.active[ 0 ] ); 633 } 634 }; 635 636 $.fn.wTabs = function( options ) { 637 return this.each( function() { 638 $( this ).data( 'wTabs', new $us.WTabs( this, options ) ); 639 } ); 640 }; 641 642 $( () => $( '.w-tabs' ).wTabs() ); 643 644 // Init in Grid Layout context 645 $us.$document.on( 'usPostList.itemsLoaded usGrid.itemsLoaded', ( _, $items ) => { 646 $( '.w-tabs', $items ).wTabs(); 647 } ); 648 649}( jQuery ); 650 651/* RevSlider support for our tabs */ 652jQuery( function( $ ) { 653 $( '.w-tabs .rev_slider' ).each( function() { 654 var $slider = $( this ); 655 $slider.bind( "revolution.slide.onloaded", function( e ) { 656 $us.$canvas.on( 'contentChange', function() { 657 $slider.revredraw(); 658 } ); 659 } ); 660 } ); 661} );
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.