      1 /*
      2  * File:        FixedColumns.js
      3  * Version:     1.0.2
      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 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 columns to fix in position
     42      *  @property columns
     43      *  @type     int
     44      *  @default  1
     45 		 */
     46 		columns: 1
     47 	};
     48
     49
     50 	/**
     51 	 * @namespace Common and useful DOM elements for the class instance
     52 	 */
     53 	this.dom = {
     54 		/**
     55 		 * DataTables scrolling element
     56 		 *  @property scroller
     57 		 *  @type     node
     58 		 *  @default  null
     59 		 */
     60 		scroller: null,
     61
     62 		/**
     63 		 * DataTables header table
     64 		 *  @property header
     65 		 *  @type     node
     66 		 *  @default  null
     67 		 */
     68 		header: null,
     69
     70 		/**
     71 		 * DataTables body table
     72 		 *  @property body
     73 		 *  @type     node
     74 		 *  @default  null
     75 		 */
     76 		body: null,
     77
     78 		/**
     79 		 * DataTables footer table
     80 		 *  @property footer
     81 		 *  @type     node
     82 		 *  @default  null
     83 		 */
     84 		footer: null,
     85
     86 		/**
     87 		 * @namespace Cloned table nodes
     88 		 */
     89 		clone: {
     90 			/**
     91 			 * Cloned header table
     92 			 *  @property header
     93 			 *  @type     node
     94 			 *  @default  null
     95 			 */
     96 			header: null,
     97
     98 			/**
     99 			 * Cloned body table
    100 			 *  @property body
    101 			 *  @type     node
    102 			 *  @default  null
    103 			 */
    104 			body: null,
    105
    106 			/**
    107 			 * Cloned footer table
    108 			 *  @property footer
    109 			 *  @type     node
    110 			 *  @default  null
    111 			 */
    112 			footer: null
    113 		}
    114 	};
    115
    116 	/* Let's do it */
    117 	this._fnConstruct( oInit );
    118 };
    119
    120
    121 FixedColumns.prototype = {
    122 	/**
    123 	 * Initialisation for FixedColumns
    124 	 *  @method  _fnConstruct
    125 	 *  @param   {Object} oInit User settings for initialisation
    126 	 *  @returns void
    127 	 */
    128 	_fnConstruct: function ( oInit )
    129 	{
    130 		var that = this;
    131
    132 		/* Sanity checking */
    133 		if ( typeof this.s.dt.oInstance.fnVersionCheck != 'function' ||
    134 		     this.s.dt.oInstance.fnVersionCheck( '1.7.0' ) !== true )
    135 		{
    136 			alert( "FixedColumns 2 required DataTables 1.7.0 or later. "+
    137 				"Please upgrade your DataTables installation" );
    138 			return;
    139 		}
    140
    141 		if ( this.s.dt.oScroll.sX === "" )
    142 		{
    143 			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
    144 				"x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for "+
    145 				"column fixing when scrolling is not enabled" );
    146 			return;
    147 		}
    148
    149 		if ( typeof oInit.columns != 'undefined' )
    150 		{
    151 			if ( oInit.columns < 1 )
    152 			{
    153 				this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
    154 					"columns to be fixed), so no action will be taken" );
    155 				return;
    156 			}
    157 			this.s.columns = oInit.columns;
    158 		}
    159
    160 		/* Set up the DOM as we need it and cache nodes */
    161 		this.dom.body = this.s.dt.nTable;
    162 		this.dom.scroller = this.dom.body.parentNode;
    163 		this.dom.scroller.style.position = "relative";
    164
    165 		this.dom.header = this.s.dt.nTHead.parentNode;
    166 		this.dom.header.parentNode.parentNode.style.position = "relative";
    167
    168 		if ( this.s.dt.nTFoot )
    169 		{
    170 			this.dom.footer = this.s.dt.nTFoot.parentNode;
    171 			this.dom.footer.parentNode.parentNode.style.position = "relative";
    172 		}
    173
    174 		/* Event handlers */
    175 		$(this.dom.scroller).scroll( function () {
    176 			that._fnScroll.call( that );
    177 		} );
    178
    179 		this.s.dt.aoDrawCallback.push( {
    180 			fn: function () {
    181 				that._fnClone.call( that );
    182 				that._fnScroll.call( that );
    183 			},
    184 			sName: "FixedColumns"
    185 		} );
    186
    187 		/* Get things right to start with */
    188 		this._fnClone();
    189 		this._fnScroll();
    190 	},
    191
    192
    193 	/**
    194 	 * Clone the DataTable nodes and place them in the DOM (sized correctly)
    195 	 *  @method  _fnClone
    196 	 *  @returns void
    197 	 *  @private
    198 	 */
    199 	_fnClone: function ()
    200 	{
    201 		var
    202 			that = this,
    203 			iTableWidth = 0,
    204 			aiCellWidth = [],
    205 			i, iLen, jq,
    206 			bRubbishOldIE = ($.browser.msie && ($.browser.version == "6.0" || $.browser.version == "7.0"));
    207
    208 		/* Grab the widths that we are going to need */
    209 		for ( i=0, iLen=this.s.columns ; i<iLen ; i++ )
    210 		{
    211 			jq = $('thead th:eq('+i+')', this.dom.header);
    212 			iTableWidth += jq.outerWidth();
    213 			aiCellWidth.push( jq.width() );
    214 		}
    215
    216 		/* Header */
    217 		if ( this.dom.clone.header !== null )
    218 		{
    219 			this.dom.clone.header.parentNode.removeChild( this.dom.clone.header );
    220 		}
    221 		this.dom.clone.header = $(this.dom.header).clone(true)[0];
    222 		this.dom.clone.header.className += " FixedColumns_Cloned";
    223
    224 		$('thead tr:eq(0)', this.dom.clone.header).each( function () {
    225 			$('th:gt('+(that.s.columns-1)+')', this).remove();
    226 		} );
    227 		$('tr', this.dom.clone.header).height( $(that.dom.header).height() );
    228
    229 		$('thead tr:gt(0)', this.dom.clone.header).remove();
    230
    231 		$('thead th', this.dom.clone.header).each( function (i) {
    232 			this.style.width = aiCellWidth[i]+"px";
    233 		} );
    234
    235 		this.dom.clone.header.style.position = "absolute";
    236 		this.dom.clone.header.style.top = "0px";
    237 		this.dom.clone.header.style.left = "0px";
    238 		this.dom.clone.header.style.width = iTableWidth+"px";
    239 		this.dom.header.parentNode.appendChild( this.dom.clone.header );
    240
    241 		/* Body */
    242 		/* Remove any heights which have been applied already and let the browser figure it out */
    243 		$('tbody tr', that.dom.body).css('height', 'auto');
    244
    245 		if ( this.dom.clone.body !== null )
    246 		{
    247 			this.dom.clone.body.parentNode.removeChild( this.dom.clone.body );
    248 			this.dom.clone.body = null;
    249 		}
    250
    251 		if ( this.s.dt.aiDisplay.length > 0 )
    252 		{
    253 			this.dom.clone.body = $(this.dom.body).clone(true)[0];
    254 			this.dom.clone.body.className += " FixedColumns_Cloned";
    255 			if ( this.dom.clone.body.getAttribute('id') !== null )
    256 			{
    257 				this.dom.clone.body.removeAttribute('id');
    258 			}
    259
    260 			$('thead tr:eq(0)', this.dom.clone.body).each( function () {
    261 				$('th:gt('+(that.s.columns-1)+')', this).remove();
    262 			} );
    263
    264 			$('thead tr:gt(0)', this.dom.clone.body).remove();
    265
    266 			var jqBoxHack = $('tbody tr:eq(0) td:eq(0)', that.dom.body);
    267 			var iBoxHack = jqBoxHack.outerHeight() - jqBoxHack.height();
    268
    269 			/* Remove cells which are not needed and copy the height from the original table */
    270 			$('tbody tr', this.dom.clone.body).each( function (k) {
    271 				$('td:gt('+(that.s.columns-1)+')', this).remove();
    272
    273 				/* Can we use some kind of object detection here?! This is very nasty - damn browsers */
    274 				if ( $.browser.mozilla || $.browser.opera )
    275 				{
    276 					$('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() );
    277 				}
    278 				else
    279 				{
    280 					$('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() - iBoxHack );
    281 				}
    282
    283 				/* It's only really IE8 and Firefox which need this, but to simplify, lets apply to all.
    284 				 * The reason it is needed at all is sub-pixel rounded, which is done differently in every
    285 				 * browser... Except of course IE6 and IE7 - applying the height to them causes the cell
    286 				 * size to grow, but they don't mess around with sub-pixels so we can do nothing.
    287 				 */
    288 				if ( !bRubbishOldIE )
    289 				{
    290 					$('tbody tr:eq('+k+')', that.dom.body).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() );
    291 				}
    292 			} );
    293
    294 			$('tfoot tr:eq(0)', this.dom.clone.body).each( function () {
    295 				$('th:gt('+(that.s.columns-1)+')', this).remove();
    296 			} );
    297
    298 			$('tfoot tr:gt(0)', this.dom.clone.body).remove();
    299
    300
    301 			this.dom.clone.body.style.position = "absolute";
    302 			this.dom.clone.body.style.top = "0px";
    303 			this.dom.clone.body.style.left = "0px";
    304 			this.dom.clone.body.style.width = iTableWidth+"px";
    305 			this.dom.body.parentNode.appendChild( this.dom.clone.body );
    306 		}
    307
    308 		/* Footer */
    309 		if ( this.s.dt.nTFoot !== null )
    310 		{
    311 			if ( this.dom.clone.footer !== null )
    312 			{
    313 				this.dom.clone.footer.parentNode.removeChild( this.dom.clone.footer );
    314 			}
    315 			this.dom.clone.footer = $(this.dom.footer).clone(true)[0];
    316 			this.dom.clone.footer.className += " FixedColumns_Cloned";
    317
    318 			$('tfoot tr:eq(0)', this.dom.clone.footer).each( function () {
    319 				$('th:gt('+(that.s.columns-1)+')', this).remove();
    320 				$(this).height( $(that.dom.footer).height() );
    321 			} );
    322 			$('tr', this.dom.clone.footer).height( $(that.dom.footer).height() );
    323
    324 			$('tfoot tr:gt(0)', this.dom.clone.footer).remove();
    325
    326 			$('tfoot th', this.dom.clone.footer).each( function (i) {
    327 				this.style.width = aiCellWidth[i]+"px";
    328 			} );
    329
    330 			this.dom.clone.footer.style.position = "absolute";
    331 			this.dom.clone.footer.style.top = "0px";
    332 			this.dom.clone.footer.style.left = "0px";
    333 			this.dom.clone.footer.style.width = iTableWidth+"px";
    334 			this.dom.footer.parentNode.appendChild( this.dom.clone.footer );
    335 		}
    336 	},
    337
    338
    339 	/**
    340 	 * Set the absolute position of the fixed column tables when scrolling the DataTable
    341 	 *  @method  _fnScroll
    342 	 *  @returns void
    343 	 *  @private
    344 	 */
    345 	_fnScroll: function ()
    346 	{
    347 		var iScrollLeft = $(this.dom.scroller).scrollLeft();
    348
    349 		this.dom.clone.header.style.left = iScrollLeft+"px";
    350 		if ( this.dom.clone.body !== null )
    351 		{
    352 			this.dom.clone.body.style.left = iScrollLeft+"px";
    353 		}
    354 		if ( this.dom.footer )
    355 		{
    356 			this.dom.clone.footer.style.left = iScrollLeft+"px";
    357 		}
    358 	}
    359 };
    360