/* Output widget (beta) for TableSorter 5/5/2014 (v2.16.4) * Requires tablesorter v2.8+ and jQuery 1.7+ * Modified from: * HTML Table to CSV: http://www.kunalbabre.com/projects/table2CSV.php (License unknown?) * Download-File-JS: https://github.com/PixelsCommander/Download-File-JS (http://www.apache.org/licenses/LICENSE-2.0) */ /*jshint browser:true, jquery:true, unused:false */ /*global jQuery: false */ ;(function($){ "use strict"; var ts = $.tablesorter, output = ts.output = { event : 'outputTable', // wrap line breaks & tabs in quotes regexQuote : /([\n\t]|<[^<]+>)/, // test regexBR : /(|\n)/g, // replace regexIMG : /]+alt\s*=\s*['"]([^'"]+)['"][^>]*>/i, // match regexHTML : /<[^<]+>/g, // replace replaceCR : '\\n', replaceTab : '\\t', popupTitle : 'Output', popupStyle : 'width:100%;height:100%;', // for textarea message : 'Your device does not support downloading. Please try again in desktop browser.', init : function(c) { c.$table .off(output.event) .on(output.event, function(){ // explicitly use table.config.widgetOptions because we want // the most up-to-date values; not the "wo" from initialization output.process(c, c.widgetOptions); }); }, processRow: function(c, $rows, isHeader, isJSON) { var $this, row, col, rowlen, collen, txt, wo = c.widgetOptions, tmpRow = [], dupe = wo.output_duplicateSpans, addSpanIndex = isHeader && isJSON && wo.output_headerRows && $.isFunction(wo.output_callbackJSON), cellIndex = 0; $rows.each(function(rowIndex) { if (!tmpRow[rowIndex]) { tmpRow[rowIndex] = []; } cellIndex = 0; $(this).children().each(function(){ $this = $(this); // process rowspans if ($this.filter('[rowspan]').length) { rowlen = parseInt( $this.attr('rowspan'), 10) - 1; txt = output.formatData( wo, $this.attr(wo.output_dataAttrib) || $this.html(), isHeader ); for (row = 1; row <= rowlen; row++) { if (!tmpRow[rowIndex + row]) { tmpRow[rowIndex + row] = []; } tmpRow[rowIndex + row][cellIndex] = isHeader ? txt : dupe ? txt : ''; } } // process colspans if ($this.filter('[colspan]').length) { collen = parseInt( $this.attr('colspan'), 10) - 1; txt = output.formatData( wo, $this.attr(wo.output_dataAttrib) || $this.html(), isHeader ); for (col = 1; col <= collen; col++) { // if we're processing the header & making JSON, the header names need to be unique if ($this.filter('[rowspan]').length) { rowlen = parseInt( $this.attr('rowspan'), 10); for (row = 0; row < rowlen; row++) { if (!tmpRow[rowIndex + row]) { tmpRow[rowIndex + row] = []; } tmpRow[rowIndex + row][cellIndex + col] = addSpanIndex ? wo.output_callbackJSON($this, txt, cellIndex + col) || txt + '(' + (cellIndex + col) + ')' : isHeader ? txt : dupe ? txt : ''; } } else { tmpRow[rowIndex][cellIndex + col] = addSpanIndex ? wo.output_callbackJSON($this, txt, cellIndex + col) || txt + '(' + (cellIndex + col) + ')' : isHeader ? txt : dupe ? txt : ''; } } } // don't include hidden columns if ( $this.css('display') !== 'none' ) { // skip column if already defined while (typeof tmpRow[rowIndex][cellIndex] !== 'undefined') { cellIndex++; } tmpRow[rowIndex][cellIndex] = tmpRow[rowIndex][cellIndex] || output.formatData( wo, $this.attr(wo.output_dataAttrib) || $this.html(), isHeader ); cellIndex++; } }); }); return tmpRow; }, ignoreColumns : function(wo, data) { // ignore columns -> remove data from built array (because we've already processed any rowspan/colspan) $.each( data, function(indx, val){ data[indx] = $.grep(val, function(v, cellIndx){ return $.inArray(cellIndx, wo.output_ignoreColumns) < 0; }); }); return data; }, process : function(c, wo) { var mydata, $this, $rows, headers, csvData, len, hasStringify = window.JSON && JSON.hasOwnProperty('stringify'), indx = 0, tmpData = (wo.output_separator || ',').toLowerCase(), outputJSON = tmpData === 'json', outputArray = tmpData === 'array', separator = outputJSON || outputArray ? ',' : wo.output_separator, $el = c.$table; // regex to look for the set separator or HTML wo.output_regex = new RegExp('(' + (/\\/.test(separator) ? '\\' : '' ) + separator + ')' ); // get header cells $this = $el.find('thead tr:visible').not('.' + (ts.css.filterRow || 'tablesorter-filter-row') ); headers = output.processRow(c, $this, true, outputJSON); // all tbody rows $rows = $el.children('tbody').children('tr'); // get (f)iltered, (v)isible or all rows (look for the first letter only) $rows = /f/.test(wo.output_saveRows) ? $rows.not('.' + (wo.filter_filteredRow || 'filtered') ) : /v/.test(wo.output_saveRows) ? $rows.filter(':visible') : $rows; // process to array of arrays csvData = output.processRow(c, $rows); len = headers.length; if (wo.output_ignoreColumns.length) { headers = output.ignoreColumns(wo, headers); csvData = output.ignoreColumns(wo, csvData); } if (outputJSON) { tmpData = []; $.each( csvData, function(indx, val){ // multiple header rows & output_headerRows = true, pick the last row... tmpData.push( output.row2Hash( headers[ (len > 1 && wo.output_headerRows) ? indx % len : len - 1], val ) ); }); // requires JSON stringify; if it doesn't exist, the output will show [object Object],... in the output window mydata = hasStringify ? JSON.stringify(tmpData) : tmpData; } else { tmpData = output.row2CSV(wo, wo.output_headerRows ? headers : [ headers[ (len > 1 && wo.output_headerRows) ? indx % len : len - 1] ], outputArray) .concat( output.row2CSV(wo, csvData, outputArray) ); // stringify the array; if stringify doesn't exist the array will be flattened mydata = outputArray && hasStringify ? JSON.stringify(tmpData) : tmpData.join('\n'); } // callback; if true returned, continue processing if ($.isFunction(wo.output_callback) && !wo.output_callback(c, mydata)) { return; } if ( /p/.test( (wo.output_delivery || '').toLowerCase() ) ) { output.popup(mydata, wo.output_popupStyle, outputJSON || outputArray); } else { output.download(wo, mydata); } }, // end process row2CSV : function(wo, tmpRow, outputArray) { var tmp, rowIndex, csvData = [], rowLen = tmpRow.length; for (rowIndex = 0; rowIndex < rowLen; rowIndex++) { // remove any blank rows tmp = tmpRow[rowIndex].join('').replace(/\"/g,''); if (tmpRow[rowIndex].length > 0 && tmp !== '') { csvData[csvData.length] = outputArray ? tmpRow[rowIndex] : tmpRow[rowIndex].join(wo.output_separator); } } return csvData; }, row2Hash : function(keys, values) { var json = {}; $.each(values, function(indx, val) { if ( indx < keys.length ) { json[ keys[indx] ] = val; } }); return json; }, formatData : function(wo, input, isHeader) { var txt, quotes = (wo.output_separator || ',').toLowerCase(), separator = quotes === 'json' || quotes === 'array', // replace " with “ if undefined result = input.replace(/\"/g, wo.output_replaceQuote || '\u201c'); // replace line breaks with \\n & tabs with \\t result = result.replace(output.regexBR, output.replaceCR).replace(/\t/g, output.replaceTab); // extract img alt text txt = result.match(output.regexIMG); if (!wo.output_includeHTML && txt !== null) { result = txt[1]; } // replace/remove html result = wo.output_includeHTML && !isHeader ? result : result.replace(output.regexHTML, ''); result = wo.output_trimSpaces || isHeader ? $.trim(result) : result; // JSON & array outputs don't need quotes quotes = separator ? false : wo.output_wrapQuotes || wo.output_regex.test(result) || output.regexQuote.test(result); return quotes ? '"' + result + '"' : result; }, popup : function(data, style, wrap) { var generator = window.open('', output.popupTitle, style); generator.document.write( '' + output.popupTitle + '' + '' + '' ); generator.document.close(); generator.focus(); // select all text and focus within the textarea in the popup // $(generator.document).find('textarea').select().focus(); return true; }, // modified from https://github.com/PixelsCommander/Download-File-JS download : function (wo, data){ var e, link, processedData = wo.output_encoding + encodeURIComponent(data); // iOS devices do not support downloading. We have to inform user about this. if (/(iP)/g.test(navigator.userAgent)) { alert(output.message); return false; } // If in Chrome or Safari - download via virtual link click if ( /(chrome|safari)/.test(navigator.userAgent.toLowerCase()) ) { // Creating new link node. link = document.createElement('a'); link.href = processedData; link.download = wo.output_saveFileName; // Dispatching click event. if (document.createEvent) { e = document.createEvent('MouseEvents'); e.initEvent('click', true, true); link.dispatchEvent(e); return true; } } // Force file download (whether supported by server). processedData += '?download'; window.open(processedData, '_self'); return true; }, remove : function(c) { c.$table.off(output.event); } }; ts.addWidget({ id: "output", options: { output_separator : ',', // set to "json", "array" or any separator output_ignoreColumns : [], // columns to ignore [0, 1,... ] (zero-based index) output_dataAttrib : 'data-name', // header attrib containing modified header name output_headerRows : false, // if true, include multiple header rows (JSON only) output_delivery : 'popup', // popup, download output_saveRows : 'filtered', // all, visible or filtered output_duplicateSpans: true, // duplicate output data in tbody colspan/rowspan output_replaceQuote : '\u201c;', // left double quote output_includeHTML : false, output_trimSpaces : true, output_wrapQuotes : false, output_popupStyle : 'width=500,height=300', output_saveFileName : 'mytable.csv', // callback executed when processing completes // return true to continue download/output // return false to stop delivery & do something else with the data output_callback : function(config, data){ return true; }, // JSON callback executed when a colspan is encountered in the header output_callbackJSON : function($cell, txt, cellIndex) { return txt + '(' + (cellIndex) + ')'; }, // output data type (with BOM or Windows-1252 is needed for excel) // NO BOM : 'data:text/csv;charset=utf8,' // With BOM : 'data:text/csv;charset=utf8,%EF%BB%BF' // WIN 1252 : 'data:text/csv;charset=windows-1252' output_encoding : 'data:text/csv;charset=utf8,' }, init: function(table, thisWidget, c) { output.init(c); }, remove: function(table, c){ output.remove(c); } }); })(jQuery);