1;(function ( $, window, undefined ) { 2 3 /** Default settings */ 4 var defaults = { 5 active: null, 6 event: 'click', 7 disabled: [], 8 collapsible: 'accordion', 9 startCollapsed: false, 10 rotate: false, 11 setHash: false, 12 animation: 'default', 13 animationQueue: false, 14 duration: 500, 15 fluidHeight: true, 16 scrollToAccordion: false, 17 scrollToAccordionOnLoad: true, 18 scrollToAccordionOffset: 0, 19 accordionTabElement: '<div></div>', 20 click: function(){}, 21 activate: function(){}, 22 deactivate: function(){}, 23 load: function(){}, 24 activateState: function(){}, 25 classes: { 26 stateDefault: 'r-tabs-state-default', 27 stateActive: 'r-tabs-state-active', 28 stateDisabled: 'r-tabs-state-disabled', 29 stateExcluded: 'r-tabs-state-excluded', 30 container: 'r-tabs', 31 ul: 'r-tabs-nav', 32 tab: 'r-tabs-tab', 33 anchor: 'r-tabs-anchor', 34 panel: 'r-tabs-panel', 35 accordionTitle: 'r-tabs-accordion-title' 36 } 37 }; 38 39 /** 40 * Responsive Tabs 41 * @constructor 42 * @param {object} element - The HTML element the validator should be bound to 43 * @param {object} options - An option map 44 */ 45 function ResponsiveTabs(element, options) { 46 this.element = element; // Selected DOM element 47 this.$element = $(element); // Selected jQuery element 48 49 this.tabs = []; // Create tabs array 50 this.state = ''; // Define the plugin state (tabs/accordion) 51 this.rotateInterval = 0; // Define rotate interval 52 this.$queue = $({}); 53 54 // Extend the defaults with the passed options 55 this.options = $.extend( {}, defaults, options); 56 57 this.init(); 58 } 59 60 61 /** 62 * This function initializes the tab plugin 63 */ 64 ResponsiveTabs.prototype.init = function () { 65 var _this = this; 66 67 // Load all the elements 68 this.tabs = this._loadElements(); 69 this._loadClasses(); 70 this._loadEvents(); 71 72 // Window resize bind to check state 73 $(window).on('resize', function(e) { 74 _this._setState(e); 75 if(_this.options.fluidHeight !== true) { 76 _this._equaliseHeights(); 77 } 78 }); 79 80 // Hashchange event 81 $(window).on('hashchange', function(e) { 82 var tabRef = _this._getTabRefBySelector(window.location.hash); 83 var oTab = _this._getTab(tabRef); 84 85 // Check if a tab is found that matches the hash 86 if(tabRef >= 0 && !oTab._ignoreHashChange && !oTab.disabled) { 87 // If so, open the tab and auto close the current one 88 _this._openTab(e, _this._getTab(tabRef), true); 89 } 90 }); 91 92 // Start rotate event if rotate option is defined 93 if(this.options.rotate !== false) { 94 this.startRotation(); 95 } 96 97 // Set fluid height 98 if(this.options.fluidHeight !== true) { 99 _this._equaliseHeights(); 100 } 101 102 // -------------------- 103 // Define plugin events 104 // 105 106 // Activate: this event is called when a tab is selected 107 this.$element.bind('tabs-click', function(e, oTab) { 108 _this.options.click.call(this, e, oTab); 109 }); 110 111 // Activate: this event is called when a tab is selected 112 this.$element.bind('tabs-activate', function(e, oTab) { 113 _this.options.activate.call(this, e, oTab); 114 }); 115 // Deactivate: this event is called when a tab is closed 116 this.$element.bind('tabs-deactivate', function(e, oTab) { 117 _this.options.deactivate.call(this, e, oTab); 118 }); 119 // Activate State: this event is called when the plugin switches states 120 this.$element.bind('tabs-activate-state', function(e, state) { 121 _this.options.activateState.call(this, e, state); 122 }); 123 124 // Load: this event is called when the plugin has been loaded 125 this.$element.bind('tabs-load', function(e) { 126 var startTab; 127 128 _this._setState(e); // Set state 129 130 // Check if the panel should be collaped on load 131 if(_this.options.startCollapsed !== true && !(_this.options.startCollapsed === 'accordion' && _this.state === 'accordion')) { 132 133 startTab = _this._getStartTab(); 134 135 // Open the initial tab 136 _this._openTab(e, startTab); // Open first tab 137 138 // Call the callback function 139 _this.options.load.call(this, e, startTab); // Call the load callback 140 } 141 }); 142 // Trigger loaded event 143 this.$element.trigger('tabs-load'); 144 }; 145 146 // 147 // PRIVATE FUNCTIONS 148 // 149 150 /** 151 * This function loads the tab elements and stores them in an array 152 * @returns {Array} Array of tab elements 153 */ 154 ResponsiveTabs.prototype._loadElements = function() { 155 var _this = this; 156 var $ul = this.$element.children('ul:first'); 157 var tabs = []; 158 var id = 0; 159 160 // Add the classes to the basic html elements 161 this.$element.addClass(_this.options.classes.container); // Tab container 162 $ul.addClass(_this.options.classes.ul); // List container 163 164 // Get tab buttons and store their data in an array 165 $('li', $ul).each(function() { 166 var $tab = $(this);
167 var isExcluded = $tab.hasClass(_this.options.classes.stateExcluded); 168 var $anchor, $panel, $accordionTab, $accordionAnchor, panelSelector; 169 170 // Check if the tab should be excluded 171 if(!isExcluded) { 172 173 $anchor = $('a', $tab); 174 panelSelector = $anchor.attr('href'); 175 $panel = $(panelSelector); 176 $accordionTab = $(_this.options.accordionTabElement).insertBefore($panel); 177 $accordionAnchor = $('<a></a>').attr('href', panelSelector).html($anchor.html()).appendTo($accordionTab); 178 179 var oTab = { 180 _ignoreHashChange: false, 181 id: id, 182 disabled: ($.inArray(id, _this.options.disabled) !== -1), 183 tab: $(this), 184 anchor: $('a', $tab), 185 panel: $panel, 186 selector: panelSelector, 187 accordionTab: $accordionTab, 188 accordionAnchor: $accordionAnchor, 189 active: false 190 }; 191 192 // 1up the ID 193 id++; 194 // Add to tab array 195 tabs.push(oTab); 196 } 197 }); 198 return tabs; 199 }; 200 201 202 /** 203 * This function adds classes to the tab elements based on the options 204 */ 205 ResponsiveTabs.prototype._loadClasses = function() { 206 for (var i=0; i<this.tabs.length; i++) { 207 this.tabs[i].tab.addClass(this.options.classes.stateDefault).addClass(this.options.classes.tab); 208 this.tabs[i].anchor.addClass(this.options.classes.anchor); 209 this.tabs[i].panel.addClass(this.options.classes.stateDefault).addClass(this.options.classes.panel); 210 this.tabs[i].accordionTab.addClass(this.options.classes.accordionTitle); 211 this.tabs[i].accordionAnchor.addClass(this.options.classes.anchor); 212 if(this.tabs[i].disabled) { 213 this.tabs[i].tab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); 214 this.tabs[i].accordionTab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); 215 } 216 } 217 }; 218 219 /** 220 * This function adds events to the tab elements 221 */ 222 ResponsiveTabs.prototype._loadEvents = function() { 223 var _this = this; 224 225 // Define activate event on a tab element 226 var fActivate = function(e) { 227 var current = _this._getCurrentTab(); // Fetch current tab 228 var activatedTab = e.data.tab; 229 230 e.preventDefault(); 231 232 // Trigger click event for whenever a tab is clicked/touched even if the tab is disabled 233 activatedTab.tab.trigger('tabs-click', activatedTab); 234 235 // Make sure this tab isn't disabled 236 if(!activatedTab.disabled) { 237 238 // Check if hash has to be set in the URL location 239 if(_this.options.setHash) { 240 // Set the hash using the history api if available to tackle Chromes repaint bug on hash change 241 if(history.pushState) { 242 history.pushState(null, null, window.location.origin + window.location.pathname + window.location.search + activatedTab.selector); 243 } else { 244 // Otherwise fallback to the hash update for sites that don't support the history api 245 window.location.hash = activatedTab.selector; 246 } 247 } 248 249 e.data.tab._ignoreHashChange = true; 250 251 // Check if the activated tab isnt the current one or if its collapsible. If not, do nothing 252 if(current !== activatedTab || _this._isCollapisble()) { 253 // The activated tab is either another tab of the current one. If it's the current tab it is collapsible 254 // Either way, the current tab can be closed 255 _this._closeTab(e, current); 256 257 // Check if the activated tab isnt the current one or if it isnt collapsible 258 if(current !== activatedTab || !_this._isCollapisble()) { 259 _this._openTab(e, activatedTab, false, true); 260 } 261 } 262 } 263 }; 264 265 // Loop tabs 266 for (var i=0; i<this.tabs.length; i++) { 267 // Add activate function to the tab and accordion selection element 268 this.tabs[i].anchor.on(_this.options.event, {tab: _this.tabs[i]}, fActivate); 269 this.tabs[i].accordionAnchor.on(_this.options.event, {tab: _this.tabs[i]}, fActivate); 270 } 271 }; 272 273 /** 274 * This function gets the tab that should be opened at start 275 * @returns {Object} Tab object 276 */ 277 ResponsiveTabs.prototype._getStartTab = function() { 278 var tabRef = this._getTabRefBySelector(window.location.hash); 279 var startTab; 280 281 // Check if the page has a hash set that is linked to a tab 282 if(tabRef >= 0 && !this._getTab(tabRef).disabled) { 283 // If so, set the current tab to the linked tab 284 startTab = this._getTab(tabRef); 285 } else if(this.options.active > 0 && !this._getTab(this.options.active).disabled) { 286 startTab = this._getTab(this.options.active); 287 } else { 288 // If not, just get the first one 289 startTab = this._getTab(0); 290 } 291 292 return startTab; 293 }; 294 295 /** 296 * This function sets the current state of the plugin 297 * @param {Event} e - The event that triggers the state change 298 */ 299 ResponsiveTabs.prototype._setState = function(e) { 300 var $ul = $('ul:first', this.$element); 301 var oldState = this.state; 302 var startCollapsedIsState = (typeof this.options.startCollapsed === 'string'); 303 var startTab; 304 305 // The state is based on the visibility of the tabs list 306 if($ul.is(':visible')){ 307 // Tab list is visible, so the state is 'tabs' 308 this.state = 'tabs'; 309 } else { 310 // Tab list is invisible, so the state is 'accordion' 311 this.state = 'accordion'; 312 } 313 314 // If the new state is different from the old state 315 if(this.state !== oldState) { 316 // If so, the state activate trigger must be called 317 this.$element.trigger('tabs-activate-state', {oldState: oldState, newState: this.state}); 318 319 // Check if the state switch should open a tab 320 if(oldState && startCollapsedIsState && this.options.startCollapsed !== this.state && this._getCurrentTab() === undefined) { 321 // Get initial tab 322 startTab = this._getStartTab(e); 323 // Open the initial tab 324 this._openTab(e, startTab); // Open first tab 325 } 326 } 327 }; 328 329 /** 330 * This function opens a tab 331 * @param {Event} e - The event that triggers the tab opening 332 * @param {Object} oTab - The tab object that should be opened 333 * @param {Boolean} closeCurrent - Defines if the current tab should be closed 334 * @param {Boolean} stopRotation - Defines if the tab rotation loop should be stopped 335 */ 336 ResponsiveTabs.prototype._openTab = function(e, oTab, closeCurrent, stopRotation) { 337 var _this = this; 338 var scrollOffset; 339 340 // Check if the current tab has to be closed 341 if(closeCurrent) { 342 this._closeTab(e, this._getCurrentTab()); 343 } 344 345 // Check if the rotation has to be stopped when activated 346 if(stopRotation && this.rotateInterval > 0) { 347 this.stopRotation(); 348 } 349 350 // Set this tab to active 351 oTab.active = true; 352 // Set active classes to the tab button and accordion tab button 353 oTab.tab.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); 354 oTab.accordionTab.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); 355 356 // Run panel transiton 357 _this._doTransition(oTab.panel, _this.options.animation, 'open', function() { 358 var scrollOnLoad = (e.type !== 'tabs-load' || _this.options.scrollToAccordionOnLoad); 359 360 // When finished, set active class to the panel 361 oTab.panel.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); 362 363 // And if enabled and state is accordion, scroll to the accordion tab
364 if(_this.getState() === 'accordion' && _this.options.scrollToAccordion && (!_this._isInView(oTab.accordionTab) || _this.options.animation !== 'default') && scrollOnLoad) { 365 366 // Add offset element's height to scroll position 367 scrollOffset = oTab.accordionTab.offset().top - _this.options.scrollToAccordionOffset; 368 369 // Check if the animation option is enabled, and if the duration isn't 0 370 if(_this.options.animation !== 'default' && _this.options.duration > 0) { 371 // If so, set scrollTop with animate and use the 'animation' duration 372 $('html, body').animate({ 373 scrollTop: scrollOffset 374 }, _this.options.duration); 375 } else { 376 // If not, just set scrollTop 377 $('html, body').scrollTop(scrollOffset); 378 } 379 } 380 }); 381 382 this.$element.trigger('tabs-activate', oTab); 383 }; 384 385 /** 386 * This function closes a tab 387 * @param {Event} e - The event that is triggered when a tab is closed 388 * @param {Object} oTab - The tab object that should be closed 389 */ 390 ResponsiveTabs.prototype._closeTab = function(e, oTab) { 391 var _this = this; 392 var doQueueOnState = typeof _this.options.animationQueue === 'string'; 393 var doQueue; 394 395 if(oTab !== undefined) {
396 if(doQueueOnState && _this.getState() === _this.options.animationQueue) { 397 doQueue = true; 398 } else if(doQueueOnState) { 399 doQueue = false; 400 } else { 401 doQueue = _this.options.animationQueue; 402 } 403 404 // Deactivate tab 405 oTab.active = false; 406 // Set default class to the tab button 407 oTab.tab.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); 408 409 // Run panel transition 410 _this._doTransition(oTab.panel, _this.options.animation, 'close', function() { 411 // Set default class to the accordion tab button and tab panel 412 oTab.accordionTab.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); 413 oTab.panel.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); 414 }, !doQueue); 415 416 this.$element.trigger('tabs-deactivate', oTab); 417 } 418 }; 419 420 /** 421 * This function runs an effect on a panel 422 * @param {Element} panel - The HTML element of the tab panel 423 * @param {String} method - The transition method reference 424 * @param {String} state - The state (open/closed) that the panel should transition to 425 * @param {Function} callback - The callback function that is called after the transition 426 * @param {Boolean} dequeue - Defines if the event queue should be dequeued after the transition 427 */ 428 ResponsiveTabs.prototype._doTransition = function(panel, method, state, callback, dequeue) { 429 var effect; 430 var _this = this; 431 432 // Get effect based on method 433 switch(method) { 434 case 'slide': 435 effect = (state === 'open') ? 'slideDown' : 'slideUp'; 436 break; 437 case 'fade': 438 effect = (state === 'open') ? 'fadeIn' : 'fadeOut'; 439 break; 440 default: 441 effect = (state === 'open') ? 'show' : 'hide'; 442 // When default is used, set the duration to 0 443 _this.options.duration = 0; 444 break; 445 } 446 447 // Add the transition to a custom queue 448 this.$queue.queue('responsive-tabs',function(next){ 449 // Run the transition on the panel 450 panel[effect]({ 451 duration: _this.options.duration, 452 complete: function() { 453 // Call the callback function 454 callback.call(panel, method, state); 455 // Run the next function in the queue 456 next(); 457 } 458 }); 459 }); 460 461 // When the panel is openend, dequeue everything so the animation starts 462 if(state === 'open' || dequeue) { 463 this.$queue.dequeue('responsive-tabs'); 464 } 465 466 }; 467 468 /** 469 * This function returns the collapsibility of the tab in this state 470 * @returns {Boolean} The collapsibility of the tab 471 */ 472 ResponsiveTabs.prototype._isCollapisble = function() { 473 return (typeof this.options.collapsible === 'boolean' && this.options.collapsible) || (typeof this.options.collapsible === 'string' && this.options.collapsible === this.getState()); 474 }; 475 476 /** 477 * This function returns a tab by numeric reference 478 * @param {Integer} numRef - Numeric tab reference 479 * @returns {Object} Tab object 480 */ 481 ResponsiveTabs.prototype._getTab = function(numRef) { 482 return this.tabs[numRef]; 483 }; 484 485 /** 486 * This function returns the numeric tab reference based on a hash selector 487 * @param {String} selector - Hash selector 488 * @returns {Integer} Numeric tab reference 489 */ 490 ResponsiveTabs.prototype._getTabRefBySelector = function(selector) { 491 // Loop all tabs 492 for (var i=0; i<this.tabs.length; i++) { 493 // Check if the hash selector is equal to the tab selector 494 if(this.tabs[i].selector === selector) { 495 return i; 496 } 497 } 498 // If none is found return a negative index 499 return -1; 500 }; 501 502 /** 503 * This function returns the current tab element 504 * @returns {Object} Current tab element 505 */ 506 ResponsiveTabs.prototype._getCurrentTab = function() { 507 return this._getTab(this._getCurrentTabRef()); 508 }; 509 510 /** 511 * This function returns the next tab's numeric reference 512 * @param {Integer} currentTabRef - Current numeric tab reference 513 * @returns {Integer} Numeric tab reference 514 */ 515 ResponsiveTabs.prototype._getNextTabRef = function(currentTabRef) { 516 var tabRef = (currentTabRef || this._getCurrentTabRef()); 517 var nextTabRef = (tabRef === this.tabs.length - 1) ? 0 : tabRef + 1; 518 return (this._getTab(nextTabRef).disabled) ? this._getNextTabRef(nextTabRef) : nextTabRef; 519 }; 520 521 /** 522 * This function returns the previous tab's numeric reference 523 * @returns {Integer} Numeric tab reference 524 */ 525 ResponsiveTabs.prototype._getPreviousTabRef = function() { 526 return (this._getCurrentTabRef() === 0) ? this.tabs.length - 1 : this._getCurrentTabRef() - 1; 527 }; 528 529 /** 530 * This function returns the current tab's numeric reference 531 * @returns {Integer} Numeric tab reference 532 */ 533 ResponsiveTabs.prototype._getCurrentTabRef = function() { 534 // Loop all tabs 535 for (var i=0; i<this.tabs.length; i++) { 536 // If this tab is active, return it 537 if(this.tabs[i].active) { 538 return i; 539 } 540 } 541 // No tabs have been found, return negative index 542 return -1; 543 }; 544 545 /** 546 * This function gets the tallest tab and applied the height to all tabs 547 */ 548 ResponsiveTabs.prototype._equaliseHeights = function() { 549 var maxHeight = 0; 550 551 $.each($.map(this.tabs, function(tab) { 552 maxHeight = Math.max(maxHeight, tab.panel.css('minHeight', '').height()); 553 return tab.panel; 554 }), function() { 555 this.css('minHeight', maxHeight); 556 }); 557 }; 558 559 // 560 // HELPER FUNCTIONS 561 // 562 563 ResponsiveTabs.prototype._isInView = function($element) { 564 var docViewTop = $(window).scrollTop(), 565 docViewBottom = docViewTop + $(window).height(), 566 elemTop = $element.offset().top, 567 elemBottom = elemTop + $element.height(); 568 return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); 569 }; 570 571 // 572 // PUBLIC FUNCTIONS 573 // 574 575 /** 576 * This function activates a tab 577 * @param {Integer} tabRef - Numeric tab reference 578 * @param {Boolean} stopRotation - Defines if the tab rotation should stop after activation 579 */ 580 ResponsiveTabs.prototype.activate = function(tabRef, stopRotation) { 581 var e = jQuery.Event('tabs-activate'); 582 var oTab = this._getTab(tabRef); 583 if(!oTab.disabled) { 584 this._openTab(e, oTab, true, stopRotation || true); 585 } 586 }; 587 588 /** 589 * This function deactivates a tab 590 * @param {Integer} tabRef - Numeric tab reference 591 */ 592 ResponsiveTabs.prototype.deactivate = function(tabRef) { 593 var e = jQuery.Event('tabs-dectivate'); 594 var oTab = this._getTab(tabRef); 595 if(!oTab.disabled) { 596 this._closeTab(e, oTab); 597 } 598 }; 599 600 /** 601 * This function enables a tab 602 * @param {Integer} tabRef - Numeric tab reference 603 */ 604 ResponsiveTabs.prototype.enable = function(tabRef) { 605 var oTab = this._getTab(tabRef); 606 if(oTab){ 607 oTab.disabled = false;
608 oTab.tab.addClass(this.options.classes.stateDefault).removeClass(this.options.classes.stateDisabled); 609 oTab.accordionTab.addClass(this.options.classes.stateDefault).removeClass(this.options.classes.stateDisabled); 610 } 611 }; 612 613 /** 614 * This function disable a tab 615 * @param {Integer} tabRef - Numeric tab reference 616 */ 617 ResponsiveTabs.prototype.disable = function(tabRef) { 618 var oTab = this._getTab(tabRef); 619 if(oTab){ 620 oTab.disabled = true; 621 oTab.tab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); 622 oTab.accordionTab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); 623 } 624 }; 625 626 /** 627 * This function gets the current state of the plugin 628 * @returns {String} State of the plugin 629 */ 630 ResponsiveTabs.prototype.getState = function() { 631 return this.state; 632 }; 633 634 /** 635 * This function starts the rotation of the tabs 636 * @param {Integer} speed - The speed of the rotation 637 */ 638 ResponsiveTabs.prototype.startRotation = function(speed) { 639 var _this = this; 640 // Make sure not all tabs are disabled 641 if(this.tabs.length > this.options.disabled.length) { 642 this.rotateInterval = setInterval(function(){ 643 var e = jQuery.Event('rotate'); 644 _this._openTab(e, _this._getTab(_this._getNextTabRef()), true); 645 }, speed || (($.isNumeric(_this.options.rotate)) ? _this.options.rotate : 4000) ); 646 } else { 647 throw new Error("Rotation is not possible if all tabs are disabled"); 648 } 649 }; 650 651 /** 652 * This function stops the rotation of the tabs 653 */ 654 ResponsiveTabs.prototype.stopRotation = function() { 655 window.clearInterval(this.rotateInterval); 656 this.rotateInterval = 0; 657 }; 658 659 /** 660 * This function can be used to get/set options 661 * @return {any} Option value 662 */ 663 ResponsiveTabs.prototype.option = function(key, value) { 664 if(value) { 665 this.options[key] = value; 666 } 667 return this.options[key]; 668 }; 669 670 /** jQuery wrapper */ 671 $.fn.responsiveTabs = function ( options ) { 672 var args = arguments; 673 var instance; 674 675 if (options === undefined || typeof options === 'object') { 676 return this.each(function () { 677 if (!$.data(this, 'responsivetabs')) { 678 $.data(this, 'responsivetabs', new ResponsiveTabs( this, options )); 679 } 680 }); 681 } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 682 instance = $.data(this[0], 'responsivetabs'); 683 684 // Allow instances to be destroyed via the 'destroy' method 685 if (options === 'destroy') { 686 // TODO: destroy instance classes, etc 687 $.data(this, 'responsivetabs', null); 688 } 689 690 if (instance instanceof ResponsiveTabs && typeof instance[options] === 'function') { 691 return instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); 692 } else { 693 return this; 694 } 695 } 696 }; 697 698}(jQuery, window));
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.