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