text: /*! FixedHeader 2.1.2
   * ©2010-2014 SpryMedia Ltd - datatables.net/license
   */
  
  /**
   * @summary     FixedHeader
   * @description Fix a table's header or footer, so it is always visible while
   *              Scrolling
   * @version     2.1.2
   * @file        dataTables.fixedHeader.js
   * @author      SpryMedia Ltd (www.sprymedia.co.uk)
   * @contact     www.sprymedia.co.uk/contact
   * @copyright   Copyright 2009-2014 SpryMedia Ltd.
   *
   * This source file is free software, available under the following license:
   *   MIT license - http://datatables.net/license/mit
   *
   * This source file is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
   *
   * For details please refer to: http://www.datatables.net
   */
  
  /* Global scope for FixedColumns for backwards compatibility - will be removed
   * in future. Not documented in 1.1.x.
   */
  
  /* Global scope for FixedColumns */
  var FixedHeader;
  
  (function(window, document, undefined) {
  
  
  var factory = function( $, DataTable ) {
  "use strict";
  
  /*
   * Function: FixedHeader
   * Purpose:  Provide 'fixed' header, footer and columns for a DataTable
   * Returns:  object:FixedHeader - must be called with 'new'
   * Inputs:   mixed:mTable - target table
   *  @param {object} dt DataTables instance or HTML table node. With DataTables
   *    1.10 this can also be a jQuery collection (with just a single table in its
   *    result set), a jQuery selector, DataTables API instance or settings
   *    object.
   *  @param {object} [oInit] initialisation settings, with the following
   *    properties (each optional)
   *    * bool:top -    fix the header (default true)
   *    * bool:bottom - fix the footer (default false)
   *    * int:left -    fix the left column(s) (default 0)
   *    * int:right -   fix the right column(s) (default 0)
   *    * int:zTop -    fixed header zIndex
   *    * int:zBottom - fixed footer zIndex
   *    * int:zLeft -   fixed left zIndex
   *    * int:zRight -  fixed right zIndex
   */
  FixedHeader = function ( mTable, oInit ) {
  	/* Sanity check - you just know it will happen */
  	if ( ! this instanceof FixedHeader )
  	{
  		alert( "FixedHeader warning: FixedHeader must be initialised with the 'new' keyword." );
  		return;
  	}
  
  	var that = this;
  	var oSettings = {
  		"aoCache": [],
  		"oSides": {
  			"top": true,
  			"bottom": false,
  			"left": 0,
  			"right": 0
  		},
  		"oZIndexes": {
  			"top": 104,
  			"bottom": 103,
  			"left": 102,
  			"right": 101
  		},
  		"oCloneOnDraw": {
  			"top": false,
  			"bottom": false,
  			"left": true,
  			"right": true
  		},
  		"oMes": {
  			"iTableWidth": 0,
  			"iTableHeight": 0,
  			"iTableLeft": 0,
  			"iTableRight": 0, /* note this is left+width, not actually "right" */
  			"iTableTop": 0,
  			"iTableBottom": 0 /* note this is top+height, not actually "bottom" */
  		},
  		"oOffset": {
  			"top": 0
  		},
  		"nTable": null,
  		"bFooter": false,
  		"bInitComplete": false
  	};
  
  	/*
  	 * Function: fnGetSettings
  	 * Purpose:  Get the settings for this object
  	 * Returns:  object: - settings object
  	 * Inputs:   -
  	 */
  	this.fnGetSettings = function () {
  		return oSettings;
  	};
  
  	/*
  	 * Function: fnUpdate
  	 * Purpose:  Update the positioning and copies of the fixed elements
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	this.fnUpdate = function () {
  		this._fnUpdateClones();
  		this._fnUpdatePositions();
  	};
  
  	/*
  	 * Function: fnPosition
  	 * Purpose:  Update the positioning of the fixed elements
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	this.fnPosition = function () {
  		this._fnUpdatePositions();
  	};
  
  
  	var dt = $.fn.dataTable.Api ?
  		new $.fn.dataTable.Api( mTable ).settings()[0] :
  		mTable.fnSettings();
  
  	dt._oPluginFixedHeader = this;
  
  	/* Let's do it */
  	this.fnInit( dt, oInit );
  
  };
  
  
  /*
   * Variable: FixedHeader
   * Purpose:  Prototype for FixedHeader
   * Scope:    global
   */
  FixedHeader.prototype = {
  	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  	 * Initialisation
  	 */
  
  	/*
  	 * Function: fnInit
  	 * Purpose:  The "constructor"
  	 * Returns:  -
  	 * Inputs:   {as FixedHeader function}
  	 */
  	fnInit: function ( oDtSettings, oInit )
  	{
  		var s = this.fnGetSettings();
  		var that = this;
  
  		/* Record the user definable settings */
  		this.fnInitSettings( s, oInit );
  
  		if ( oDtSettings.oScroll.sX !== "" || oDtSettings.oScroll.sY !== "" )
  		{
  			alert( "FixedHeader 2 is not supported with DataTables' scrolling mode at this time" );
  			return;
  		}
  
  		s.nTable = oDtSettings.nTable;
  		oDtSettings.aoDrawCallback.unshift( {
  			"fn": function () {
  				FixedHeader.fnMeasure();
  				that._fnUpdateClones.call(that);
  				that._fnUpdatePositions.call(that);
  			},
  			"sName": "FixedHeader"
  		} );
  
  		s.bFooter = ($('>tfoot', s.nTable).length > 0) ? true : false;
  
  		/* Add the 'sides' that are fixed */
  		if ( s.oSides.top )
  		{
  			s.aoCache.push( that._fnCloneTable( "fixedHeader", "FixedHeader_Header", that._fnCloneThead ) );
  		}
  		if ( s.oSides.bottom )
  		{
  			s.aoCache.push( that._fnCloneTable( "fixedFooter", "FixedHeader_Footer", that._fnCloneTfoot ) );
  		}
  		if ( s.oSides.left )
  		{
  			s.aoCache.push( that._fnCloneTable( "fixedLeft", "FixedHeader_Left", that._fnCloneTLeft, s.oSides.left ) );
  		}
  		if ( s.oSides.right )
  		{
  			s.aoCache.push( that._fnCloneTable( "fixedRight", "FixedHeader_Right", that._fnCloneTRight, s.oSides.right ) );
  		}
  
  		/* Event listeners for window movement */
  		FixedHeader.afnScroll.push( function () {
  			that._fnUpdatePositions.call(that);
  		} );
  
  		$(window).resize( function () {
  			FixedHeader.fnMeasure();
  			that._fnUpdateClones.call(that);
  			that._fnUpdatePositions.call(that);
  		} );
  
  		$(s.nTable)
  			.on('column-reorder.dt', function () {
  				FixedHeader.fnMeasure();
  				that._fnUpdateClones( true );
  				that._fnUpdatePositions();
  			} )
  			.on('column-visibility.dt', function () {
  				FixedHeader.fnMeasure();
  				that._fnUpdateClones( true );
  				that._fnUpdatePositions();
  			} );
  
  		/* Get things right to start with */
  		FixedHeader.fnMeasure();
  		that._fnUpdateClones();
  		that._fnUpdatePositions();
  
  		s.bInitComplete = true;
  	},
  
  
  	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  	 * Support functions
  	 */
  
  	/*
  	 * Function: fnInitSettings
  	 * Purpose:  Take the user's settings and copy them to our local location
  	 * Returns:  -
  	 * Inputs:   object:s - the local settings object
  	 *           object:oInit - the user's settings object
  	 */
  	fnInitSettings: function ( s, oInit )
  	{
  		if ( oInit !== undefined )
  		{
  			if ( oInit.top !== undefined ) {
  				s.oSides.top = oInit.top;
  			}
  			if ( oInit.bottom !== undefined ) {
  				s.oSides.bottom = oInit.bottom;
  			}
  			if ( typeof oInit.left == 'boolean' ) {
  				s.oSides.left = oInit.left ? 1 : 0;
  			}
  			else if ( oInit.left !== undefined ) {
  				s.oSides.left = oInit.left;
  			}
  			if ( typeof oInit.right == 'boolean' ) {
  				s.oSides.right = oInit.right ? 1 : 0;
  			}
  			else if ( oInit.right !== undefined ) {
  				s.oSides.right = oInit.right;
  			}
  
  			if ( oInit.zTop !== undefined ) {
  				s.oZIndexes.top = oInit.zTop;
  			}
  			if ( oInit.zBottom !== undefined ) {
  				s.oZIndexes.bottom = oInit.zBottom;
  			}
  			if ( oInit.zLeft !== undefined ) {
  				s.oZIndexes.left = oInit.zLeft;
  			}
  			if ( oInit.zRight !== undefined ) {
  				s.oZIndexes.right = oInit.zRight;
  			}
  
  			if ( oInit.offsetTop !== undefined ) {
  				s.oOffset.top = oInit.offsetTop;
  			}
  			if ( oInit.alwaysCloneTop !== undefined ) {
  				s.oCloneOnDraw.top = oInit.alwaysCloneTop;
  			}
  			if ( oInit.alwaysCloneBottom !== undefined ) {
  				s.oCloneOnDraw.bottom = oInit.alwaysCloneBottom;
  			}
  			if ( oInit.alwaysCloneLeft !== undefined ) {
  				s.oCloneOnDraw.left = oInit.alwaysCloneLeft;
  			}
  			if ( oInit.alwaysCloneRight !== undefined ) {
  				s.oCloneOnDraw.right = oInit.alwaysCloneRight;
  			}
  		}
  	},
  
  	/*
  	 * Function: _fnCloneTable
  	 * Purpose:  Clone the table node and do basic initialisation
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	_fnCloneTable: function ( sType, sClass, fnClone, iCells )
  	{
  		var s = this.fnGetSettings();
  		var nCTable;
  
  		/* We know that the table _MUST_ has a DIV wrapped around it, because this is simply how
  		 * DataTables works. Therefore, we can set this to be relatively position (if it is not
  		 * alreadu absolute, and use this as the base point for the cloned header
  		 */
  		if ( $(s.nTable.parentNode).css('position') != "absolute" )
  		{
  			s.nTable.parentNode.style.position = "relative";
  		}
  
  		/* Just a shallow clone will do - we only want the table node */
  		nCTable = s.nTable.cloneNode( false );
  		nCTable.removeAttribute( 'id' );
  
  		var nDiv = document.createElement( 'div' );
  		nDiv.style.position = "absolute";
  		nDiv.style.top = "0px";
  		nDiv.style.left = "0px";
  		nDiv.className += " FixedHeader_Cloned "+sType+" "+sClass;
  
  		/* Set the zIndexes */
  		if ( sType == "fixedHeader" )
  		{
  			nDiv.style.zIndex = s.oZIndexes.top;
  		}
  		if ( sType == "fixedFooter" )
  		{
  			nDiv.style.zIndex = s.oZIndexes.bottom;
  		}
  		if ( sType == "fixedLeft" )
  		{
  			nDiv.style.zIndex = s.oZIndexes.left;
  		}
  		else if ( sType == "fixedRight" )
  		{
  			nDiv.style.zIndex = s.oZIndexes.right;
  		}
  
  		/* remove margins since we are going to position it absolutely */
  		nCTable.style.margin = "0";
  
  		/* Insert the newly cloned table into the DOM, on top of the "real" header */
  		nDiv.appendChild( nCTable );
  		document.body.appendChild( nDiv );
  
  		return {
  			"nNode": nCTable,
  			"nWrapper": nDiv,
  			"sType": sType,
  			"sPosition": "",
  			"sTop": "",
  			"sLeft": "",
  			"fnClone": fnClone,
  			"iCells": iCells
  		};
  	},
  
  	/*
  	 * Function: _fnMeasure
  	 * Purpose:  Get the current positioning of the table in the DOM
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	_fnMeasure: function ()
  	{
  		var
  			s = this.fnGetSettings(),
  			m = s.oMes,
  			jqTable = $(s.nTable),
  			oOffset = jqTable.offset(),
  			iParentScrollTop = this._fnSumScroll( s.nTable.parentNode, 'scrollTop' ),
  			iParentScrollLeft = this._fnSumScroll( s.nTable.parentNode, 'scrollLeft' );
  
  		m.iTableWidth = jqTable.outerWidth();
  		m.iTableHeight = jqTable.outerHeight();
  		m.iTableLeft = oOffset.left + s.nTable.parentNode.scrollLeft;
  		m.iTableTop = oOffset.top + iParentScrollTop;
  		m.iTableRight = m.iTableLeft + m.iTableWidth;
  		m.iTableRight = FixedHeader.oDoc.iWidth - m.iTableLeft - m.iTableWidth;
  		m.iTableBottom = FixedHeader.oDoc.iHeight - m.iTableTop - m.iTableHeight;
  	},
  
  	/*
  	 * Function: _fnSumScroll
  	 * Purpose:  Sum node parameters all the way to the top
  	 * Returns:  int: sum
  	 * Inputs:   node:n - node to consider
  	 *           string:side - scrollTop or scrollLeft
  	 */
  	_fnSumScroll: function ( n, side )
  	{
  		var i = n[side];
  		while ( n = n.parentNode )
  		{
  			if ( n.nodeName == 'HTML' || n.nodeName == 'BODY' )
  			{
  				break;
  			}
  			i = n[side];
  		}
  		return i;
  	},
  
  	/*
  	 * Function: _fnUpdatePositions
  	 * Purpose:  Loop over the fixed elements for this table and update their positions
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	_fnUpdatePositions: function ()
  	{
  		var s = this.fnGetSettings();
  		this._fnMeasure();
  
  		for ( var i=0, iLen=s.aoCache.length ; i<iLen ; i++ )
  		{
  			if ( s.aoCache[i].sType == "fixedHeader" )
  			{
  				this._fnScrollFixedHeader( s.aoCache[i] );
  			}
  			else if ( s.aoCache[i].sType == "fixedFooter" )
  			{
  				this._fnScrollFixedFooter( s.aoCache[i] );
  			}
  			else if ( s.aoCache[i].sType == "fixedLeft" )
  			{
  				this._fnScrollHorizontalLeft( s.aoCache[i] );
  			}
  			else
  			{
  				this._fnScrollHorizontalRight( s.aoCache[i] );
  			}
  		}
  	},
  
  	/*
  	 * Function: _fnUpdateClones
  	 * Purpose:  Loop over the fixed elements for this table and call their cloning functions
  	 * Returns:  -
  	 * Inputs:   -
  	 */
  	_fnUpdateClones: function ( full )
  	{
  		var s = this.fnGetSettings();
  
  		if ( full ) {
  			// This is a little bit of a hack to force a full clone draw. When
  			// `full` is set to true, we want to reclone the source elements,
  			// regardless of the clone-on-draw settings
  			s.bInitComplete = false;
  		}
  
  		for ( var i=0, iLen=s.aoCache.length ; i<iLen ; i++ )
  		{
  			s.aoCache[i].fnClone.call( this, s.aoCache[i] );
  		}
  
  		if ( full ) {
  			s.bInitComplete = true;
  		}
  	},
  
  
  	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  	 * Scrolling functions
  	 */
  
  	/*
  	 * Function: _fnScrollHorizontalLeft
  	 * Purpose:  Update the positioning of the scrolling elements
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnScrollHorizontalRight: function ( oCache )
  	{
  		var
  			s = this.fnGetSettings(),
  			oMes = s.oMes,
  			oWin = FixedHeader.oWin,
  			oDoc = FixedHeader.oDoc,
  			nTable = oCache.nWrapper,
  			iFixedWidth = $(nTable).outerWidth();
  
  		if ( oWin.iScrollRight < oMes.iTableRight )
  		{
  			/* Fully right aligned */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', (oMes.iTableLeft+oMes.iTableWidth-iFixedWidth)+"px", 'left', nTable.style );
  		}
  		else if ( oMes.iTableLeft < oDoc.iWidth-oWin.iScrollRight-iFixedWidth )
  		{
  			/* Middle */
  			this._fnUpdateCache( oCache, 'sPosition', 'fixed', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop-oWin.iScrollTop)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', (oWin.iWidth-iFixedWidth)+"px", 'left', nTable.style );
  		}
  		else
  		{
  			/* Fully left aligned */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  	},
  
  	/*
  	 * Function: _fnScrollHorizontalLeft
  	 * Purpose:  Update the positioning of the scrolling elements
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnScrollHorizontalLeft: function ( oCache )
  	{
  		var
  			s = this.fnGetSettings(),
  			oMes = s.oMes,
  			oWin = FixedHeader.oWin,
  			oDoc = FixedHeader.oDoc,
  			nTable = oCache.nWrapper,
  			iCellWidth = $(nTable).outerWidth();
  
  		if ( oWin.iScrollLeft < oMes.iTableLeft )
  		{
  			/* Fully left align */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  		else if ( oWin.iScrollLeft < oMes.iTableLeft+oMes.iTableWidth-iCellWidth )
  		{
  			this._fnUpdateCache( oCache, 'sPosition', 'fixed', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop-oWin.iScrollTop)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', "0px", 'left', nTable.style );
  		}
  		else
  		{
  			/* Fully right align */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', (oMes.iTableLeft+oMes.iTableWidth-iCellWidth)+"px", 'left', nTable.style );
  		}
  	},
  
  	/*
  	 * Function: _fnScrollFixedFooter
  	 * Purpose:  Update the positioning of the scrolling elements
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnScrollFixedFooter: function ( oCache )
  	{
  		var
  			s = this.fnGetSettings(),
  			oMes = s.oMes,
  			oWin = FixedHeader.oWin,
  			oDoc = FixedHeader.oDoc,
  			nTable = oCache.nWrapper,
  			iTheadHeight = $("thead", s.nTable).outerHeight(),
  			iCellHeight = $(nTable).outerHeight();
  
  		if ( oWin.iScrollBottom < oMes.iTableBottom )
  		{
  			/* Below */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop+oMes.iTableHeight-iCellHeight)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  		else if ( oWin.iScrollBottom < oMes.iTableBottom+oMes.iTableHeight-iCellHeight-iTheadHeight )
  		{
  			this._fnUpdateCache( oCache, 'sPosition', 'fixed', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oWin.iHeight-iCellHeight)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', (oMes.iTableLeft-oWin.iScrollLeft)+"px", 'left', nTable.style );
  		}
  		else
  		{
  			/* Above */
  			this._fnUpdateCache( oCache, 'sPosition', 'absolute', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop+iCellHeight)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  	},
  
  	/*
  	 * Function: _fnScrollFixedHeader
  	 * Purpose:  Update the positioning of the scrolling elements
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnScrollFixedHeader: function ( oCache )
  	{
  		var
  			s = this.fnGetSettings(),
  			oMes = s.oMes,
  			oWin = FixedHeader.oWin,
  			oDoc = FixedHeader.oDoc,
  			nTable = oCache.nWrapper,
  			iTbodyHeight = 0,
  			anTbodies = s.nTable.getElementsByTagName('tbody');
  
  		for (var i = 0; i < anTbodies.length; ++i) {
  			iTbodyHeight += anTbodies[i].offsetHeight;
  		}
  
  		if ( oMes.iTableTop > oWin.iScrollTop + s.oOffset.top )
  		{
  			/* Above the table */
  			this._fnUpdateCache( oCache, 'sPosition', "absolute", 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  		else if ( oWin.iScrollTop + s.oOffset.top > oMes.iTableTop+iTbodyHeight )
  		{
  			/* At the bottom of the table */
  			this._fnUpdateCache( oCache, 'sPosition', "absolute", 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop+iTbodyHeight)+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style );
  		}
  		else
  		{
  			/* In the middle of the table */
  			this._fnUpdateCache( oCache, 'sPosition', 'fixed', 'position', nTable.style );
  			this._fnUpdateCache( oCache, 'sTop', s.oOffset.top+"px", 'top', nTable.style );
  			this._fnUpdateCache( oCache, 'sLeft', (oMes.iTableLeft-oWin.iScrollLeft)+"px", 'left', nTable.style );
  		}
  	},
  
  	/*
  	 * Function: _fnUpdateCache
  	 * Purpose:  Check the cache and update cache and value if needed
  	 * Returns:  -
  	 * Inputs:   object:oCache - local cache object
  	 *           string:sCache - cache property
  	 *           string:sSet - value to set
  	 *           string:sProperty - object property to set
  	 *           object:oObj - object to update
  	 */
  	_fnUpdateCache: function ( oCache, sCache, sSet, sProperty, oObj )
  	{
  		if ( oCache[sCache] != sSet )
  		{
  			oObj[sProperty] = sSet;
  			oCache[sCache] = sSet;
  		}
  	},
  
  
  
  	/**
  	 * Copy the classes of all child nodes from one element to another. This implies
  	 * that the two have identical structure - no error checking is performed to that
  	 * fact.
  	 *  @param {element} source Node to copy classes from
  	 *  @param {element} dest Node to copy classes too
  	 */
  	_fnClassUpdate: function ( source, dest )
  	{
  		var that = this;
  
  		if ( source.nodeName.toUpperCase() === "TR" || source.nodeName.toUpperCase() === "TH" ||
  			 source.nodeName.toUpperCase() === "TD" || source.nodeName.toUpperCase() === "SPAN" )
  		{
  			dest.className = source.className;
  		}
  
  		$(source).children().each( function (i) {
  			that._fnClassUpdate( $(source).children()[i], $(dest).children()[i] );
  		} );
  	},
  
  
  	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  	 * Cloning functions
  	 */
  
  	/*
  	 * Function: _fnCloneThead
  	 * Purpose:  Clone the thead element
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnCloneThead: function ( oCache )
  	{
  		var s = this.fnGetSettings();
  		var nTable = oCache.nNode;
  
  		if ( s.bInitComplete && !s.oCloneOnDraw.top )
  		{
  			this._fnClassUpdate( $('thead', s.nTable)[0], $('thead', nTable)[0] );
  			return;
  		}
  
  		/* Set the wrapper width to match that of the cloned table */
  		var iDtWidth = $(s.nTable).outerWidth();
  		oCache.nWrapper.style.width = iDtWidth+"px";
  		nTable.style.width = iDtWidth+"px";
  
  		/* Remove any children the cloned table has */
  		while ( nTable.childNodes.length > 0 )
  		{
  			$('thead th', nTable).unbind( 'click' );
  			nTable.removeChild( nTable.childNodes[0] );
  		}
  
  		/* Clone the DataTables header */
  		var nThead = $('thead', s.nTable).clone(true)[0];
  		nTable.appendChild( nThead );
  
  		/* Copy the widths across - apparently a clone isn't good enough for this */
  		var a = [];
  		var b = [];
  
  		$("thead>tr th", s.nTable).each( function (i) {
  			a.push( $(this).width() );
  		} );
  
  		$("thead>tr td", s.nTable).each( function (i) {
  			b.push( $(this).width() );
  		} );
  
  		$("thead>tr th", s.nTable).each( function (i) {
  			$("thead>tr th:eq("+i+")", nTable).width( a[i] );
  			$(this).width( a[i] );
  		} );
  
  		$("thead>tr td", s.nTable).each( function (i) {
  			$("thead>tr td:eq("+i+")", nTable).width( b[i] );
  			$(this).width( b[i] );
  		} );
  
  		// Stop DataTables 1.9 from putting a focus ring on the headers when
  		// clicked to sort
  		$('th.sorting, th.sorting_desc, th.sorting_asc', nTable).bind( 'click', function () {
  			this.blur();
  		} );
  	},
  
  	/*
  	 * Function: _fnCloneTfoot
  	 * Purpose:  Clone the tfoot element
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnCloneTfoot: function ( oCache )
  	{
  		var s = this.fnGetSettings();
  		var nTable = oCache.nNode;
  
  		/* Set the wrapper width to match that of the cloned table */
  		oCache.nWrapper.style.width = $(s.nTable).outerWidth()+"px";
  
  		/* Remove any children the cloned table has */
  		while ( nTable.childNodes.length > 0 )
  		{
  			nTable.removeChild( nTable.childNodes[0] );
  		}
  
  		/* Clone the DataTables footer */
  		var nTfoot = $('tfoot', s.nTable).clone(true)[0];
  		nTable.appendChild( nTfoot );
  
  		/* Copy the widths across - apparently a clone isn't good enough for this */
  		$("tfoot:eq(0)>tr th", s.nTable).each( function (i) {
  			$("tfoot:eq(0)>tr th:eq("+i+")", nTable).width( $(this).width() );
  		} );
  
  		$("tfoot:eq(0)>tr td", s.nTable).each( function (i) {
  			$("tfoot:eq(0)>tr td:eq("+i+")", nTable).width( $(this).width() );
  		} );
  	},
  
  	/*
  	 * Function: _fnCloneTLeft
  	 * Purpose:  Clone the left column(s)
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnCloneTLeft: function ( oCache )
  	{
  		var s = this.fnGetSettings();
  		var nTable = oCache.nNode;
  		var nBody = $('tbody', s.nTable)[0];
  
  		/* Remove any children the cloned table has */
  		while ( nTable.childNodes.length > 0 )
  		{
  			nTable.removeChild( nTable.childNodes[0] );
  		}
  
  		/* Is this the most efficient way to do this - it looks horrible... */
  		nTable.appendChild( $("thead", s.nTable).clone(true)[0] );
  		nTable.appendChild( $("tbody", s.nTable).clone(true)[0] );
  		if ( s.bFooter )
  		{
  			nTable.appendChild( $("tfoot", s.nTable).clone(true)[0] );
  		}
  
  		/* Remove unneeded cells */
  		var sSelector = 'gt(' + (oCache.iCells - 1) + ')';
  		$('thead tr', nTable).each( function (k) {
  			$('th:' + sSelector, this).remove();
  		} );
  
  		$('tfoot tr', nTable).each( function (k) {
  			$('th:' + sSelector, this).remove();
  		} );
  
  		$('tbody tr', nTable).each( function (k) {
  			$('td:' + sSelector, this).remove();
  		} );
  
  		this.fnEqualiseHeights( 'thead', nBody.parentNode, nTable );
  		this.fnEqualiseHeights( 'tbody', nBody.parentNode, nTable );
  		this.fnEqualiseHeights( 'tfoot', nBody.parentNode, nTable );
  
  		var iWidth = 0;
  		for (var i = 0; i < oCache.iCells; i++) {
  			iWidth += $('thead tr th:eq(' + i + ')', s.nTable).outerWidth();
  		}
  		nTable.style.width = iWidth+"px";
  		oCache.nWrapper.style.width = iWidth+"px";
  	},
  
  	/*
  	 * Function: _fnCloneTRight
  	 * Purpose:  Clone the right most column(s)
  	 * Returns:  -
  	 * Inputs:   object:oCache - the cached values for this fixed element
  	 */
  	_fnCloneTRight: function ( oCache )
  	{
  		var s = this.fnGetSettings();
  		var nBody = $('tbody', s.nTable)[0];
  		var nTable = oCache.nNode;
  		var iCols = $('tbody tr:eq(0) td', s.nTable).length;
  
  		/* Remove any children the cloned table has */
  		while ( nTable.childNodes.length > 0 )
  		{
  			nTable.removeChild( nTable.childNodes[0] );
  		}
  
  		/* Is this the most efficient way to do this - it looks horrible... */
  		nTable.appendChild( $("thead", s.nTable).clone(true)[0] );
  		nTable.appendChild( $("tbody", s.nTable).clone(true)[0] );
  		if ( s.bFooter )
  		{
  			nTable.appendChild( $("tfoot", s.nTable).clone(true)[0] );
  		}
  		$('thead tr th:lt('+(iCols-oCache.iCells)+')', nTable).remove();
  		$('tfoot tr th:lt('+(iCols-oCache.iCells)+')', nTable).remove();
  
  		/* Remove unneeded cells */
  		$('tbody tr', nTable).each( function (k) {
  			$('td:lt('+(iCols-oCache.iCells)+')', this).remove();
  		} );
  
  		this.fnEqualiseHeights( 'thead', nBody.parentNode, nTable );
  		this.fnEqualiseHeights( 'tbody', nBody.parentNode, nTable );
  		this.fnEqualiseHeights( 'tfoot', nBody.parentNode, nTable );
  
  		var iWidth = 0;
  		for (var i = 0; i < oCache.iCells; i++) {
  			iWidth += $('thead tr th:eq('+(iCols-1-i)+')', s.nTable).outerWidth();
  		}
  		nTable.style.width = iWidth+"px";
  		oCache.nWrapper.style.width = iWidth+"px";
  	},
  
  
  	/**
  	 * Equalise the heights of the rows in a given table node in a cross browser way. Note that this
  	 * is more or less lifted as is from FixedColumns
  	 *  @method  fnEqualiseHeights
  	 *  @returns void
  	 *  @param   {string} parent Node type - thead, tbody or tfoot
  	 *  @param   {element} original Original node to take the heights from
  	 *  @param   {element} clone Copy the heights to
  	 *  @private
  	 */
  	"fnEqualiseHeights": function ( parent, original, clone )
  	{
  		var that = this;
  		var originals = $(parent +' tr', original);
  		var height;
  
  		$(parent+' tr', clone).each( function (k) {
  			height = originals.eq( k ).css('height');
  
  			// This is nasty :-(. IE has a sub-pixel error even when setting
  			// the height below (the Firefox fix) which causes the fixed column
  			// to go out of alignment. Need to add a pixel before the assignment
  			// Can this be feature detected? Not sure how...
  			if ( navigator.appName == 'Microsoft Internet Explorer' ) {
  				height = parseInt( height, 10 ) + 1;
  			}
  
  			$(this).css( 'height', height );
  
  			// For Firefox to work, we need to also set the height of the
  			// original row, to the value that we read from it! Otherwise there
  			// is a sub-pixel rounding error
  			originals.eq( k ).css( 'height', height );
  		} );
  	}
  };
  
  
  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   * Static properties and methods
   *   We use these for speed! This information is common to all instances of FixedHeader, so no
   * point if having them calculated and stored for each different instance.
   */
  
  /*
   * Variable: oWin
   * Purpose:  Store information about the window positioning
   * Scope:    FixedHeader
   */
  FixedHeader.oWin = {
  	"iScrollTop": 0,
  	"iScrollRight": 0,
  	"iScrollBottom": 0,
  	"iScrollLeft": 0,
  	"iHeight": 0,
  	"iWidth": 0
  };
  
  /*
   * Variable: oDoc
   * Purpose:  Store information about the document size
   * Scope:    FixedHeader
   */
  FixedHeader.oDoc = {
  	"iHeight": 0,
  	"iWidth": 0
  };
  
  /*
   * Variable: afnScroll
   * Purpose:  Array of functions that are to be used for the scrolling components
   * Scope:    FixedHeader
   */
  FixedHeader.afnScroll = [];
  
  /*
   * Function: fnMeasure
   * Purpose:  Update the measurements for the window and document
   * Returns:  -
   * Inputs:   -
   */
  FixedHeader.fnMeasure = function ()
  {
  	var
  		jqWin = $(window),
  		jqDoc = $(document),
  		oWin = FixedHeader.oWin,
  		oDoc = FixedHeader.oDoc;
  
  	oDoc.iHeight = jqDoc.height();
  	oDoc.iWidth = jqDoc.width();
  
  	oWin.iHeight = jqWin.height();
  	oWin.iWidth = jqWin.width();
  	oWin.iScrollTop = jqWin.scrollTop();
  	oWin.iScrollLeft = jqWin.scrollLeft();
  	oWin.iScrollRight = oDoc.iWidth - oWin.iScrollLeft - oWin.iWidth;
  	oWin.iScrollBottom = oDoc.iHeight - oWin.iScrollTop - oWin.iHeight;
  };
  
  
  FixedHeader.version = "2.1.2";
  
  
  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   * Global processing
   */
  
  /*
   * Just one 'scroll' event handler in FixedHeader, which calls the required components. This is
   * done as an optimisation, to reduce calculation and proagation time
   */
  $(window).scroll( function () {
  	FixedHeader.fnMeasure();
  
  	for ( var i=0, iLen=FixedHeader.afnScroll.length ; i<iLen ; i++ ) {
  		FixedHeader.afnScroll[i]();
  	}
  } );
  
  
  $.fn.dataTable.FixedHeader = FixedHeader;
  $.fn.DataTable.FixedHeader = FixedHeader;
  
  
  return FixedHeader;
  }; // /factory
  
  
  // Define as an AMD module if possible
  if ( typeof define === 'function' && define.amd ) {
  	define( ['jquery', 'datatables'], factory );
  }
  else if ( typeof exports === 'object' ) {
      // Node/CommonJS
      factory( require('jquery'), require('datatables') );
  }
  else if ( jQuery && !jQuery.fn.dataTable.FixedHeader ) {
  	// Otherwise simply initialise as normal, stopping multiple evaluation
  	factory( jQuery, jQuery.fn.dataTable );
  }
  
  
  })(window, document);
  
  