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