﻿/**
 * "Tabbed content" (compact tabbed content sections)
 * jquery.compact.tabbed.js
 * @version 0.92
 * Changelog:
 *   *  2.0 has been rewritten using jquery.
 *   *  2.5 includes ARIA roles, states and properties .
 *   *  2.6 includes improved ARIA keyboard nav, and removed dependency on large utilities file .
 *   *  0.1 reset version number - separated from central compact file into own plugin file.
 *   *  0.6 animation of transition between tabs added. Added option to control whether current state is saved in cookie or not.
 *   *  0.7 new option added controlsPosition, to allow controls to be inserted after content (or before).
 *   *  0.8 when using variableHeight mode, height is set to auto after animation to allow for automatic resizing.
 *   *  0.9 improved ARIA roles, states and properties (now treated as a live region).
 *   *  0.91 By default, selecting a tab now updates the URL fragment id. Also, when a user sets the fragment id in the URL, if it matches a tabbed section, this section will be expanded on load.
 *   *  0.92 now supports content before and after tabbed sections (within compact container)
 *
 * @author Andrew Ramsden <http://irama.org/>
 * @see http://irama.org/web/dhtml/compact/tabbed/
 * @license GNU GENERAL PUBLIC LICENSE (GPL) <http://www.gnu.org/licenses/gpl.html>
 * 
 * @requires jQuery (tested with 1.3.1) <http://jquery.com/>
 * @requires jQuery jARIA plugin <http://outstandingelephant.com/jaria/>
 * @requires jQuery Compact base plugin <http://irama.org/web/dhtml/compact/>
 * 
 * @optional (but reccommended) jQuery ResizeEvents plugin <http://irama.org/web/dhtml/resize-events/>
 * @optional (but reccommended) jQuery Got Style? plugin <http://irama.org/web/dhtml/got-style/>
 * @optional (but reccommended) jQuery ARIA keyboard navigation plugin <http://irama.org/web/dhtml/aria/key-nav/>
 * @optional jQuery Cookies plugin <http://plugins.jquery.com/project/cookie>
 *
 * Notes:
 *    * The selector can be changed to target whichever element contains all of the 
 *      collapsable content sections.
 *    * Each section within MUST have a unique id.
 */
;
if (typeof jQuery.compact == 'undefined') {
	jQuery.compact = {};
}

jQuery.compact.tabbed = {};

/**
 * Default options, these can be overridden for each call to compactTabbed
 */
jQuery.compact.tabbed.defaultOptions = {
	type                  : 'tabbed',
	variableHeight        : false, // if set to true, height will animate based on content of current panel. If set to false, height of largest section will be set on load (relies on setting a height attribute for all content images in img tag markup).	
	transitionSpeed       : 'fast', // 'slow', 'def' or 'fast'
	heightAdjustSpeed     : 'def',
	rememberStateInCookie : true, // if cookie plugin is available the last focussed tab will be remembered
	controlsPosition      : 'before', // Controls (tabs) are inserted before or after content 'before' (default) or 'after'
	cookieExpiryDays      : 30
};

/**
 * Global configuration (these apply to every instance of compactTabbed, etc...)
 * Adjust to suit your preferred markup.
 */
jQuery.compact.tabbed.conf = {
	containerSelector    : '.'+$.compact._conf.compactClass+'.tabbed',
	activeClass          : $.compact._conf.activeClass,
	cookieRefPrefix      : 'c.tabbed#',
	viewportElClass      : 'viewport',
	viewportInnerElClass : 'viewport-inner', // extra div used for styling
	sectionClass         : 'section',
	headingClass         : 'heading',
	contentClass         : 'content',
	buttonClass          : 'button',
	closedClass          : 'closed',
	openClass            : 'open',
	currentClass         : 'current',
	updateURL            : true, // Update the fragment id at the end of the URL
	updateURLForIE6_7    : false // IE 6 and 7 will flicker when URL hash is updated
};


(function($) {// start closure
	
	$.compact.tabbed.initialised = [];


	$.fn.compactTabbed = function (options) {
		
		options = options || {};
		
		$(this).each(function () {
			initCompactTabbed.apply(this, [options])
		});
		
		return $(this); // facilitate chaining
	};
	
	$.compact.tabbed.init = function (containerEl, /* optional */ options) {
		options = options || {};
		
		initCompactTabbed.apply(containerEl, [options]);
	};
	
	
	$.compact.tabbed.handleResizeEvent = function() {
		for (n=0; n<$.compact.tabbed.initialised.length; n++) {
			adjustTabbedViewportHeight('#'+$.compact.tabbed.initialised[n]);
		}
	};
	
	
	initCompactTabbed = function (options) {
		
		// is CSS supported? (if gotStyle plugin is not avialable, assume it is supported).
			if (typeof $.browser.gotStyle != 'undefined' && $.browser.gotStyle(this) === false) {
				$.debug('DEBUG: Your browser does not have CSS support available or enabled, tabbed script exiting');
				return;
			}
		
		// must have a unique id
			if ($(this).attr('id') == '') {
				$.debug('DEBUG: Each "compact tabbed" ('+$.compact.tabbed.conf.containerSelector+') container must have a unique id');
				return false;
			}
			if (!$(this).is($.compact.tabbed.conf.containerSelector)) {
				// add the appropriate classes
				$(this).addClass($.compact.tabbed.conf.containerSelector.split('.').join(' '));
			}
		
		// Work out whether the URL should be updated
			updateURL = false;
			if ($.browser.msie && $.browser.version <=7) {
				if ($.compact.tabbed.conf.updateURLForIE6_7) {
					updateURL = true;
				}
			} else if ($.compact.tabbed.conf.updateURL) {
				updateURL = true;
			}
		
		// Merge runtime options with defaults
		// Note: The first argument sent to extend is an empty object to
		// prevent extend from overriding the default $.AKN.defaultOptions object.
			options = (typeof options == 'undefined')
				? $.compact.tabbed.defaultOptions
				: $.extend({}, $.compact.tabbed.defaultOptions, options)
			;
			options.updateURL = updateURL;
			$(this)
				.data('options', options)
				.ariaState('live','assertive') // All changes occur due to user-initiated actions, and are expected, be assertive!
			;
		
		
		
		
		// setup tabs
			var tabGroup = this;
			var tabList = $('<ul class="nl tabs"></ul>');
			var viewport = $('<div class="'+$.compact.tabbed.conf.viewportElClass+'"><div class="'+$.compact.tabbed.conf.viewportInnerElClass+'"></div></div>');
			var viewPortInner = viewport.find('.'+$.compact.tabbed.conf.viewportInnerElClass);
			
			//viewport.css('overflow','auto');
			//viewPortInner.css('overflow','auto');
			
			var firstTab = null;
			if (options.rememberStateInCookie) {
				var savedTabId = $.compact._cookie($.compact.tabbed.conf.cookieRefPrefix+$(this).attr('id')+'.current');
			} else {
				var savedTabId = null;
			}
			
		// ensure content remains in correct order
			// get content before sections
				preContent = $(tabGroup).children('.'+$.compact.tabbed.conf.sectionClass+':first').prevAll();
			
			// get all sections
				allSections = $(tabGroup).children('.'+$.compact.tabbed.conf.sectionClass);
			
			// get all other content (will be pushed after tabbed sections)
				otherContent = $(tabGroup).children().not(preContent).not(allSections);
				
			// check for matching frag id
				
				if (allSections.filter('[id='+$.frag()+']').size() > 0) {
					var matchedFragId = $.frag();
				} else {
					var matchedFragId = null;
				}
			
			viewPortInner.append(allSections);
			// preContent will end up on top
			$(tabGroup)
				.append(viewport)
				.append(otherContent)
			;
			
			
			// if open class is set, this section should be the default section (unless another state is set in cookie).
				var firstOpenSection = allSections.filter('.'+$.compact.tabbed.conf.openClass+':first');
				var defaultSection = (firstOpenSection.size()>0)?firstOpenSection.eq(0).attr('id'):null;
				
				
			// for each section... applied to each '.section' in this 'div.compact.tabbed' group
			allSections.each(function () {
				
				// make sure section has an id
					if($(this).attr('id') == '') {
						$.debug('DEBUG: Each tabbed section must have an unique id');
						
						$(this).addClass('error');
						return false;
					}
					var id = $(this).attr('id');
				
				
				// IDs for Aria
					var contentId = id+'-'+$.compact.tabbed.conf.contentClass;
					var buttonId = id+'-'+$.compact.tabbed.conf.buttonClass;
				
				// Get the the first and biggest heading for the link text
					heading = $(this).firstHeading(true);
					heading.addClass($.compact.tabbed.conf.headingClass);
					hText = heading.text();
				
				// add to tab list
					listItem = $('<li><a href="#'+id+'" id="'+buttonId+'"><span>'+hText+'</span></a></li>');
					listItem.find('a')
						.click(activateTab)
						.ariaState('pressed', 'false')
					;
					
					tabList.append(listItem);
				
				// wrap content
					var tempContent = $(this).children().not(heading);
					tempContent.remove();
					var tempContainer = $('<div class="'+$.compact.tabbed.conf.contentClass+'" id="'+contentId+'"></div>')
						.append(tempContent)
						//.css('overflow','auto')
						.ariaRole('tabpanel')
						.ariaState({
							hidden     : 'true',
							expanded   : 'false',
							labelledby : buttonId,
							atomic     : 'true' // each section can be considered atomic (label should also be presented with changes)
						})
					;
					$(this).append(tempContainer);
				
				// open or close tab
					if (matchedFragId !== null) {
						// fragment ID from URL matches a section
							initialiseSectionState.apply(this, [matchedFragId, listItem, tabGroup]);
					} else if (savedTabId !== null && $('#' + savedTabId).size() > 0) {
						// last tab is available from cookie
							//$.debug('DEBUG: jQuery(#' + savedTabId + ').size() ' + ($('#' + savedTabId).size()));
							initialiseSectionState.apply(this, [savedTabId, listItem, tabGroup]);
					} else if (defaultSection !== null) {
						// default (open) section was found in HTML
							initialiseSectionState.apply(this,[defaultSection, listItem, tabGroup]);
					} else {
						// set first tab as current
							if (firstTab === null) {
							// force init and expand
								initialiseSectionState.apply(this,[jQuery(this).attr('id'), listItem, tabGroup]);
							} else {
							// force init and collapse 
								initialiseSectionState.apply(this, [firstTab, listItem, tabGroup]);
							}
					}
				
				if (firstTab === null) {
					firstTab = this;
				}
				
			});
			
			// if ARIA keyboard nav plugin is available, use it
			if ($.fn.managefocus) {
				tabList
					.managefocus(
						'a',
						{
							role        : 'tablist',
							ignoreKeys  : [38,40],
							keyHandlers : {
								//38 : function(ev){ev.preventDefault();}, // prevent window scrolling up
								38 : tabNavigationEv, // up key will open tab
								40 : tabNavigationEv // down key will open tab
							}
						}
					)
					.controls(tabGroup)
				;
			}
			
			if (options.controlsPosition == 'before') {
				//$(tabGroup).prepend(tabList);
				viewport.eq(0).before(tabList);
			} else {
				$(tabGroup).append(tabList);
			}
		
			
		
		
		// Add active class
			$(this).addClass($.compact.tabbed.conf.activeClass);
			$.compact.tabbed.initialised[$.compact.tabbed.initialised.length++] = $(this).attr('id');
			
		
		// now active styles are applied, if container should have static height...
			if (!options.variableHeight) {
				// static height
				// find height of tallest tab, set to that height
					viewPortInner.height(getTallestTabbedSection(this));
			}
			
			return;
	};
	
	
	initialiseSectionState = function (expandThisSectionId, listItem, tabGroup) {
		if ($(this).attr('id') == expandThisSectionId) {
			expandSection.apply(this);
			listItem.addClass($.compact.tabbed.conf.currentClass);
			$(tabGroup).addClass($.compact.tabbed.conf.currentClass+'-'+$(this).attr('id'));
		} else {
			collapseSection.apply(this);
			$(this).hide();
			listItem.removeClass($.compact.tabbed.conf.currentClass);
		}
	}
	
	
	adjustTabbedViewportHeight = function (tabbedContainerSelector) {
		// init
			containerEl = $(tabbedContainerSelector);
			options = containerEl.data('options');	
			if (! options.variableHeight) {
				// static height
				// find height of tallest tab, animate to that height
					viewportEl = $(containerEl).find('.'+$.compact.tabbed.conf.viewportInnerElClass+':first');
					viewportEl.stop().animate({'height': getTallestTabbedSection(containerEl)}, options.heightAdjustSpeed);
					
			}
	};
	
	getTallestTabbedSection = function (containerEl) {
		
		//return 50;
		tallest = 0;
		$(containerEl).find('.'+$.compact.tabbed.conf.sectionClass).each(function() {
			if ((temp=$(this).fullHeight()) > tallest) {
				tallest = temp;
			}
		});
		return tallest;
	};
	
	tabNavigationEv = function (eventObj) {
		activateTab.apply(this);
		eventObj.preventDefault();
		return false;
	};
	
	function expandSection() {
		$(this)
			.removeClass($.compact.tabbed.conf.closedClass)
			.addClass($.compact.tabbed.conf.openClass)
			.ariaState({
				hidden : 'false',
				expanded : 'true'
			})
		;
	};
	
	function collapseSection() {
		$(this)
			.removeClass($.compact.tabbed.conf.openClass)
			.addClass($.compact.tabbed.conf.closedClass)
			.ariaState({
				hidden : 'true',
				expanded : 'false'
			})
		;
	};
	function activateSectionById (sectionId) {
		return $($.compact.tabbed.conf.containerSelector+' #'+sectionId).each(activateSection);
	};
	function activateSection () {
			
		// init
			containerEl = $(this).parents($.compact.tabbed.conf.containerSelector+':first');
			options = containerEl.data('options');
			viewportEl = $(this).parents('.'+$.compact.tabbed.conf.viewportInnerElClass+':first');
			newTab = $(this);
			previousTab = $(this).siblings('.'+$.compact.tabbed.conf.openClass);//'.'+$.compact.tabbed.conf.sectionClass+
		
		// remove previously "current" class, add new "current class"
			containerEl
				.removeClass($.compact.tabbed.conf.currentClass+'-'+previousTab.attr('id'))
				.addClass($.compact.tabbed.conf.currentClass+'-'+newTab.attr('id'))
			;
		
		// collapse open tab
			containerEl.ariaState('busy','true');
			collapseSection.apply(previousTab);
			previousTab.fadeOut(options.transitionSpeed, function(){
				// after fading out previous tab, expand newTab
					expandSection.apply(newTab);
					newTab.fadeIn(options.transitionSpeed)
					containerEl
						.ariaState('relevant','all') // content was hidden, other content was revealed
						.ariaState('busy','false')
					;
			});
			
		
		
		// check height if container should have variable height
			if (options.variableHeight && previousTab.fullHeight() != newTab.fullHeight()) {
				// animate the height difference
					viewportEl.stop().animate({'height': $(this).fullHeight()}, options.heightAdjustSpeed, '', function(){
						// after animation, set height to auto
							viewportEl.css('height','auto');
					});
			}
			
		// set cookie
			if (options.rememberStateInCookie) {	
				$.compact._cookie(
					$.compact.tabbed.conf.cookieRefPrefix+containerEl.attr('id')+'.current',
					$(this).attr('id'),
					{expires : options.cookieExpiryDays}
				);
			}
		
		
		// update the URL
			if (options.updateURL) {
				currentCompactFrag = $(this).attr('id');
				$.frag(currentCompactFrag,false);
			}
		// update tab status
			$('a[href$="#'+$(this).attr('id')+'"]').each(setCurrentTab);
		return;
	};
	function activateTab () {
		// activate matching section, strip hash from href to get correct id
		href = $(this).attr('href');
		activateSectionById(href.substr(href.indexOf('#')+1));
		setCurrentTab.apply(this);
		return false; // so anchor href isn't followed
	};
	function setCurrentTab () {
		$(this).parent().siblings()
			.removeClass($.compact.tabbed.conf.currentClass)
			.find('a').ariaState('pressed','false')
		;
		$(this).parent()
			.addClass($.compact.tabbed.conf.currentClass)
			.find('a').ariaState('pressed','true')
		;
	};
	
	
	
	
})(jQuery); /* end closure */
