/** * @summary Scroller * @description Virtual rendering for DataTables * @file Scroller.js * @version 1.1.0 * @author Allan Jardine (www.sprymedia.co.uk) * @license GPL v2 or BSD 3 point style * @contact www.sprymedia.co.uk/contact * * @copyright Copyright 2011-2012 Allan Jardine, all rights reserved. * * This source file is free software, under either the GPL v2 license or a * BSD style license, available at: * http://datatables.net/license_gpl2 * http://datatables.net/license_bsd */ (/** @lends */function($, window, document) { /** * Scroller is a virtual rendering plug-in for DataTables which allows large * datasets to be drawn on screen every quickly. What the virtual rendering means * is that only the visible portion of the table (and a bit to either side to make * the scrolling smooth) is drawn, while the scrolling container gives the * visual impression that the whole table is visible. This is done by making use * of the pagination abilities of DataTables and moving the table around in the * scrolling container DataTables adds to the page. The scrolling container is * forced to the height it would be for the full table display using an extra * element. * * Note that rows in the table MUST all be the same height. Information in a cell * which expands on to multiple lines will cause some odd behaviour in the scrolling. * * Scroller is initialised by simply including the letter 'S' in the sDom for the * table you want to have this feature enabled on. Note that the 'S' must come * AFTER the 't' parameter in sDom. * * Key features include: * * * @class * @constructor * @param {object} oDT DataTables settings object * @param {object} [oOpts={}] Configuration object for FixedColumns. Options are defined by {@link Scroller.oDefaults} * * @requires jQuery 1.4+ * @requires DataTables 1.9.0+ * * @example * $(document).ready(function() { * $('#example').dataTable( { * "sScrollY": "200px", * "sAjaxSource": "media/dataset/large.txt", * "sDom": "frtiS", * "bDeferRender": true * } ); * } ); */ var Scroller = function ( oDTSettings, oOpts ) { /* Sanity check - you just know it will happen */ if ( ! this instanceof Scroller ) { alert( "Scroller warning: Scroller must be initialised with the 'new' keyword." ); return; } if ( typeof oOpts == 'undefined' ) { oOpts = {}; } /** * Settings object which contains customisable information for the Scroller instance * @namespace * @extends Scroller.DEFAULTS */ this.s = { /** * DataTables settings object * @type object * @default Passed in as first parameter to constructor */ "dt": oDTSettings, /** * Pixel location of the top of the drawn table in the viewport * @type int * @default 0 */ "tableTop": 0, /** * Pixel location of the bottom of the drawn table in the viewport * @type int * @default 0 */ "tableBottom": 0, /** * Pixel location of the boundary for when the next data set should be loaded and drawn * when scrolling up the way. * @type int * @default 0 * @private */ "redrawTop": 0, /** * Pixel location of the boundary for when the next data set should be loaded and drawn * when scrolling down the way. Note that this is actually caluated as the offset from * the top. * @type int * @default 0 * @private */ "redrawBottom": 0, /** * Height of rows in the table * @type int * @default 0 */ "rowHeight": null, /** * Auto row height or not indicator * @type bool * @default 0 */ "autoHeight": true, /** * Pixel height of the viewport * @type int * @default 0 */ "viewportHeight": 0, /** * Number of rows calculated as visible in the visible viewport * @type int * @default 0 */ "viewportRows": 0, /** * setTimeout reference for state saving, used when state saving is enabled in the DataTable * and when the user scrolls the viewport in order to stop the cookie set taking too much * CPU! * @type int * @default 0 */ "stateTO": null, /** * setTimeout reference for the redraw, used when server-side processing is enabled in the * DataTables in order to prevent DoSing the server * @type int * @default null */ "drawTO": null }; this.s = $.extend( this.s, Scroller.oDefaults, oOpts ); /** * DOM elements used by the class instance * @namespace * */ this.dom = { "force": document.createElement('div'), "scroller": null, "table": null }; /* Attach the instance to the DataTables instance so it can be accessed */ this.s.dt.oScroller = this; /* Let's do it */ this._fnConstruct(); }; Scroller.prototype = { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Calculate the pixel position from the top of the scrolling container for a given row * @param {int} iRow Row number to calculate the position of * @returns {int} Pixels * @example * $(document).ready(function() { * $('#example').dataTable( { * "sScrollY": "200px", * "sAjaxSource": "media/dataset/large.txt", * "sDom": "frtiS", * "bDeferRender": true, * "fnInitComplete": function (o) { * // Find where row 25 is * alert( o.oScroller.fnRowToPixels( 25 ) ); * } * } ); * } ); */ "fnRowToPixels": function ( iRow ) { return iRow * this.s.rowHeight; }, /** * Calculate the row number that will be found at the given pixel position (y-scroll) * @param {int} iPixels Offset from top to caluclate the row number of * @returns {int} Row index * @example * $(document).ready(function() { * $('#example').dataTable( { * "sScrollY": "200px", * "sAjaxSource": "media/dataset/large.txt", * "sDom": "frtiS", * "bDeferRender": true, * "fnInitComplete": function (o) { * // Find what row number is at 500px * alert( o.oScroller.fnPixelsToRow( 500 ) ); * } * } ); * } ); */ "fnPixelsToRow": function ( iPixels ) { return parseInt( iPixels / this.s.rowHeight, 10 ); }, /** * Calculate the row number that will be found at the given pixel position (y-scroll) * @param {int} iRow Row index to scroll to * @param {bool} [bAnimate=true] Animate the transision or not * @returns {void} * @example * $(document).ready(function() { * $('#example').dataTable( { * "sScrollY": "200px", * "sAjaxSource": "media/dataset/large.txt", * "sDom": "frtiS", * "bDeferRender": true, * "fnInitComplete": function (o) { * // Immediately scroll to row 1000 * o.oScroller.fnScrollToRow( 1000 ); * } * } ); * * // Sometime later on use the following to scroll to row 500... * var oSettings = $('#example').dataTable().fnSettings(); * oSettings.oScroller.fnScrollToRow( 500 ); * } ); */ "fnScrollToRow": function ( iRow, bAnimate ) { var px = this.fnRowToPixels( iRow ); if ( typeof bAnimate == 'undefined' || bAnimate ) { $(this.dom.scroller).animate( { "scrollTop": px } ); } else { $(this.dom.scroller).scrollTop( px ); } }, /** * Calculate and store information about how many rows are to be displayed in the scrolling * viewport, based on current dimensions in the browser's rendering. This can be particularly * useful if the table is initially drawn in a hidden element - for example in a tab. * @param {bool} [bRedraw=true] Redraw the table automatically after the recalculation, with * the new dimentions forming the basis for the draw. * @returns {void} * @example * $(document).ready(function() { * // Make the example container hidden to throw off the browser's sizing * document.getElementById('container').style.display = "none"; * var oTable = $('#example').dataTable( { * "sScrollY": "200px", * "sAjaxSource": "media/dataset/large.txt", * "sDom": "frtiS", * "bDeferRender": true, * "fnInitComplete": function (o) { * // Immediately scroll to row 1000 * o.oScroller.fnScrollToRow( 1000 ); * } * } ); * * setTimeout( function () { * // Make the example container visible and recalculate the scroller sizes * document.getElementById('container').style.display = "block"; * oTable.fnSettings().oScroller.fnMeasure(); * }, 3000 ); */ "fnMeasure": function ( bRedraw ) { if ( this.s.autoHeight ) { this._fnCalcRowHeight(); } this.s.viewportHeight = $(this.dom.scroller).height(); this.s.viewportRows = parseInt( this.s.viewportHeight/this.s.rowHeight, 10 )+1; this.s.dt._iDisplayLength = this.s.viewportRows * this.s.displayBuffer; if ( this.s.trace ) { console.log( 'Row height: '+this.s.rowHeight +' '+ 'Viewport height: '+this.s.viewportHeight +' '+ 'Viewport rows: '+ this.s.viewportRows +' '+ 'Display rows: '+ this.s.dt._iDisplayLength ); } if ( typeof bRedraw == 'undefined' || bRedraw ) { this.s.dt.oInstance.fnDraw(); } }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Private methods (they are of course public in JS, but recommended as private) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Initialisation for Scroller * @returns {void} * @private */ "_fnConstruct": function () { var that = this; /* Insert a div element that we can use to force the DT scrolling container to * the height that would be required if the whole table was being displayed */ this.dom.force.style.position = "absolute"; this.dom.force.style.top = "0px"; this.dom.force.style.left = "0px"; this.dom.force.style.width = "1px"; this.dom.scroller = $('div.'+this.s.dt.oClasses.sScrollBody, this.s.dt.nTableWrapper)[0]; this.dom.scroller.appendChild( this.dom.force ); this.dom.scroller.style.position = "relative"; this.dom.table = $('>table', this.dom.scroller)[0]; this.dom.table.style.position = "absolute"; this.dom.table.style.top = "0px"; this.dom.table.style.left = "0px"; // Add class to 'announce' that we are a Scroller table $(this.s.dt.nTableWrapper).addClass('DTS'); // Add a 'loading' indicator if ( this.s.loadingIndicator ) { $(this.dom.scroller.parentNode) .css('position', 'relative') .append('
'+this.s.dt.oLanguage.sLoadingRecords+'
'); } /* Initial size calculations */ if ( this.s.rowHeight && this.s.rowHeight != 'auto' ) { this.s.autoHeight = false; } this.fnMeasure( false ); /* Scrolling callback to see if a page change is needed */ $(this.dom.scroller).scroll( function () { that._fnScroll.call( that ); } ); /* In iOS we catch the touchstart event incase the user tries to scroll * while the display is already scrolling */ $(this.dom.scroller).bind('touchstart', function () { that._fnScroll.call( that ); } ); /* Update the scroller when the DataTable is redrawn */ this.s.dt.aoDrawCallback.push( { "fn": function () { if ( that.s.dt.bInitialised ) { that._fnDrawCallback.call( that ); } }, "sName": "Scroller" } ); /* Add a state saving parameter to the DT state saving so we can restore the exact * position of the scrolling */ this.s.dt.oApi._fnCallbackReg( this.s.dt, 'aoStateSaveParams', function (oS, oData) { oData.iScroller = that.dom.scroller.scrollTop; }, "Scroller_State" ); }, /** * Scrolling function - fired whenever the scrolling position is changed. This method needs * to use the stored values to see if the table should be redrawn as we are moving towards * the end of the information that is currently drawn or not. If needed, then it will redraw * the table based on the new position. * @returns {void} * @private */ "_fnScroll": function () { var that = this, iScrollTop = this.dom.scroller.scrollTop, iTopRow; /* If the table has been sorted or filtered, then we use the redraw that * DataTables as done, rather than performing our own */ if ( this.s.dt.bFiltered || this.s.dt.bSorted ) { return; } if ( this.s.trace ) { console.log( 'Scroll: '+iScrollTop+'px - boundaries: '+this.s.redrawTop+' / '+this.s.redrawBottom+'. '+ ' Showing rows '+this.fnPixelsToRow(iScrollTop)+ ' to '+this.fnPixelsToRow(iScrollTop+$(this.dom.scroller).height())+ ' in the viewport, with rows '+this.s.dt._iDisplayStart+ ' to '+(this.s.dt._iDisplayEnd)+' rendered by the DataTable' ); } /* Update the table's information display for what is now in the viewport */ this._fnInfo(); /* We dont' want to state save on every scroll event - that's heavy handed, so * use a timeout to update the state saving only when the scrolling has finished */ clearTimeout( this.s.stateTO ); this.s.stateTO = setTimeout( function () { that.s.dt.oApi._fnSaveState( that.s.dt ); }, 250 ); /* Check if the scroll point is outside the trigger boundary which would required * a DataTables redraw */ if ( iScrollTop < this.s.redrawTop || iScrollTop > this.s.redrawBottom ) { var preRows = ((this.s.displayBuffer-1)/2) * this.s.viewportRows; iTopRow = parseInt( iScrollTop / this.s.rowHeight, 10 ) - preRows; if ( iTopRow < 0 ) { /* At the start of the table */ iTopRow = 0; } else if ( iTopRow + this.s.dt._iDisplayLength > this.s.dt.fnRecordsDisplay() ) { /* At the end of the table */ iTopRow = this.s.dt.fnRecordsDisplay() - this.s.dt._iDisplayLength; if ( iTopRow < 0 ) { iTopRow = 0; } } else if ( iTopRow % 2 !== 0 ) { /* For the row-striping classes (odd/even) we want only to start on evens * otherwise the stripes will change between draws and look rubbish */ iTopRow++; } if ( iTopRow != this.s.dt._iDisplayStart ) { /* Cache the new table position for quick lookups */ this.s.tableTop = $(this.s.dt.nTable).offset().top; this.s.tableBottom = $(this.s.dt.nTable).height() + this.s.tableTop; /* Do the DataTables redraw based on the calculated start point - note that when * using server-side processing we introduce a small delay to not DoS the server... */ if ( this.s.dt.oFeatures.bServerSide ) { clearTimeout( this.s.drawTO ); this.s.drawTO = setTimeout( function () { that.s.dt._iDisplayStart = iTopRow; that.s.dt.oApi._fnCalculateEnd( that.s.dt ); that.s.dt.oApi._fnDraw( that.s.dt ); }, this.s.serverWait ); } else { this.s.dt._iDisplayStart = iTopRow; this.s.dt.oApi._fnCalculateEnd( this.s.dt ); this.s.dt.oApi._fnDraw( this.s.dt ); } if ( this.s.trace ) { console.log( 'Scroll forcing redraw - top DT render row: '+ iTopRow ); } } } }, /** * Draw callback function which is fired when the DataTable is redrawn. The main function of * this method is to position the drawn table correctly the scrolling container for the rows * that is displays as a result of the scrolling position. * @returns {void} * @private */ "_fnDrawCallback": function () { var that = this, iScrollTop = this.dom.scroller.scrollTop, iScrollBottom = iScrollTop + this.s.viewportHeight; /* Set the height of the scrolling forcer to be suitable for the number of rows * in this draw */ this.dom.force.style.height = (this.s.rowHeight * this.s.dt.fnRecordsDisplay())+"px"; /* Calculate the position that the top of the table should be at */ var iTableTop = (this.s.rowHeight*this.s.dt._iDisplayStart); if ( this.s.dt._iDisplayStart === 0 ) { iTableTop = 0; } else if ( this.s.dt._iDisplayStart === this.s.dt.fnRecordsDisplay() - this.s.dt._iDisplayLength ) { iTableTop = this.s.rowHeight * this.s.dt._iDisplayStart; } this.dom.table.style.top = iTableTop+"px"; /* Cache some information for the scroller */ this.s.tableTop = iTableTop; this.s.tableBottom = $(this.s.dt.nTable).height() + this.s.tableTop; this.s.redrawTop = iScrollTop - ( (iScrollTop - this.s.tableTop) * this.s.boundaryScale ); this.s.redrawBottom = iScrollTop + ( (this.s.tableBottom - iScrollBottom) * this.s.boundaryScale ); if ( this.s.trace ) { console.log( "Table redraw. Table top: "+iTableTop+"px "+ "Table bottom: "+this.s.tableBottom+" "+ "Scroll boundary top: "+this.s.redrawTop+" "+ "Scroll boundary bottom: "+this.s.redrawBottom+" "+ "Rows drawn: "+this.s.dt._iDisplayLength); } /* Because of the order of the DT callbacks, the info update will * take precidence over the one we want here. So a 'thread' break is * needed */ setTimeout( function () { that._fnInfo.call( that ); }, 0 ); /* Restore the scrolling position that was saved by DataTable's state saving * Note that this is done on the second draw when data is Ajax sourced, and the * first draw when DOM soured */ if ( this.s.dt.oFeatures.bStateSave && this.s.dt.oLoadedState !== null && typeof this.s.dt.oLoadedState.iScroller != 'undefined' ) { if ( (this.s.dt.sAjaxSource !== null && this.s.dt.iDraw == 2) || (this.s.dt.sAjaxSource === null && this.s.dt.iDraw == 1) ) { setTimeout( function () { $(that.dom.scroller).scrollTop( that.s.dt.oLoadedState.iScroller ); that.s.redrawTop = that.s.dt.oLoadedState.iScroller - (that.s.viewportHeight/2); }, 0 ); } } }, /** * Automatic calculation of table row height. This is just a little tricky here as using * initialisation DataTables has tale the table out of the document, so we need to create * a new table and insert it into the document, calculate the row height and then whip the * table out. * @returns {void} * @private */ "_fnCalcRowHeight": function () { var nTable = this.s.dt.nTable.cloneNode( false ); var nContainer = $( '
'+ '
'+ '
'+ '
'+ '
' )[0]; $(nTable).append( ''+ ''+ ' '+ ''+ '' ); $('div.'+this.s.dt.oClasses.sScrollBody, nContainer).append( nTable ); document.body.appendChild( nContainer ); this.s.rowHeight = $('tbody tr', nTable).outerHeight(); document.body.removeChild( nContainer ); }, /** * Update any information elements that are controlled by the DataTable based on the scrolling * viewport and what rows are visible in it. This function basically acts in the same way as * _fnUpdateInfo in DataTables, and effectively replaces that function. * @returns {void} * @private */ "_fnInfo": function () { if ( !this.s.dt.oFeatures.bInfo ) { return; } var dt = this.s.dt, iScrollTop = this.dom.scroller.scrollTop, iStart = this.fnPixelsToRow(iScrollTop)+1, iMax = dt.fnRecordsTotal(), iTotal = dt.fnRecordsDisplay(), iPossibleEnd = this.fnPixelsToRow(iScrollTop+$(this.dom.scroller).height()), iEnd = iTotal < iPossibleEnd ? iTotal : iPossibleEnd, sStart = dt.fnFormatNumber( iStart ), sEnd = dt.fnFormatNumber( iEnd ), sMax = dt.fnFormatNumber( iMax ), sTotal = dt.fnFormatNumber( iTotal ), sOut; if ( dt.fnRecordsDisplay() === 0 && dt.fnRecordsDisplay() == dt.fnRecordsTotal() ) { /* Empty record set */ sOut = dt.oLanguage.sInfoEmpty+ dt.oLanguage.sInfoPostFix; } else if ( dt.fnRecordsDisplay() === 0 ) { /* Rmpty record set after filtering */ sOut = dt.oLanguage.sInfoEmpty +' '+ dt.oLanguage.sInfoFiltered.replace('_MAX_', sMax)+ dt.oLanguage.sInfoPostFix; } else if ( dt.fnRecordsDisplay() == dt.fnRecordsTotal() ) { /* Normal record set */ sOut = dt.oLanguage.sInfo. replace('_START_', sStart). replace('_END_', sEnd). replace('_TOTAL_', sTotal)+ dt.oLanguage.sInfoPostFix; } else { /* Record set after filtering */ sOut = dt.oLanguage.sInfo. replace('_START_', sStart). replace('_END_', sEnd). replace('_TOTAL_', sTotal) +' '+ dt.oLanguage.sInfoFiltered.replace('_MAX_', dt.fnFormatNumber(dt.fnRecordsTotal()))+ dt.oLanguage.sInfoPostFix; } var n = dt.aanFeatures.i; if ( typeof n != 'undefined' ) { for ( var i=0, iLen=n.length ; i