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