diff --git a/lib/jquery.easytabs.js b/lib/jquery.easytabs.js index 64f612f..af2bb62 100644 --- a/lib/jquery.easytabs.js +++ b/lib/jquery.easytabs.js @@ -11,13 +11,6 @@ */ ( function($) { - // Triggers an event on an element and returns the event result - function fire(obj, name, data) { - var event = $.Event(name); - obj.trigger(event, data); - return event.result !== false; - } - $.easytabs = function(container, options) { var defaults = { @@ -43,8 +36,14 @@ panelClass: "", cache: true }, + + // Attach to plugin anything that should be available via + // the $container.data('easytabs') object plugin = this, $container = $(container), + + // Internal instance variables + // (not available via easytabs object) $defaultTab, $defaultTabLink, transitions, @@ -55,12 +54,21 @@ normal: 400, slow: 600 }, + + // Shorthand variable so that we don't need to call + // plugin.settings throughout the plugin code settings; + // ============================================================= + // Functions available via easytabs object + // ============================================================= + plugin.init = function() { plugin.settings = settings = $.extend({}, defaults, options); + // Add jQuery UI's crazy class names to markup, + // so that markup will match theme CSS if ( settings.uiTabs ) { settings.tabActiveClass = 'ui-tabs-selected'; settings.containerClass = 'ui-tabs ui-widget ui-widget-content ui-corner-all'; @@ -81,19 +89,31 @@ $('a.anchor').remove().prependTo('body'); + // Store easytabs object on container so we can easily set + // properties throughout $container.data('easytabs', {}); plugin.setTransitions(); - plugin.getTabs(); - plugin.addClasses(); - plugin.setDefaultTab(); - plugin.bindToTabClicks(); - plugin.initHashChange(); - plugin.initCycle(); + plugin.getTabs(); + + addClasses(); + + setDefaultTab(); + + bindToTabClicks(); + + initHashChange(); + + initCycle(); + + // Append data-easytabs HTML attribute to make easy to query for + // easytabs instances via CSS pseudo-selector $container.attr('data-easytabs', true); }; + // Set transitions for switching between tabs based on options. + // Could be used to update transitions if settings are changes. plugin.setTransitions = function() { transitions = ( settings.animate ) ? { show: settings.transitionIn, @@ -113,24 +133,27 @@ }; }; - plugin.bindToTabClicks = function() { - plugin.tabs.children("a").bind("click.easytabs", function(e) { - settings.cycle = false; - skipUpdateToHash = false; - plugin.selectTab( $(this) ); - e.preventDefault(); - }); - }; - + // Find and instantiate tabs and panels. + // Could be used to reset tab and panel collection if markup is + // modified. plugin.getTabs = function() { var $matchingPanel; + // Find the initial set of elements matching the setting.tabs + // CSS selector within the container plugin.tabs = $container.find(settings.tabs), + + // Instantiate panels as empty jquery object plugin.panels = $(), plugin.tabs.each(function(){ var $tab = $(this), $a = $tab.children('a'), + + // targetId is the ID of the panel, which is either the + // `href` attribute for non-ajax tabs, or in the + // `data-target` attribute for ajax tabs since the `href` is + // the ajax URL targetId = $tab.children('a').data('target'); $tab.data('easytabs', {}); @@ -145,7 +168,9 @@ $matchingPanel = $container.find("#" + targetId); + // If tab has a matching panel, add it to panels if ( $matchingPanel.length ) { + // Store panel height before hiding $matchingPanel.data('easytabs', { position: $matchingPanel.css('position'), @@ -155,20 +180,195 @@ plugin.panels = plugin.panels.add($matchingPanel.hide()); $tab.data('easytabs').panel = $matchingPanel; + + // Otherwise, remove tab from tabs collection } else { - plugin.tabs = plugin.tabs.not($tab); // excludes tabs from set that don't have a target div + plugin.tabs = plugin.tabs.not($tab); } }); }; - plugin.addClasses = function() { + // Select tab and fire callback + plugin.selectTab = function($clicked, callback) { + var url = window.location, + hash = url.hash.match(/^[^\?]*/)[0], + $targetPanel = $clicked.parent().data('easytabs').panel, + ajaxUrl = $clicked.parent().data('easytabs').ajax; + + // Tab is collapsible and active => toggle collapsed state + if( settings.collapsible && ! skipUpdateToHash && ($clicked.hasClass(settings.tabActiveClass) || $clicked.hasClass(settings.collapsedClass)) ) { + plugin.toggleTabCollapse($clicked, $targetPanel, ajaxUrl, callback); + + // Tab is not active and panel is not active => select tab + } else if( ! $clicked.hasClass(settings.tabActiveClass) || ! $targetPanel.hasClass(settings.panelActiveClass) ){ + activateTab($clicked, $targetPanel, ajaxUrl, callback); + } + + }; + + // Toggle tab collapsed state and fire callback + plugin.toggleTabCollapse = function($clicked, $targetPanel, ajaxUrl, callback) { + plugin.panels.stop(true,true); + + if( fire($container,"easytabs:before", [$clicked, $targetPanel, settings]) ){ + plugin.tabs.filter("." + settings.tabActiveClass).removeClass(settings.tabActiveClass).children().removeClass(settings.tabActiveClass); + + // If panel is collapsed, uncollapse it + if( $clicked.hasClass(settings.collapsedClass) ){ + + // If ajax panel and not already cached + if( ajaxUrl && (!settings.cache || !$clicked.parent().data('easytabs').cached) ) { + $container.trigger('easytabs:ajax:beforeSend', [$clicked, $targetPanel]); + + $targetPanel.load(ajaxUrl, function(response, status, xhr){ + $clicked.parent().data('easytabs').cached = true; + $container.trigger('easytabs:ajax:complete', [$clicked, $targetPanel, response, status, xhr]); + }); + } + + // Update CSS classes of tab and panel + $clicked.parent() + .removeClass(settings.collapsedClass) + .addClass(settings.tabActiveClass) + .children() + .removeClass(settings.collapsedClass) + .addClass(settings.tabActiveClass); + + $targetPanel + .addClass(settings.panelActiveClass) + [transitions.uncollapse](transitions.speed, function(){ + $container.trigger('easytabs:midTransition', [$clicked, $targetPanel, settings]); + if(typeof callback == 'function') callback(); + }); + + // Otherwise, collapse it + } else { + + // Update CSS classes of tab and panel + $clicked.addClass(settings.collapsedClass) + .parent() + .addClass(settings.collapsedClass); + + $targetPanel + .removeClass(settings.panelActiveClass) + [transitions.collapse](transitions.speed, function(){ + $container.trigger("easytabs:midTransition", [$clicked, $targetPanel, settings]); + if(typeof callback == 'function') callback(); + }); + } + } + }; + + + // Find tab with target panel matching value + plugin.matchTab = function(hash) { + return plugin.tabs.find("[href='" + hash + "'],[data-target='" + hash + "']").first(); + }; + + // Find panel with `id` matching value + plugin.matchInPanel = function(hash) { + return ( hash ? plugin.panels.filter(':has(' + hash + ')').first() : [] ); + }; + + // Select matching tab when URL hash changes + plugin.selectTabFromHashChange = function() { + var hash = window.location.hash.match(/^[^\?]*/)[0], + $tab = plugin.matchTab(hash), + $panel; + + if ( settings.updateHash ) { + + // If hash directly matches tab + if( $tab.length ){ + skipUpdateToHash = true; + plugin.selectTab( $tab ); + + } else { + $panel = plugin.matchInPanel(hash); + + // If panel contains element matching hash + if ( $panel.length ) { + hash = '#' + $panel.attr('id'); + $tab = plugin.matchTab(hash); + skipUpdateToHash = true; + plugin.selectTab( $tab ); + + // If default tab is not active... + } else if ( ! $defaultTab.hasClass(settings.tabActiveClass) && ! settings.cycle ) { + + // ...and hash is blank or matches a parent of the tab container or + // if the last tab (before the hash updated) was one of the other tabs in this container. + if ( hash === '' || plugin.matchTab(lastHash).length || $container.closest(hash).length ) { + skipUpdateToHash = true; + plugin.selectTab( $defaultTabLink ); + } + } + } + } + }; + + // Cycle through tabs + plugin.cycleTabs = function(tabNumber){ + if(settings.cycle){ + tabNumber = tabNumber % plugin.tabs.length; + $tab = $( plugin.tabs[tabNumber] ).children("a").first(); + skipUpdateToHash = true; + plugin.selectTab( $tab, function() { + setTimeout(function(){ plugin.cycleTabs(tabNumber + 1); }, settings.cycle); + }); + } + }; + + // Convenient public methods + plugin.publicMethods = { + select: function(tabSelector){ + var $tab; + + // Find tab container that matches selector (like 'li#tab-one' which contains tab link) + if ( ($tab = plugin.tabs.filter(tabSelector)).length === 0 ) { + + // Find direct tab link that matches href (like 'a[href="#panel-1"]') + if ( ($tab = plugin.tabs.find("a[href='" + tabSelector + "']")).length === 0 ) { + + // Find direct tab link that matches selector (like 'a#tab-1') + if ( ($tab = plugin.tabs.find("a" + tabSelector)).length === 0 ) { + + // Find direct tab link that matches data-target (lik 'a[data-target="#panel-1"]') + if ( ($tab = plugin.tabs.find("[data-target='" + tabSelector + "']")).length === 0 ) { + $.error('Tab \'' + tabSelector + '\' does not exist in tab set'); + } + } + } + } else { + // Select the child tab link, since the first option finds the tab container (like
  • ) + $tab = $tab.children("a").first(); + } + plugin.selectTab($tab); + } + }; + + // ============================================================= + // Private functions + // ============================================================= + + // Triggers an event on an element and returns the event result + var fire = function(obj, name, data) { + var event = $.Event(name); + obj.trigger(event, data); + return event.result !== false; + } + + // Add CSS classes to markup (if specified), called by init + var addClasses = function() { $container.addClass(settings.containerClass); plugin.tabs.parent().addClass(settings.tabsClass); plugin.tabs.addClass(settings.tabClass); plugin.panels.addClass(settings.panelClass); }; - plugin.setDefaultTab = function(){ + // Set the default tab, whether from hash (bookmarked) or option, + // called by init + var setDefaultTab = function(){ var hash = window.location.hash.match(/^[^\?]*/)[0], $selectedTab = plugin.matchTab(hash).parent(), $panel; @@ -198,10 +398,11 @@ $defaultTabLink = $defaultTab.children("a").first(); - plugin.activateDefaultTab($selectedTab); + activateDefaultTab($selectedTab); }; - plugin.activateDefaultTab = function($selectedTab) { + // Activate defaultTab (or collapse by default), called by setDefaultTab + var activateDefaultTab = function($selectedTab) { var defaultPanel, defaultAjaxUrl; @@ -210,6 +411,7 @@ .addClass(settings.collapsedClass) .children() .addClass(settings.collapsedClass); + } else { defaultPanel = $( $defaultTab.data('easytabs').panel ); @@ -234,111 +436,39 @@ } }; - plugin.getHeightForHidden = function($targetPanel){ + // Bind tab-select funtionality to namespaced click event, called by + // init + var bindToTabClicks = function() { + plugin.tabs.children("a").bind("click.easytabs", function(e) { - if ( $targetPanel.data('easytabs') && $targetPanel.data('easytabs').lastHeight ) { - return $targetPanel.data('easytabs').lastHeight; - } + // Stop cycling when a tab is clicked + settings.cycle = false; - var display = $targetPanel.css('display'), // this is the only property easytabs changes, so we need to grab its value on each tab change - height = $targetPanel - // Workaround, because firefox returns wrong height if element itself has absolute positioning - .wrap($('
    ', {position: 'absolute', 'visibility': 'hidden', 'overflow': 'hidden'})) - .css({'position':'relative','visibility':'hidden','display':'block'}) - .outerHeight(); + // Hash will be updated when tab is clicked, + // don't cause tab to re-select when hash-change event is fired + skipUpdateToHash = false; - $targetPanel.unwrap(); - // Return element to previous state - $targetPanel.css({ - position: $targetPanel.data('easytabs').position, - visibility: $targetPanel.data('easytabs').visibility, - display: display + // Select the panel for the clicked tab + plugin.selectTab( $(this) ); + + // Don't follow the link to the anchor + e.preventDefault(); }); - // Cache height - $targetPanel.data('easytabs').lastHeight = height; - return height; }; - plugin.setAndReturnHeight = function($visiblePanel) { - // Since the height of the visible panel may have been manipulated due to interaction, - // we want to re-cache the visible height on each tab change - var height = $visiblePanel.outerHeight(); - - if( $visiblePanel.data('easytabs') ) { - $visiblePanel.data('easytabs').lastHeight = height; - } else { - $visiblePanel.data('easytabs', {lastHeight: height}); - } - return height; - }; - - plugin.selectTab = function($clicked, callback) { - var url = window.location, - hash = url.hash.match(/^[^\?]*/)[0], - $targetPanel = $clicked.parent().data('easytabs').panel, - ajaxUrl = $clicked.parent().data('easytabs').ajax; - - // Tab is collapsible and active => toggle collapsed state - if( settings.collapsible && ! skipUpdateToHash && ($clicked.hasClass(settings.tabActiveClass) || $clicked.hasClass(settings.collapsedClass)) ) { - plugin.toggleTabCollapse($clicked, $targetPanel, ajaxUrl, callback); - - // Tab is not active and panel is not active => select tab - } else if( ! $clicked.hasClass(settings.tabActiveClass) || ! $targetPanel.hasClass(settings.panelActiveClass) ){ - plugin.activateTab($clicked, $targetPanel, ajaxUrl, callback); - } - - }; - - plugin.toggleTabCollapse = function($clicked, $targetPanel, ajaxUrl, callback) { - plugin.panels.stop(true,true); - - if( fire($container,"easytabs:before", [$clicked, $targetPanel, settings]) ){ - plugin.tabs.filter("." + settings.tabActiveClass).removeClass(settings.tabActiveClass).children().removeClass(settings.tabActiveClass); - - // If panel is collapsed, uncollapse it - if( $clicked.hasClass(settings.collapsedClass) ){ - - if( ajaxUrl && (!settings.cache || !$clicked.parent().data('easytabs').cached) ) { - $container.trigger('easytabs:ajax:beforeSend', [$clicked, $targetPanel]); - - $targetPanel.load(ajaxUrl, function(response, status, xhr){ - $clicked.parent().data('easytabs').cached = true; - $container.trigger('easytabs:ajax:complete', [$clicked, $targetPanel, response, status, xhr]); - }); - } - - $clicked.parent() - .removeClass(settings.collapsedClass) - .addClass(settings.tabActiveClass) - .children() - .removeClass(settings.collapsedClass) - .addClass(settings.tabActiveClass); - - $targetPanel - .addClass(settings.panelActiveClass) - [transitions.uncollapse](transitions.speed, function(){ - $container.trigger('easytabs:midTransition', [$clicked, $targetPanel, settings]); - if(typeof callback == 'function') callback(); - }); - - // Otherwise, collapse it - } else { - - $clicked.addClass(settings.collapsedClass) - .parent() - .addClass(settings.collapsedClass); - - $targetPanel - .removeClass(settings.panelActiveClass) - [transitions.collapse](transitions.speed, function(){ - $container.trigger("easytabs:midTransition", [$clicked, $targetPanel, settings]); - if(typeof callback == 'function') callback(); - }); - } - } - }; - - plugin.activateTab = function($clicked, $targetPanel, ajaxUrl, callback) { + // Activate a given tab/panel, called from plugin.selectTab: + // + // * fire `easytabs:before` hook + // * get ajax if new tab is an uncached ajax tab + // * animate out previously-active panel + // * fire `easytabs:midTransition` hook + // * update URL hash + // * animate in newly-active panel + // * update CSS classes for inactive and active tabs/panels + // + // TODO: This could probably be broken out into many more modular + // functions + var activateTab = function($clicked, $targetPanel, ajaxUrl, callback) { plugin.panels.stop(true,true); if( fire($container,"easytabs:before", [$clicked, $targetPanel, settings]) ){ @@ -351,8 +481,8 @@ hash = window.location.hash.match(/^[^\?]*/)[0]; if (settings.animate) { - targetHeight = plugin.getHeightForHidden($targetPanel); - visibleHeight = $visiblePanel.length ? plugin.setAndReturnHeight($visiblePanel) : 0; + targetHeight = getHeightForHidden($targetPanel); + visibleHeight = $visiblePanel.length ? setAndReturnHeight($visiblePanel) : 0; heightDifference = targetHeight - visibleHeight; } @@ -432,62 +562,56 @@ } }; - plugin.matchTab = function(hash) { - return plugin.tabs.find("[href='" + hash + "'],[data-target='" + hash + "']").first(); - }; + // Get heights of panels to enable animation between panels of + // differing heights, called by activateTab + var getHeightForHidden = function($targetPanel){ - plugin.matchInPanel = function(hash) { - return ( hash ? plugin.panels.filter(':has(' + hash + ')').first() : [] ); - }; - - plugin.selectTabFromHashChange = function() { - var hash = window.location.hash.match(/^[^\?]*/)[0], - $tab = plugin.matchTab(hash), - $panel; - - if ( settings.updateHash ) { - - // If hash directly matches tab - if( $tab.length ){ - skipUpdateToHash = true; - plugin.selectTab( $tab ); - - } else { - $panel = plugin.matchInPanel(hash); - - // If panel contains element matching hash - if ( $panel.length ) { - hash = '#' + $panel.attr('id'); - $tab = plugin.matchTab(hash); - skipUpdateToHash = true; - plugin.selectTab( $tab ); - - // If default tab is not active... - } else if ( ! $defaultTab.hasClass(settings.tabActiveClass) && ! settings.cycle ) { - - // ...and hash is blank or matches a parent of the tab container or - // if the last tab (before the hash updated) was one of the other tabs in this container. - if ( hash === '' || plugin.matchTab(lastHash).length || $container.closest(hash).length ) { - skipUpdateToHash = true; - plugin.selectTab( $defaultTabLink ); - } - } - } + if ( $targetPanel.data('easytabs') && $targetPanel.data('easytabs').lastHeight ) { + return $targetPanel.data('easytabs').lastHeight; } + + // this is the only property easytabs changes, so we need to grab its value on each tab change + var display = $targetPanel.css('display'), + + // Workaround, because firefox returns wrong height if element itself has absolute positioning + height = $targetPanel + .wrap($('
    ', {position: 'absolute', 'visibility': 'hidden', 'overflow': 'hidden'})) + .css({'position':'relative','visibility':'hidden','display':'block'}) + .outerHeight(); + + $targetPanel.unwrap(); + + // Return element to previous state + $targetPanel.css({ + position: $targetPanel.data('easytabs').position, + visibility: $targetPanel.data('easytabs').visibility, + display: display + }); + + // Cache height + $targetPanel.data('easytabs').lastHeight = height; + + return height; }; - plugin.cycleTabs = function(tabNumber){ - if(settings.cycle){ - tabNumber = tabNumber % plugin.tabs.length; - $tab = $( plugin.tabs[tabNumber] ).children("a").first(); - skipUpdateToHash = true; - plugin.selectTab( $tab, function() { - setTimeout(function(){ plugin.cycleTabs(tabNumber + 1); }, settings.cycle); - }); + // Since the height of the visible panel may have been manipulated due to interaction, + // we want to re-cache the visible height on each tab change, called + // by activateTab + var setAndReturnHeight = function($visiblePanel) { + var height = $visiblePanel.outerHeight(); + + if( $visiblePanel.data('easytabs') ) { + $visiblePanel.data('easytabs').lastHeight = height; + } else { + $visiblePanel.data('easytabs', {lastHeight: height}); } + return height; }; - plugin.initHashChange = function(){ + // Setup hash-change callback for forward- and back-button + // functionality, called by init + var initHashChange = function(){ + // enabling back-button with jquery.hashchange plugin // http://benalman.com/projects/jquery-hashchange-plugin/ if(typeof $(window).hashchange === 'function'){ @@ -501,7 +625,8 @@ } }; - plugin.initCycle = function(){ + // Begin cycling if set in options, called by init + var initCycle = function(){ var tabNumber; if (settings.cycle) { tabNumber = plugin.tabs.index($defaultTab); @@ -509,32 +634,6 @@ } }; - plugin.publicMethods = { - select: function(tabSelector){ - var $tab; - - // Find tab container that matches selector (like 'li#tab-one' which contains tab link) - if ( ($tab = plugin.tabs.filter(tabSelector)).length === 0 ) { - - // Find direct tab link that matches href (like 'a[href="#panel-1"]') - if ( ($tab = plugin.tabs.find("a[href='" + tabSelector + "']")).length === 0 ) { - - // Find direct tab link that matches selector (like 'a#tab-1') - if ( ($tab = plugin.tabs.find("a" + tabSelector)).length === 0 ) { - - // Find direct tab link that matches data-target (lik 'a[data-target="#panel-1"]') - if ( ($tab = plugin.tabs.find("[data-target='" + tabSelector + "']")).length === 0 ) { - $.error('Tab \'' + tabSelector + '\' does not exist in tab set'); - } - } - } - } else { - // Select the child tab link, since the first option finds the tab container (like
  • ) - $tab = $tab.children("a").first(); - } - plugin.selectTab($tab); - } - }; plugin.init(); @@ -542,6 +641,7 @@ $.fn.easytabs = function(options) { var args = arguments; + return this.each(function() { var $this = $(this), plugin = $this.data('easytabs');