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