Commit 43dbff64 authored by Leigh B Stoller's avatar Leigh B Stoller

Add Download option at Rob's request, to get raw data. Using the

builtin tablesorter widget and code from their demo page.
parent adc0b305
/*! Widget: output - updated 3/26/2015 (v2.21.3) *//*
* 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\x09\x0d\x0a]|<[^<]+>)/, // test if cell needs wrapping quotes
regexBR : /(<br([\s\/])?>|\n)/g, // replace
regexIMG : /<img[^>]+alt\s*=\s*['"]([^'"]+)['"][^>]*>/i, // match
regexHTML : /<[^<]+>/g, // replace
replaceCR : '\x0d\x0a',
replaceTab : '\x09',
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, unless option is set
if ( !wo.output_hiddenColumns && $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');
if (wo.output_includeFooter) {
// clone, to force the tfoot rows to the end of this selection of rows
// otherwise they appear after the thead (the order in the HTML)
$rows = $rows.add( $el.children('tfoot').children('tr').clone() );
}
// 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/i.test( wo.output_delivery || '' ) ) {
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
if (!wo.output_trimSpaces) {
result = result.replace(output.regexBR, output.replaceCR).replace(/\t/g, output.replaceTab);
} else {
result = result.replace(output.regexBR, '');
}
// 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(
'<html><head><title>' + output.popupTitle + '</title></head><body>' +
'<textarea wrap="' + (wrap ? 'on' : 'off') + '" style="' + output.popupStyle + '">' + data + '\n</textarea>' +
'</body></html>'
);
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
// & http://html5-demos.appspot.com/static/a.download.html
download : function (wo, data){
var e, blob, gotBlob,
nav = window.navigator,
link = document.createElement('a');
// iOS devices do not support downloading. We have to inform user about this.
if (/(iP)/g.test(nav.userAgent)) {
alert(output.message);
return false;
}
// test for blob support
try {
gotBlob = !!new Blob();
} catch (err) {
gotBlob = false;
}
// Use HTML5 Blob if browser supports it
if ( gotBlob ) {
window.URL = window.URL || window.webkitURL;
// prepend BOM for utf-8 encoding - see https://github.com/eligrey/FileSaver.js/blob/master/FileSaver.js#L140
blob = new Blob( [ '\ufeff', data ], { type: wo.output_encoding } );
if (nav.msSaveBlob) {
// IE 10+
nav.msSaveBlob(blob, wo.output_saveFileName);
} else {
// all other browsers
link.href = window.URL.createObjectURL(blob);
link.download = wo.output_saveFileName;
// Dispatching click event; using $(link).trigger() won't work
if (document.createEvent) {
e = document.createEvent('MouseEvents');
// event.initMouseEvent(type, canBubble, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
link.dispatchEvent(e);
}
}
return false;
}
// fallback to force file download (whether supported by server).
// not sure if this actually works in IE9 and older...
window.open( wo.output_encoding + encodeURIComponent(data) + '?download' , '_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_hiddenColumns : false, // include hidden columns in the output
output_includeFooter : false, // include footer rows in the output
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) + ')'; },
// the need to modify this for Excel no longer exists
output_encoding : 'data:application/octet-stream;charset=utf8,'
},
init: function(table, thisWidget, c) {
output.init(c);
},
remove: function(table, c){
output.remove(c);
}
});
})(jQuery);
require(window.APT_OPTIONS.configObject,
['js/quickvm_sup', 'moment'],
function (sup, moment)
['js/quickvm_sup', 'moment',
'js/lib/text!template/output-dropdown.html'],
function (sup, moment, dropdownString)
{
'use strict';
......@@ -16,6 +17,7 @@ function (sup, moment)
if (window.MAX) {
default_max = new Date(window.MAX * 1000);
}
$('#output_dropdown').html(dropdownString);
// Format dates with moment before display.
$('.format-date').each(function() {
......@@ -31,8 +33,7 @@ function (sup, moment)
defaultValues: {min: default_min, max: default_max},
arrows: false,
});
InitTable("uid");
InitTable("pid");
InitTable("sumstats");
// Handler for the date range search button.
$('#slider-go-button').click(function() {
......@@ -48,6 +49,7 @@ function (sup, moment)
{
var tablename = "#tablesorter_" + name;
var searchname = "#search_" + name;
var $this = $('#output_dropdown');
var table = $(tablename)
.tablesorter({
......@@ -56,7 +58,7 @@ function (sup, moment)
//cssChildRow: "tablesorter-childRow",
// initialize zebra and filter widgets
widgets: ["zebra", "filter", "resizable", "math"],
widgets: ["zebra", "filter", "resizable", "math", "output"],
widgetOptions: {
// include child row content while filtering, if true
......@@ -78,6 +80,49 @@ function (sup, moment)
math_ignore : [0],
// integers
math_mask : '',
// ',' 'json', 'array' or separator (e.g. ',')
output_separator : ',',
// columns to ignore [0, 1,... ] (zero-based index)
output_ignoreColumns : [],
// include hidden columns in the output
output_hiddenColumns : false,
// include footer rows in the output
output_includeFooter : true,
// data-attribute containing alternate cell text
output_dataAttrib : 'data-name',
// output all header rows (multiple rows)
output_headerRows : true,
// (p)opup, (d)ownload
output_delivery : 'p',
// (a)ll, (f)iltered or (v)isible
output_saveRows : 'f',
// duplicate output data in tbody colspan/rowspan
output_duplicateSpans: true,
// change quote to left double quote
output_replaceQuote : '\u201c;',
// output includes all cell HTML (except header cells)
output_includeHTML : true,
// remove extra white-space characters (trim)
output_trimSpaces : false,
// wrap every cell output in quotes
output_wrapQuotes : false,
output_popupStyle : 'width=580,height=310',
output_saveFileName : 'mytable.csv',
// callbackJSON used when outputting JSON &
// any header cells has a colspan - unique
// names required
output_callbackJSON : function($cell,txt,cellIndex) {
return txt + '(' + cellIndex + ')'; },
// 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; },
output_encoding :
'data:application/octet-stream;charset=utf8,'
}
});
// Target the $('.search') input using built in functioning
......@@ -85,6 +130,57 @@ function (sup, moment)
// Allows using filter_liveSearch or delayed search &
// pressing escape to cancel the search
$.tablesorter.filter.bindSearch(table, $(searchname));
//
// All this output stuff from the example page.
//
$this.find('.dropdown-toggle').click(function(e){
// this is needed because clicking inside the dropdown will close
// the menu with only bootstrap controlling it.
$this.find('.dropdown-menu').toggle();
return false;
});
// make separator & replace quotes buttons update the value
$this.find('.output-separator').click(function(){
$this.find('.output-separator').removeClass('active');
var txt = $(this).addClass('active').html()
$this.find('.output-separator-input').val( txt );
$this.find('.output-filename').val(function(i, v){
// change filename extension based on separator
var filetype = (txt === 'json' || txt === 'array') ? 'js' :
txt === ',' ? 'csv' : 'txt';
return v.replace(/\.\w+$/, '.' + filetype);
});
return false;
});
$this.find('.output-quotes').click(function(){
$this.find('.output-quotes').removeClass('active');
$this.find('.output-replacequotes')
.val( $(this).addClass('active').text() );
return false;
});
// clicking the download button; all you really need is to
// trigger an "output" event on the table
$this.find('.download').click(function(){
var typ,
wo = table[0].config.widgetOptions;
var saved = $this.find('.output-filter-all :checked').attr('class');
wo.output_separator = $this.find('.output-separator-input').val();
wo.output_delivery =
$this.find('.output-download-popup :checked')
.attr('class') === "output-download" ? 'd' : 'p';
wo.output_saveRows = saved === "output-filter" ? 'f' :
saved === 'output-visible' ? 'v' : 'a';
wo.output_replaceQuote = $this.find('.output-replacequotes').val();
wo.output_trimSpaces = $this.find('.output-trim').is(':checked');
wo.output_includeHTML = $this.find('.output-html').is(':checked');
wo.output_wrapQuotes = $this.find('.output-wrap').is(':checked');
wo.output_headerRows = $this.find('.output-headers').is(':checked');
wo.output_saveFileName = $this.find('.output-filename').val();
table.trigger('outputTable');
return false;
});
}
$(document).ready(initialize);
......
......@@ -116,9 +116,10 @@ function ShowByCreator()
$uid_array[$uid][$cluster]["pcount"] = $pcount;
$uid_array[$uid][$cluster]["phours"] = $phours;
}
echo "<div id='output_dropdown'></div>\n";
echo "<input class='form-control search' type='search' data-column='0'
id='search_uid' placeholder='Search'>\n";
echo " <table class='tablesorter' id='tablesorter_uid'>
id='search_sumstats' placeholder='Search'>\n";
echo " <table class='tablesorter' id='tablesorter_sumstats'>
<thead>
<tr>
<th rowspan=2>UID</th>
......@@ -285,9 +286,10 @@ function ShowByProject()
$pid_array[$pid][$cluster]["pcount"] = $pcount;
$pid_array[$pid][$cluster]["phours"] = $phours;
}
echo "<div id='output_dropdown'></div>\n";
echo "<input class='form-control search' type='search' data-column='0'
id='search_pid' placeholder='Search'>\n";
echo " <table class='tablesorter' id='tablesorter_pid'>
id='search_sumstats' placeholder='Search'>\n";
echo " <table class='tablesorter' id='tablesorter_sumstats'>
<thead>
<tr>
<th rowspan=2>PID</th>
......@@ -443,6 +445,7 @@ echo "<script src='js/lib/jQRangeSlider/jQRuler.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.min.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.widgets.min.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.widget-math.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.widget-output.js'></script>\n";
echo "<script src='js/lib/bootstrap.js'></script>\n";
echo "<script src='js/lib/require.js' data-main='js/sumstats'></script>\n";
......
<style>
#output-dropdown {
min-width: 280px;
font-size: 12px;
padding: 10px;
}
#output-dropdown h5 {
text-align: right;
margin-right: 4px;
}
div.output-download-popup {
margin-bottom: 5px;
}
</style>
<!-- Split button -->
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default btn-sm download">Download</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" id='output-dropdown' role="menu">
<li><h5><strong>Output options</strong></h5></li>
<li>
<label>Separator: <input class="output-separator-input" type="text" size="2" value=","></label>
<button type="button" class="output-separator btn btn-default btn-xs active" title="comma">,</button>
<button type="button" class="output-separator btn btn-default btn-xs" title="semi-colon">;</button>
<button type="button" class="output-separator btn btn-default btn-xs" title="tab"> </button>
<button type="button" class="output-separator btn btn-default btn-xs" title="space"> </button>
<button type="button" class="output-separator btn btn-default btn-xs" title="output JSON">json</button>
<button type="button" class="output-separator btn btn-default btn-xs" title="output Array (see note)">array</button>
</li>
<li>
<div class="btn-group output-download-popup" data-toggle="buttons" title="Download file or open in Popup window">
<label class="btn btn-default btn-sm active">
<input type="radio" name="delivery1" class="output-popup" checked=""> Popup
</label>
<label class="btn btn-default btn-sm">
<input type="radio" name="delivery1" class="output-download"> Download
</label>
</div>
</li>
<li>
<div class="btn-group output-filter-all" data-toggle="buttons" title="Output only filtered, visible or all rows">
<label class="btn btn-default btn-sm active">
<input type="radio" name="getrows1" class="output-filter" checked="checked"> Filtered
</label>
<label class="btn btn-default btn-sm">
<input type="radio" name="getrows1" class="output-visible"> Visible
</label>
<label class="btn btn-default btn-sm">
<input type="radio" name="getrows1" class="output-all"> All
</label>
</div>
</li>
<li class="divider"></li>
<li>
<label>Replace quotes: <input class="output-replacequotes" type="text" size="2" value="'"></label>
<button type="button" class="output-quotes btn btn-default btn-xs active" title="single quote">'</button>
<button type="button" class="output-quotes btn btn-default btn-xs" title="left double quote">?</button>
<button type="button" class="output-quotes btn btn-default btn-xs" title="escaped quote">\"</button>
</li>
<li><label title="Remove extra white space from each cell">Trim spaces: <input class="output-trim" type="checkbox" checked=""></label></li>
<li><label title="Include HTML from cells in output">Include HTML: <input class="output-html" type="checkbox"></label></li>
<li><label title="Wrap all values in quotes">Wrap in Quotes: <input class="output-wrap" type="checkbox"></label></li>
<li><label title="Choose a download filename">Filename: <input class="output-filename" type="text" size="15" value="mytable.csv"></label></li>
</ul>
</div>
</div>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment