;(function ( $, window, undefined ) { /** Default settings */ var defaults = { active: null, event: 'click', disabled: [], collapsible: 'accordion', startCollapsed: false, rotate: false, setHash: false, animation: 'default', animationQueue: false, duration: 500, fluidHeight: true, scrollToAccordion: false, scrollToAccordionOnLoad: true, scrollToAccordionOffset: 0, accordionTabElement: '
', click: function(){}, activate: function(){}, deactivate: function(){}, load: function(){}, activateState: function(){}, classes: { stateDefault: 'r-tabs-state-default', stateActive: 'r-tabs-state-active', stateDisabled: 'r-tabs-state-disabled', stateExcluded: 'r-tabs-state-excluded', container: 'r-tabs', ul: 'r-tabs-nav', tab: 'r-tabs-tab', anchor: 'r-tabs-anchor', panel: 'r-tabs-panel', accordionTitle: 'r-tabs-accordion-title' } }; /** * Responsive Tabs * @constructor * @param {object} element - The HTML element the validator should be bound to * @param {object} options - An option map */ function ResponsiveTabs(element, options) { this.element = element; // Selected DOM element this.$element = $(element); // Selected jQuery element this.tabs = []; // Create tabs array this.state = ''; // Define the plugin state (tabs/accordion) this.rotateInterval = 0; // Define rotate interval this.$queue = $({}); // Extend the defaults with the passed options this.options = $.extend( {}, defaults, options); this.init(); } /** * This function initializes the tab plugin */ ResponsiveTabs.prototype.init = function () { var _this = this; // Load all the elements this.tabs = this._loadElements(); this._loadClasses(); this._loadEvents(); // Window resize bind to check state $(window).on('resize', function(e) { _this._setState(e); if(_this.options.fluidHeight !== true) { _this._equaliseHeights(); } }); // Hashchange event $(window).on('hashchange', function(e) { var tabRef = _this._getTabRefBySelector(window.location.hash); var oTab = _this._getTab(tabRef); // Check if a tab is found that matches the hash if(tabRef >= 0 && !oTab._ignoreHashChange && !oTab.disabled) { // If so, open the tab and auto close the current one _this._openTab(e, _this._getTab(tabRef), true); } }); // Start rotate event if rotate option is defined if(this.options.rotate !== false) { this.startRotation(); } // Set fluid height if(this.options.fluidHeight !== true) { _this._equaliseHeights(); } // -------------------- // Define plugin events // // Activate: this event is called when a tab is selected this.$element.bind('tabs-click', function(e, oTab) { _this.options.click.call(this, e, oTab); }); // Activate: this event is called when a tab is selected this.$element.bind('tabs-activate', function(e, oTab) { _this.options.activate.call(this, e, oTab); }); // Deactivate: this event is called when a tab is closed this.$element.bind('tabs-deactivate', function(e, oTab) { _this.options.deactivate.call(this, e, oTab); }); // Activate State: this event is called when the plugin switches states this.$element.bind('tabs-activate-state', function(e, state) { _this.options.activateState.call(this, e, state); }); // Load: this event is called when the plugin has been loaded this.$element.bind('tabs-load', function(e) { var startTab; _this._setState(e); // Set state // Check if the panel should be collaped on load if(_this.options.startCollapsed !== true && !(_this.options.startCollapsed === 'accordion' && _this.state === 'accordion')) { startTab = _this._getStartTab(); // Open the initial tab _this._openTab(e, startTab); // Open first tab // Call the callback function _this.options.load.call(this, e, startTab); // Call the load callback } }); // Trigger loaded event this.$element.trigger('tabs-load'); }; // // PRIVATE FUNCTIONS // /** * This function loads the tab elements and stores them in an array * @returns {Array} Array of tab elements */ ResponsiveTabs.prototype._loadElements = function() { var _this = this; var $ul = this.$element.children('ul:first'); var tabs = []; var id = 0; // Add the classes to the basic html elements this.$element.addClass(_this.options.classes.container); // Tab container $ul.addClass(_this.options.classes.ul); // List container // Get tab buttons and store their data in an array $('li', $ul).each(function() { var $tab = $(this); var isExcluded = $tab.hasClass(_this.options.classes.stateExcluded); var $anchor, $panel, $accordionTab, $accordionAnchor, panelSelector; // Check if the tab should be excluded if(!isExcluded) { $anchor = $('a', $tab); panelSelector = $anchor.attr('href'); $panel = $(panelSelector); $accordionTab = $(_this.options.accordionTabElement).insertBefore($panel); $accordionAnchor = $('').attr('href', panelSelector).html($anchor.html()).appendTo($accordionTab); var oTab = { _ignoreHashChange: false, id: id, disabled: ($.inArray(id, _this.options.disabled) !== -1), tab: $(this), anchor: $('a', $tab), panel: $panel, selector: panelSelector, accordionTab: $accordionTab, accordionAnchor: $accordionAnchor, active: false }; // 1up the ID id++; // Add to tab array tabs.push(oTab); } }); return tabs; }; /** * This function adds classes to the tab elements based on the options */ ResponsiveTabs.prototype._loadClasses = function() { for (var i=0; i= 0 && !this._getTab(tabRef).disabled) { // If so, set the current tab to the linked tab startTab = this._getTab(tabRef); } else if(this.options.active > 0 && !this._getTab(this.options.active).disabled) { startTab = this._getTab(this.options.active); } else { // If not, just get the first one startTab = this._getTab(0); } return startTab; }; /** * This function sets the current state of the plugin * @param {Event} e - The event that triggers the state change */ ResponsiveTabs.prototype._setState = function(e) { var $ul = $('ul:first', this.$element); var oldState = this.state; var startCollapsedIsState = (typeof this.options.startCollapsed === 'string'); var startTab; // The state is based on the visibility of the tabs list if($ul.is(':visible')){ // Tab list is visible, so the state is 'tabs' this.state = 'tabs'; } else { // Tab list is invisible, so the state is 'accordion' this.state = 'accordion'; } // If the new state is different from the old state if(this.state !== oldState) { // If so, the state activate trigger must be called this.$element.trigger('tabs-activate-state', {oldState: oldState, newState: this.state}); // Check if the state switch should open a tab if(oldState && startCollapsedIsState && this.options.startCollapsed !== this.state && this._getCurrentTab() === undefined) { // Get initial tab startTab = this._getStartTab(e); // Open the initial tab this._openTab(e, startTab); // Open first tab } } }; /** * This function opens a tab * @param {Event} e - The event that triggers the tab opening * @param {Object} oTab - The tab object that should be opened * @param {Boolean} closeCurrent - Defines if the current tab should be closed * @param {Boolean} stopRotation - Defines if the tab rotation loop should be stopped */ ResponsiveTabs.prototype._openTab = function(e, oTab, closeCurrent, stopRotation) { var _this = this; var scrollOffset; // Check if the current tab has to be closed if(closeCurrent) { this._closeTab(e, this._getCurrentTab()); } // Check if the rotation has to be stopped when activated if(stopRotation && this.rotateInterval > 0) { this.stopRotation(); } // Set this tab to active oTab.active = true; // Set active classes to the tab button and accordion tab button oTab.tab.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); oTab.accordionTab.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); // Run panel transiton _this._doTransition(oTab.panel, _this.options.animation, 'open', function() { var scrollOnLoad = (e.type !== 'tabs-load' || _this.options.scrollToAccordionOnLoad); // When finished, set active class to the panel oTab.panel.removeClass(_this.options.classes.stateDefault).addClass(_this.options.classes.stateActive); // And if enabled and state is accordion, scroll to the accordion tab if(_this.getState() === 'accordion' && _this.options.scrollToAccordion && (!_this._isInView(oTab.accordionTab) || _this.options.animation !== 'default') && scrollOnLoad) { // Add offset element's height to scroll position scrollOffset = oTab.accordionTab.offset().top - _this.options.scrollToAccordionOffset; // Check if the animation option is enabled, and if the duration isn't 0 if(_this.options.animation !== 'default' && _this.options.duration > 0) { // If so, set scrollTop with animate and use the 'animation' duration $('html, body').animate({ scrollTop: scrollOffset }, _this.options.duration); } else { // If not, just set scrollTop $('html, body').scrollTop(scrollOffset); } } }); this.$element.trigger('tabs-activate', oTab); }; /** * This function closes a tab * @param {Event} e - The event that is triggered when a tab is closed * @param {Object} oTab - The tab object that should be closed */ ResponsiveTabs.prototype._closeTab = function(e, oTab) { var _this = this; var doQueueOnState = typeof _this.options.animationQueue === 'string'; var doQueue; if(oTab !== undefined) { if(doQueueOnState && _this.getState() === _this.options.animationQueue) { doQueue = true; } else if(doQueueOnState) { doQueue = false; } else { doQueue = _this.options.animationQueue; } // Deactivate tab oTab.active = false; // Set default class to the tab button oTab.tab.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); // Run panel transition _this._doTransition(oTab.panel, _this.options.animation, 'close', function() { // Set default class to the accordion tab button and tab panel oTab.accordionTab.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); oTab.panel.removeClass(_this.options.classes.stateActive).addClass(_this.options.classes.stateDefault); }, !doQueue); this.$element.trigger('tabs-deactivate', oTab); } }; /** * This function runs an effect on a panel * @param {Element} panel - The HTML element of the tab panel * @param {String} method - The transition method reference * @param {String} state - The state (open/closed) that the panel should transition to * @param {Function} callback - The callback function that is called after the transition * @param {Boolean} dequeue - Defines if the event queue should be dequeued after the transition */ ResponsiveTabs.prototype._doTransition = function(panel, method, state, callback, dequeue) { var effect; var _this = this; // Get effect based on method switch(method) { case 'slide': effect = (state === 'open') ? 'slideDown' : 'slideUp'; break; case 'fade': effect = (state === 'open') ? 'fadeIn' : 'fadeOut'; break; default: effect = (state === 'open') ? 'show' : 'hide'; // When default is used, set the duration to 0 _this.options.duration = 0; break; } // Add the transition to a custom queue this.$queue.queue('responsive-tabs',function(next){ // Run the transition on the panel panel[effect]({ duration: _this.options.duration, complete: function() { // Call the callback function callback.call(panel, method, state); // Run the next function in the queue next(); } }); }); // When the panel is openend, dequeue everything so the animation starts if(state === 'open' || dequeue) { this.$queue.dequeue('responsive-tabs'); } }; /** * This function returns the collapsibility of the tab in this state * @returns {Boolean} The collapsibility of the tab */ ResponsiveTabs.prototype._isCollapisble = function() { return (typeof this.options.collapsible === 'boolean' && this.options.collapsible) || (typeof this.options.collapsible === 'string' && this.options.collapsible === this.getState()); }; /** * This function returns a tab by numeric reference * @param {Integer} numRef - Numeric tab reference * @returns {Object} Tab object */ ResponsiveTabs.prototype._getTab = function(numRef) { return this.tabs[numRef]; }; /** * This function returns the numeric tab reference based on a hash selector * @param {String} selector - Hash selector * @returns {Integer} Numeric tab reference */ ResponsiveTabs.prototype._getTabRefBySelector = function(selector) { // Loop all tabs for (var i=0; i= docViewTop)); }; // // PUBLIC FUNCTIONS // /** * This function activates a tab * @param {Integer} tabRef - Numeric tab reference * @param {Boolean} stopRotation - Defines if the tab rotation should stop after activation */ ResponsiveTabs.prototype.activate = function(tabRef, stopRotation) { var e = jQuery.Event('tabs-activate'); var oTab = this._getTab(tabRef); if(!oTab.disabled) { this._openTab(e, oTab, true, stopRotation || true); } }; /** * This function deactivates a tab * @param {Integer} tabRef - Numeric tab reference */ ResponsiveTabs.prototype.deactivate = function(tabRef) { var e = jQuery.Event('tabs-dectivate'); var oTab = this._getTab(tabRef); if(!oTab.disabled) { this._closeTab(e, oTab); } }; /** * This function enables a tab * @param {Integer} tabRef - Numeric tab reference */ ResponsiveTabs.prototype.enable = function(tabRef) { var oTab = this._getTab(tabRef); if(oTab){ oTab.disabled = false; oTab.tab.addClass(this.options.classes.stateDefault).removeClass(this.options.classes.stateDisabled); oTab.accordionTab.addClass(this.options.classes.stateDefault).removeClass(this.options.classes.stateDisabled); } }; /** * This function disable a tab * @param {Integer} tabRef - Numeric tab reference */ ResponsiveTabs.prototype.disable = function(tabRef) { var oTab = this._getTab(tabRef); if(oTab){ oTab.disabled = true; oTab.tab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); oTab.accordionTab.removeClass(this.options.classes.stateDefault).addClass(this.options.classes.stateDisabled); } }; /** * This function gets the current state of the plugin * @returns {String} State of the plugin */ ResponsiveTabs.prototype.getState = function() { return this.state; }; /** * This function starts the rotation of the tabs * @param {Integer} speed - The speed of the rotation */ ResponsiveTabs.prototype.startRotation = function(speed) { var _this = this; // Make sure not all tabs are disabled if(this.tabs.length > this.options.disabled.length) { this.rotateInterval = setInterval(function(){ var e = jQuery.Event('rotate'); _this._openTab(e, _this._getTab(_this._getNextTabRef()), true); }, speed || (($.isNumeric(_this.options.rotate)) ? _this.options.rotate : 4000) ); } else { throw new Error("Rotation is not possible if all tabs are disabled"); } }; /** * This function stops the rotation of the tabs */ ResponsiveTabs.prototype.stopRotation = function() { window.clearInterval(this.rotateInterval); this.rotateInterval = 0; }; /** * This function can be used to get/set options * @return {any} Option value */ ResponsiveTabs.prototype.option = function(key, value) { if(value) { this.options[key] = value; } return this.options[key]; }; /** jQuery wrapper */ $.fn.responsiveTabs = function ( options ) { var args = arguments; var instance; if (options === undefined || typeof options === 'object') { return this.each(function () { if (!$.data(this, 'responsivetabs')) { $.data(this, 'responsivetabs', new ResponsiveTabs( this, options )); } }); } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { instance = $.data(this[0], 'responsivetabs'); // Allow instances to be destroyed via the 'destroy' method if (options === 'destroy') { // TODO: destroy instance classes, etc $.data(this, 'responsivetabs', null); } if (instance instanceof ResponsiveTabs && typeof instance[options] === 'function') { return instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); } else { return this; } } }; }(jQuery, window));