/* * File: FixedHeader.js * Version: 2.0.4 * Description: "Fix" a header at the top of the table, so it scrolls with the table * Author: Allan Jardine (www.sprymedia.co.uk) * Created: Wed 16 Sep 2009 19:46:30 BST * Language: Javascript * License: LGPL * Project: Just a little bit of fun - enjoy :-) * Contact: www.sprymedia.co.uk/contact * * Copyright 2009-2010 Allan Jardine, all rights reserved. */ /* * Function: FixedHeader * Purpose: Provide 'fixed' header, footer and columns on an HTML table * Returns: object:FixedHeader - must be called with 'new' * Inputs: mixed:mTable - target table * 1. DataTable object - when using FixedHeader with DataTables, or * 2. HTML table node - when using FixedHeader without DataTables * object:oInit - initialisation settings, with the following properties (each optional) * bool:top - fix the header (default true) * bool:bottom - fix the footer (default false) * bool:left - fix the left most column (default false) * bool:right - fix the right most column (default false) * int:zTop - fixed header zIndex * int:zBottom - fixed footer zIndex * int:zLeft - fixed left zIndex * int:zRight - fixed right zIndex */ var FixedHeader = function ( mTable, oInit ) { /* Sanity check - you just know it will happen */ if ( typeof this.fnInit != 'function' ) { alert( "FixedHeader warning: FixedHeader must be initialised with the 'new' keyword." ); return; } var that = this; var oSettings = { "aoCache": [], "oSides": { "top": true, "bottom": false, "left": false, "right": false }, "oZIndexes": { "top": 104, "bottom": 103, "left": 102, "right": 101 }, "oMes": { "iTableWidth": 0, "iTableHeight": 0, "iTableLeft": 0, "iTableRight": 0, /* note this is left+width, not actually "right" */ "iTableTop": 0, "iTableBottom": 0 /* note this is top+height, not actually "bottom" */ }, "nTable": null, "bUseAbsPos": false, "bFooter": false }; /* * Function: fnGetSettings * Purpose: Get the settings for this object * Returns: object: - settings object * Inputs: - */ this.fnGetSettings = function () { return oSettings; }; /* * Function: fnUpdate * Purpose: Update the positioning and copies of the fixed elements * Returns: - * Inputs: - */ this.fnUpdate = function () { this._fnUpdateClones(); this._fnUpdatePositions(); }; /* Let's do it */ this.fnInit( mTable, oInit ); }; /* * Variable: FixedHeader * Purpose: Prototype for FixedHeader * Scope: global */ FixedHeader.prototype = { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Initialisation */ /* * Function: fnInit * Purpose: The "constructor" * Returns: - * Inputs: {as FixedHeader function} */ fnInit: function ( oTable, oInit ) { var s = this.fnGetSettings(); var that = this; /* Record the user definable settings */ this.fnInitSettings( s, oInit ); /* DataTables specific stuff */ if ( typeof oTable.fnSettings == 'function' ) { if ( typeof oTable.fnVersionCheck == 'functon' && oTable.fnVersionCheck( '1.6.0' ) !== true ) { alert( "FixedHeader 2 required DataTables 1.6.0 or later. "+ "Please upgrade your DataTables installation" ); return; } var oDtSettings = oTable.fnSettings(); if ( oDtSettings.oScroll.sX != "" || oDtSettings.oScroll.sY != "" ) { alert( "FixedHeader 2 is not supported with DataTables' scrolling mode at this time" ); return; } s.nTable = oDtSettings.nTable; oDtSettings.aoDrawCallback.push( { "fn": function () { FixedHeader.fnMeasure(); that._fnUpdateClones.call(that); that._fnUpdatePositions.call(that); }, "sName": "FixedHeader" } ); } else { s.nTable = oTable; } s.bFooter = ($('>tfoot', s.nTable).length > 0) ? true : false; /* "Detect" browsers that don't support absolute positioing - or have bugs */ s.bUseAbsPos = (jQuery.browser.msie && (jQuery.browser.version=="6.0"||jQuery.browser.version=="7.0")); /* Add the 'sides' that are fixed */ if ( s.oSides.top ) { s.aoCache.push( that._fnCloneTable( "fixedHeader", "FixedHeader_Header", that._fnCloneThead ) ); } if ( s.oSides.bottom ) { s.aoCache.push( that._fnCloneTable( "fixedFooter", "FixedHeader_Footer", that._fnCloneTfoot ) ); } if ( s.oSides.left ) { s.aoCache.push( that._fnCloneTable( "fixedLeft", "FixedHeader_Left", that._fnCloneTLeft ) ); } if ( s.oSides.right ) { s.aoCache.push( that._fnCloneTable( "fixedRight", "FixedHeader_Right", that._fnCloneTRight ) ); } /* Event listeners for window movement */ FixedHeader.afnScroll.push( function () { that._fnUpdatePositions.call(that); } ); jQuery(window).resize( function () { FixedHeader.fnMeasure(); that._fnUpdateClones.call(that); that._fnUpdatePositions.call(that); } ); /* Get things right to start with */ FixedHeader.fnMeasure(); that._fnUpdateClones(); that._fnUpdatePositions(); }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Support functions */ /* * Function: fnInitSettings * Purpose: Take the user's settings and copy them to our local store * Returns: - * Inputs: object:s - the local settings object * object:oInit - the user's settings object */ fnInitSettings: function ( s, oInit ) { if ( typeof oInit != 'undefined' ) { if ( typeof oInit.top != 'undefined' ) { s.oSides.top = oInit.top; } if ( typeof oInit.bottom != 'undefined' ) { s.oSides.bottom = oInit.bottom; } if ( typeof oInit.left != 'undefined' ) { s.oSides.left = oInit.left; } if ( typeof oInit.right != 'undefined' ) { s.oSides.right = oInit.right; } if ( typeof oInit.zTop != 'undefined' ) { s.oZIndexes.top = oInit.zTop; } if ( typeof oInit.zBottom != 'undefined' ) { s.oZIndexes.bottom = oInit.zBottom; } if ( typeof oInit.zLeft != 'undefined' ) { s.oZIndexes.left = oInit.zLeft; } if ( typeof oInit.zRight != 'undefined' ) { s.oZIndexes.right = oInit.zRight; } } /* Detect browsers which have poor position:fixed support so we can use absolute positions. * This is much slower since the position must be updated for each scroll, but widens * compatibility */ s.bUseAbsPos = (jQuery.browser.msie && (jQuery.browser.version=="6.0"||jQuery.browser.version=="7.0")); }, /* * Function: _fnCloneTable * Purpose: Clone the table node and do basic initialisation * Returns: - * Inputs: - */ _fnCloneTable: function ( sType, sClass, fnClone ) { var s = this.fnGetSettings(); var nCTable; /* We know that the table _MUST_ has a DIV wrapped around it, because this is simply how * DataTables works. Therefore, we can set this to be relatively position (if it is not * alreadu absolute, and use this as the base point for the cloned header */ if ( jQuery(s.nTable.parentNode).css('position') != "absolute" ) { s.nTable.parentNode.style.position = "relative"; } /* Just a shallow clone will do - we only want the table node */ nCTable = s.nTable.cloneNode( false ); var nDiv = document.createElement( 'div' ); nDiv.style.position = "absolute"; nDiv.className += " FixedHeader_Cloned "+sType+" "+sClass; /* Set the zIndexes */ if ( sType == "fixedHeader" ) { nDiv.style.zIndex = s.oZIndexes.top; } if ( sType == "fixedFooter" ) { nDiv.style.zIndex = s.oZIndexes.bottom; } if ( sType == "fixedLeft" ) { nDiv.style.zIndex = s.oZIndexes.left; } else if ( sType == "fixedRight" ) { nDiv.style.zIndex = s.oZIndexes.right; } /* Insert the newly cloned table into the DOM, on top of the "real" header */ nDiv.appendChild( nCTable ); document.body.appendChild( nDiv ); return { "nNode": nCTable, "nWrapper": nDiv, "sType": sType, "sPosition": "", "sTop": "", "sLeft": "", "fnClone": fnClone }; }, /* * Function: _fnUpdatePositions * Purpose: Get the current positioning of the table in the DOM * Returns: - * Inputs: - */ _fnMeasure: function () { var s = this.fnGetSettings(), m = s.oMes, jqTable = jQuery(s.nTable), oOffset = jqTable.offset(), iParentScrollTop = this._fnSumScroll( s.nTable.parentNode, 'scrollTop' ), iParentScrollLeft = this._fnSumScroll( s.nTable.parentNode, 'scrollLeft' ); m.iTableWidth = jqTable.outerWidth(); m.iTableHeight = jqTable.outerHeight(); m.iTableLeft = oOffset.left + s.nTable.parentNode.scrollLeft; m.iTableTop = oOffset.top + iParentScrollTop; m.iTableRight = m.iTableLeft + m.iTableWidth; m.iTableRight = FixedHeader.oDoc.iWidth - m.iTableLeft - m.iTableWidth; m.iTableBottom = FixedHeader.oDoc.iHeight - m.iTableTop - m.iTableHeight; }, /* * Function: _fnSumScroll * Purpose: Sum node parameters all the way to the top * Returns: int: sum * Inputs: node:n - node to consider * string:side - scrollTop or scrollLeft */ _fnSumScroll: function ( n, side ) { var i = n[side]; while ( n = n.parentNode ) { if ( n.nodeName != 'HTML' && n.nodeName != 'BODY' ) { break; } i = n[side]; } return i; }, /* * Function: _fnUpdatePositions * Purpose: Loop over the fixed elements for this table and update their positions * Returns: - * Inputs: - */ _fnUpdatePositions: function () { var s = this.fnGetSettings(); this._fnMeasure(); for ( var i=0, iLen=s.aoCache.length ; i oWin.iScrollTop ) { /* Above the table */ this._fnUpdateCache( oCache, 'sPosition', "absolute", 'position', nTable.style ); this._fnUpdateCache( oCache, 'sTop', oMes.iTableTop+"px", 'top', nTable.style ); this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style ); } else if ( oWin.iScrollTop > oMes.iTableTop+iTbodyHeight ) { /* At the bottom of the table */ this._fnUpdateCache( oCache, 'sPosition', "absolute", 'position', nTable.style ); this._fnUpdateCache( oCache, 'sTop', (oMes.iTableTop+iTbodyHeight)+"px", 'top', nTable.style ); this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style ); } else { /* In the middle of the table */ if ( s.bUseAbsPos ) { this._fnUpdateCache( oCache, 'sPosition', "absolute", 'position', nTable.style ); this._fnUpdateCache( oCache, 'sTop', oWin.iScrollTop+"px", 'top', nTable.style ); this._fnUpdateCache( oCache, 'sLeft', oMes.iTableLeft+"px", 'left', nTable.style ); } else { this._fnUpdateCache( oCache, 'sPosition', 'fixed', 'position', nTable.style ); this._fnUpdateCache( oCache, 'sTop', "0px", 'top', nTable.style ); this._fnUpdateCache( oCache, 'sLeft', (oMes.iTableLeft-oWin.iScrollLeft)+"px", 'left', nTable.style ); } } }, /* * Function: _fnUpdateCache * Purpose: Check the cache and update cache and value if needed * Returns: - * Inputs: object:oCache - local cache object * string:sCache - cache property * string:sSet - value to set * string:sProperty - object property to set * object:oObj - object to update */ _fnUpdateCache: function ( oCache, sCache, sSet, sProperty, oObj ) { if ( oCache[sCache] != sSet ) { oObj[sProperty] = sSet; oCache[sCache] = sSet; } }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Cloning functions */ /* * Function: _fnCloneThead * Purpose: Clone the thead element * Returns: - * Inputs: object:oCache - the cahced values for this fixed element */ _fnCloneThead: function ( oCache ) { var s = this.fnGetSettings(); var nTable = oCache.nNode; /* Set the wrapper width to match that of the cloned table */ oCache.nWrapper.style.width = jQuery(s.nTable).outerWidth()+"px"; /* Remove any children the cloned table has */ while ( nTable.childNodes.length > 0 ) { jQuery('thead th', nTable).unbind( 'click' ); nTable.removeChild( nTable.childNodes[0] ); } /* Clone the DataTables header */ var nThead = jQuery('thead', s.nTable).clone(true)[0]; nTable.appendChild( nThead ); /* Copy the widths across - apparently a clone isn't good enough for this */ jQuery("thead:eq(0)>tr th", s.nTable).each( function (i) { jQuery("thead:eq(0)>tr th:eq("+i+")", nTable).width( jQuery(this).width() ); } ); jQuery("thead:eq(0)>tr td", s.nTable).each( function (i) { jQuery("thead:eq(0)>tr th:eq("+i+")", nTable)[0].style.width( jQuery(this).width() ); } ); }, /* * Function: _fnCloneTfoot * Purpose: Clone the tfoot element * Returns: - * Inputs: object:oCache - the cahced values for this fixed element */ _fnCloneTfoot: function ( oCache ) { var s = this.fnGetSettings(); var nTable = oCache.nNode; /* Set the wrapper width to match that of the cloned table */ oCache.nWrapper.style.width = jQuery(s.nTable).outerWidth()+"px"; /* Remove any children the cloned table has */ while ( nTable.childNodes.length > 0 ) { nTable.removeChild( nTable.childNodes[0] ); } /* Clone the DataTables footer */ var nTfoot = jQuery('tfoot', s.nTable).clone(true)[0]; nTable.appendChild( nTfoot ); /* Copy the widths across - apparently a clone isn't good enough for this */ jQuery("tfoot:eq(0)>tr th", s.nTable).each( function (i) { jQuery("tfoot:eq(0)>tr th:eq("+i+")", nTable).width( jQuery(this).width() ); } ); jQuery("tfoot:eq(0)>tr td", s.nTable).each( function (i) { jQuery("tfoot:eq(0)>tr th:eq("+i+")", nTable)[0].style.width( jQuery(this).width() ); } ); }, /* * Function: _fnCloneTLeft * Purpose: Clone the left column * Returns: - * Inputs: object:oCache - the cahced values for this fixed element */ _fnCloneTLeft: function ( oCache ) { var s = this.fnGetSettings(); var nTable = oCache.nNode; var iCols = jQuery('tbody tr:eq(0) td', s.nTable).length; var bRubbishOldIE = ($.browser.msie && ($.browser.version == "6.0" || $.browser.version == "7.0")); /* Remove any children the cloned table has */ while ( nTable.childNodes.length > 0 ) { nTable.removeChild( nTable.childNodes[0] ); } /* Is this the most efficient way to do this - it looks horrible... */ nTable.appendChild( jQuery("thead", s.nTable).clone(true)[0] ); nTable.appendChild( jQuery("tbody", s.nTable).clone(true)[0] ); if ( s.bFooter ) { nTable.appendChild( jQuery("tfoot", s.nTable).clone(true)[0] ); } jQuery('thead tr th:gt(0)', nTable).remove(); jQuery('tfoot tr th:gt(0)', nTable).remove(); /* Basically the same as used in FixedColumns - remove and copy heights */ $('tbody tr', nTable).each( function (k) { $('td:gt(0)', this).remove(); /* Can we use some kind of object detection here?! This is very nasty - damn browsers */ if ( $.browser.mozilla || $.browser.opera ) { $('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() ); } else { $('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() - iBoxHack ); } if ( !bRubbishOldIE ) { $('tbody tr:eq('+k+')', that.dom.body).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() ); } } ); var iWidth = jQuery('thead tr th:eq(0)', s.nTable).outerWidth(); nTable.style.width = iWidth+"px"; oCache.nWrapper.style.width = iWidth+"px"; }, /* * Function: _fnCloneTRight * Purpose: Clone the right most colun * Returns: - * Inputs: object:oCache - the cahced values for this fixed element */ _fnCloneTRight: function ( oCache ) { var s = this.fnGetSettings(); var nTable = oCache.nNode; var iCols = jQuery('tbody tr:eq(0) td', s.nTable).length; var bRubbishOldIE = ($.browser.msie && ($.browser.version == "6.0" || $.browser.version == "7.0")); /* Remove any children the cloned table has */ while ( nTable.childNodes.length > 0 ) { nTable.removeChild( nTable.childNodes[0] ); } /* Is this the most efficient way to do this - it looks horrible... */ nTable.appendChild( jQuery("thead", s.nTable).clone(true)[0] ); nTable.appendChild( jQuery("tbody", s.nTable).clone(true)[0] ); if ( s.bFooter ) { nTable.appendChild( jQuery("tfoot", s.nTable).clone(true)[0] ); } jQuery('thead tr th:not(:nth-child('+iCols+'n))', nTable).remove(); jQuery('tfoot tr th:not(:nth-child('+iCols+'n))', nTable).remove(); /* Basically the same as used in FixedColumns - remove and copy heights */ $('tbody tr', nTable).each( function (k) { $('td:lt('+iCols-1+')', this).remove(); /* Can we use some kind of object detection here?! This is very nasty - damn browsers */ if ( $.browser.mozilla || $.browser.opera ) { $('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() ); } else { $('td', this).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() - iBoxHack ); } if ( !bRubbishOldIE ) { $('tbody tr:eq('+k+')', that.dom.body).height( $('tbody tr:eq('+k+')', that.dom.body).outerHeight() ); } } ); var iWidth = jQuery('thead tr th:eq('+(iCols-1)+')', s.nTable).outerWidth(); nTable.style.width = iWidth+"px"; oCache.nWrapper.style.width = iWidth+"px"; } }; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Static properties and methods * We use these for speed! This information is common to all instances of FixedHeader, so no * point if having them calculated and stored for each different instance. */ /* * Variable: oWin * Purpose: Store information about the window positioning * Scope: FixedHeader */ FixedHeader.oWin = { "iScrollTop": 0, "iScrollRight": 0, "iScrollBottom": 0, "iScrollLeft": 0, "iHeight": 0, "iWidth": 0 }; /* * Variable: oDoc * Purpose: Store information about the document size * Scope: FixedHeader */ FixedHeader.oDoc = { "iHeight": 0, "iWidth": 0 }; /* * Variable: afnScroll * Purpose: Array of functions that are to be used for the scrolling components * Scope: FixedHeader */ FixedHeader.afnScroll = []; /* * Function: fnMeasure * Purpose: Update the measurements for the window and document * Returns: - * Inputs: - */ FixedHeader.fnMeasure = function () { var jqWin = jQuery(window), jqDoc = jQuery(document), oWin = FixedHeader.oWin, oDoc = FixedHeader.oDoc; oDoc.iHeight = jqDoc.height(); oDoc.iWidth = jqDoc.width(); oWin.iHeight = jqWin.height(); oWin.iWidth = jqWin.width(); oWin.iScrollTop = jqWin.scrollTop(); oWin.iScrollLeft = jqWin.scrollLeft(); oWin.iScrollRight = oDoc.iWidth - oWin.iScrollLeft - oWin.iWidth; oWin.iScrollBottom = oDoc.iHeight - oWin.iScrollTop - oWin.iHeight; }; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Global processing */ /* * Just one 'scroll' event handler in FixedHeader, which calls the required components. This is * done as an optimisation, to reduce calculation and proagation time */ jQuery(window).scroll( function () { FixedHeader.fnMeasure(); for ( var i=0, iLen=FixedHeader.afnScroll.length ; i