I'm using DataTables and I'm adding buttons to download the table. Since I have multiple buttons (csv, excel, etc.) and I want more than one of them to prompt for a file name with a proposed value, I want to use a common function for each to stay DRY.
When I use the code from this answer with an anonymous function, it works as I expect.
filename: function(){
const filename = prompt("Please enter your file name", "");
if (filename != null) {
return filename;
}
}
But when I move that to a named function, everything works except that when I cancel the prompt dialog, the file is downloaded with a default name download.csv
. I want it to cancel the prompt and the download.
function propose_filename () {
const date = new Date()
const proposed = $(document).prop('title') + " " + date.toISOString()
const filename = prompt("Please enter your file name", proposed)
if (filename != null) {
return filename;
}
}
And I'm using it here:
layout: {
bottomStart: {
buttons: [
{
extend: 'csv',
filename: propose_filename
},
{
extend: 'excel',
filename: propose_filename
},
]
}
}
For demonstration, these JSFiddles use a different table, etc., but have my propose_filename
function. Both the anonymous function and the named function work correctly. One notable difference between these and my non-working code is that the JSFiddle uses the dom
feature of DataTables which is deprecated while I'm using the layout
feature. Since I tried to make it use layout
without any luck it may imply that JSFiddle is using DataTables 1 (and I don't know how to tell or change it. My code is using DataTables 2 and the layout
option.
The Excel button is the one that prompts for a filename.
JSFiddle Anonymous function
JSFiddle Named function
The snippet is sandboxed so Chrome and Safari block downloads.
function propose_filename () {
const date = new Date()
const proposed = $(document).prop('title') + " " + date.toISOString()
const filename = prompt("Please enter your file name", proposed)
if (filename != null) {
return filename;
}
}
$(document).ready(function() {
table = $("#MainTable").DataTable({
columnDefs: [
{ type: "natural", target: 0 },
{ orderSequence: ["asc", "desc"], targets: "_all" },
],
order: [[2, "desc"]],
paging: false,
language: {searchPlaceholder: "regex"},
search: {regex: true, smart: false},
scrollY: "65vh",
layout: {
bottomStart: {
buttons: [
{
extend: 'csv',
filename: propose_filename
},
{
extend: 'excel',
filename: propose_filename
},
]
}
}
});
table
.columns()
.every(function () {
const column = this;
const title = column.footer().textContent;
$('<input type="text" placeholder="Filter ' + title + '" />')
.appendTo($(column.footer()).empty())
.on("keyup change clear", function () {
if (column.search() !== this.value) {
column.search(this.value).draw();
}
});
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Stack Overflow Button Test</title>
<link rel="stylesheet" href="https://cdn.datatables.net/2.2.1/css/dataTables.dataTables.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js"></script>
<script src="https://cdn.datatables.net/plug-ins/2.2.1/sorting/natural.js"></script>
<script src="https://cdn.datatables.net/buttons/3.2.0/js/buttons.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.2.0/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.2.0/js/buttons.html5.min.js"></script>
<script src="https://stuk.github.io/jszip/dist/jszip.min.js"></script>
</head>
<body>
<h1>Stack Overflow Button Test</h1>
<div class="centerdiv">
<table id="MainTable" class="display compact data-order="[[ 2, "desc" ]]">
<thead>
<tr><th>SKU</th><th>Description</th><th>Qty</th></tr>
</thead>
<tfoot>
<tr>
<th>SKU<input />
</th>
<th>Description<input />
</th>
<th>Qty<input />
</th>
</tr>
</tfoot>
<tbody>
<tr>
<td>A</td>
<td>Red Bike</td>
<td>200</td>
</tr>
<tr>
<td>B</td>
<td>Blue Wagon</td>
<td>100</td>
</tr>
<tr>
<td>C</td>
<td>Green Trike</td>
<td>82</td>
</tr>
<tr>
<td>D</td>
<td>Yellow Cart</td>
<td>53</td>
</tr>
</tbody>
</table>
</div>
<script src="stackoverflow_test.js"></script>
</body>
</html>
I did some additional testing by putting the files immediately above on my local system and testing them using Chrome, Opera, Thunderbird and Safari with a local URL file:///Users/dennis/tmp/index.html
and got the same behavior in each. Canceling the filename dialog caused the file to be downloaded anyway.
How can I make the cancel flow all the way through?
This is what I understood from your question:
You want to prompt for the filename and if the user cancels the prompt, then you want to cancel the download.
This is not the expected behaviour from the filename
option. The jsfiddle demos(Demo - Anonymous function, Demo - Named function) you included appear to behave like this. But if you actually check the console, an error is thrown when the user cancels the prompt. This is becuase the Datatables
did not get the proper string filename.
The Buttons extension version you have included in the demos is 1.2.4. So when the user cancels the prompt, filename
is returned as null
and an error is thrown at this line:
if ( filename.indexOf( '*' ) !== -1 ) {
filename = $.trim( filename.replace( '*', $('title').text() ) );
}
In the new versions of Buttons extension (3.2.0 - which you have included in your Reproducer Demo), null
is returned if the user defined function returns null
or undefined
for the filename
. So the Datatables will use the default value for the filename
and continue to download it, even after the user cancels the filename prompt.
if (filename === undefined || filename === null) {
return null;
}
So how to cancel the file download, when the user cancels the filename prompt?
One option is you can throw your own exception.
function propose_filename () {
const date = new Date()
const proposed = $(document).prop('title') + " " + date.toISOString()
const filename = prompt("Please enter your file name", proposed);
if (filename != null) {
return filename;
}
throw new Error('File cannot be downloaded!');
}
Here is the working demo. You can see that it is working both as named function and anonymous function.
Update based on the comment: I would like to avoid leaving an uncaught exception.
Two solutions:
window.onerror
:window.onerror = (a, b, c, d, e) => {
if (e.message == 'File cannot be downloaded!') {
// Handle filename error
console.log('File not downloaded!')
return true;
}
}
Errors
, then there is an another option. You can override the default button action
and implement your own action
. Here you can call the function to get the filename and after getting the filename you can call the default action to export the file.Example for the Excle HTML5 button:
// Excel HTML5 export button
{
text: 'Excel - Custom Action',
extend: "excelHtml5",
exportOptions: { orthogonal: "export" },
action: function (e, dt, node, config, cb) {
// Do custom processing
let filename = propose_filename_without_error();
if (typeof filename === 'string') {
// Call the default excelHtml5 action method to create the CSV file
DataTable.ext.buttons.excelHtml5.action.call(this, e, dt, node, config, cb);
} else {
console.log('File not downloaded!')
}
}
}
I've updated the demo with the above code.