]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
baculum: Add buttons and colvis DataTables plugins
authorMarcin Haba <marcin.haba@bacula.pl>
Sat, 22 Feb 2020 18:09:59 +0000 (19:09 +0100)
committerMarcin Haba <marcin.haba@bacula.pl>
Sat, 22 Feb 2020 20:45:10 +0000 (21:45 +0100)
gui/baculum/LICENSE
gui/baculum/protected/Common/Class/BClientScript.php
gui/baculum/protected/Web/JavaScript/buttons.colVis.js [new file with mode: 0644]
gui/baculum/protected/Web/JavaScript/buttons.html5.js [new file with mode: 0644]
gui/baculum/protected/Web/JavaScript/dataTables.buttons.js [new file with mode: 0644]
gui/baculum/protected/Web/Layouts/Main.tpl
gui/baculum/themes/Baculum-v2/css/buttons.dataTables.css [new file with mode: 0644]

index b38fa7fb8692cc7a97170f3fd44f8dd6b673bb9f..7a510719423e31a7006e4680a6a8a816f198c580 100644 (file)
@@ -838,14 +838,17 @@ limitations under the License.
 Baculum uses DataTables library for tables.
 
 Files:
-  protected/Web/JavaScript/datatables.js
   protected/Web/JavaScript/datatables.js
   protected/Web/JavaScript/dataTables.responsive.js
+  protected/Web/JavaScript/dataTables.buttons.js
   protected/Web/JavaScript/responsive.jqueryui.js
+  protected/Web/JavaScript/buttons.colVis.js
+  protected/Web/JavaScript/buttons.html5.js
   themes/Baculum-v2/css/datatables.css
   themes/Baculum-v2/css/responsive.dataTables.css
+  themes/Baculum-v2/css/buttons.dataTables.css
 
-Copyright (C) 2008-2018, SpryMedia Ltd.
+Copyright (C) 2008-2020, SpryMedia Ltd.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -865,6 +868,16 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 
+
+The Buttons DataTables plugin uses FileSaver.js for saving files on
+the client-side.
+
+Files:
+  protected/Web/JavaScript/buttons.html5.js
+
+FileSaver.js (1.3.3) - MIT license
+Copyright © 2016 Eli Grey - http://eligrey.com
+
 #####################################################################
 
 Baculum uses Font Awesome Free for icons.
index 03b24142668b1fc4d1743e7558c8a6c5a1b08db2..33b56e8fe6f05e0efbb617227f55f449327fdedb 100644 (file)
@@ -31,7 +31,7 @@ Prado::using('System.Web.UI.WebControls.TClientScript');
  */
 class BClientScript extends TClientScript {
 
-       const SCRIPTS_VERSION = 4;
+       const SCRIPTS_VERSION = 5;
 
        public function getScriptUrl()
        {
diff --git a/gui/baculum/protected/Web/JavaScript/buttons.colVis.js b/gui/baculum/protected/Web/JavaScript/buttons.colVis.js
new file mode 100644 (file)
index 0000000..b9529d2
--- /dev/null
@@ -0,0 +1,206 @@
+/*!
+ * Column visibility buttons for Buttons and DataTables.
+ * 2016 SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net', 'datatables.net-buttons'], function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               module.exports = function (root, $) {
+                       if ( ! root ) {
+                               root = window;
+                       }
+
+                       if ( ! $ || ! $.fn.dataTable ) {
+                               $ = require('datatables.net')(root, $).$;
+                       }
+
+                       if ( ! $.fn.dataTable.Buttons ) {
+                               require('datatables.net-buttons')(root, $);
+                       }
+
+                       return factory( $, root, root.document );
+               };
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+$.extend( DataTable.ext.buttons, {
+       // A collection of column visibility buttons
+       colvis: function ( dt, conf ) {
+               return {
+                       extend: 'collection',
+                       text: function ( dt ) {
+                               return dt.i18n( 'buttons.colvis', 'Column visibility' );
+                       },
+                       className: 'buttons-colvis',
+                       buttons: [ {
+                               extend: 'columnsToggle',
+                               columns: conf.columns,
+                               columnText: conf.columnText
+                       } ]
+               };
+       },
+
+       // Selected columns with individual buttons - toggle column visibility
+       columnsToggle: function ( dt, conf ) {
+               var columns = dt.columns( conf.columns ).indexes().map( function ( idx ) {
+                       return {
+                               extend: 'columnToggle',
+                               columns: idx,
+                               columnText: conf.columnText
+                       };
+               } ).toArray();
+
+               return columns;
+       },
+
+       // Single button to toggle column visibility
+       columnToggle: function ( dt, conf ) {
+               return {
+                       extend: 'columnVisibility',
+                       columns: conf.columns,
+                       columnText: conf.columnText
+               };
+       },
+
+       // Selected columns with individual buttons - set column visibility
+       columnsVisibility: function ( dt, conf ) {
+               var columns = dt.columns( conf.columns ).indexes().map( function ( idx ) {
+                       return {
+                               extend: 'columnVisibility',
+                               columns: idx,
+                               visibility: conf.visibility,
+                               columnText: conf.columnText
+                       };
+               } ).toArray();
+
+               return columns;
+       },
+
+       // Single button to set column visibility
+       columnVisibility: {
+               columns: undefined, // column selector
+               text: function ( dt, button, conf ) {
+                       return conf._columnText( dt, conf );
+               },
+               className: 'buttons-columnVisibility',
+               action: function ( e, dt, button, conf ) {
+                       var col = dt.columns( conf.columns );
+                       var curr = col.visible();
+
+                       col.visible( conf.visibility !== undefined ?
+                               conf.visibility :
+                               ! (curr.length ? curr[0] : false )
+                       );
+               },
+               init: function ( dt, button, conf ) {
+                       var that = this;
+                       button.attr( 'data-cv-idx', conf.columns );
+
+                       dt
+                               .on( 'column-visibility.dt'+conf.namespace, function (e, settings) {
+                                       if ( ! settings.bDestroying && settings.nTable == dt.settings()[0].nTable ) {
+                                               that.active( dt.column( conf.columns ).visible() );
+                                       }
+                               } )
+                               .on( 'column-reorder.dt'+conf.namespace, function (e, settings, details) {
+                                       if ( dt.columns( conf.columns ).count() !== 1 ) {
+                                               return;
+                                       }
+
+                                       // This button controls the same column index but the text for the column has
+                                       // changed
+                                       button.text( conf._columnText( dt, conf ) );
+
+                                       // Since its a different column, we need to check its visibility
+                                       that.active( dt.column( conf.columns ).visible() );
+                               } );
+
+                       this.active( dt.column( conf.columns ).visible() );
+               },
+               destroy: function ( dt, button, conf ) {
+                       dt
+                               .off( 'column-visibility.dt'+conf.namespace )
+                               .off( 'column-reorder.dt'+conf.namespace );
+               },
+
+               _columnText: function ( dt, conf ) {
+                       // Use DataTables' internal data structure until this is presented
+                       // is a public API. The other option is to use
+                       // `$( column(col).node() ).text()` but the node might not have been
+                       // populated when Buttons is constructed.
+                       var idx = dt.column( conf.columns ).index();
+                       var title = dt.settings()[0].aoColumns[ idx ].sTitle
+                               .replace(/\n/g," ")        // remove new lines
+                               .replace(/<br\s*\/?>/gi, " ")  // replace line breaks with spaces
+                               .replace(/<select(.*?)<\/select>/g, "") // remove select tags, including options text
+                               .replace(/<!\-\-.*?\-\->/g, "") // strip HTML comments
+                               .replace(/<.*?>/g, "")   // strip HTML
+                               .replace(/^\s+|\s+$/g,""); // trim
+
+                       return conf.columnText ?
+                               conf.columnText( dt, idx, title ) :
+                               title;
+               }
+       },
+
+
+       colvisRestore: {
+               className: 'buttons-colvisRestore',
+
+               text: function ( dt ) {
+                       return dt.i18n( 'buttons.colvisRestore', 'Restore visibility' );
+               },
+
+               init: function ( dt, button, conf ) {
+                       conf._visOriginal = dt.columns().indexes().map( function ( idx ) {
+                               return dt.column( idx ).visible();
+                       } ).toArray();
+               },
+
+               action: function ( e, dt, button, conf ) {
+                       dt.columns().every( function ( i ) {
+                               // Take into account that ColReorder might have disrupted our
+                               // indexes
+                               var idx = dt.colReorder && dt.colReorder.transpose ?
+                                       dt.colReorder.transpose( i, 'toOriginal' ) :
+                                       i;
+
+                               this.visible( conf._visOriginal[ idx ] );
+                       } );
+               }
+       },
+
+
+       colvisGroup: {
+               className: 'buttons-colvisGroup',
+
+               action: function ( e, dt, button, conf ) {
+                       dt.columns( conf.show ).visible( true, false );
+                       dt.columns( conf.hide ).visible( false, false );
+
+                       dt.columns.adjust();
+               },
+
+               show: [],
+
+               hide: []
+       }
+} );
+
+
+return DataTable.Buttons;
+}));
diff --git a/gui/baculum/protected/Web/JavaScript/buttons.html5.js b/gui/baculum/protected/Web/JavaScript/buttons.html5.js
new file mode 100644 (file)
index 0000000..c102b74
--- /dev/null
@@ -0,0 +1,1459 @@
+/*!
+ * HTML5 export buttons for Buttons and DataTables.
+ * 2016 SpryMedia Ltd - datatables.net/license
+ *
+ * FileSaver.js (1.3.3) - MIT license
+ * Copyright © 2016 Eli Grey - http://eligrey.com
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net', 'datatables.net-buttons'], function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               module.exports = function (root, $, jszip, pdfmake) {
+                       if ( ! root ) {
+                               root = window;
+                       }
+
+                       if ( ! $ || ! $.fn.dataTable ) {
+                               $ = require('datatables.net')(root, $).$;
+                       }
+
+                       if ( ! $.fn.dataTable.Buttons ) {
+                               require('datatables.net-buttons')(root, $);
+                       }
+
+                       return factory( $, root, root.document, jszip, pdfmake );
+               };
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, jszip, pdfmake, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+// Allow the constructor to pass in JSZip and PDFMake from external requires.
+// Otherwise, use globally defined variables, if they are available.
+function _jsZip () {
+       return jszip || window.JSZip;
+}
+function _pdfMake () {
+       return pdfmake || window.pdfMake;
+}
+
+DataTable.Buttons.pdfMake = function (_) {
+       if ( ! _ ) {
+               return _pdfMake();
+       }
+       pdfmake = m_ake;
+}
+
+DataTable.Buttons.jszip = function (_) {
+       if ( ! _ ) {
+               return _jsZip();
+       }
+       jszip = _;
+}
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * FileSaver.js dependency
+ */
+
+/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
+
+var _saveAs = (function(view) {
+       "use strict";
+       // IE <10 is explicitly unsupported
+       if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
+               return;
+       }
+       var
+                 doc = view.document
+                 // only get URL when necessary in case Blob.js hasn't overridden it yet
+               , get_URL = function() {
+                       return view.URL || view.webkitURL || view;
+               }
+               , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+               , can_use_save_link = "download" in save_link
+               , click = function(node) {
+                       var event = new MouseEvent("click");
+                       node.dispatchEvent(event);
+               }
+               , is_safari = /constructor/i.test(view.HTMLElement) || view.safari
+               , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
+               , throw_outside = function(ex) {
+                       (view.setImmediate || view.setTimeout)(function() {
+                               throw ex;
+                       }, 0);
+               }
+               , force_saveable_type = "application/octet-stream"
+               // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
+               , arbitrary_revoke_timeout = 1000 * 40 // in ms
+               , revoke = function(file) {
+                       var revoker = function() {
+                               if (typeof file === "string") { // file is an object URL
+                                       get_URL().revokeObjectURL(file);
+                               } else { // file is a File
+                                       file.remove();
+                               }
+                       };
+                       setTimeout(revoker, arbitrary_revoke_timeout);
+               }
+               , dispatch = function(filesaver, event_types, event) {
+                       event_types = [].concat(event_types);
+                       var i = event_types.length;
+                       while (i--) {
+                               var listener = filesaver["on" + event_types[i]];
+                               if (typeof listener === "function") {
+                                       try {
+                                               listener.call(filesaver, event || filesaver);
+                                       } catch (ex) {
+                                               throw_outside(ex);
+                                       }
+                               }
+                       }
+               }
+               , auto_bom = function(blob) {
+                       // prepend BOM for UTF-8 XML and text/* types (including HTML)
+                       // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
+                       if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
+                               return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
+                       }
+                       return blob;
+               }
+               , FileSaver = function(blob, name, no_auto_bom) {
+                       if (!no_auto_bom) {
+                               blob = auto_bom(blob);
+                       }
+                       // First try a.download, then web filesystem, then object URLs
+                       var
+                                 filesaver = this
+                               , type = blob.type
+                               , force = type === force_saveable_type
+                               , object_url
+                               , dispatch_all = function() {
+                                       dispatch(filesaver, "writestart progress write writeend".split(" "));
+                               }
+                               // on any filesys errors revert to saving with object URLs
+                               , fs_error = function() {
+                                       if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
+                                               // Safari doesn't allow downloading of blob urls
+                                               var reader = new FileReader();
+                                               reader.onloadend = function() {
+                                                       var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
+                                                       var popup = view.open(url, '_blank');
+                                                       if(!popup) view.location.href = url;
+                                                       url=undefined; // release reference before dispatching
+                                                       filesaver.readyState = filesaver.DONE;
+                                                       dispatch_all();
+                                               };
+                                               reader.readAsDataURL(blob);
+                                               filesaver.readyState = filesaver.INIT;
+                                               return;
+                                       }
+                                       // don't create more object URLs than needed
+                                       if (!object_url) {
+                                               object_url = get_URL().createObjectURL(blob);
+                                       }
+                                       if (force) {
+                                               view.location.href = object_url;
+                                       } else {
+                                               var opened = view.open(object_url, "_blank");
+                                               if (!opened) {
+                                                       // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
+                                                       view.location.href = object_url;
+                                               }
+                                       }
+                                       filesaver.readyState = filesaver.DONE;
+                                       dispatch_all();
+                                       revoke(object_url);
+                               }
+                       ;
+                       filesaver.readyState = filesaver.INIT;
+
+                       if (can_use_save_link) {
+                               object_url = get_URL().createObjectURL(blob);
+                               setTimeout(function() {
+                                       save_link.href = object_url;
+                                       save_link.download = name;
+                                       click(save_link);
+                                       dispatch_all();
+                                       revoke(object_url);
+                                       filesaver.readyState = filesaver.DONE;
+                               });
+                               return;
+                       }
+
+                       fs_error();
+               }
+               , FS_proto = FileSaver.prototype
+               , saveAs = function(blob, name, no_auto_bom) {
+                       return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
+               }
+       ;
+       // IE 10+ (native saveAs)
+       if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
+               return function(blob, name, no_auto_bom) {
+                       name = name || blob.name || "download";
+
+                       if (!no_auto_bom) {
+                               blob = auto_bom(blob);
+                       }
+                       return navigator.msSaveOrOpenBlob(blob, name);
+               };
+       }
+
+       FS_proto.abort = function(){};
+       FS_proto.readyState = FS_proto.INIT = 0;
+       FS_proto.WRITING = 1;
+       FS_proto.DONE = 2;
+
+       FS_proto.error =
+       FS_proto.onwritestart =
+       FS_proto.onprogress =
+       FS_proto.onwrite =
+       FS_proto.onabort =
+       FS_proto.onerror =
+       FS_proto.onwriteend =
+               null;
+
+       return saveAs;
+}(
+          typeof self !== "undefined" && self
+       || typeof window !== "undefined" && window
+       || this.content
+));
+
+
+// Expose file saver on the DataTables API. Can't attach to `DataTables.Buttons`
+// since this file can be loaded before Button's core!
+DataTable.fileSave = _saveAs;
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Local (private) functions
+ */
+
+/**
+ * Get the sheet name for Excel exports.
+ *
+ * @param {object}     config Button configuration
+ */
+var _sheetname = function ( config )
+{
+       var sheetName = 'Sheet1';
+
+       if ( config.sheetName ) {
+               sheetName = config.sheetName.replace(/[\[\]\*\/\\\?\:]/g, '');
+       }
+
+       return sheetName;
+};
+
+/**
+ * Get the newline character(s)
+ *
+ * @param {object}     config Button configuration
+ * @return {string}                            Newline character
+ */
+var _newLine = function ( config )
+{
+       return config.newline ?
+               config.newline :
+               navigator.userAgent.match(/Windows/) ?
+                       '\r\n' :
+                       '\n';
+};
+
+/**
+ * Combine the data from the `buttons.exportData` method into a string that
+ * will be used in the export file.
+ *
+ * @param      {DataTable.Api} dt               DataTables API instance
+ * @param      {object}                                config Button configuration
+ * @return {object}                                                     The data to export
+ */
+var _exportData = function ( dt, config )
+{
+       var newLine = _newLine( config );
+       var data = dt.buttons.exportData( config.exportOptions );
+       var boundary = config.fieldBoundary;
+       var separator = config.fieldSeparator;
+       var reBoundary = new RegExp( boundary, 'g' );
+       var escapeChar = config.escapeChar !== undefined ?
+               config.escapeChar :
+               '\\';
+       var join = function ( a ) {
+               var s = '';
+
+               // If there is a field boundary, then we might need to escape it in
+               // the source data
+               for ( var i=0, ien=a.length ; i<ien ; i++ ) {
+                       if ( i > 0 ) {
+                               s += separator;
+                       }
+
+                       s += boundary ?
+                               boundary + ('' + a[i]).replace( reBoundary, escapeChar+boundary ) + boundary :
+                               a[i];
+               }
+
+               return s;
+       };
+
+       var header = config.header ? join( data.header )+newLine : '';
+       var footer = config.footer && data.footer ? newLine+join( data.footer ) : '';
+       var body = [];
+
+       for ( var i=0, ien=data.body.length ; i<ien ; i++ ) {
+               body.push( join( data.body[i] ) );
+       }
+
+       return {
+               str: header + body.join( newLine ) + footer,
+               rows: body.length
+       };
+};
+
+/**
+ * Older versions of Safari (prior to tech preview 18) don't support the
+ * download option required.
+ *
+ * @return {Boolean} `true` if old Safari
+ */
+var _isDuffSafari = function ()
+{
+       var safari = navigator.userAgent.indexOf('Safari') !== -1 &&
+               navigator.userAgent.indexOf('Chrome') === -1 &&
+               navigator.userAgent.indexOf('Opera') === -1;
+
+       if ( ! safari ) {
+               return false;
+       }
+
+       var version = navigator.userAgent.match( /AppleWebKit\/(\d+\.\d+)/ );
+       if ( version && version.length > 1 && version[1]*1 < 603.1 ) {
+               return true;
+       }
+
+       return false;
+};
+
+/**
+ * Convert from numeric position to letter for column names in Excel
+ * @param  {int} n Column number
+ * @return {string} Column letter(s) name
+ */
+function createCellPos( n ){
+       var ordA = 'A'.charCodeAt(0);
+       var ordZ = 'Z'.charCodeAt(0);
+       var len = ordZ - ordA + 1;
+       var s = "";
+
+       while( n >= 0 ) {
+               s = String.fromCharCode(n % len + ordA) + s;
+               n = Math.floor(n / len) - 1;
+       }
+
+       return s;
+}
+
+try {
+       var _serialiser = new XMLSerializer();
+       var _ieExcel;
+}
+catch (t) {}
+
+/**
+ * Recursively add XML files from an object's structure to a ZIP file. This
+ * allows the XSLX file to be easily defined with an object's structure matching
+ * the files structure.
+ *
+ * @param {JSZip} zip ZIP package
+ * @param {object} obj Object to add (recursive)
+ */
+function _addToZip( zip, obj ) {
+       if ( _ieExcel === undefined ) {
+               // Detect if we are dealing with IE's _awful_ serialiser by seeing if it
+               // drop attributes
+               _ieExcel = _serialiser
+                       .serializeToString(
+                               $.parseXML( excelStrings['xl/worksheets/sheet1.xml'] )
+                       )
+                       .indexOf( 'xmlns:r' ) === -1;
+       }
+
+       $.each( obj, function ( name, val ) {
+               if ( $.isPlainObject( val ) ) {
+                       var newDir = zip.folder( name );
+                       _addToZip( newDir, val );
+               }
+               else {
+                       if ( _ieExcel ) {
+                               // IE's XML serialiser will drop some name space attributes from
+                               // from the root node, so we need to save them. Do this by
+                               // replacing the namespace nodes with a regular attribute that
+                               // we convert back when serialised. Edge does not have this
+                               // issue
+                               var worksheet = val.childNodes[0];
+                               var i, ien;
+                               var attrs = [];
+
+                               for ( i=worksheet.attributes.length-1 ; i>=0 ; i-- ) {
+                                       var attrName = worksheet.attributes[i].nodeName;
+                                       var attrValue = worksheet.attributes[i].nodeValue;
+
+                                       if ( attrName.indexOf( ':' ) !== -1 ) {
+                                               attrs.push( { name: attrName, value: attrValue } );
+
+                                               worksheet.removeAttribute( attrName );
+                                       }
+                               }
+
+                               for ( i=0, ien=attrs.length ; i<ien ; i++ ) {
+                                       var attr = val.createAttribute( attrs[i].name.replace( ':', '_dt_b_namespace_token_' ) );
+                                       attr.value = attrs[i].value;
+                                       worksheet.setAttributeNode( attr );
+                               }
+                       }
+
+                       var str = _serialiser.serializeToString(val);
+
+                       // Fix IE's XML
+                       if ( _ieExcel ) {
+                               // IE doesn't include the XML declaration
+                               if ( str.indexOf( '<?xml' ) === -1 ) {
+                                       str = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+str;
+                               }
+
+                               // Return namespace attributes to being as such
+                               str = str.replace( /_dt_b_namespace_token_/g, ':' );
+
+                               // Remove testing name space that IE puts into the space preserve attr
+                               str = str.replace( /xmlns:NS[\d]+="" NS[\d]+:/g, '' );
+                       }
+
+                       // Safari, IE and Edge will put empty name space attributes onto
+                       // various elements making them useless. This strips them out
+                       str = str.replace( /<([^<>]*?) xmlns=""([^<>]*?)>/g, '<$1 $2>' );
+
+                       zip.file( name, str );
+               }
+       } );
+}
+
+/**
+ * Create an XML node and add any children, attributes, etc without needing to
+ * be verbose in the DOM.
+ *
+ * @param  {object} doc      XML document
+ * @param  {string} nodeName Node name
+ * @param  {object} opts     Options - can be `attr` (attributes), `children`
+ *   (child nodes) and `text` (text content)
+ * @return {node}            Created node
+ */
+function _createNode( doc, nodeName, opts ) {
+       var tempNode = doc.createElement( nodeName );
+
+       if ( opts ) {
+               if ( opts.attr ) {
+                       $(tempNode).attr( opts.attr );
+               }
+
+               if ( opts.children ) {
+                       $.each( opts.children, function ( key, value ) {
+                               tempNode.appendChild( value );
+                       } );
+               }
+
+               if ( opts.text !== null && opts.text !== undefined ) {
+                       tempNode.appendChild( doc.createTextNode( opts.text ) );
+               }
+       }
+
+       return tempNode;
+}
+
+/**
+ * Get the width for an Excel column based on the contents of that column
+ * @param  {object} data Data for export
+ * @param  {int}    col  Column index
+ * @return {int}         Column width
+ */
+function _excelColWidth( data, col ) {
+       var max = data.header[col].length;
+       var len, lineSplit, str;
+
+       if ( data.footer && data.footer[col].length > max ) {
+               max = data.footer[col].length;
+       }
+
+       for ( var i=0, ien=data.body.length ; i<ien ; i++ ) {
+               var point = data.body[i][col];
+               str = point !== null && point !== undefined ?
+                       point.toString() :
+                       '';
+
+               // If there is a newline character, workout the width of the column
+               // based on the longest line in the string
+               if ( str.indexOf('\n') !== -1 ) {
+                       lineSplit = str.split('\n');
+                       lineSplit.sort( function (a, b) {
+                               return b.length - a.length;
+                       } );
+
+                       len = lineSplit[0].length;
+               }
+               else {
+                       len = str.length;
+               }
+
+               if ( len > max ) {
+                       max = len;
+               }
+
+               // Max width rather than having potentially massive column widths
+               if ( max > 40 ) {
+                       return 54; // 40 * 1.35
+               }
+       }
+
+       max *= 1.35;
+
+       // And a min width
+       return max > 6 ? max : 6;
+}
+
+// Excel - Pre-defined strings to build a basic XLSX file
+var excelStrings = {
+       "_rels/.rels":
+               '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+
+               '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'+
+                       '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'+
+               '</Relationships>',
+
+       "xl/_rels/workbook.xml.rels":
+               '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+
+               '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'+
+                       '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'+
+                       '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'+
+               '</Relationships>',
+
+       "[Content_Types].xml":
+               '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+
+               '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'+
+                       '<Default Extension="xml" ContentType="application/xml" />'+
+                       '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />'+
+                       '<Default Extension="jpeg" ContentType="image/jpeg" />'+
+                       '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" />'+
+                       '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" />'+
+                       '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" />'+
+               '</Types>',
+
+       "xl/workbook.xml":
+               '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+
+               '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'+
+                       '<fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="24816"/>'+
+                       '<workbookPr showInkAnnotation="0" autoCompressPictures="0"/>'+
+                       '<bookViews>'+
+                               '<workbookView xWindow="0" yWindow="0" windowWidth="25600" windowHeight="19020" tabRatio="500"/>'+
+                       '</bookViews>'+
+                       '<sheets>'+
+                               '<sheet name="Sheet1" sheetId="1" r:id="rId1"/>'+
+                       '</sheets>'+
+                       '<definedNames/>'+
+               '</workbook>',
+
+       "xl/worksheets/sheet1.xml":
+               '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+
+               '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">'+
+                       '<sheetData/>'+
+                       '<mergeCells count="0"/>'+
+               '</worksheet>',
+
+       "xl/styles.xml":
+               '<?xml version="1.0" encoding="UTF-8"?>'+
+               '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">'+
+                       '<numFmts count="6">'+
+                               '<numFmt numFmtId="164" formatCode="#,##0.00_-\ [$$-45C]"/>'+
+                               '<numFmt numFmtId="165" formatCode="&quot;£&quot;#,##0.00"/>'+
+                               '<numFmt numFmtId="166" formatCode="[$€-2]\ #,##0.00"/>'+
+                               '<numFmt numFmtId="167" formatCode="0.0%"/>'+
+                               '<numFmt numFmtId="168" formatCode="#,##0;(#,##0)"/>'+
+                               '<numFmt numFmtId="169" formatCode="#,##0.00;(#,##0.00)"/>'+
+                       '</numFmts>'+
+                       '<fonts count="5" x14ac:knownFonts="1">'+
+                               '<font>'+
+                                       '<sz val="11" />'+
+                                       '<name val="Calibri" />'+
+                               '</font>'+
+                               '<font>'+
+                                       '<sz val="11" />'+
+                                       '<name val="Calibri" />'+
+                                       '<color rgb="FFFFFFFF" />'+
+                               '</font>'+
+                               '<font>'+
+                                       '<sz val="11" />'+
+                                       '<name val="Calibri" />'+
+                                       '<b />'+
+                               '</font>'+
+                               '<font>'+
+                                       '<sz val="11" />'+
+                                       '<name val="Calibri" />'+
+                                       '<i />'+
+                               '</font>'+
+                               '<font>'+
+                                       '<sz val="11" />'+
+                                       '<name val="Calibri" />'+
+                                       '<u />'+
+                               '</font>'+
+                       '</fonts>'+
+                       '<fills count="6">'+
+                               '<fill>'+
+                                       '<patternFill patternType="none" />'+
+                               '</fill>'+
+                               '<fill>'+ // Excel appears to use this as a dotted background regardless of values but
+                                       '<patternFill patternType="none" />'+ // to be valid to the schema, use a patternFill
+                               '</fill>'+
+                               '<fill>'+
+                                       '<patternFill patternType="solid">'+
+                                               '<fgColor rgb="FFD9D9D9" />'+
+                                               '<bgColor indexed="64" />'+
+                                       '</patternFill>'+
+                               '</fill>'+
+                               '<fill>'+
+                                       '<patternFill patternType="solid">'+
+                                               '<fgColor rgb="FFD99795" />'+
+                                               '<bgColor indexed="64" />'+
+                                       '</patternFill>'+
+                               '</fill>'+
+                               '<fill>'+
+                                       '<patternFill patternType="solid">'+
+                                               '<fgColor rgb="ffc6efce" />'+
+                                               '<bgColor indexed="64" />'+
+                                       '</patternFill>'+
+                               '</fill>'+
+                               '<fill>'+
+                                       '<patternFill patternType="solid">'+
+                                               '<fgColor rgb="ffc6cfef" />'+
+                                               '<bgColor indexed="64" />'+
+                                       '</patternFill>'+
+                               '</fill>'+
+                       '</fills>'+
+                       '<borders count="2">'+
+                               '<border>'+
+                                       '<left />'+
+                                       '<right />'+
+                                       '<top />'+
+                                       '<bottom />'+
+                                       '<diagonal />'+
+                               '</border>'+
+                               '<border diagonalUp="false" diagonalDown="false">'+
+                                       '<left style="thin">'+
+                                               '<color auto="1" />'+
+                                       '</left>'+
+                                       '<right style="thin">'+
+                                               '<color auto="1" />'+
+                                       '</right>'+
+                                       '<top style="thin">'+
+                                               '<color auto="1" />'+
+                                       '</top>'+
+                                       '<bottom style="thin">'+
+                                               '<color auto="1" />'+
+                                       '</bottom>'+
+                                       '<diagonal />'+
+                               '</border>'+
+                       '</borders>'+
+                       '<cellStyleXfs count="1">'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" />'+
+                       '</cellStyleXfs>'+
+                       '<cellXfs count="67">'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="1" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="2" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="3" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="4" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment horizontal="left"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment horizontal="center"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment horizontal="right"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment horizontal="fill"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment textRotation="90"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1">'+
+                                       '<alignment wrapText="1"/>'+
+                               '</xf>'+
+                               '<xf numFmtId="9"   fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="164" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="165" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="166" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="167" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="168" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="169" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="3" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="4" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="1" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                               '<xf numFmtId="2" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/>'+
+                       '</cellXfs>'+
+                       '<cellStyles count="1">'+
+                               '<cellStyle name="Normal" xfId="0" builtinId="0" />'+
+                       '</cellStyles>'+
+                       '<dxfs count="0" />'+
+                       '<tableStyles count="0" defaultTableStyle="TableStyleMedium9" defaultPivotStyle="PivotStyleMedium4" />'+
+               '</styleSheet>'
+};
+// Note we could use 3 `for` loops for the styles, but when gzipped there is
+// virtually no difference in size, since the above can be easily compressed
+
+// Pattern matching for special number formats. Perhaps this should be exposed
+// via an API in future?
+// Ref: section 3.8.30 - built in formatters in open spreadsheet
+//   https://www.ecma-international.org/news/TC45_current_work/Office%20Open%20XML%20Part%204%20-%20Markup%20Language%20Reference.pdf
+var _excelSpecials = [
+       { match: /^\-?\d+\.\d%$/,       style: 60, fmt: function (d) { return d/100; } }, // Precent with d.p.
+       { match: /^\-?\d+\.?\d*%$/,     style: 56, fmt: function (d) { return d/100; } }, // Percent
+       { match: /^\-?\$[\d,]+.?\d*$/,  style: 57 }, // Dollars
+       { match: /^\-?£[\d,]+.?\d*$/,   style: 58 }, // Pounds
+       { match: /^\-?€[\d,]+.?\d*$/,   style: 59 }, // Euros
+       { match: /^\-?\d+$/,            style: 65 }, // Numbers without thousand separators
+       { match: /^\-?\d+\.\d{2}$/,     style: 66 }, // Numbers 2 d.p. without thousands separators
+       { match: /^\([\d,]+\)$/,        style: 61, fmt: function (d) { return -1 * d.replace(/[\(\)]/g, ''); } },  // Negative numbers indicated by brackets
+       { match: /^\([\d,]+\.\d{2}\)$/, style: 62, fmt: function (d) { return -1 * d.replace(/[\(\)]/g, ''); } },  // Negative numbers indicated by brackets - 2d.p.
+       { match: /^\-?[\d,]+$/,         style: 63 }, // Numbers with thousand separators
+       { match: /^\-?[\d,]+\.\d{2}$/,  style: 64 }  // Numbers with 2 d.p. and thousands separators
+];
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Buttons
+ */
+
+//
+// Copy to clipboard
+//
+DataTable.ext.buttons.copyHtml5 = {
+       className: 'buttons-copy buttons-html5',
+
+       text: function ( dt ) {
+               return dt.i18n( 'buttons.copy', 'Copy' );
+       },
+
+       action: function ( e, dt, button, config ) {
+               this.processing( true );
+
+               var that = this;
+               var exportData = _exportData( dt, config );
+               var info = dt.buttons.exportInfo( config );
+               var newline = _newLine(config);
+               var output = exportData.str;
+               var hiddenDiv = $('<div/>')
+                       .css( {
+                               height: 1,
+                               width: 1,
+                               overflow: 'hidden',
+                               position: 'fixed',
+                               top: 0,
+                               left: 0
+                       } );
+
+               if ( info.title ) {
+                       output = info.title + newline + newline + output;
+               }
+
+               if ( info.messageTop ) {
+                       output = info.messageTop + newline + newline + output;
+               }
+
+               if ( info.messageBottom ) {
+                       output = output + newline + newline + info.messageBottom;
+               }
+
+               if ( config.customize ) {
+                       output = config.customize( output, config, dt );
+               }
+
+               var textarea = $('<textarea readonly/>')
+                       .val( output )
+                       .appendTo( hiddenDiv );
+
+               // For browsers that support the copy execCommand, try to use it
+               if ( document.queryCommandSupported('copy') ) {
+                       hiddenDiv.appendTo( dt.table().container() );
+                       textarea[0].focus();
+                       textarea[0].select();
+
+                       try {
+                               var successful = document.execCommand( 'copy' );
+                               hiddenDiv.remove();
+
+                               if (successful) {
+                                       dt.buttons.info(
+                                               dt.i18n( 'buttons.copyTitle', 'Copy to clipboard' ),
+                                               dt.i18n( 'buttons.copySuccess', {
+                                                       1: 'Copied one row to clipboard',
+                                                       _: 'Copied %d rows to clipboard'
+                                               }, exportData.rows ),
+                                               2000
+                                       );
+
+                                       this.processing( false );
+                                       return;
+                               }
+                       }
+                       catch (t) {}
+               }
+
+               // Otherwise we show the text box and instruct the user to use it
+               var message = $('<span>'+dt.i18n( 'buttons.copyKeys',
+                               'Press <i>ctrl</i> or <i>\u2318</i> + <i>C</i> to copy the table data<br>to your system clipboard.<br><br>'+
+                               'To cancel, click this message or press escape.' )+'</span>'
+                       )
+                       .append( hiddenDiv );
+
+               dt.buttons.info( dt.i18n( 'buttons.copyTitle', 'Copy to clipboard' ), message, 0 );
+
+               // Select the text so when the user activates their system clipboard
+               // it will copy that text
+               textarea[0].focus();
+               textarea[0].select();
+
+               // Event to hide the message when the user is done
+               var container = $(message).closest('.dt-button-info');
+               var close = function () {
+                       container.off( 'click.buttons-copy' );
+                       $(document).off( '.buttons-copy' );
+                       dt.buttons.info( false );
+               };
+
+               container.on( 'click.buttons-copy', close );
+               $(document)
+                       .on( 'keydown.buttons-copy', function (e) {
+                               if ( e.keyCode === 27 ) { // esc
+                                       close();
+                                       that.processing( false );
+                               }
+                       } )
+                       .on( 'copy.buttons-copy cut.buttons-copy', function () {
+                               close();
+                               that.processing( false );
+                       } );
+       },
+
+       exportOptions: {},
+
+       fieldSeparator: '\t',
+
+       fieldBoundary: '',
+
+       header: true,
+
+       footer: false,
+
+       title: '*',
+
+       messageTop: '*',
+
+       messageBottom: '*'
+};
+
+//
+// CSV export
+//
+DataTable.ext.buttons.csvHtml5 = {
+       bom: false,
+
+       className: 'buttons-csv buttons-html5',
+
+       available: function () {
+               return window.FileReader !== undefined && window.Blob;
+       },
+
+       text: function ( dt ) {
+               return dt.i18n( 'buttons.csv', 'CSV' );
+       },
+
+       action: function ( e, dt, button, config ) {
+               this.processing( true );
+
+               // Set the text
+               var output = _exportData( dt, config ).str;
+               var info = dt.buttons.exportInfo(config);
+               var charset = config.charset;
+
+               if ( config.customize ) {
+                       output = config.customize( output, config, dt );
+               }
+
+               if ( charset !== false ) {
+                       if ( ! charset ) {
+                               charset = document.characterSet || document.charset;
+                       }
+
+                       if ( charset ) {
+                               charset = ';charset='+charset;
+                       }
+               }
+               else {
+                       charset = '';
+               }
+
+               if ( config.bom ) {
+                       output = '\ufeff' + output;
+               }
+
+               _saveAs(
+                       new Blob( [output], {type: 'text/csv'+charset} ),
+                       info.filename,
+                       true
+               );
+
+               this.processing( false );
+       },
+
+       filename: '*',
+
+       extension: '.csv',
+
+       exportOptions: {},
+
+       fieldSeparator: ',',
+
+       fieldBoundary: '"',
+
+       escapeChar: '"',
+
+       charset: null,
+
+       header: true,
+
+       footer: false
+};
+
+//
+// Excel (xlsx) export
+//
+DataTable.ext.buttons.excelHtml5 = {
+       className: 'buttons-excel buttons-html5',
+
+       available: function () {
+               return window.FileReader !== undefined && _jsZip() !== undefined && ! _isDuffSafari() && _serialiser;
+       },
+
+       text: function ( dt ) {
+               return dt.i18n( 'buttons.excel', 'Excel' );
+       },
+
+       action: function ( e, dt, button, config ) {
+               this.processing( true );
+
+               var that = this;
+               var rowPos = 0;
+               var dataStartRow, dataEndRow;
+               var getXml = function ( type ) {
+                       var str = excelStrings[ type ];
+
+                       //str = str.replace( /xmlns:/g, 'xmlns_' ).replace( /mc:/g, 'mc_' );
+
+                       return $.parseXML( str );
+               };
+               var rels = getXml('xl/worksheets/sheet1.xml');
+               var relsGet = rels.getElementsByTagName( "sheetData" )[0];
+
+               var xlsx = {
+                       _rels: {
+                               ".rels": getXml('_rels/.rels')
+                       },
+                       xl: {
+                               _rels: {
+                                       "workbook.xml.rels": getXml('xl/_rels/workbook.xml.rels')
+                               },
+                               "workbook.xml": getXml('xl/workbook.xml'),
+                               "styles.xml": getXml('xl/styles.xml'),
+                               "worksheets": {
+                                       "sheet1.xml": rels
+                               }
+
+                       },
+                       "[Content_Types].xml": getXml('[Content_Types].xml')
+               };
+
+               var data = dt.buttons.exportData( config.exportOptions );
+               var currentRow, rowNode;
+               var addRow = function ( row ) {
+                       currentRow = rowPos+1;
+                       rowNode = _createNode( rels, "row", { attr: {r:currentRow} } );
+
+                       for ( var i=0, ien=row.length ; i<ien ; i++ ) {
+                               // Concat both the Cell Columns as a letter and the Row of the cell.
+                               var cellId = createCellPos(i) + '' + currentRow;
+                               var cell = null;
+
+                               // For null, undefined of blank cell, continue so it doesn't create the _createNode
+                               if ( row[i] === null || row[i] === undefined || row[i] === '' ) {
+                                       if ( config.createEmptyCells === true ) {
+                                               row[i] = '';
+                                       }
+                                       else {
+                                               continue;
+                                       }
+                               }
+
+                               var originalContent = row[i];
+                               row[i] = $.trim( row[i] );
+
+                               // Special number formatting options
+                               for ( var j=0, jen=_excelSpecials.length ; j<jen ; j++ ) {
+                                       var special = _excelSpecials[j];
+
+                                       // TODO Need to provide the ability for the specials to say
+                                       // if they are returning a string, since at the moment it is
+                                       // assumed to be a number
+                                       if ( row[i].match && ! row[i].match(/^0\d+/) && row[i].match( special.match ) ) {
+                                               var val = row[i].replace(/[^\d\.\-]/g, '');
+
+                                               if ( special.fmt ) {
+                                                       val = special.fmt( val );
+                                               }
+
+                                               cell = _createNode( rels, 'c', {
+                                                       attr: {
+                                                               r: cellId,
+                                                               s: special.style
+                                                       },
+                                                       children: [
+                                                               _createNode( rels, 'v', { text: val } )
+                                                       ]
+                                               } );
+
+                                               break;
+                                       }
+                               }
+
+                               if ( ! cell ) {
+                                       if ( typeof row[i] === 'number' || (
+                                               row[i].match &&
+                                               row[i].match(/^-?\d+(\.\d+)?$/) &&
+                                               ! row[i].match(/^0\d+/) )
+                                       ) {
+                                               // Detect numbers - don't match numbers with leading zeros
+                                               // or a negative anywhere but the start
+                                               cell = _createNode( rels, 'c', {
+                                                       attr: {
+                                                               t: 'n',
+                                                               r: cellId
+                                                       },
+                                                       children: [
+                                                               _createNode( rels, 'v', { text: row[i] } )
+                                                       ]
+                                               } );
+                                       }
+                                       else {
+                                               // String output - replace non standard characters for text output
+                                               var text = ! originalContent.replace ?
+                                                       originalContent :
+                                                       originalContent.replace(/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '');
+
+                                               cell = _createNode( rels, 'c', {
+                                                       attr: {
+                                                               t: 'inlineStr',
+                                                               r: cellId
+                                                       },
+                                                       children:{
+                                                               row: _createNode( rels, 'is', {
+                                                                       children: {
+                                                                               row: _createNode( rels, 't', {
+                                                                                       text: text,
+                                                                                       attr: {
+                                                                                               'xml:space': 'preserve'
+                                                                                       }
+                                                                               } )
+                                                                       }
+                                                               } )
+                                                       }
+                                               } );
+                                       }
+                               }
+
+                               rowNode.appendChild( cell );
+                       }
+
+                       relsGet.appendChild(rowNode);
+                       rowPos++;
+               };
+
+               if ( config.customizeData ) {
+                       config.customizeData( data );
+               }
+
+               var mergeCells = function ( row, colspan ) {
+                       var mergeCells = $('mergeCells', rels);
+
+                       mergeCells[0].appendChild( _createNode( rels, 'mergeCell', {
+                               attr: {
+                                       ref: 'A'+row+':'+createCellPos(colspan)+row
+                               }
+                       } ) );
+                       mergeCells.attr( 'count', parseFloat(mergeCells.attr( 'count' ))+1 );
+                       $('row:eq('+(row-1)+') c', rels).attr( 's', '51' ); // centre
+               };
+
+               // Title and top messages
+               var exportInfo = dt.buttons.exportInfo( config );
+               if ( exportInfo.title ) {
+                       addRow( [exportInfo.title], rowPos );
+                       mergeCells( rowPos, data.header.length-1 );
+               }
+
+               if ( exportInfo.messageTop ) {
+                       addRow( [exportInfo.messageTop], rowPos );
+                       mergeCells( rowPos, data.header.length-1 );
+               }
+
+
+               // Table itself
+               if ( config.header ) {
+                       addRow( data.header, rowPos );
+                       $('row:last c', rels).attr( 's', '2' ); // bold
+               }
+       
+               dataStartRow = rowPos;
+
+               for ( var n=0, ie=data.body.length ; n<ie ; n++ ) {
+                       addRow( data.body[n], rowPos );
+               }
+       
+               dataEndRow = rowPos;
+
+               if ( config.footer && data.footer ) {
+                       addRow( data.footer, rowPos);
+                       $('row:last c', rels).attr( 's', '2' ); // bold
+               }
+
+               // Below the table
+               if ( exportInfo.messageBottom ) {
+                       addRow( [exportInfo.messageBottom], rowPos );
+                       mergeCells( rowPos, data.header.length-1 );
+               }
+
+               // Set column widths
+               var cols = _createNode( rels, 'cols' );
+               $('worksheet', rels).prepend( cols );
+
+               for ( var i=0, ien=data.header.length ; i<ien ; i++ ) {
+                       cols.appendChild( _createNode( rels, 'col', {
+                               attr: {
+                                       min: i+1,
+                                       max: i+1,
+                                       width: _excelColWidth( data, i ),
+                                       customWidth: 1
+                               }
+                       } ) );
+               }
+
+               // Workbook modifications
+               var workbook = xlsx.xl['workbook.xml'];
+
+               $( 'sheets sheet', workbook ).attr( 'name', _sheetname( config ) );
+
+               // Auto filter for columns
+               if ( config.autoFilter ) {
+                       $('mergeCells', rels).before( _createNode( rels, 'autoFilter', {
+                               attr: {
+                                       ref: 'A'+dataStartRow+':'+createCellPos(data.header.length-1)+dataEndRow
+                               }
+                       } ) );
+
+                       $('definedNames', workbook).append( _createNode( workbook, 'definedName', {
+                               attr: {
+                                       name: '_xlnm._FilterDatabase',
+                                       localSheetId: '0',
+                                       hidden: 1
+                               },
+                               text: _sheetname(config)+'!$A$'+dataStartRow+':'+createCellPos(data.header.length-1)+dataEndRow
+                       } ) );
+               }
+
+               // Let the developer customise the document if they want to
+               if ( config.customize ) {
+                       config.customize( xlsx, config, dt );
+               }
+
+               // Excel doesn't like an empty mergeCells tag
+               if ( $('mergeCells', rels).children().length === 0 ) {
+                       $('mergeCells', rels).remove();
+               }
+
+               var jszip = _jsZip();
+               var zip = new jszip();
+               var zipConfig = {
+                       type: 'blob',
+                       mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+               };
+
+               _addToZip( zip, xlsx );
+
+               if ( zip.generateAsync ) {
+                       // JSZip 3+
+                       zip
+                               .generateAsync( zipConfig )
+                               .then( function ( blob ) {
+                                       _saveAs( blob, exportInfo.filename );
+                                       that.processing( false );
+                               } );
+               }
+               else {
+                       // JSZip 2.5
+                       _saveAs(
+                               zip.generate( zipConfig ),
+                               exportInfo.filename
+                       );
+                       this.processing( false );
+               }
+       },
+
+       filename: '*',
+
+       extension: '.xlsx',
+
+       exportOptions: {},
+
+       header: true,
+
+       footer: false,
+
+       title: '*',
+
+       messageTop: '*',
+
+       messageBottom: '*',
+
+       createEmptyCells: false,
+
+       autoFilter: false,
+
+       sheetName: ''
+};
+
+//
+// PDF export - using pdfMake - http://pdfmake.org
+//
+DataTable.ext.buttons.pdfHtml5 = {
+       className: 'buttons-pdf buttons-html5',
+
+       available: function () {
+               return window.FileReader !== undefined && _pdfMake();
+       },
+
+       text: function ( dt ) {
+               return dt.i18n( 'buttons.pdf', 'PDF' );
+       },
+
+       action: function ( e, dt, button, config ) {
+               this.processing( true );
+
+               var that = this;
+               var data = dt.buttons.exportData( config.exportOptions );
+               var info = dt.buttons.exportInfo( config );
+               var rows = [];
+
+               if ( config.header ) {
+                       rows.push( $.map( data.header, function ( d ) {
+                               return {
+                                       text: typeof d === 'string' ? d : d+'',
+                                       style: 'tableHeader'
+                               };
+                       } ) );
+               }
+
+               for ( var i=0, ien=data.body.length ; i<ien ; i++ ) {
+                       rows.push( $.map( data.body[i], function ( d ) {
+                               if ( d === null || d === undefined ) {
+                                       d = '';
+                               }
+                               return {
+                                       text: typeof d === 'string' ? d : d+'',
+                                       style: i % 2 ? 'tableBodyEven' : 'tableBodyOdd'
+                               };
+                       } ) );
+               }
+
+               if ( config.footer && data.footer) {
+                       rows.push( $.map( data.footer, function ( d ) {
+                               return {
+                                       text: typeof d === 'string' ? d : d+'',
+                                       style: 'tableFooter'
+                               };
+                       } ) );
+               }
+
+               var doc = {
+                       pageSize: config.pageSize,
+                       pageOrientation: config.orientation,
+                       content: [
+                               {
+                                       table: {
+                                               headerRows: 1,
+                                               body: rows
+                                       },
+                                       layout: 'noBorders'
+                               }
+                       ],
+                       styles: {
+                               tableHeader: {
+                                       bold: true,
+                                       fontSize: 11,
+                                       color: 'white',
+                                       fillColor: '#2d4154',
+                                       alignment: 'center'
+                               },
+                               tableBodyEven: {},
+                               tableBodyOdd: {
+                                       fillColor: '#f3f3f3'
+                               },
+                               tableFooter: {
+                                       bold: true,
+                                       fontSize: 11,
+                                       color: 'white',
+                                       fillColor: '#2d4154'
+                               },
+                               title: {
+                                       alignment: 'center',
+                                       fontSize: 15
+                               },
+                               message: {}
+                       },
+                       defaultStyle: {
+                               fontSize: 10
+                       }
+               };
+
+               if ( info.messageTop ) {
+                       doc.content.unshift( {
+                               text: info.messageTop,
+                               style: 'message',
+                               margin: [ 0, 0, 0, 12 ]
+                       } );
+               }
+
+               if ( info.messageBottom ) {
+                       doc.content.push( {
+                               text: info.messageBottom,
+                               style: 'message',
+                               margin: [ 0, 0, 0, 12 ]
+                       } );
+               }
+
+               if ( info.title ) {
+                       doc.content.unshift( {
+                               text: info.title,
+                               style: 'title',
+                               margin: [ 0, 0, 0, 12 ]
+                       } );
+               }
+
+               if ( config.customize ) {
+                       config.customize( doc, config, dt );
+               }
+
+               var pdf = _pdfMake().createPdf( doc );
+
+               if ( config.download === 'open' && ! _isDuffSafari() ) {
+                       pdf.open();
+               }
+               else {
+                       pdf.download( info.filename );
+               }
+
+               this.processing( false );
+       },
+
+       title: '*',
+
+       filename: '*',
+
+       extension: '.pdf',
+
+       exportOptions: {},
+
+       orientation: 'portrait',
+
+       pageSize: 'A4',
+
+       header: true,
+
+       footer: false,
+
+       messageTop: '*',
+
+       messageBottom: '*',
+
+       customize: null,
+
+       download: 'download'
+};
+
+
+return DataTable.Buttons;
+}));
diff --git a/gui/baculum/protected/Web/JavaScript/dataTables.buttons.js b/gui/baculum/protected/Web/JavaScript/dataTables.buttons.js
new file mode 100644 (file)
index 0000000..045bb8c
--- /dev/null
@@ -0,0 +1,2015 @@
+/*! Buttons for DataTables 1.6.1
+ * ©2016-2019 SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+       if ( typeof define === 'function' && define.amd ) {
+               // AMD
+               define( ['jquery', 'datatables.net'], function ( $ ) {
+                       return factory( $, window, document );
+               } );
+       }
+       else if ( typeof exports === 'object' ) {
+               // CommonJS
+               module.exports = function (root, $) {
+                       if ( ! root ) {
+                               root = window;
+                       }
+
+                       if ( ! $ || ! $.fn.dataTable ) {
+                               $ = require('datatables.net')(root, $).$;
+                       }
+
+                       return factory( $, root, root.document );
+               };
+       }
+       else {
+               // Browser
+               factory( jQuery, window, document );
+       }
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+// Used for namespacing events added to the document by each instance, so they
+// can be removed on destroy
+var _instCounter = 0;
+
+// Button namespacing counter for namespacing events on individual buttons
+var _buttonCounter = 0;
+
+var _dtButtons = DataTable.ext.buttons;
+
+/**
+ * [Buttons description]
+ * @param {[type]}
+ * @param {[type]}
+ */
+var Buttons = function( dt, config )
+{
+       // If not created with a `new` keyword then we return a wrapper function that
+       // will take the settings object for a DT. This allows easy use of new instances
+       // with the `layout` option - e.g. `topLeft: $.fn.dataTable.Buttons( ... )`.
+       if ( !(this instanceof Buttons) ) {
+               return function (settings) {
+                       return new Buttons( settings, dt ).container();
+               };
+       }
+
+       // If there is no config set it to an empty object
+       if ( typeof( config ) === 'undefined' ) {
+               config = {};    
+       }
+       
+       // Allow a boolean true for defaults
+       if ( config === true ) {
+               config = {};
+       }
+
+       // For easy configuration of buttons an array can be given
+       if ( $.isArray( config ) ) {
+               config = { buttons: config };
+       }
+
+       this.c = $.extend( true, {}, Buttons.defaults, config );
+
+       // Don't want a deep copy for the buttons
+       if ( config.buttons ) {
+               this.c.buttons = config.buttons;
+       }
+
+       this.s = {
+               dt: new DataTable.Api( dt ),
+               buttons: [],
+               listenKeys: '',
+               namespace: 'dtb'+(_instCounter++)
+       };
+
+       this.dom = {
+               container: $('<'+this.c.dom.container.tag+'/>')
+                       .addClass( this.c.dom.container.className )
+       };
+
+       this._constructor();
+};
+
+
+$.extend( Buttons.prototype, {
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+        * Public methods
+        */
+
+       /**
+        * Get the action of a button
+        * @param  {int|string} Button index
+        * @return {function}
+        *//**
+        * Set the action of a button
+        * @param  {node} node Button element
+        * @param  {function} action Function to set
+        * @return {Buttons} Self for chaining
+        */
+       action: function ( node, action )
+       {
+               var button = this._nodeToButton( node );
+
+               if ( action === undefined ) {
+                       return button.conf.action;
+               }
+
+               button.conf.action = action;
+
+               return this;
+       },
+
+       /**
+        * Add an active class to the button to make to look active or get current
+        * active state.
+        * @param  {node} node Button element
+        * @param  {boolean} [flag] Enable / disable flag
+        * @return {Buttons} Self for chaining or boolean for getter
+        */
+       active: function ( node, flag ) {
+               var button = this._nodeToButton( node );
+               var klass = this.c.dom.button.active;
+               var jqNode = $(button.node);
+
+               if ( flag === undefined ) {
+                       return jqNode.hasClass( klass );
+               }
+
+               jqNode.toggleClass( klass, flag === undefined ? true : flag );
+
+               return this;
+       },
+
+       /**
+        * Add a new button
+        * @param {object} config Button configuration object, base string name or function
+        * @param {int|string} [idx] Button index for where to insert the button
+        * @return {Buttons} Self for chaining
+        */
+       add: function ( config, idx )
+       {
+               var buttons = this.s.buttons;
+
+               if ( typeof idx === 'string' ) {
+                       var split = idx.split('-');
+                       var base = this.s;
+
+                       for ( var i=0, ien=split.length-1 ; i<ien ; i++ ) {
+                               base = base.buttons[ split[i]*1 ];
+                       }
+
+                       buttons = base.buttons;
+                       idx = split[ split.length-1 ]*1;
+               }
+
+               this._expandButton( buttons, config, base !== undefined, idx );
+               this._draw();
+
+               return this;
+       },
+
+       /**
+        * Get the container node for the buttons
+        * @return {jQuery} Buttons node
+        */
+       container: function ()
+       {
+               return this.dom.container;
+       },
+
+       /**
+        * Disable a button
+        * @param  {node} node Button node
+        * @return {Buttons} Self for chaining
+        */
+       disable: function ( node ) {
+               var button = this._nodeToButton( node );
+
+               $(button.node).addClass( this.c.dom.button.disabled );
+
+               return this;
+       },
+
+       /**
+        * Destroy the instance, cleaning up event handlers and removing DOM
+        * elements
+        * @return {Buttons} Self for chaining
+        */
+       destroy: function ()
+       {
+               // Key event listener
+               $('body').off( 'keyup.'+this.s.namespace );
+
+               // Individual button destroy (so they can remove their own events if
+               // needed). Take a copy as the array is modified by `remove`
+               var buttons = this.s.buttons.slice();
+               var i, ien;
+               
+               for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       this.remove( buttons[i].node );
+               }
+
+               // Container
+               this.dom.container.remove();
+
+               // Remove from the settings object collection
+               var buttonInsts = this.s.dt.settings()[0];
+
+               for ( i=0, ien=buttonInsts.length ; i<ien ; i++ ) {
+                       if ( buttonInsts.inst === this ) {
+                               buttonInsts.splice( i, 1 );
+                               break;
+                       }
+               }
+
+               return this;
+       },
+
+       /**
+        * Enable / disable a button
+        * @param  {node} node Button node
+        * @param  {boolean} [flag=true] Enable / disable flag
+        * @return {Buttons} Self for chaining
+        */
+       enable: function ( node, flag )
+       {
+               if ( flag === false ) {
+                       return this.disable( node );
+               }
+
+               var button = this._nodeToButton( node );
+               $(button.node).removeClass( this.c.dom.button.disabled );
+
+               return this;
+       },
+
+       /**
+        * Get the instance name for the button set selector
+        * @return {string} Instance name
+        */
+       name: function ()
+       {
+               return this.c.name;
+       },
+
+       /**
+        * Get a button's node of the buttons container if no button is given
+        * @param  {node} [node] Button node
+        * @return {jQuery} Button element, or container
+        */
+       node: function ( node )
+       {
+               if ( ! node ) {
+                       return this.dom.container;
+               }
+
+               var button = this._nodeToButton( node );
+               return $(button.node);
+       },
+
+       /**
+        * Set / get a processing class on the selected button
+        * @param {element} node Triggering button node
+        * @param  {boolean} flag true to add, false to remove, undefined to get
+        * @return {boolean|Buttons} Getter value or this if a setter.
+        */
+       processing: function ( node, flag )
+       {
+               var dt = this.s.dt;
+               var button = this._nodeToButton( node );
+
+               if ( flag === undefined ) {
+                       return $(button.node).hasClass( 'processing' );
+               }
+
+               $(button.node).toggleClass( 'processing', flag );
+
+               $(dt.table().node()).triggerHandler( 'buttons-processing.dt', [
+                       flag, dt.button( node ), dt, $(node), button.conf
+               ] );
+
+               return this;
+       },
+
+       /**
+        * Remove a button.
+        * @param  {node} node Button node
+        * @return {Buttons} Self for chaining
+        */
+       remove: function ( node )
+       {
+               var button = this._nodeToButton( node );
+               var host = this._nodeToHost( node );
+               var dt = this.s.dt;
+
+               // Remove any child buttons first
+               if ( button.buttons.length ) {
+                       for ( var i=button.buttons.length-1 ; i>=0 ; i-- ) {
+                               this.remove( button.buttons[i].node );
+                       }
+               }
+
+               // Allow the button to remove event handlers, etc
+               if ( button.conf.destroy ) {
+                       button.conf.destroy.call( dt.button(node), dt, $(node), button.conf );
+               }
+
+               this._removeKey( button.conf );
+
+               $(button.node).remove();
+
+               var idx = $.inArray( button, host );
+               host.splice( idx, 1 );
+
+               return this;
+       },
+
+       /**
+        * Get the text for a button
+        * @param  {int|string} node Button index
+        * @return {string} Button text
+        *//**
+        * Set the text for a button
+        * @param  {int|string|function} node Button index
+        * @param  {string} label Text
+        * @return {Buttons} Self for chaining
+        */
+       text: function ( node, label )
+       {
+               var button = this._nodeToButton( node );
+               var buttonLiner = this.c.dom.collection.buttonLiner;
+               var linerTag = button.inCollection && buttonLiner && buttonLiner.tag ?
+                       buttonLiner.tag :
+                       this.c.dom.buttonLiner.tag;
+               var dt = this.s.dt;
+               var jqNode = $(button.node);
+               var text = function ( opt ) {
+                       return typeof opt === 'function' ?
+                               opt( dt, jqNode, button.conf ) :
+                               opt;
+               };
+
+               if ( label === undefined ) {
+                       return text( button.conf.text );
+               }
+
+               button.conf.text = label;
+
+               if ( linerTag ) {
+                       jqNode.children( linerTag ).html( text(label) );
+               }
+               else {
+                       jqNode.html( text(label) );
+               }
+
+               return this;
+       },
+
+
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+        * Constructor
+        */
+
+       /**
+        * Buttons constructor
+        * @private
+        */
+       _constructor: function ()
+       {
+               var that = this;
+               var dt = this.s.dt;
+               var dtSettings = dt.settings()[0];
+               var buttons =  this.c.buttons;
+
+               if ( ! dtSettings._buttons ) {
+                       dtSettings._buttons = [];
+               }
+
+               dtSettings._buttons.push( {
+                       inst: this,
+                       name: this.c.name
+               } );
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       this.add( buttons[i] );
+               }
+
+               dt.on( 'destroy', function ( e, settings ) {
+                       if ( settings === dtSettings ) {
+                               that.destroy();
+                       }
+               } );
+
+               // Global key event binding to listen for button keys
+               $('body').on( 'keyup.'+this.s.namespace, function ( e ) {
+                       if ( ! document.activeElement || document.activeElement === document.body ) {
+                               // SUse a string of characters for fast lookup of if we need to
+                               // handle this
+                               var character = String.fromCharCode(e.keyCode).toLowerCase();
+
+                               if ( that.s.listenKeys.toLowerCase().indexOf( character ) !== -1 ) {
+                                       that._keypress( character, e );
+                               }
+                       }
+               } );
+       },
+
+
+       /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+        * Private methods
+        */
+
+       /**
+        * Add a new button to the key press listener
+        * @param {object} conf Resolved button configuration object
+        * @private
+        */
+       _addKey: function ( conf )
+       {
+               if ( conf.key ) {
+                       this.s.listenKeys += $.isPlainObject( conf.key ) ?
+                               conf.key.key :
+                               conf.key;
+               }
+       },
+
+       /**
+        * Insert the buttons into the container. Call without parameters!
+        * @param  {node} [container] Recursive only - Insert point
+        * @param  {array} [buttons] Recursive only - Buttons array
+        * @private
+        */
+       _draw: function ( container, buttons )
+       {
+               if ( ! container ) {
+                       container = this.dom.container;
+                       buttons = this.s.buttons;
+               }
+
+               container.children().detach();
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       container.append( buttons[i].inserter );
+                       container.append( ' ' );
+
+                       if ( buttons[i].buttons && buttons[i].buttons.length ) {
+                               this._draw( buttons[i].collection, buttons[i].buttons );
+                       }
+               }
+       },
+
+       /**
+        * Create buttons from an array of buttons
+        * @param  {array} attachTo Buttons array to attach to
+        * @param  {object} button Button definition
+        * @param  {boolean} inCollection true if the button is in a collection
+        * @private
+        */
+       _expandButton: function ( attachTo, button, inCollection, attachPoint )
+       {
+               var dt = this.s.dt;
+               var buttonCounter = 0;
+               var buttons = ! $.isArray( button ) ?
+                       [ button ] :
+                       button;
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       var conf = this._resolveExtends( buttons[i] );
+
+                       if ( ! conf ) {
+                               continue;
+                       }
+
+                       // If the configuration is an array, then expand the buttons at this
+                       // point
+                       if ( $.isArray( conf ) ) {
+                               this._expandButton( attachTo, conf, inCollection, attachPoint );
+                               continue;
+                       }
+
+                       var built = this._buildButton( conf, inCollection );
+                       if ( ! built ) {
+                               continue;
+                       }
+
+                       if ( attachPoint !== undefined ) {
+                               attachTo.splice( attachPoint, 0, built );
+                               attachPoint++;
+                       }
+                       else {
+                               attachTo.push( built );
+                       }
+
+                       if ( built.conf.buttons ) {
+                               built.collection = $('<'+this.c.dom.collection.tag+'/>');
+
+                               built.conf._collection = built.collection;
+
+                               this._expandButton( built.buttons, built.conf.buttons, true, attachPoint );
+                       }
+
+                       // init call is made here, rather than buildButton as it needs to
+                       // be selectable, and for that it needs to be in the buttons array
+                       if ( conf.init ) {
+                               conf.init.call( dt.button( built.node ), dt, $(built.node), conf );
+                       }
+
+                       buttonCounter++;
+               }
+       },
+
+       /**
+        * Create an individual button
+        * @param  {object} config            Resolved button configuration
+        * @param  {boolean} inCollection `true` if a collection button
+        * @return {jQuery} Created button node (jQuery)
+        * @private
+        */
+       _buildButton: function ( config, inCollection )
+       {
+               var buttonDom = this.c.dom.button;
+               var linerDom = this.c.dom.buttonLiner;
+               var collectionDom = this.c.dom.collection;
+               var dt = this.s.dt;
+               var text = function ( opt ) {
+                       return typeof opt === 'function' ?
+                               opt( dt, button, config ) :
+                               opt;
+               };
+
+               if ( inCollection && collectionDom.button ) {
+                       buttonDom = collectionDom.button;
+               }
+
+               if ( inCollection && collectionDom.buttonLiner ) {
+                       linerDom = collectionDom.buttonLiner;
+               }
+
+               // Make sure that the button is available based on whatever requirements
+               // it has. For example, Flash buttons require Flash
+               if ( config.available && ! config.available( dt, config ) ) {
+                       return false;
+               }
+
+               var action = function ( e, dt, button, config ) {
+                       config.action.call( dt.button( button ), e, dt, button, config );
+
+                       $(dt.table().node()).triggerHandler( 'buttons-action.dt', [
+                               dt.button( button ), dt, button, config 
+                       ] );
+               };
+
+               var tag = config.tag || buttonDom.tag;
+               var clickBlurs = config.clickBlurs === undefined ? true : config.clickBlurs
+               var button = $('<'+tag+'/>')
+                       .addClass( buttonDom.className )
+                       .attr( 'tabindex', this.s.dt.settings()[0].iTabIndex )
+                       .attr( 'aria-controls', this.s.dt.table().node().id )
+                       .on( 'click.dtb', function (e) {
+                               e.preventDefault();
+
+                               if ( ! button.hasClass( buttonDom.disabled ) && config.action ) {
+                                       action( e, dt, button, config );
+                               }
+                               if( clickBlurs ) {
+                                       button.blur();
+                               }
+                       } )
+                       .on( 'keyup.dtb', function (e) {
+                               if ( e.keyCode === 13 ) {
+                                       if ( ! button.hasClass( buttonDom.disabled ) && config.action ) {
+                                               action( e, dt, button, config );
+                                       }
+                               }
+                       } );
+
+               // Make `a` tags act like a link
+               if ( tag.toLowerCase() === 'a' ) {
+                       button.attr( 'href', '#' );
+               }
+
+               // Button tags should have `type=button` so they don't have any default behaviour
+               if ( tag.toLowerCase() === 'button' ) {
+                       button.attr( 'type', 'button' );
+               }
+
+               if ( linerDom.tag ) {
+                       var liner = $('<'+linerDom.tag+'/>')
+                               .html( text( config.text ) )
+                               .addClass( linerDom.className );
+
+                       if ( linerDom.tag.toLowerCase() === 'a' ) {
+                               liner.attr( 'href', '#' );
+                       }
+
+                       button.append( liner );
+               }
+               else {
+                       button.html( text( config.text ) );
+               }
+
+               if ( config.enabled === false ) {
+                       button.addClass( buttonDom.disabled );
+               }
+
+               if ( config.className ) {
+                       button.addClass( config.className );
+               }
+
+               if ( config.titleAttr ) {
+                       button.attr( 'title', text( config.titleAttr ) );
+               }
+
+               if ( config.attr ) {
+                       button.attr( config.attr );
+               }
+
+               if ( ! config.namespace ) {
+                       config.namespace = '.dt-button-'+(_buttonCounter++);
+               }
+
+               var buttonContainer = this.c.dom.buttonContainer;
+               var inserter;
+               if ( buttonContainer && buttonContainer.tag ) {
+                       inserter = $('<'+buttonContainer.tag+'/>')
+                               .addClass( buttonContainer.className )
+                               .append( button );
+               }
+               else {
+                       inserter = button;
+               }
+
+               this._addKey( config );
+
+               // Style integration callback for DOM manipulation
+               // Note that this is _not_ documented. It is currently
+               // for style integration only
+               if( this.c.buttonCreated ) {
+                       inserter = this.c.buttonCreated( config, inserter );
+               }
+
+               return {
+                       conf:         config,
+                       node:         button.get(0),
+                       inserter:     inserter,
+                       buttons:      [],
+                       inCollection: inCollection,
+                       collection:   null
+               };
+       },
+
+       /**
+        * Get the button object from a node (recursive)
+        * @param  {node} node Button node
+        * @param  {array} [buttons] Button array, uses base if not defined
+        * @return {object} Button object
+        * @private
+        */
+       _nodeToButton: function ( node, buttons )
+       {
+               if ( ! buttons ) {
+                       buttons = this.s.buttons;
+               }
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       if ( buttons[i].node === node ) {
+                               return buttons[i];
+                       }
+
+                       if ( buttons[i].buttons.length ) {
+                               var ret = this._nodeToButton( node, buttons[i].buttons );
+
+                               if ( ret ) {
+                                       return ret;
+                               }
+                       }
+               }
+       },
+
+       /**
+        * Get container array for a button from a button node (recursive)
+        * @param  {node} node Button node
+        * @param  {array} [buttons] Button array, uses base if not defined
+        * @return {array} Button's host array
+        * @private
+        */
+       _nodeToHost: function ( node, buttons )
+       {
+               if ( ! buttons ) {
+                       buttons = this.s.buttons;
+               }
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       if ( buttons[i].node === node ) {
+                               return buttons;
+                       }
+
+                       if ( buttons[i].buttons.length ) {
+                               var ret = this._nodeToHost( node, buttons[i].buttons );
+
+                               if ( ret ) {
+                                       return ret;
+                               }
+                       }
+               }
+       },
+
+       /**
+        * Handle a key press - determine if any button's key configured matches
+        * what was typed and trigger the action if so.
+        * @param  {string} character The character pressed
+        * @param  {object} e Key event that triggered this call
+        * @private
+        */
+       _keypress: function ( character, e )
+       {
+               // Check if this button press already activated on another instance of Buttons
+               if ( e._buttonsHandled ) {
+                       return;
+               }
+
+               var run = function ( conf, node ) {
+                       if ( ! conf.key ) {
+                               return;
+                       }
+
+                       if ( conf.key === character ) {
+                               e._buttonsHandled = true;
+                               $(node).click();
+                       }
+                       else if ( $.isPlainObject( conf.key ) ) {
+                               if ( conf.key.key !== character ) {
+                                       return;
+                               }
+
+                               if ( conf.key.shiftKey && ! e.shiftKey ) {
+                                       return;
+                               }
+
+                               if ( conf.key.altKey && ! e.altKey ) {
+                                       return;
+                               }
+
+                               if ( conf.key.ctrlKey && ! e.ctrlKey ) {
+                                       return;
+                               }
+
+                               if ( conf.key.metaKey && ! e.metaKey ) {
+                                       return;
+                               }
+
+                               // Made it this far - it is good
+                               e._buttonsHandled = true;
+                               $(node).click();
+                       }
+               };
+
+               var recurse = function ( a ) {
+                       for ( var i=0, ien=a.length ; i<ien ; i++ ) {
+                               run( a[i].conf, a[i].node );
+
+                               if ( a[i].buttons.length ) {
+                                       recurse( a[i].buttons );
+                               }
+                       }
+               };
+
+               recurse( this.s.buttons );
+       },
+
+       /**
+        * Remove a key from the key listener for this instance (to be used when a
+        * button is removed)
+        * @param  {object} conf Button configuration
+        * @private
+        */
+       _removeKey: function ( conf )
+       {
+               if ( conf.key ) {
+                       var character = $.isPlainObject( conf.key ) ?
+                               conf.key.key :
+                               conf.key;
+
+                       // Remove only one character, as multiple buttons could have the
+                       // same listening key
+                       var a = this.s.listenKeys.split('');
+                       var idx = $.inArray( character, a );
+                       a.splice( idx, 1 );
+                       this.s.listenKeys = a.join('');
+               }
+       },
+
+       /**
+        * Resolve a button configuration
+        * @param  {string|function|object} conf Button config to resolve
+        * @return {object} Button configuration
+        * @private
+        */
+       _resolveExtends: function ( conf )
+       {
+               var dt = this.s.dt;
+               var i, ien;
+               var toConfObject = function ( base ) {
+                       var loop = 0;
+
+                       // Loop until we have resolved to a button configuration, or an
+                       // array of button configurations (which will be iterated
+                       // separately)
+                       while ( ! $.isPlainObject(base) && ! $.isArray(base) ) {
+                               if ( base === undefined ) {
+                                       return;
+                               }
+
+                               if ( typeof base === 'function' ) {
+                                       base = base( dt, conf );
+
+                                       if ( ! base ) {
+                                               return false;
+                                       }
+                               }
+                               else if ( typeof base === 'string' ) {
+                                       if ( ! _dtButtons[ base ] ) {
+                                               throw 'Unknown button type: '+base;
+                                       }
+
+                                       base = _dtButtons[ base ];
+                               }
+
+                               loop++;
+                               if ( loop > 30 ) {
+                                       // Protect against misconfiguration killing the browser
+                                       throw 'Buttons: Too many iterations';
+                               }
+                       }
+
+                       return $.isArray( base ) ?
+                               base :
+                               $.extend( {}, base );
+               };
+
+               conf = toConfObject( conf );
+
+               while ( conf && conf.extend ) {
+                       // Use `toConfObject` in case the button definition being extended
+                       // is itself a string or a function
+                       if ( ! _dtButtons[ conf.extend ] ) {
+                               throw 'Cannot extend unknown button type: '+conf.extend;
+                       }
+
+                       var objArray = toConfObject( _dtButtons[ conf.extend ] );
+                       if ( $.isArray( objArray ) ) {
+                               return objArray;
+                       }
+                       else if ( ! objArray ) {
+                               // This is a little brutal as it might be possible to have a
+                               // valid button without the extend, but if there is no extend
+                               // then the host button would be acting in an undefined state
+                               return false;
+                       }
+
+                       // Stash the current class name
+                       var originalClassName = objArray.className;
+
+                       conf = $.extend( {}, objArray, conf );
+
+                       // The extend will have overwritten the original class name if the
+                       // `conf` object also assigned a class, but we want to concatenate
+                       // them so they are list that is combined from all extended buttons
+                       if ( originalClassName && conf.className !== originalClassName ) {
+                               conf.className = originalClassName+' '+conf.className;
+                       }
+
+                       // Buttons to be added to a collection  -gives the ability to define
+                       // if buttons should be added to the start or end of a collection
+                       var postfixButtons = conf.postfixButtons;
+                       if ( postfixButtons ) {
+                               if ( ! conf.buttons ) {
+                                       conf.buttons = [];
+                               }
+
+                               for ( i=0, ien=postfixButtons.length ; i<ien ; i++ ) {
+                                       conf.buttons.push( postfixButtons[i] );
+                               }
+
+                               conf.postfixButtons = null;
+                       }
+
+                       var prefixButtons = conf.prefixButtons;
+                       if ( prefixButtons ) {
+                               if ( ! conf.buttons ) {
+                                       conf.buttons = [];
+                               }
+
+                               for ( i=0, ien=prefixButtons.length ; i<ien ; i++ ) {
+                                       conf.buttons.splice( i, 0, prefixButtons[i] );
+                               }
+
+                               conf.prefixButtons = null;
+                       }
+
+                       // Although we want the `conf` object to overwrite almost all of
+                       // the properties of the object being extended, the `extend`
+                       // property should come from the object being extended
+                       conf.extend = objArray.extend;
+               }
+
+               return conf;
+       },
+
+       /**
+        * Display (and replace if there is an existing one) a popover attached to a button
+        * @param {string|node} content Content to show
+        * @param {DataTable.Api} hostButton DT API instance of the button
+        * @param {object} inOpts Options (see object below for all options)
+        */
+       _popover: function ( content, hostButton, inOpts ) {
+               var dt = hostButton;
+               var buttonsSettings = this.c;
+               var options = $.extend( {
+                       align: 'button-left', // button-right, dt-container
+                       autoClose: false,
+                       background: true,
+                       backgroundClassName: 'dt-button-background',
+                       contentClassName: buttonsSettings.dom.collection.className,
+                       collectionLayout: '',
+                       collectionTitle: '',
+                       dropup: false,
+                       fade: 400,
+                       rightAlignClassName: 'dt-button-right',
+                       tag: buttonsSettings.dom.collection.tag
+               }, inOpts );
+               var hostNode = hostButton.node();
+
+               var close = function () {
+                       $('.dt-button-collection').stop().fadeOut( options.fade, function () {
+                               $(this).detach();
+                       } );
+
+                       $(dt.buttons( '[aria-haspopup="true"][aria-expanded="true"]' ).nodes())
+                               .attr('aria-expanded', 'false');
+
+                       $('div.dt-button-background').off( 'click.dtb-collection' );
+                       Buttons.background( false, options.backgroundClassName, options.fade, hostNode );
+
+                       $('body').off( '.dtb-collection' );
+                       dt.off( 'buttons-action.b-internal' );
+               };
+
+               if (content === false) {
+                       close();
+               }
+
+               var existingExpanded = $(dt.buttons( '[aria-haspopup="true"][aria-expanded="true"]' ).nodes());
+               if ( existingExpanded.length ) {
+                       hostNode = existingExpanded.eq(0);
+
+                       close();
+               }
+
+               var display = $('<div/>')
+                       .addClass('dt-button-collection')
+                       .addClass(options.collectionLayout)
+                       .css('display', 'none');
+
+               content = $(content)
+                       .addClass(options.contentClassName)
+                       .attr('role', 'menu')
+                       .appendTo(display);
+
+               hostNode.attr( 'aria-expanded', 'true' );
+
+               if ( hostNode.parents('body')[0] !== document.body ) {
+                       hostNode = document.body.lastChild;
+               }
+
+               if ( options.collectionTitle ) {
+                       display.prepend('<div class="dt-button-collection-title">'+options.collectionTitle+'</div>');
+               }
+
+               display
+                       .insertAfter( hostNode )
+                       .fadeIn( options.fade );
+
+               var tableContainer = $( hostButton.table().container() );
+               var position = display.css( 'position' );
+
+               if ( options.align === 'dt-container' ) {
+                       hostNode = hostNode.parent();
+                       display.css('width', tableContainer.width());
+               }
+
+               if ( position === 'absolute' ) {
+                       var hostPosition = hostNode.position();
+
+                       display.css( {
+                               top: hostPosition.top + hostNode.outerHeight(),
+                               left: hostPosition.left
+                       } );
+
+                       // calculate overflow when positioned beneath
+                       var collectionHeight = display.outerHeight();
+                       var collectionWidth = display.outerWidth();
+                       var tableBottom = tableContainer.offset().top + tableContainer.height();
+                       var listBottom = hostPosition.top + hostNode.outerHeight() + collectionHeight;
+                       var bottomOverflow = listBottom - tableBottom;
+
+                       // calculate overflow when positioned above
+                       var listTop = hostPosition.top - collectionHeight;
+                       var tableTop = tableContainer.offset().top;
+                       var topOverflow = tableTop - listTop;
+
+                       // if bottom overflow is larger, move to the top because it fits better, or if dropup is requested
+                       var moveTop = hostPosition.top - collectionHeight - 5;
+                       if ( (bottomOverflow > topOverflow || options.dropup) && -moveTop < tableTop ) {
+                               display.css( 'top', moveTop);
+                       }
+
+                       // Right alignment is enabled on a class, e.g. bootstrap:
+                       // $.fn.dataTable.Buttons.defaults.dom.collection.className += " dropdown-menu-right"; 
+                       if ( display.hasClass( options.rightAlignClassName ) || options.align === 'button-right' ) {
+                               display.css( 'left', hostPosition.left + hostNode.outerWidth() - collectionWidth );
+                       }
+
+                       // Right alignment in table container
+                       var listRight = hostPosition.left + collectionWidth;
+                       var tableRight = tableContainer.offset().left + tableContainer.width();
+                       if ( listRight > tableRight ) {
+                               display.css( 'left', hostPosition.left - ( listRight - tableRight ) );
+                       }
+
+                       // Right alignment to window
+                       var listOffsetRight = hostNode.offset().left + collectionWidth;
+                       if ( listOffsetRight > $(window).width() ) {
+                               display.css( 'left', hostPosition.left - (listOffsetRight-$(window).width()) );
+                       }
+               }
+               else {
+                       // Fix position - centre on screen
+                       var top = display.height() / 2;
+                       if ( top > $(window).height() / 2 ) {
+                               top = $(window).height() / 2;
+                       }
+
+                       display.css( 'marginTop', top*-1 );
+               }
+
+               if ( options.background ) {
+                       Buttons.background( true, options.backgroundClassName, options.fade, hostNode );
+               }
+
+               // This is bonkers, but if we don't have a click listener on the
+               // background element, iOS Safari will ignore the body click
+               // listener below. An empty function here is all that is
+               // required to make it work...
+               $('div.dt-button-background').on( 'click.dtb-collection', function () {} );
+
+               $('body')
+                       .on( 'click.dtb-collection', function (e) {
+                               // andSelf is deprecated in jQ1.8, but we want 1.7 compat
+                               var back = $.fn.addBack ? 'addBack' : 'andSelf';
+
+                               if ( ! $(e.target).parents()[back]().filter( content ).length ) {
+                                       close();
+                               }
+                       } )
+                       .on( 'keyup.dtb-collection', function (e) {
+                               if ( e.keyCode === 27 ) {
+                                       close();
+                               }
+                       } );
+
+               if ( options.autoClose ) {
+                       setTimeout( function () {
+                               dt.on( 'buttons-action.b-internal', function (e, btn, dt, node) {
+                                       if ( node[0] === hostNode[0] ) {
+                                               return;
+                                       }
+                                       close();
+                               } );
+                       }, 0);
+               }
+       }
+} );
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Statics
+ */
+
+/**
+ * Show / hide a background layer behind a collection
+ * @param  {boolean} Flag to indicate if the background should be shown or
+ *   hidden 
+ * @param  {string} Class to assign to the background
+ * @static
+ */
+Buttons.background = function ( show, className, fade, insertPoint ) {
+       if ( fade === undefined ) {
+               fade = 400;
+       }
+       if ( ! insertPoint ) {
+               insertPoint = document.body;
+       }
+
+       if ( show ) {
+               $('<div/>')
+                       .addClass( className )
+                       .css( 'display', 'none' )
+                       .insertAfter( insertPoint )
+                       .stop()
+                       .fadeIn( fade );
+       }
+       else {
+               $('div.'+className)
+                       .stop()
+                       .fadeOut( fade, function () {
+                               $(this)
+                                       .removeClass( className )
+                                       .remove();
+                       } );
+       }
+};
+
+/**
+ * Instance selector - select Buttons instances based on an instance selector
+ * value from the buttons assigned to a DataTable. This is only useful if
+ * multiple instances are attached to a DataTable.
+ * @param  {string|int|array} Instance selector - see `instance-selector`
+ *   documentation on the DataTables site
+ * @param  {array} Button instance array that was attached to the DataTables
+ *   settings object
+ * @return {array} Buttons instances
+ * @static
+ */
+Buttons.instanceSelector = function ( group, buttons )
+{
+       if ( group === undefined || group === null ) {
+               return $.map( buttons, function ( v ) {
+                       return v.inst;
+               } );
+       }
+
+       var ret = [];
+       var names = $.map( buttons, function ( v ) {
+               return v.name;
+       } );
+
+       // Flatten the group selector into an array of single options
+       var process = function ( input ) {
+               if ( $.isArray( input ) ) {
+                       for ( var i=0, ien=input.length ; i<ien ; i++ ) {
+                               process( input[i] );
+                       }
+                       return;
+               }
+
+               if ( typeof input === 'string' ) {
+                       if ( input.indexOf( ',' ) !== -1 ) {
+                               // String selector, list of names
+                               process( input.split(',') );
+                       }
+                       else {
+                               // String selector individual name
+                               var idx = $.inArray( $.trim(input), names );
+
+                               if ( idx !== -1 ) {
+                                       ret.push( buttons[ idx ].inst );
+                               }
+                       }
+               }
+               else if ( typeof input === 'number' ) {
+                       // Index selector
+                       ret.push( buttons[ input ].inst );
+               }
+       };
+       
+       process( group );
+
+       return ret;
+};
+
+/**
+ * Button selector - select one or more buttons from a selector input so some
+ * operation can be performed on them.
+ * @param  {array} Button instances array that the selector should operate on
+ * @param  {string|int|node|jQuery|array} Button selector - see
+ *   `button-selector` documentation on the DataTables site
+ * @return {array} Array of objects containing `inst` and `idx` properties of
+ *   the selected buttons so you know which instance each button belongs to.
+ * @static
+ */
+Buttons.buttonSelector = function ( insts, selector )
+{
+       var ret = [];
+       var nodeBuilder = function ( a, buttons, baseIdx ) {
+               var button;
+               var idx;
+
+               for ( var i=0, ien=buttons.length ; i<ien ; i++ ) {
+                       button = buttons[i];
+
+                       if ( button ) {
+                               idx = baseIdx !== undefined ?
+                                       baseIdx+i :
+                                       i+'';
+
+                               a.push( {
+                                       node: button.node,
+                                       name: button.conf.name,
+                                       idx:  idx
+                               } );
+
+                               if ( button.buttons ) {
+                                       nodeBuilder( a, button.buttons, idx+'-' );
+                               }
+                       }
+               }
+       };
+
+       var run = function ( selector, inst ) {
+               var i, ien;
+               var buttons = [];
+               nodeBuilder( buttons, inst.s.buttons );
+
+               var nodes = $.map( buttons, function (v) {
+                       return v.node;
+               } );
+
+               if ( $.isArray( selector ) || selector instanceof $ ) {
+                       for ( i=0, ien=selector.length ; i<ien ; i++ ) {
+                               run( selector[i], inst );
+                       }
+                       return;
+               }
+
+               if ( selector === null || selector === undefined || selector === '*' ) {
+                       // Select all
+                       for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+                               ret.push( {
+                                       inst: inst,
+                                       node: buttons[i].node
+                               } );
+                       }
+               }
+               else if ( typeof selector === 'number' ) {
+                       // Main button index selector
+                       ret.push( {
+                               inst: inst,
+                               node: inst.s.buttons[ selector ].node
+                       } );
+               }
+               else if ( typeof selector === 'string' ) {
+                       if ( selector.indexOf( ',' ) !== -1 ) {
+                               // Split
+                               var a = selector.split(',');
+
+                               for ( i=0, ien=a.length ; i<ien ; i++ ) {
+                                       run( $.trim(a[i]), inst );
+                               }
+                       }
+                       else if ( selector.match( /^\d+(\-\d+)*$/ ) ) {
+                               // Sub-button index selector
+                               var indexes = $.map( buttons, function (v) {
+                                       return v.idx;
+                               } );
+
+                               ret.push( {
+                                       inst: inst,
+                                       node: buttons[ $.inArray( selector, indexes ) ].node
+                               } );
+                       }
+                       else if ( selector.indexOf( ':name' ) !== -1 ) {
+                               // Button name selector
+                               var name = selector.replace( ':name', '' );
+
+                               for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+                                       if ( buttons[i].name === name ) {
+                                               ret.push( {
+                                                       inst: inst,
+                                                       node: buttons[i].node
+                                               } );
+                                       }
+                               }
+                       }
+                       else {
+                               // jQuery selector on the nodes
+                               $( nodes ).filter( selector ).each( function () {
+                                       ret.push( {
+                                               inst: inst,
+                                               node: this
+                                       } );
+                               } );
+                       }
+               }
+               else if ( typeof selector === 'object' && selector.nodeName ) {
+                       // Node selector
+                       var idx = $.inArray( selector, nodes );
+
+                       if ( idx !== -1 ) {
+                               ret.push( {
+                                       inst: inst,
+                                       node: nodes[ idx ]
+                               } );
+                       }
+               }
+       };
+
+
+       for ( var i=0, ien=insts.length ; i<ien ; i++ ) {
+               var inst = insts[i];
+
+               run( selector, inst );
+       }
+
+       return ret;
+};
+
+
+/**
+ * Buttons defaults. For full documentation, please refer to the docs/option
+ * directory or the DataTables site.
+ * @type {Object}
+ * @static
+ */
+Buttons.defaults = {
+       buttons: [ 'copy', 'excel', 'csv', 'pdf', 'print' ],
+       name: 'main',
+       tabIndex: 0,
+       dom: {
+               container: {
+                       tag: 'div',
+                       className: 'dt-buttons'
+               },
+               collection: {
+                       tag: 'div',
+                       className: ''
+               },
+               button: {
+                       // Flash buttons will not work with `<button>` in IE - it has to be `<a>`
+                       tag: 'ActiveXObject' in window ?
+                               'a' :
+                               'button',
+                       className: 'dt-button',
+                       active: 'active',
+                       disabled: 'disabled'
+               },
+               buttonLiner: {
+                       tag: 'span',
+                       className: ''
+               }
+       }
+};
+
+/**
+ * Version information
+ * @type {string}
+ * @static
+ */
+Buttons.version = '1.6.1';
+
+
+$.extend( _dtButtons, {
+       collection: {
+               text: function ( dt ) {
+                       return dt.i18n( 'buttons.collection', 'Collection' );
+               },
+               className: 'buttons-collection',
+               init: function ( dt, button, config ) {
+                       button.attr( 'aria-expanded', false );
+               },
+               action: function ( e, dt, button, config ) {
+                       e.stopPropagation();
+
+                       if ( config._collection.parents('body').length ) {
+                               this.popover(false, config);
+                       }
+                       else {
+                               this.popover(config._collection, config);
+                       }
+               },
+               attr: {
+                       'aria-haspopup': true
+               }
+               // Also the popover options, defined in Buttons.popover
+       },
+       copy: function ( dt, conf ) {
+               if ( _dtButtons.copyHtml5 ) {
+                       return 'copyHtml5';
+               }
+               if ( _dtButtons.copyFlash && _dtButtons.copyFlash.available( dt, conf ) ) {
+                       return 'copyFlash';
+               }
+       },
+       csv: function ( dt, conf ) {
+               // Common option that will use the HTML5 or Flash export buttons
+               if ( _dtButtons.csvHtml5 && _dtButtons.csvHtml5.available( dt, conf ) ) {
+                       return 'csvHtml5';
+               }
+               if ( _dtButtons.csvFlash && _dtButtons.csvFlash.available( dt, conf ) ) {
+                       return 'csvFlash';
+               }
+       },
+       excel: function ( dt, conf ) {
+               // Common option that will use the HTML5 or Flash export buttons
+               if ( _dtButtons.excelHtml5 && _dtButtons.excelHtml5.available( dt, conf ) ) {
+                       return 'excelHtml5';
+               }
+               if ( _dtButtons.excelFlash && _dtButtons.excelFlash.available( dt, conf ) ) {
+                       return 'excelFlash';
+               }
+       },
+       pdf: function ( dt, conf ) {
+               // Common option that will use the HTML5 or Flash export buttons
+               if ( _dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available( dt, conf ) ) {
+                       return 'pdfHtml5';
+               }
+               if ( _dtButtons.pdfFlash && _dtButtons.pdfFlash.available( dt, conf ) ) {
+                       return 'pdfFlash';
+               }
+       },
+       pageLength: function ( dt ) {
+               var lengthMenu = dt.settings()[0].aLengthMenu;
+               var vals = $.isArray( lengthMenu[0] ) ? lengthMenu[0] : lengthMenu;
+               var lang = $.isArray( lengthMenu[0] ) ? lengthMenu[1] : lengthMenu;
+               var text = function ( dt ) {
+                       return dt.i18n( 'buttons.pageLength', {
+                               "-1": 'Show all rows',
+                               _:    'Show %d rows'
+                       }, dt.page.len() );
+               };
+
+               return {
+                       extend: 'collection',
+                       text: text,
+                       className: 'buttons-page-length',
+                       autoClose: true,
+                       buttons: $.map( vals, function ( val, i ) {
+                               return {
+                                       text: lang[i],
+                                       className: 'button-page-length',
+                                       action: function ( e, dt ) {
+                                               dt.page.len( val ).draw();
+                                       },
+                                       init: function ( dt, node, conf ) {
+                                               var that = this;
+                                               var fn = function () {
+                                                       that.active( dt.page.len() === val );
+                                               };
+
+                                               dt.on( 'length.dt'+conf.namespace, fn );
+                                               fn();
+                                       },
+                                       destroy: function ( dt, node, conf ) {
+                                               dt.off( 'length.dt'+conf.namespace );
+                                       }
+                               };
+                       } ),
+                       init: function ( dt, node, conf ) {
+                               var that = this;
+                               dt.on( 'length.dt'+conf.namespace, function () {
+                                       that.text( conf.text );
+                               } );
+                       },
+                       destroy: function ( dt, node, conf ) {
+                               dt.off( 'length.dt'+conf.namespace );
+                       }
+               };
+       }
+} );
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables API
+ *
+ * For complete documentation, please refer to the docs/api directory or the
+ * DataTables site
+ */
+
+// Buttons group and individual button selector
+DataTable.Api.register( 'buttons()', function ( group, selector ) {
+       // Argument shifting
+       if ( selector === undefined ) {
+               selector = group;
+               group = undefined;
+       }
+
+       this.selector.buttonGroup = group;
+
+       var res = this.iterator( true, 'table', function ( ctx ) {
+               if ( ctx._buttons ) {
+                       return Buttons.buttonSelector(
+                               Buttons.instanceSelector( group, ctx._buttons ),
+                               selector
+                       );
+               }
+       }, true );
+
+       res._groupSelector = group;
+       return res;
+} );
+
+// Individual button selector
+DataTable.Api.register( 'button()', function ( group, selector ) {
+       // just run buttons() and truncate
+       var buttons = this.buttons( group, selector );
+
+       if ( buttons.length > 1 ) {
+               buttons.splice( 1, buttons.length );
+       }
+
+       return buttons;
+} );
+
+// Active buttons
+DataTable.Api.registerPlural( 'buttons().active()', 'button().active()', function ( flag ) {
+       if ( flag === undefined ) {
+               return this.map( function ( set ) {
+                       return set.inst.active( set.node );
+               } );
+       }
+
+       return this.each( function ( set ) {
+               set.inst.active( set.node, flag );
+       } );
+} );
+
+// Get / set button action
+DataTable.Api.registerPlural( 'buttons().action()', 'button().action()', function ( action ) {
+       if ( action === undefined ) {
+               return this.map( function ( set ) {
+                       return set.inst.action( set.node );
+               } );
+       }
+
+       return this.each( function ( set ) {
+               set.inst.action( set.node, action );
+       } );
+} );
+
+// Enable / disable buttons
+DataTable.Api.register( ['buttons().enable()', 'button().enable()'], function ( flag ) {
+       return this.each( function ( set ) {
+               set.inst.enable( set.node, flag );
+       } );
+} );
+
+// Disable buttons
+DataTable.Api.register( ['buttons().disable()', 'button().disable()'], function () {
+       return this.each( function ( set ) {
+               set.inst.disable( set.node );
+       } );
+} );
+
+// Get button nodes
+DataTable.Api.registerPlural( 'buttons().nodes()', 'button().node()', function () {
+       var jq = $();
+
+       // jQuery will automatically reduce duplicates to a single entry
+       $( this.each( function ( set ) {
+               jq = jq.add( set.inst.node( set.node ) );
+       } ) );
+
+       return jq;
+} );
+
+// Get / set button processing state
+DataTable.Api.registerPlural( 'buttons().processing()', 'button().processing()', function ( flag ) {
+       if ( flag === undefined ) {
+               return this.map( function ( set ) {
+                       return set.inst.processing( set.node );
+               } );
+       }
+
+       return this.each( function ( set ) {
+               set.inst.processing( set.node, flag );
+       } );
+} );
+
+// Get / set button text (i.e. the button labels)
+DataTable.Api.registerPlural( 'buttons().text()', 'button().text()', function ( label ) {
+       if ( label === undefined ) {
+               return this.map( function ( set ) {
+                       return set.inst.text( set.node );
+               } );
+       }
+
+       return this.each( function ( set ) {
+               set.inst.text( set.node, label );
+       } );
+} );
+
+// Trigger a button's action
+DataTable.Api.registerPlural( 'buttons().trigger()', 'button().trigger()', function () {
+       return this.each( function ( set ) {
+               set.inst.node( set.node ).trigger( 'click' );
+       } );
+} );
+
+// Button resolver to the popover
+DataTable.Api.register( 'button().popover()', function (content, options) {
+       return this.map( function ( set ) {
+               return set.inst._popover( content, this.button(this[0].node), options );
+       } );
+} );
+
+// Get the container elements
+DataTable.Api.register( 'buttons().containers()', function () {
+       var jq = $();
+       var groupSelector = this._groupSelector;
+
+       // We need to use the group selector directly, since if there are no buttons
+       // the result set will be empty
+       this.iterator( true, 'table', function ( ctx ) {
+               if ( ctx._buttons ) {
+                       var insts = Buttons.instanceSelector( groupSelector, ctx._buttons );
+
+                       for ( var i=0, ien=insts.length ; i<ien ; i++ ) {
+                               jq = jq.add( insts[i].container() );
+                       }
+               }
+       } );
+
+       return jq;
+} );
+
+DataTable.Api.register( 'buttons().container()', function () {
+       // API level of nesting is `buttons()` so we can zip into the containers method
+       return this.containers().eq(0);
+} );
+
+// Add a new button
+DataTable.Api.register( 'button().add()', function ( idx, conf ) {
+       var ctx = this.context;
+
+       // Don't use `this` as it could be empty - select the instances directly
+       if ( ctx.length ) {
+               var inst = Buttons.instanceSelector( this._groupSelector, ctx[0]._buttons );
+
+               if ( inst.length ) {
+                       inst[0].add( conf, idx );
+               }
+       }
+
+       return this.button( this._groupSelector, idx );
+} );
+
+// Destroy the button sets selected
+DataTable.Api.register( 'buttons().destroy()', function () {
+       this.pluck( 'inst' ).unique().each( function ( inst ) {
+               inst.destroy();
+       } );
+
+       return this;
+} );
+
+// Remove a button
+DataTable.Api.registerPlural( 'buttons().remove()', 'buttons().remove()', function () {
+       this.each( function ( set ) {
+               set.inst.remove( set.node );
+       } );
+
+       return this;
+} );
+
+// Information box that can be used by buttons
+var _infoTimer;
+DataTable.Api.register( 'buttons.info()', function ( title, message, time ) {
+       var that = this;
+
+       if ( title === false ) {
+               this.off('destroy.btn-info');
+               $('#datatables_buttons_info').fadeOut( function () {
+                       $(this).remove();
+               } );
+               clearTimeout( _infoTimer );
+               _infoTimer = null;
+
+               return this;
+       }
+
+       if ( _infoTimer ) {
+               clearTimeout( _infoTimer );
+       }
+
+       if ( $('#datatables_buttons_info').length ) {
+               $('#datatables_buttons_info').remove();
+       }
+
+       title = title ? '<h2>'+title+'</h2>' : '';
+
+       $('<div id="datatables_buttons_info" class="dt-button-info"/>')
+               .html( title )
+               .append( $('<div/>')[ typeof message === 'string' ? 'html' : 'append' ]( message ) )
+               .css( 'display', 'none' )
+               .appendTo( 'body' )
+               .fadeIn();
+
+       if ( time !== undefined && time !== 0 ) {
+               _infoTimer = setTimeout( function () {
+                       that.buttons.info( false );
+               }, time );
+       }
+
+       this.on('destroy.btn-info', function () {
+               that.buttons.info(false);
+       });
+
+       return this;
+} );
+
+// Get data from the table for export - this is common to a number of plug-in
+// buttons so it is included in the Buttons core library
+DataTable.Api.register( 'buttons.exportData()', function ( options ) {
+       if ( this.context.length ) {
+               return _exportData( new DataTable.Api( this.context[0] ), options );
+       }
+} );
+
+// Get information about the export that is common to many of the export data
+// types (DRY)
+DataTable.Api.register( 'buttons.exportInfo()', function ( conf ) {
+       if ( ! conf ) {
+               conf = {};
+       }
+
+       return {
+               filename: _filename( conf ),
+               title: _title( conf ),
+               messageTop: _message(this, conf.message || conf.messageTop, 'top'),
+               messageBottom: _message(this, conf.messageBottom, 'bottom')
+       };
+} );
+
+
+
+/**
+ * Get the file name for an exported file.
+ *
+ * @param {object}     config Button configuration
+ * @param {boolean} incExtension Include the file name extension
+ */
+var _filename = function ( config )
+{
+       // Backwards compatibility
+       var filename = config.filename === '*' && config.title !== '*' && config.title !== undefined && config.title !== null && config.title !== '' ?
+               config.title :
+               config.filename;
+
+       if ( typeof filename === 'function' ) {
+               filename = filename();
+       }
+
+       if ( filename === undefined || filename === null ) {
+               return null;
+       }
+
+       if ( filename.indexOf( '*' ) !== -1 ) {
+               filename = $.trim( filename.replace( '*', $('head > title').text() ) );
+       }
+
+       // Strip characters which the OS will object to
+       filename = filename.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, "");
+
+       var extension = _stringOrFunction( config.extension );
+       if ( ! extension ) {
+               extension = '';
+       }
+
+       return filename + extension;
+};
+
+/**
+ * Simply utility method to allow parameters to be given as a function
+ *
+ * @param {undefined|string|function} option Option
+ * @return {null|string} Resolved value
+ */
+var _stringOrFunction = function ( option )
+{
+       if ( option === null || option === undefined ) {
+               return null;
+       }
+       else if ( typeof option === 'function' ) {
+               return option();
+       }
+       return option;
+};
+
+/**
+ * Get the title for an exported file.
+ *
+ * @param {object} config      Button configuration
+ */
+var _title = function ( config )
+{
+       var title = _stringOrFunction( config.title );
+
+       return title === null ?
+               null : title.indexOf( '*' ) !== -1 ?
+                       title.replace( '*', $('head > title').text() || 'Exported data' ) :
+                       title;
+};
+
+var _message = function ( dt, option, position )
+{
+       var message = _stringOrFunction( option );
+       if ( message === null ) {
+               return null;
+       }
+
+       var caption = $('caption', dt.table().container()).eq(0);
+       if ( message === '*' ) {
+               var side = caption.css( 'caption-side' );
+               if ( side !== position ) {
+                       return null;
+               }
+
+               return caption.length ?
+                       caption.text() :
+                       '';
+       }
+
+       return message;
+};
+
+
+
+
+
+
+
+var _exportTextarea = $('<textarea/>')[0];
+var _exportData = function ( dt, inOpts )
+{
+       var config = $.extend( true, {}, {
+               rows:           null,
+               columns:        '',
+               modifier:       {
+                       search: 'applied',
+                       order:  'applied'
+               },
+               orthogonal:     'display',
+               stripHtml:      true,
+               stripNewlines:  true,
+               decodeEntities: true,
+               trim:           true,
+               format:         {
+                       header: function ( d ) {
+                               return strip( d );
+                       },
+                       footer: function ( d ) {
+                               return strip( d );
+                       },
+                       body: function ( d ) {
+                               return strip( d );
+                       }
+               },
+               customizeData: null
+       }, inOpts );
+
+       var strip = function ( str ) {
+               if ( typeof str !== 'string' ) {
+                       return str;
+               }
+
+               // Always remove script tags
+               str = str.replace( /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '' );
+
+               // Always remove comments
+               str = str.replace( /<!\-\-.*?\-\->/g, '' );
+
+               if ( config.stripHtml ) {
+                       str = str.replace( /<[^>]*>/g, '' );
+               }
+
+               if ( config.trim ) {
+                       str = str.replace( /^\s+|\s+$/g, '' );
+               }
+
+               if ( config.stripNewlines ) {
+                       str = str.replace( /\n/g, ' ' );
+               }
+
+               if ( config.decodeEntities ) {
+                       _exportTextarea.innerHTML = str;
+                       str = _exportTextarea.value;
+               }
+
+               return str;
+       };
+
+
+       var header = dt.columns( config.columns ).indexes().map( function (idx) {
+               var el = dt.column( idx ).header();
+               return config.format.header( el.innerHTML, idx, el );
+       } ).toArray();
+
+       var footer = dt.table().footer() ?
+               dt.columns( config.columns ).indexes().map( function (idx) {
+                       var el = dt.column( idx ).footer();
+                       return config.format.footer( el ? el.innerHTML : '', idx, el );
+               } ).toArray() :
+               null;
+       
+       // If Select is available on this table, and any rows are selected, limit the export
+       // to the selected rows. If no rows are selected, all rows will be exported. Specify
+       // a `selected` modifier to control directly.
+       var modifier = $.extend( {}, config.modifier );
+       if ( dt.select && typeof dt.select.info === 'function' && modifier.selected === undefined ) {
+               if ( dt.rows( config.rows, $.extend( { selected: true }, modifier ) ).any() ) {
+                       $.extend( modifier, { selected: true } )
+               }
+       }
+
+       var rowIndexes = dt.rows( config.rows, modifier ).indexes().toArray();
+       var selectedCells = dt.cells( rowIndexes, config.columns );
+       var cells = selectedCells
+               .render( config.orthogonal )
+               .toArray();
+       var cellNodes = selectedCells
+               .nodes()
+               .toArray();
+
+       var columns = header.length;
+       var rows = columns > 0 ? cells.length / columns : 0;
+       var body = [];
+       var cellCounter = 0;
+
+       for ( var i=0, ien=rows ; i<ien ; i++ ) {
+               var row = [ columns ];
+
+               for ( var j=0 ; j<columns ; j++ ) {
+                       row[j] = config.format.body( cells[ cellCounter ], i, j, cellNodes[ cellCounter ] );
+                       cellCounter++;
+               }
+
+               body[i] = row;
+       }
+
+       var data = {
+               header: header,
+               footer: footer,
+               body:   body
+       };
+
+       if ( config.customizeData ) {
+               config.customizeData( data );
+       }
+
+       return data;
+};
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables interface
+ */
+
+// Attach to DataTables objects for global access
+$.fn.dataTable.Buttons = Buttons;
+$.fn.DataTable.Buttons = Buttons;
+
+
+
+// DataTables creation - check if the buttons have been defined for this table,
+// they will have been if the `B` option was used in `dom`, otherwise we should
+// create the buttons instance here so they can be inserted into the document
+// using the API. Listen for `init` for compatibility with pre 1.10.10, but to
+// be removed in future.
+$(document).on( 'init.dt plugin-init.dt', function (e, settings) {
+       if ( e.namespace !== 'dt' ) {
+               return;
+       }
+
+       var opts = settings.oInit.buttons || DataTable.defaults.buttons;
+
+       if ( opts && ! settings._buttons ) {
+               new Buttons( settings, opts ).container();
+       }
+} );
+
+function _init ( settings ) {
+       var api = new DataTable.Api( settings );
+       var opts = api.init().buttons || DataTable.defaults.buttons;
+
+       return new Buttons( api, opts ).container();
+}
+
+// DataTables `dom` feature option
+DataTable.ext.feature.push( {
+       fnInit: _init,
+       cFeature: "B"
+} );
+
+// DataTables 2 layout feature
+if ( DataTable.ext.features ) {
+       DataTable.ext.features.register( 'buttons', _init );
+}
+
+
+return Buttons;
+}));
index 4617ccb5d7f9ab8a266a1575a70907aae272de38..2abc57f6f18cd2c40c11cd08e2f7f52b528c9ae0 100644 (file)
@@ -18,6 +18,9 @@
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/misc.js %> />
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/graph.js %> />
                        <com:BClientScript ScriptUrl=<%~ ../JavaScript/statistics.js %> />
+                       <com:BClientScript ScriptUrl=<%~ ../JavaScript/dataTables.buttons.js %> />
+                       <com:BClientScript ScriptUrl=<%~ ../JavaScript/buttons.html5.js %> />
+                       <com:BClientScript ScriptUrl=<%~ ../JavaScript/buttons.colVis.js %> />
                        <!-- Top container -->
                        <div class="w3-bar w3-top w3-black w3-large" style="z-index:4">
                                <button type="button" class="w3-bar-item w3-button w3-hover-none w3-hover-text-light-grey" onclick="W3SideBar.open();"><i class="fa fa-bars"></i>  Menu</button>
diff --git a/gui/baculum/themes/Baculum-v2/css/buttons.dataTables.css b/gui/baculum/themes/Baculum-v2/css/buttons.dataTables.css
new file mode 100644 (file)
index 0000000..f7bbb37
--- /dev/null
@@ -0,0 +1,240 @@
+@keyframes dtb-spinner {
+  100% {
+    transform: rotate(360deg);
+  }
+}
+@-o-keyframes dtb-spinner {
+  100% {
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-ms-keyframes dtb-spinner {
+  100% {
+    -ms-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-webkit-keyframes dtb-spinner {
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-moz-keyframes dtb-spinner {
+  100% {
+    -moz-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+div.dt-button-info {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  width: 400px;
+  margin-top: -100px;
+  margin-left: -200px;
+  background-color: white;
+  border: 2px solid #111;
+  box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.3);
+  border-radius: 3px;
+  text-align: center;
+  z-index: 21;
+}
+div.dt-button-info h2 {
+  padding: 0.5em;
+  margin: 0;
+  font-weight: normal;
+  border-bottom: 1px solid #ddd;
+  background-color: #f3f3f3;
+}
+div.dt-button-info > div {
+  padding: 1em;
+}
+
+div.dt-button-collection-title {
+  text-align: center;
+  padding: 0.3em 0 0.5em;
+  font-size: 0.9em;
+}
+
+div.dt-button-collection-title:empty {
+  display: none;
+}
+
+div.dt-buttons {
+  position: relative;
+  float: left;
+}
+div.dt-buttons .dt-button {
+  margin-right: 0;
+}
+div.dt-buttons .dt-button span.ui-icon {
+  display: inline-block;
+  vertical-align: middle;
+  margin-top: -2px;
+}
+div.dt-buttons .dt-button:active {
+  outline: none;
+}
+div.dt-buttons .dt-button:hover > span {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+div.dt-button-collection {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 150px;
+  margin-top: 3px;
+  padding: 8px 8px 4px 8px;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.4);
+  background-color: #f3f3f3;
+  background-color: rgba(255, 255, 255, 0.3);
+  overflow: hidden;
+  z-index: 2002;
+  border-radius: 5px;
+  box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
+  z-index: 2002;
+  -webkit-column-gap: 0;
+  -moz-column-gap: 0;
+  -ms-column-gap: 0;
+  -o-column-gap: 0;
+  column-gap: 0;
+}
+div.dt-button-collection .dt-button {
+  position: relative;
+  left: 0;
+  right: 0;
+  width: 100%;
+  box-sizing: border-box;
+  display: block;
+  float: none;
+  margin-right: 0;
+  margin-bottom: 4px;
+}
+div.dt-button-collection .dt-button:hover > span {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+div.dt-button-collection.fixed {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  margin-left: -75px;
+  border-radius: 0;
+}
+div.dt-button-collection.fixed.two-column {
+  margin-left: -200px;
+}
+div.dt-button-collection.fixed.three-column {
+  margin-left: -225px;
+}
+div.dt-button-collection.fixed.four-column {
+  margin-left: -300px;
+}
+div.dt-button-collection > :last-child {
+  display: block !important;
+  -webkit-column-gap: 8px;
+  -moz-column-gap: 8px;
+  -ms-column-gap: 8px;
+  -o-column-gap: 8px;
+  column-gap: 8px;
+}
+div.dt-button-collection > :last-child > * {
+  -webkit-column-break-inside: avoid;
+  break-inside: avoid;
+}
+div.dt-button-collection.two-column {
+  width: 400px;
+}
+div.dt-button-collection.two-column > :last-child {
+  padding-bottom: 1px;
+  -webkit-column-count: 2;
+  -moz-column-count: 2;
+  -ms-column-count: 2;
+  -o-column-count: 2;
+  column-count: 2;
+}
+div.dt-button-collection.three-column {
+  width: 450px;
+}
+div.dt-button-collection.three-column > :last-child {
+  padding-bottom: 1px;
+  -webkit-column-count: 3;
+  -moz-column-count: 3;
+  -ms-column-count: 3;
+  -o-column-count: 3;
+  column-count: 3;
+}
+div.dt-button-collection.four-column {
+  width: 600px;
+}
+div.dt-button-collection.four-column > :last-child {
+  padding-bottom: 1px;
+  -webkit-column-count: 4;
+  -moz-column-count: 4;
+  -ms-column-count: 4;
+  -o-column-count: 4;
+  column-count: 4;
+}
+div.dt-button-collection .dt-button {
+  border-radius: 0;
+}
+
+div.dt-button-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.7);
+  /* Fallback */
+  background: -ms-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
+  /* IE10 Consumer Preview */
+  background: -moz-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
+  /* Firefox */
+  background: -o-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
+  /* Opera */
+  background: -webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, rgba(0, 0, 0, 0.3)), color-stop(1, rgba(0, 0, 0, 0.7)));
+  /* Webkit (Safari/Chrome 10) */
+  background: -webkit-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
+  /* Webkit (Chrome 11+) */
+  background: radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
+  /* W3C Markup, IE10 Release Preview */
+  z-index: 2001;
+}
+
+@media screen and (max-width: 640px) {
+  div.dt-buttons {
+    float: none !important;
+    text-align: center;
+  }
+}
+button.dt-button.processing,
+div.dt-button.processing,
+a.dt-button.processing {
+  color: rgba(0, 0, 0, 0.2);
+}
+button.dt-button.processing:after,
+div.dt-button.processing:after,
+a.dt-button.processing:after {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 16px;
+  height: 16px;
+  margin: -8px 0 0 -8px;
+  box-sizing: border-box;
+  display: block;
+  content: ' ';
+  border: 2px solid #282828;
+  border-radius: 50%;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  animation: dtb-spinner 1500ms infinite linear;
+  -o-animation: dtb-spinner 1500ms infinite linear;
+  -ms-animation: dtb-spinner 1500ms infinite linear;
+  -webkit-animation: dtb-spinner 1500ms infinite linear;
+  -moz-animation: dtb-spinner 1500ms infinite linear;
+}