      1 /*
      2  * File:        TableTools.js
      3  * Version:     1.1.4
      4  * CVS:         $Id$
      5  * Description: Copy, save and print functions for DataTables
      6  * Author:      Allan Jardine (www.sprymedia.co.uk)
      7  * Created:     Wed  1 Apr 2009 08:41:58 BST
      8  * Modified:    $Date$ by $Author$
      9  * Language:    Javascript
     10  * License:     LGPL
     11  * Project:     Just a little bit of fun :-)
     12  * Contact:     www.sprymedia.co.uk/contact
     13  *
     14  * Copyright 2009-2010 Allan Jardine, all rights reserved.
     15  *
     16  */
     17
     18 /*
     19  * Variable: TableToolsInit
     20  * Purpose:  Parameters for TableTools customisation
     21  * Scope:    global
     22  */
     23 var TableToolsInit = {
     24 	oFeatures: {
     25 		bCsv: true,
     26 		bXls: true,
     27 		bCopy: true,
     28 		bPrint: true
     29 	},
     30 	oBom: {
     31 		bCsv: true,
     32 		bXls: true
     33 	},
     34 	bIncFooter: true,
     35 	bIncHiddenColumns: false,
     36 	sPrintMessage: "", /* Message with will print with the table */
     37 	sPrintInfo: "<h6>Print view</h6><p>Please use your browser's print function to "+
     38 		"print this table. Press escape when finished.", /* The 'fading' message */
     39 	sTitle: "",
     40 	sSwfPath: "media/swf/ZeroClipboard.swf",
     41 	iButtonHeight: 30,
     42 	iButtonWidth: 30,
     43 	sCsvBoundary: "'",
     44 	_iNextId: 1 /* Internal useage - but needs to be global */
     45 };
     46
     47
     48 (function($) {
     49 /*
     50  * Function: TableTools
     51  * Purpose:  TableTools "class"
     52  * Returns:  same as _fnInit
     53  * Inputs:   same as _fnInit
     54  */
     55 function TableTools ( oInit )
     56 {
     57 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     58 	 * Private parameters
     59 	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
     60 	var _oSettings;
     61 	var nTools = null;
     62 	var _nTableWrapper;
     63 	var _aoPrintHidden = [];
     64 	var _iPrintScroll = 0;
     65 	var _nPrintMessage = null;
     66 	var _DTSettings;
     67 	var _sLastData;
     68 	var _iId;
     69
     70
     71 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     72 	 * Initialisation
     73 	 */
     74
     75 	/*
     76 	 * Function: _fnInit
     77 	 * Purpose:  Initialise the table tools
     78 	 * Returns:  node: - The created node for the table tools wrapping
     79  	 * Inputs:   object:oInit - object with:
     80  	 *             oDTSettings - DataTables settings
     81 	 */
     82 	function _fnInit( oInit )
     83 	{
     84 		_nTools = document.createElement('div');
     85 		_nTools.className = "TableTools";
     86 		_iId = TableToolsInit._iNextId++;
     87
     88 		/* Copy the init object */
     89 		_oSettings = $.extend( true, {}, TableToolsInit );
     90
     91 		_DTSettings = oInit.oDTSettings;
     92
     93 		_nTableWrapper = fnFindParentClass( _DTSettings.nTable, "dataTables_wrapper" );
     94
     95 		ZeroClipboard.moviePath = _oSettings.sSwfPath;
     96
     97 		if ( _oSettings.oFeatures.bCopy ) {
     98 			fnFeatureClipboard();
     99 		}
    100 		if ( _oSettings.oFeatures.bCsv ) {
    101 			fnFeatureSaveCSV();
    102 		}
    103 		if ( _oSettings.oFeatures.bXls ) {
    104 			fnFeatureSaveXLS();
    105 		}
    106 		if ( _oSettings.oFeatures.bPrint ) {
    107 			fnFeaturePrint();
    108 		}
    109
    110 		return _nTools;
    111 	}
    112
    113
    114 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    115 	 * Feature buttons
    116 	 */
    117
    118 	/*
    119 	 * Function: fnFeatureSaveCSV
    120 	 * Purpose:  Add a button for saving a CSV file
    121 	 * Returns:  -
    122 	 * Inputs:   -
    123 	 */
    124 	function fnFeatureSaveCSV ()
    125 	{
    126 		var sBaseClass = "TableTools_button TableTools_csv";
    127 		var nButton = document.createElement( 'div' );
    128 		nButton.id = "ToolTables_CSV_"+_iId;
    129 		nButton.style.height = _oSettings.iButtonHeight+'px';
    130 		nButton.style.width = _oSettings.iButtonWidth+'px';
    131 		nButton.className = sBaseClass;
    132 		_nTools.appendChild( nButton );
    133
    134 		var clip = new ZeroClipboard.Client();
    135 		clip.setHandCursor( true );
    136 		clip.setAction( 'save' );
    137 		clip.setCharSet( 'UTF8' );
    138 		clip.setBomInc( _oSettings.oBom.bCsv );
    139 		clip.setFileName( fnGetTitle()+'.csv' );
    140
    141 		clip.addEventListener('mouseOver', function(client) {
    142 			nButton.className = sBaseClass+'_hover';
    143 		} );
    144
    145 		clip.addEventListener('mouseOut', function(client) {
    146 			nButton.className = sBaseClass;
    147 		} );
    148
    149 		clip.addEventListener('mouseDown', function(client) {
    150 			fnFlashSetText( clip, fnGetDataTablesData(",", TableToolsInit.sCsvBoundary) );
    151 		} );
    152
    153 		fnGlue( clip, nButton, "ToolTables_CSV_"+_iId, "Save as CSV" );
    154 	}
    155
    156
    157 	/*
    158 	 * Function: fnFeatureSaveXLS
    159 	 * Purpose:  Add a button for saving an XLS file
    160 	 * Returns:  -
    161 	 * Inputs:   -
    162 	 */
    163 	function fnFeatureSaveXLS ()
    164 	{
    165 		var sBaseClass = "TableTools_button TableTools_xls";
    166 		var nButton = document.createElement( 'div' );
    167 		nButton.id = "ToolTables_XLS_"+_iId;
    168 		nButton.style.height = _oSettings.iButtonHeight+'px';
    169 		nButton.style.width = _oSettings.iButtonWidth+'px';
    170 		nButton.className = sBaseClass;
    171 		_nTools.appendChild( nButton );
    172
    173 		var clip = new ZeroClipboard.Client();
    174 		clip.setHandCursor( true );
    175 		clip.setAction( 'save' );
    176 		clip.setCharSet( 'UTF16LE' );
    177 		clip.setBomInc( _oSettings.oBom.bXls );
    178 		clip.setFileName( fnGetTitle()+'.xls' );
    179
    180 		clip.addEventListener('mouseOver', function(client) {
    181 			nButton.className = sBaseClass+'_hover';
    182 		} );
    183
    184 		clip.addEventListener('mouseOut', function(client) {
    185 			nButton.className = sBaseClass;
    186 		} );
    187
    188 		clip.addEventListener('mouseDown', function(client) {
    189 			fnFlashSetText( clip, fnGetDataTablesData("\t") );
    190 		} );
    191
    192 		fnGlue( clip, nButton, "ToolTables_XLS_"+_iId, "Save for Excel" );
    193 	}
    194
    195
    196 	/*
    197 	 * Function: fnFeatureClipboard
    198 	 * Purpose:  Add a button for copying data to clipboard
    199 	 * Returns:  -
    200 	 * Inputs:   -
    201 	 */
    202 	function fnFeatureClipboard ()
    203 	{
    204 		var sBaseClass = "TableTools_button TableTools_clipboard";
    205 		var nButton = document.createElement( 'div' );
    206 		nButton.id = "ToolTables_Copy_"+_iId;
    207 		nButton.style.height = _oSettings.iButtonHeight+'px';
    208 		nButton.style.width = _oSettings.iButtonWidth+'px';
    209 		nButton.className = sBaseClass;
    210 		_nTools.appendChild( nButton );
    211
    212 		var clip = new ZeroClipboard.Client();
    213 		clip.setHandCursor( true );
    214 		clip.setAction( 'copy' );
    215
    216 		clip.addEventListener('mouseOver', function(client) {
    217 			nButton.className = sBaseClass+'_hover';
    218 		} );
    219
    220 		clip.addEventListener('mouseOut', function(client) {
    221 			nButton.className = sBaseClass;
    222 		} );
    223
    224 		clip.addEventListener('mouseDown', function(client) {
    225 			fnFlashSetText( clip, fnGetDataTablesData("\t") );
    226 		} );
    227
    228 		clip.addEventListener('complete', function (client, text) {
    229 			var aData = _sLastData.split('\n');
    230 			alert( 'Copied '+(aData.length-1)+' rows to the clipboard' );
    231 		} );
    232
    233 		fnGlue( clip, nButton, "ToolTables_Copy_"+_iId, "Copy to clipboard" );
    234 	}
    235
    236
    237 	/*
    238 	 * Function: fnFeaturePrint
    239 	 * Purpose:  Add a button for printing data
    240 	 * Returns:  -
    241 	 * Inputs:   -
    242 	 * Notes:    Fun one this function. In order to print the table, we want the table to retain
    243 	 *   it's position in the DOM, so all styles still apply, but we don't want to print all the
    244 	 *   other nonesense. So we hide that nonesese and add an event handler for 'esc' which will
    245 	 *   restore a normal view.
    246 	 */
    247 	function fnFeaturePrint ()
    248 	{
    249 		var sBaseClass = "TableTools_button TableTools_print";
    250 		var nButton = document.createElement( 'div' );
    251 		nButton.style.height = _oSettings.iButtonHeight+'px';
    252 		nButton.style.width = _oSettings.iButtonWidth+'px';
    253 		nButton.className = sBaseClass;
    254 		nButton.title = "Print table";
    255 		_nTools.appendChild( nButton );
    256
    257 		/* Could do this in CSS - but might as well be consistent with the flash buttons */
    258 		$(nButton).hover( function(client) {
    259 			nButton.className = sBaseClass+'_hover';
    260 		}, function(client) {
    261 			nButton.className = sBaseClass;
    262 		} );
    263
    264 		$(nButton).click( function() {
    265 			/* Parse through the DOM hiding everything that isn't needed for the table */
    266 			fnPrintHideNodes( _DTSettings.nTable );
    267
    268 			/* Show the whole table */
    269 			_iPrintSaveStart = _DTSettings._iDisplayStart;
    270 			_iPrintSaveLength = _DTSettings._iDisplayLength;
    271 			_DTSettings._iDisplayStart = 0;
    272 			_DTSettings._iDisplayLength = -1;
    273 			_DTSettings.oApi._fnCalculateEnd( _DTSettings );
    274 			_DTSettings.oApi._fnDraw( _DTSettings );
    275
    276 			/* Remove the other DataTables feature nodes - but leave the table! and info div */
    277 			var anFeature = _DTSettings.anFeatures;
    278 			for ( var cFeature in anFeature )
    279 			{
    280 				if ( cFeature != 'i' && cFeature != 't' )
    281 				{
    282 					_aoPrintHidden.push( {
    283 						node: anFeature[cFeature],
    284 						display: "block"
    285 					} );
    286 					anFeature[cFeature].style.display = "none";
    287 				}
    288 			}
    289
    290 			/* Add a node telling the user what is going on */
    291 			var nInfo = document.createElement( "div" );
    292 			nInfo.className = "TableTools_PrintInfo";
    293 			nInfo.innerHTML = _oSettings.sPrintInfo;
    294 			document.body.appendChild( nInfo );
    295
    296 			/* Add a message at the top of the page */
    297 			if ( _oSettings.sPrintMessage !== "" )
    298 			{
    299 				_nPrintMessage = document.createElement( "p" );
    300 				_nPrintMessage.className = "TableTools_PrintMessage";
    301 				_nPrintMessage.innerHTML = _oSettings.sPrintMessage;
    302 				document.body.insertBefore( _nPrintMessage, document.body.childNodes[0] );
    303 			}
    304
    305 			/* Cache the scrolling and the jump to the top of the t=page */
    306 			_iPrintScroll = $(window).scrollTop();
    307 			window.scrollTo( 0, 0 );
    308
    309 			$(document).bind( "keydown", null, fnPrintEnd );
    310
    311 			setTimeout( function() {
    312 				$(nInfo).fadeOut( "normal", function() {
    313 					document.body.removeChild( nInfo );
    314 				} );
    315 			}, 2000 );
    316 		} );
    317 	}
    318
    319
    320 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    321 	 * Printing functions
    322 	 */
    323
    324 	/*
    325 	 * Function: fnPrintEnd
    326 	 * Purpose:  Printing is finished, resume normal display
    327 	 * Returns:  -
    328 	 * Inputs:   event
    329 	 */
    330 	function fnPrintEnd ( e )
    331 	{
    332 		/* Only interested in the escape key */
    333 		if ( e.keyCode == 27 )
    334 		{
    335 			/* Show all hidden nodes */
    336 			fnPrintShowNodes();
    337
    338 			/* Restore the scroll */
    339 			window.scrollTo( 0, _iPrintScroll );
    340
    341 			/* Drop the print message */
    342 			if ( _nPrintMessage )
    343 			{
    344 				document.body.removeChild( _nPrintMessage );
    345 				_nPrintMessage = null;
    346 			}
    347
    348 			/* Restore the table length */
    349 			_DTSettings._iDisplayStart = _iPrintSaveStart;
    350 			_DTSettings._iDisplayLength = _iPrintSaveLength;
    351 			_DTSettings.oApi._fnCalculateEnd( _DTSettings );
    352 			_DTSettings.oApi._fnDraw( _DTSettings );
    353
    354 			$(document).unbind( "keydown", fnPrintEnd );
    355 		}
    356 	}
    357
    358
    359 	/*
    360 	 * Function: fnPrintShowNodes
    361 	 * Purpose:  Resume the display of all TableTools hidden nodes
    362 	 * Returns:  -
    363 	 * Inputs:   -
    364 	 */
    365 	function fnPrintShowNodes( )
    366 	{
    367 		for ( var i=0, iLen=_aoPrintHidden.length ; i<iLen ; i++ )
    368 		{
    369 			_aoPrintHidden[i].node.style.display = _aoPrintHidden[i].display;
    370 		}
    371 		_aoPrintHidden.splice( 0, _aoPrintHidden.length );
    372 	}
    373
    374
    375 	/*
    376 	 * Function: fnPrintHideNodes
    377 	 * Purpose:  Hide nodes which are not needed in order to display the table
    378 	 * Returns:  -
    379 	 * Inputs:   node:nNode - the table node - we parse back up
    380 	 * Notes:    Recursive
    381 	 */
    382 	function fnPrintHideNodes( nNode )
    383 	{
    384 		var nParent = nNode.parentNode;
    385 		var nChildren = nParent.childNodes;
    386 		for ( var i=0, iLen=nChildren.length ; i<iLen ; i++ )
    387 		{
    388 			if ( nChildren[i] != nNode && nChildren[i].nodeType == 1 )
    389 			{
    390 				/* If our node is shown (don't want to show nodes which were previously hidden) */
    391 				var sDisplay = $(nChildren[i]).css("display");
    392 			 	if ( sDisplay != "none" )
    393 				{
    394 					/* Cache the node and it's previous state so we can restore it */
    395 					_aoPrintHidden.push( {
    396 						node: nChildren[i],
    397 						display: sDisplay
    398 					} );
    399 					nChildren[i].style.display = "none";
    400 				}
    401 			}
    402 		}
    403
    404 		if ( nParent.nodeName != "BODY" )
    405 		{
    406 			fnPrintHideNodes( nParent );
    407 		}
    408 	}
    409
    410
    411
    412
    413
    414 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    415 	 * Support functions
    416 	 */
    417
    418 	/*
    419 	 * Function: fnGlue
    420 	 * Purpose:  Wait until the id is in the DOM before we "glue" the swf
    421 	 * Returns:  -
    422 	 * Inputs:   object:clip - Zero clipboard object
    423 	 *           node:node - node to glue swf to
    424 	 *           string:id - id of the element to look for
    425 	 *           string:text - title of the flash movie
    426 	 * Notes:    Recursive (setTimeout)
    427 	 */
    428 	function fnGlue ( clip, node, id, text )
    429 	{
    430 		if ( document.getElementById(id) )
    431 		{
    432 			clip.glue( node, text );
    433 		}
    434 		else
    435 		{
    436 			setTimeout( function () {
    437 				fnGlue( clip, node, id, text );
    438 			}, 100 );
    439 		}
    440 	}
    441
    442
    443 	/*
    444 	 * Function: fnGetTitle
    445 	 * Purpose:  Get the title of the page (from DOM or user set) for file saving
    446 	 * Returns:
    447 	 * Inputs:
    448 	 */
    449 	function fnGetTitle( )
    450 	{
    451 		var sTitle;
    452 		if ( _oSettings.sTitle !== "" ) {
    453 			sTitle = _oSettings.sTitle;
    454 		} else {
    455 			sTitle = document.getElementsByTagName('title')[0].innerHTML;
    456 		}
    457
    458 		/* Strip characters which the OS will object to - checking for UTF8 support in the scripting
    459 		 * engine
    460 		 */
    461 		if ( "\u00A1".toString().length < 4 ) {
    462 			return sTitle.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, "");
    463 		} else {
    464 			return sTitle.replace(/[^a-zA-Z0-9_\.,\-_ !\(\)]/g, "");
    465 		}
    466 	}
    467
    468
    469 	/*
    470 	 * Function: fnFindParentClass
    471 	 * Purpose:  Parse back up the DOM to a node with a particular node
    472 	 * Returns:  node: - found node
    473 	 * Inputs:   node:n - Node to test
    474 	 *           string:sClass - class to find
    475 	 * Notes:    Recursive
    476 	 */
    477 	function fnFindParentClass ( n, sClass )
    478 	{
    479 		if ( n.className.match(sClass) || n.nodeName == "BODY" )
    480 		{
    481 			return n;
    482 		}
    483 		else
    484 		{
    485 			return fnFindParentClass( n.parentNode, sClass );
    486 		}
    487 	}
    488
    489
    490 	/*
    491 	 * Function: fnBoundData
    492 	 * Purpose:  Wrap data up with a boundary string
    493 	 * Returns:  string: - bound data
    494 	 * Inputs:   string:sData - data to bound
    495 	 *           string:sBoundary - bounding char(s)
    496 	 *           regexp:regex - search for the bounding chars - constructed outside for efficincy
    497 	 *             in the loop
    498 	 */
    499 	function fnBoundData( sData, sBoundary, regex )
    500 	{
    501 		if ( sBoundary === "" )
    502 		{
    503 			return sData;
    504 		}
    505 		else
    506 		{
    507 			return sBoundary + sData.replace(regex, "\\"+sBoundary) + sBoundary;
    508 		}
    509 	}
    510
    511
    512 	/*
    513 	 * Function: fnHtmlDecode
    514 	 * Purpose:  Decode HTML entities
    515 	 * Returns:  string: - decoded string
    516 	 * Inputs:   string:sData - encoded string
    517 	 */
    518 	function fnHtmlDecode( sData )
    519 	{
    520 		var
    521 			aData = fnChunkData( sData, 2048 ),
    522 			n = document.createElement('div'),
    523 			i, iLen, iIndex,
    524 			sReturn = "", sInner;
    525
    526 		/* nodeValue has a limit in browsers - so we chunk the data into smaller segments to build
    527 		 * up the string. Note that the 'trick' here is to remember than we might have split over
    528 		 * an HTML entity, so we backtrack a little to make sure this doesn't happen
    529 		 */
    530 		for ( i=0, iLen=aData.length ; i<iLen ; i++ )
    531 		{
    532 			/* Magic number 8 is because no entity is longer then strlen 8 in ISO 8859-1 */
    533 			iIndex = aData[i].lastIndexOf( '&' );
    534 			if ( iIndex != -1 && aData[i].length >= 8 && iIndex > aData[i].length - 8 )
    535 			{
    536 				sInner = aData[i].substr( iIndex );
    537 				aData[i] = aData[i].substr( 0, iIndex );
    538 			}
    539
    540 			n.innerHTML = aData[i];
    541 			sReturn += n.childNodes[0].nodeValue;
    542 		}
    543
    544 		return sReturn;
    545 	}
    546
    547
    548 	//function fnHtmlDecode( sData )
    549 	//{
    550 	//	var n = document.createElement('div');
    551 	//	n.innerHTML = sData;
    552 	//	return n.childNodes[0].nodeValue;
    553 	//}
    554
    555
    556 	/*
    557 	 * Function: fnChunkData
    558 	 * Purpose:  Break a string up into an array of smaller strings
    559 	 * Returns:  array strings: - string array
    560 	 * Inputs:   string:sData - data to be broken up
    561 	 *           int:iSize - chunk size
    562 	 */
    563 	function fnChunkData( sData, iSize )
    564 	{
    565 		var asReturn = [];
    566 		var iStrlen = sData.length;
    567
    568 		for ( var i=0 ; i<iStrlen ; i+=iSize )
    569 		{
    570 			if ( i+iSize < iStrlen )
    571 			{
    572 				asReturn.push( sData.substring( i, i+iSize ) );
    573 			}
    574 			else
    575 			{
    576 				asReturn.push( sData.substring( i, iStrlen ) );
    577 			}
    578 		}
    579
    580 		return asReturn;
    581 	}
    582
    583
    584 	/*
    585 	 * Function: fnFlashSetText
    586 	 * Purpose:  Set the text for the flash clip to deal with
    587 	 * Returns:  -
    588 	 * Inputs:   object:clip - the ZeroClipboard object
    589 	 *           string:sData - the data to be set
    590 	 * Notes:    This function is required for large information sets. There is a limit on the
    591 	 *   amount of data that can be transfered between Javascript and Flash in a single call, so
    592 	 *   we use this method to build up the text in Flash by sending over chunks. It is estimated
    593 	 *   that the data limit is around 64k, although it is undocuments, and appears to be different
    594 	 *   between different flash versions. We chunk at 8KiB.
    595 	 */
    596 	function fnFlashSetText( clip, sData )
    597 	{
    598 		var asData = fnChunkData( sData, 8192 );
    599
    600 		clip.clearText();
    601 		for ( var i=0, iLen=asData.length ; i<iLen ; i++ )
    602 		{
    603 			clip.appendText( asData[i] );
    604 		}
    605 	}
    606
    607
    608 	/*
    609 	 * Function: fnGetDataTablesData
    610 	 * Purpose:  Get data from DataTables' internals and format it for output
    611 	 * Returns:  string:sData - concatinated string of data
    612 	 * Inputs:   string:sSeperator - field separator (ie. ,)
    613 	 *           string:sBoundary - field boundary (ie. ') - optional - default: ""
    614 	 */
    615 	function fnGetDataTablesData( sSeperator, sBoundary )
    616 	{
    617 		var i, iLen;
    618 		var j, jLen;
    619 		var sData = '';
    620 		var sLoopData = '';
    621 		var sNewline = navigator.userAgent.match(/Windows/) ? "\r\n" : "\n";
    622
    623 		if ( typeof sBoundary == "undefined" )
    624 		{
    625 			sBoundary = "";
    626 		}
    627 		var regex = new RegExp(sBoundary, "g"); /* Do it here for speed */
    628
    629 		/* Titles */
    630 		for ( i=0, iLen=_DTSettings.aoColumns.length ; i<iLen ; i++ )
    631 		{
    632 			if ( _oSettings.bIncHiddenColumns === true || _DTSettings.aoColumns[i].bVisible )
    633 			{
    634 				sLoopData = _DTSettings.aoColumns[i].sTitle.replace(/\n/g," ").replace( /<.*?>/g, "" );
    635 				if ( sLoopData.indexOf( '&' ) != -1 )
    636 				{
    637 					sLoopData = fnHtmlDecode( sLoopData );
    638 				}
    639
    640 				sData += fnBoundData( sLoopData, sBoundary, regex ) + sSeperator;
    641 			}
    642 		}
    643 		sData = sData.slice( 0, sSeperator.length*-1 );
    644 		sData += sNewline;
    645
    646 		/* Rows */
    647 		for ( j=0, jLen=_DTSettings.aiDisplay.length ; j<jLen ; j++ )
    648 		{
    649 			/* Columns */
    650 			for ( i=0, iLen=_DTSettings.aoColumns.length ; i<iLen ; i++ )
    651 			{
    652 				if ( _oSettings.bIncHiddenColumns === true || _DTSettings.aoColumns[i].bVisible )
    653 				{
    654 					/* Convert to strings (with small optimisation) */
    655 					var mTypeData = _DTSettings.aoData[ _DTSettings.aiDisplay[j] ]._aData[ i ];
    656 					if ( typeof mTypeData == "string" )
    657 					{
    658 						/* Strip newlines, replace img tags with alt attr. and finally strip html... */
    659 						sLoopData = mTypeData.replace(/\n/g," ");
    660 						sLoopData = sLoopData.replace(/<img.*?\s+alt\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+)).*?>/gi, '$1$2$3')
    661 						sLoopData = sLoopData.replace( /<.*?>/g, "" );
    662 					}
    663 					else
    664 					{
    665 						sLoopData = mTypeData+"";
    666 					}
    667
    668 					/* Trim and clean the data */
    669 					sLoopData = sLoopData.replace(/^\s+/, '').replace(/\s+$/, '');
    670 					if ( sLoopData.indexOf( '&' ) != -1 )
    671 					{
    672 						sLoopData = fnHtmlDecode( sLoopData );
    673 					}
    674
    675 					/* Bound it and add it to the total data */
    676 					sData += fnBoundData( sLoopData, sBoundary, regex ) + sSeperator;
    677 				}
    678 			}
    679 			sData = sData.slice( 0, sSeperator.length*-1 );
    680 			sData += sNewline;
    681 		}
    682
    683 		/* Remove the last new line */
    684 		sData.slice( 0, -1 );
    685
    686 		/* Add the footer */
    687 		if ( _oSettings.bIncFooter )
    688 		{
    689 			for ( i=0, iLen=_DTSettings.aoColumns.length ; i<iLen ; i++ )
    690 			{
    691 				if ( _DTSettings.aoColumns[i].nTf !== null &&
    692 					(_oSettings.bIncHiddenColumns === true || _DTSettings.aoColumns[i].bVisible) )
    693 				{
    694 					sLoopData = _DTSettings.aoColumns[i].nTf.innerHTML.replace(/\n/g," ").replace( /<.*?>/g, "" );
    695 					if ( sLoopData.indexOf( '&' ) != -1 )
    696 					{
    697 						sLoopData = fnHtmlDecode( sLoopData );
    698 					}
    699
    700 					sData += fnBoundData( sLoopData, sBoundary, regex ) + sSeperator;
    701 				}
    702 			}
    703 			sData = sData.slice( 0, sSeperator.length*-1 );
    704 		}
    705
    706 		/* No pointers here - this is a string copy :-) */
    707 		_sLastData = sData;
    708 		return sData;
    709 	}
    710
    711
    712 	/* Initialise our new object */
    713 	return _fnInit( oInit );
    714 }
    715
    716
    717 /*
    718  * Register a new feature with DataTables
    719  */
    720 if ( typeof $.fn.dataTable == "function" && typeof $.fn.dataTableExt.sVersion != "undefined" )
    721 {
    722 	$.fn.dataTableExt.aoFeatures.push( {
    723 		fnInit: function( oSettings ) {
    724 			return new TableTools( { oDTSettings: oSettings } );
    725 		},
    726 		cFeature: "T",
    727 		sFeature: "TableTools"
    728 	} );
    729 }
    730 else
    731 {
    732 	alert( "Warning: TableTools requires DataTables 1.5 or greater - "+
    733 		"www.datatables.net/download");
    734 }
    735 })(jQuery);
    736