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