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