javascriptfunctionbuttondatatablescancel-button

Datatables Buttons filename prompt function not canceling


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

Reproducer

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, &quot;desc&quot; ]]">
<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?


Solution

  • 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.

    Snippet from here

    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:

    1. You can handle the above error using the 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;
        }
    }
    
    1. If you don't want to deal with throwing the 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.