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