      1 /*
      2  * File:        FixedColumns.js
      3  * Version:     1.1.0
      4  * Description: "Fix" columns on the left of a scrolling DataTable
      5  * Author:      Allan Jardine (www.sprymedia.co.uk)
      6  * Created:     Sat Sep 18 09:28:54 BST 2010
      7  * Language:    Javascript
      8  * License:     GPL v2 or BSD 3 point style
      9  * Project:     Just a little bit of fun - enjoy :-)
     10  * Contact:     www.sprymedia.co.uk/contact
     11  *
     12  * Copyright 2010-2011 Allan Jardine, all rights reserved.
     13  */
     14
     15 var FixedColumns = function ( oDT, oInit ) {
     16 	/* Sanity check - you just know it will happen */
     17 	if ( typeof this._fnConstruct != 'function' )
     18 	{
     19 		alert( "FixedColumns warning: FixedColumns must be initialised with the 'new' keyword." );
     20 		return;
     21 	}
     22
     23 	if ( typeof oInit == 'undefined' )
     24 	{
     25 		oInit = {};
     26 	}
     27
     28 	/**
     29 	 * @namespace Settings object which contains customisable information for FixedColumns instance
     30 	 */
     31 	this.s = {
     32 		/**
     33 		 * DataTables settings objects
     34      *  @property dt
     35      *  @type     object
     36      *  @default  null
     37 		 */
     38 		dt: oDT.fnSettings(),
     39
     40 		/**
     41 		 * Number of left hand columns to fix in position
     42      *  @property leftColumns
     43      *  @type     int
     44      *  @default  1
     45 		 */
     46 		leftColumns: 1,
     47
     48 		/**
     49 		 * Number of right hand columns to fix in position
     50      *  @property rightColumns
     51      *  @type     int
     52      *  @default  0
     53 		 */
     54 		rightColumns: 0,
     55
     56 		/**
     57 		 * Store the heights of the rows for a draw. This can significantly speed up a draw where both
     58 		 * left and right columns are fixed
     59      *  @property heights
     60      *  @type     array int
     61      *  @default  0
     62 		 */
     63 		heights: []
     64 	};
     65
     66
     67 	/**
     68 	 * @namespace Common and useful DOM elements for the class instance
     69 	 */
     70 	this.dom = {
     71 		/**
     72 		 * DataTables scrolling element
     73 		 *  @property scroller
     74 		 *  @type     node
     75 		 *  @default  null
     76 		 */
     77 		scroller: null,
     78
     79 		/**
     80 		 * Scroll container that DataTables has added
     81 		 *  @property scrollContainer
     82 		 *  @type     node
     83 		 *  @default  null
     84 		 */
     85 		scrollContainer: null,
     86
     87 		/**
     88 		 * DataTables header table
     89 		 *  @property header
     90 		 *  @type     node
     91 		 *  @default  null
     92 		 */
     93 		header: null,
     94
     95 		/**
     96 		 * DataTables body table
     97 		 *  @property body
     98 		 *  @type     node
     99 		 *  @default  null
    100 		 */
    101 		body: null,
    102
    103 		/**
    104 		 * DataTables footer table
    105 		 *  @property footer
    106 		 *  @type     node
    107 		 *  @default  null
    108 		 */
    109 		footer: null,
    110
    111 		/**
    112 		 * @namespace Cloned table nodes
    113 		 */
    114 		clone: {
    115 			/**
    116 			 * @namespace Left column cloned table nodes
    117 			 */
    118 			left: {
    119 				/**
    120 				 * Cloned header table
    121 				 *  @property header
    122 				 *  @type     node
    123 				 *  @default  null
    124 				 */
    125 				header: null,
    126
    127 				/**
    128 				 * Cloned body table
    129 				 *  @property body
    130 				 *  @type     node
    131 				 *  @default  null
    132 				 */
    133 				body: null,
    134
    135 				/**
    136 				 * Cloned footer table
    137 				 *  @property footer
    138 				 *  @type     node
    139 				 *  @default  null
    140 				 */
    141 				footer: null
    142 			},
    143
    144 			/**
    145 			 * @namespace Right column cloned table nodes
    146 			 */
    147 			right: {
    148 				/**
    149 				 * Cloned header table
    150 				 *  @property header
    151 				 *  @type     node
    152 				 *  @default  null
    153 				 */
    154 				header: null,
    155
    156 				/**
    157 				 * Cloned body table
    158 				 *  @property body
    159 				 *  @type     node
    160 				 *  @default  null
    161 				 */
    162 				body: null,
    163
    164 				/**
    165 				 * Cloned footer table
    166 				 *  @property footer
    167 				 *  @type     node
    168 				 *  @default  null
    169 				 */
    170 				footer: null
    171 			}
    172 		}
    173 	};
    174
    175 	/* Let's do it */
    176 	this._fnConstruct( oInit );
    177 };
    178
    179
    180 FixedColumns.prototype = {
    181 	/**
    182 	 * Update the fixed columns - including headers and footers
    183 	 *  @method  fnUpdate
    184 	 *  @returns void
    185 	 */
    186 	fnUpdate: function ()
    187 	{
    188 		this._fnDraw( true );
    189 	},
    190
    191
    192 	/**
    193 	 * Initialisation for FixedColumns
    194 	 *  @method  _fnConstruct
    195 	 *  @param   {Object} oInit User settings for initialisation
    196 	 *  @returns void
    197 	 */
    198 	_fnConstruct: function ( oInit )
    199 	{
    200 		var that = this;
    201
    202 		/* Sanity checking */
    203 		if ( typeof this.s.dt.oInstance.fnVersionCheck != 'function' ||
    204 		     this.s.dt.oInstance.fnVersionCheck( '1.7.0' ) !== true )
    205 		{
    206 			alert( "FixedColumns 2 required DataTables 1.7.0 or later. "+
    207 				"Please upgrade your DataTables installation" );
    208 			return;
    209 		}
    210
    211 		if ( this.s.dt.oScroll.sX === "" )
    212 		{
    213 			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
    214 				"x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for "+
    215 				"column fixing when scrolling is not enabled" );
    216 			return;
    217 		}
    218
    219 		if ( typeof oInit.columns != 'undefined' )
    220 		{
    221 			/* Support for FixedColumns 1.0.x initialisation parameter */
    222 			this.s.leftColumns = oInit.columns;
    223 		}
    224
    225 		if ( typeof oInit.iColumns != 'undefined' )
    226 		{
    227 			this.s.leftColumns = oInit.iColumns;
    228 		}
    229
    230 		if ( typeof oInit.iRightColumns != 'undefined' )
    231 		{
    232 			this.s.rightColumns = oInit.iRightColumns;
    233 		}
    234
    235 		/* Set up the DOM as we need it and cache nodes */
    236 		this.dom.scrollContainer = $(this.s.dt.nTable).parents('div.dataTables_scroll')[0];
    237 		this.dom.scrollContainer.style.position = "relative";
    238
    239 		this.dom.body = this.s.dt.nTable;
    240 		this.dom.scroller = this.dom.body.parentNode;
    241 		this.dom.scroller.style.position = "relative";
    242
    243 		this.dom.header = this.s.dt.nTHead.parentNode;
    244 		this.dom.header.parentNode.parentNode.style.position = "relative";
    245
    246 		if ( this.s.dt.nTFoot )
    247 		{
    248 			this.dom.footer = this.s.dt.nTFoot.parentNode;
    249 			this.dom.footer.parentNode.parentNode.style.position = "relative";
    250 		}
    251
    252 		this.s.position = this.s.dt.oScroll.sY === "" ? 'absolute' : 'relative';
    253
    254 		/* Event handlers */
    255 		if ( this.s.position != "absolute" )
    256 		{
    257 			$(this.dom.scroller).scroll( function () {
    258 				that._fnPosition.call( that );
    259 			} );
    260 		}
    261
    262 		this.s.dt.aoDrawCallback.push( {
    263 			fn: function () {
    264 				that._fnDraw.call( that, false );
    265 			},
    266 			sName: "FixedColumns"
    267 		} );
    268
    269 		/* Get things right to start with */
    270 		this._fnDraw( true );
    271 	},
    272
    273
    274 	/**
    275 	 * Clone and position the fixed columns
    276 	 *  @method  _fnDraw
    277 	 *  @returns void
    278 	 *  @param   {Boolean} bAll Indicate if the headre and footer should be updated as well (true)
    279 	 *  @private
    280 	 */
    281 	_fnDraw: function ( bAll )
    282 	{
    283 		this._fnCloneLeft( bAll );
    284 		this._fnCloneRight( bAll );
    285 		this._fnPosition();
    286
    287 		this.s.heights.splice( 0, this.s.heights.length );
    288 	},
    289
    290
    291 	/**
    292 	 * Clone the right columns
    293 	 *  @method  _fnCloneRight
    294 	 *  @returns void
    295 	 *  @param   {Boolean} bAll Indicate if the headre and footer should be updated as well (true)
    296 	 *  @private
    297 	 */
    298 	_fnCloneRight: function ( bAll )
    299 	{
    300 		if ( this.s.rightColumns <= 0 )
    301 		{
    302 			return;
    303 		}
    304
    305 		var
    306 			that = this,
    307 			iTableWidth = 0,
    308 			aiCellWidth = [],
    309 			i, jq,
    310 			iColumns = $('thead tr:eq(0)', this.dom.header).children().length;
    311
    312 		/* Grab the widths that we are going to need */
    313 		for ( i=this.s.rightColumns-1 ; i>=0 ; i-- )
    314 		{
    315 			jq = $('thead tr:eq(0)', this.dom.header).children(':eq('+(iColumns-i-1)+')');
    316 			iTableWidth += jq.outerWidth();
    317 			aiCellWidth.push( jq.width() );
    318 		}
    319 		aiCellWidth.reverse();
    320
    321 		this._fnClone( this.dom.clone.right, bAll, aiCellWidth, iTableWidth,
    322 			':last', ':lt('+(iColumns-this.s.rightColumns)+')' );
    323 	},
    324
    325
    326 	/**
    327 	 * Clone the left columns
    328 	 *  @method  _fnCloneLeft
    329 	 *  @returns void
    330 	 *  @param   {Boolean} bAll Indicate if the headre and footer should be updated as well (true)
    331 	 *  @private
    332 	 */
    333 	_fnCloneLeft: function ( bAll )
    334 	{
    335 		if ( this.s.leftColumns <= 0 )
    336 		{
    337 			return;
    338 		}
    339
    340 		var
    341 			that = this,
    342 			iTableWidth = 0,
    343 			aiCellWidth = [],
    344 			i, jq;
    345
    346 		/* Grab the widths that we are going to need */
    347 		for ( i=0, iLen=this.s.leftColumns ; i<iLen ; i++ )
    348 		{
    349 			jq = $('thead tr:eq(0)', this.dom.header).children(':eq('+i+')');
    350 			iTableWidth += jq.outerWidth();
    351 			aiCellWidth.push( jq.width() );
    352 		}
    353
    354 		this._fnClone( this.dom.clone.left, bAll, aiCellWidth, iTableWidth,
    355 			':first', ':gt('+(this.s.leftColumns-1)+')' );
    356 	},
    357
    358
    359
    360 	/**
    361 	 * Clone the DataTable nodes and place them in the DOM (sized correctly)
    362 	 *  @method  _fnClone
    363 	 *  @returns void
    364 	 *  @param   {Object} oClone Object containing the header, footer and body cloned DOM elements
    365 	 *  @param   {Boolean} bAll Indicate if the headre and footer should be updated as well (true)
    366 	 *  @param   {array} aiCellWidth Array of integers with the width's to use for the cloned columns
    367 	 *  @param   {int} iTableWidth Calculated table width
    368 	 *  @param   {string} sBoxHackSelector Selector to pick which TD element to copy styles from
    369 	 *  @param   {string} sRemoveSelector Which elements to remove
    370 	 *  @private
    371 	 */
    372 	_fnClone: function ( oClone, bAll, aiCellWidth, iTableWidth, sBoxHackSelector, sRemoveSelector )
    373 	{
    374 		var
    375 			that = this,
    376 			i, iLen, jq, nTarget;
    377
    378 		/* Header */
    379 		if ( bAll )
    380 		{
    381 			if ( oClone.header !== null )
    382 			{
    383 				oClone.header.parentNode.removeChild( oClone.header );
    384 			}
    385 			oClone.header = $(this.dom.header).clone(true)[0];
    386 			oClone.header.className += " FixedColumns_Cloned";
    387
    388 			oClone.header.style.position = "absolute";
    389 			oClone.header.style.top = "0px";
    390 			oClone.header.style.left = "0px";
    391 			oClone.header.style.width = iTableWidth+"px";
    392
    393 			nTarget = this.s.position == "absolute" ? this.dom.scrollContainer :
    394 				this.dom.header.parentNode;
    395 			nTarget.appendChild( oClone.header );
    396
    397 			this._fnEqualiseHeights( 'thead', this.dom.header, oClone.header,
    398 				sBoxHackSelector, sRemoveSelector );
    399
    400 			$('thead tr:eq(0)', oClone.header).children().each( function (i) {
    401 				this.style.width = aiCellWidth[i]+"px";
    402 			} );
    403 		}
    404 		else
    405 		{
    406 			this._fnCopyClasses(oClone.header, this.dom.header);
    407 		}
    408
    409 		/* Body */
    410 		/* Remove any heights which have been applied already and let the browser figure it out */
    411 		$('tbody tr', that.dom.body).css('height', 'auto');
    412
    413 		if ( oClone.body !== null )
    414 		{
    415 			oClone.body.parentNode.removeChild( oClone.body );
    416 			oClone.body = null;
    417 		}
    418
    419 		if ( this.s.dt.aiDisplay.length > 0 )
    420 		{
    421 			oClone.body = $(this.dom.body).clone(true)[0];
    422 			oClone.body.className += " FixedColumns_Cloned";
    423 			if ( oClone.body.getAttribute('id') !== null )
    424 			{
    425 				oClone.body.removeAttribute('id');
    426 			}
    427
    428 			$('thead tr:eq(0)', oClone.body).each( function () {
    429 				$('th'+sRemoveSelector, this).remove();
    430 			} );
    431
    432 			$('thead tr:gt(0)', oClone.body).remove();
    433
    434 			this._fnEqualiseHeights( 'tbody', that.dom.body, oClone.body,
    435 				sBoxHackSelector, sRemoveSelector );
    436
    437 			$('tfoot tr:eq(0)', oClone.body).each( function () {
    438 				$('th'+sRemoveSelector, this).remove();
    439 			} );
    440
    441 			$('tfoot tr:gt(0)', oClone.body).remove();
    442
    443
    444 			oClone.body.style.position = "absolute";
    445 			oClone.body.style.top = "0px";
    446 			oClone.body.style.left = "0px";
    447 			oClone.body.style.width = iTableWidth+"px";
    448
    449 			nTarget = this.s.position == "absolute" ? this.dom.scrollContainer :
    450 				this.dom.body.parentNode;
    451 			nTarget.appendChild( oClone.body );
    452 		}
    453
    454 		/* Footer */
    455 		if ( this.s.dt.nTFoot !== null )
    456 		{
    457 			if ( bAll )
    458 			{
    459 				if ( oClone.footer !== null )
    460 				{
    461 					oClone.footer.parentNode.removeChild( oClone.footer );
    462 				}
    463 				oClone.footer = $(this.dom.footer).clone(true)[0];
    464 				oClone.footer.className += " FixedColumns_Cloned";
    465
    466 				oClone.footer.style.position = "absolute";
    467 				oClone.footer.style.top = "0px";
    468 				oClone.footer.style.left = "0px";
    469 				oClone.footer.style.width = iTableWidth+"px";
    470
    471 				nTarget = this.s.position == "absolute" ? this.dom.scrollContainer :
    472 					this.dom.footer.parentNode;
    473 				nTarget.appendChild( oClone.footer );
    474
    475 				this._fnEqualiseHeights( 'tfoot', this.dom.footer, oClone.footer,
    476 					sBoxHackSelector, sRemoveSelector );
    477
    478 				$('tfoot tr:eq(0)', oClone.footer).children().each( function (i) {
    479 					this.style.width = aiCellWidth[i]+"px";
    480 				} );
    481 			}
    482 		}
    483 	},
    484
    485
    486 	/**
    487 	 * Clone classes from one DOM node to another with (IMPORTANT) IDENTICAL structures
    488 	 *  @method  _fnCopyClasses
    489 	 *  @returns void
    490 	 *  @param   {element} clone Node to copy classes to
    491 	 *  @param   {element} original Original node to take the classes from
    492 	 *  @private
    493 	 */
    494 	_fnCopyClasses: function ( clone, original )
    495 	{
    496 		clone.className = original.className;
    497 		for ( var i=0, iLen=clone.children.length ; i<iLen ; i++ )
    498 		{
    499 			if ( original.children[i].nodeType == 1 )
    500 			{
    501 				this._fnCopyClasses( clone.children[i], original.children[i] );
    502 			}
    503 		}
    504 	},
    505
    506
    507 	/**
    508 	 * Equalise the heights of the rows in a given table node in a cross browser way
    509 	 *  @method  _fnEqualiseHeights
    510 	 *  @returns void
    511 	 *  @param   {string} parent Node type - thead, tbody or tfoot
    512 	 *  @param   {element} original Original node to take the heights from
    513 	 *  @param   {element} clone Copy the heights to
    514 	 *  @param   {string} boxHackSelector Selector to pick which TD element to copy styles from
    515 	 *  @param   {string} removeSelector Which elements to remove
    516 	 *  @private
    517 	 */
    518 	_fnEqualiseHeights: function ( parent, original, clone, boxHackSelector, removeSelector )
    519 	{
    520 		var that = this,
    521 			iHeight,
    522 			iCalculateHeights = (parent == "tbody" && this.s.heights.length > 0) ? false : true,
    523 			jqBoxHack = $(parent+' tr:eq(0)', original).children(boxHackSelector),
    524 			iBoxHack = jqBoxHack.outerHeight() - jqBoxHack.height(),
    525 			bRubbishOldIE = ($.browser.msie && ($.browser.version == "6.0" || $.browser.version == "7.0"));
    526
    527 		if ( $(parent+' tr:eq(0) th', clone).attr('rowspan') > 1 )
    528 		{
    529 			$(parent+' tr:gt(0)', clone).remove();
    530 		}
    531
    532 		/* Remove cells which are not needed and copy the height from the original table */
    533 		$(parent+' tr', clone).each( function (k) {
    534 			$(this).children(removeSelector, this).remove();
    535
    536 			/* We can store the heights of the rows calculated on the first pass of a draw, to be used
    537 			 * on the second pass (i.e. the right hand column). This significantly speeds up a draw
    538 			 * where both the left and right columns are fixed since we don't need to get the height of
    539 			 * each row twice
    540 			 */
    541 			if ( iCalculateHeights )
    542 			{
    543 				iHeight = $(parent+' tr:eq('+k+')', original).children(':first').height();
    544 				if ( parent == 'tbody' )
    545 				{
    546 					that.s.heights.push( iHeight );
    547 				}
    548 			}
    549 			else
    550 			{
    551 				iHeight = that.s.heights[k];
    552 			}
    553
    554 			/* Can we use some kind of object detection here?! This is very nasty - damn browsers */
    555 			if ( $.browser.mozilla || $.browser.opera )
    556 			{
    557 				$(this).children().height( iHeight+iBoxHack );
    558 				$(parent+' tr:eq('+k+')', original).height( iHeight+iBoxHack );
    559 			}
    560 			else if ( $.browser.msie && !bRubbishOldIE )
    561 			{
    562 				$(this).children().height( iHeight-1 ); /* wtf... */
    563 			}
    564 			else
    565 			{
    566 				$(this).children().height( iHeight );
    567 			}
    568 		} );
    569 	},
    570
    571
    572 	/**
    573 	 * Set the absolute position of the fixed column tables when scrolling the DataTable
    574 	 *  @method  _fnPosition
    575 	 *  @returns void
    576 	 *  @private
    577 	 */
    578 	_fnPosition: function ()
    579 	{
    580 		var
    581 			iScrollLeft = this.s.position == 'absolute' ? 0 : $(this.dom.scroller).scrollLeft(),
    582 			oCloneLeft = this.dom.clone.left,
    583 			oCloneRight = this.dom.clone.right,
    584 			iTableWidth = $(this.s.dt.nTable.parentNode).width();
    585
    586 		if ( this.s.position == 'absolute' )
    587 		{
    588 			var iBodyTop = $(this.dom.body.parentNode).position().top;
    589 			if ( this.dom.footer )
    590 			{
    591 				var iFooterTop = $(this.dom.footer.parentNode.parentNode).position().top;
    592 			}
    593 		}
    594
    595 		if ( this.s.leftColumns > 0 )
    596 		{
    597 			oCloneLeft.header.style.left = iScrollLeft+"px";
    598 			if ( oCloneLeft.body !== null )
    599 			{
    600 				oCloneLeft.body.style.left = iScrollLeft+"px";
    601 				if (  this.s.position == 'absolute' )
    602 				{
    603 					oCloneLeft.body.style.top = iBodyTop+"px";
    604 				}
    605 			}
    606 			if ( this.dom.footer )
    607 			{
    608 				oCloneLeft.footer.style.left = iScrollLeft+"px";
    609 				if (  this.s.position == 'absolute' )
    610 				{
    611 					oCloneLeft.footer.style.top = iFooterTop+"px";
    612 				}
    613 			}
    614 		}
    615
    616 		if ( this.s.rightColumns > 0 )
    617 		{
    618 			var iPoint = iTableWidth - $(oCloneRight.body).width() + iScrollLeft;
    619
    620 			oCloneRight.header.style.left = iPoint+"px";
    621 			if ( oCloneRight.body !== null )
    622 			{
    623 				oCloneRight.body.style.left = iPoint+"px";
    624 				if (  this.s.position == 'absolute' )
    625 				{
    626 					oCloneRight.body.style.top = iBodyTop+"px";
    627 				}
    628 			}
    629 			if ( this.dom.footer )
    630 			{
    631 				oCloneRight.footer.style.left = iPoint+"px";
    632 				if (  this.s.position == 'absolute' )
    633 				{
    634 					oCloneRight.footer.style.top = iFooterTop+"px";
    635 				}
    636 			}
    637 		}
    638 	}
    639 };
    640