I have a page that allows the user to download a dynamically-generated file. It takes a long time to generate, so I\'d like to show a \"waiting\" indicator. The problem is,
If you have download a file, which is saved, as opposed to being in the document, there's no way to determine when the download is complete, since it is not in the scope of the current document, but a separate process in the browser.
I just had this exact same problem. My solution was to use temporary files since I was generating a bunch of temporary files already. The form is submitted with:
var microBox = {
show : function(content) {
$(document.body).append('<div id="microBox_overlay"></div><div id="microBox_window"><div id="microBox_frame"><div id="microBox">' +
content + '</div></div></div>');
return $('#microBox_overlay');
},
close : function() {
$('#microBox_overlay').remove();
$('#microBox_window').remove();
}
};
$.fn.bgForm = function(content, callback) {
// Create an iframe as target of form submit
var id = 'bgForm' + (new Date().getTime());
var $iframe = $('<iframe id="' + id + '" name="' + id + '" style="display: none;" src="about:blank"></iframe>')
.appendTo(document.body);
var $form = this;
// Submittal to an iframe target prevents page refresh
$form.attr('target', id);
// The first load event is called when about:blank is loaded
$iframe.one('load', function() {
// Attach listener to load events that occur after successful form submittal
$iframe.load(function() {
microBox.close();
if (typeof(callback) == 'function') {
var iframe = $iframe[0];
var doc = iframe.contentWindow.document;
var data = doc.body.innerHTML;
callback(data);
}
});
});
this.submit(function() {
microBox.show(content);
});
return this;
};
$('#myForm').bgForm('Please wait...');
At the end of the script that generates the file I have:
header('Refresh: 0;url=fetch.php?token=' . $token);
echo '<html></html>';
This will cause the load event on the iframe to be fired. Then the wait message is closed and the file download will then start. Tested on IE7 and Firefox.
A very simple (and lame) one line solution is to use the window.onblur()
event to close the loading dialog. Of course, if it takes too long and the user decides to do something else (like reading emails) the loading dialog will close.
The core problem is that the web browser does not have an event that fires when page navigation is cancelled but does have an event that fires when a page completes loading. Anything outside of a direct browser event is going to be a hack with pros and cons.
There are four known approaches to dealing with detecting when a browser download starts:
a
tag with a download
attribute, and trigger a click event. Modern web browsers will then offer the user the option to save the already retrieved file. There are several downsides with this approach:load
event if a page loads in the iframe instead of starting a download but it does not fire any events if the download starts. Setting a cookie with the web server can then be detected by Javascript in a loop. There are several downsides with this approach:load
event on the iframe happens when the HTML document loads. There are several downsides with this approach:load
event on the iframe fires, an error has occurred, abort the XHR request, and remove the iframe. If a XHR progress
event fires, then downloading has probably started in the iframe, abort the XHR request, wait a few seconds, and then remove the iframe. This allows for larger files to be downloaded without relying on a server-side cookie. There are several downsides with this approach:Without an appropriate built-in web browser event, there aren't any perfect solutions here. However, one of the four methods above will likely be a better fit than the others depending on your use-case.
Whenever possible, stream responses to the client on the fly instead of generating everything first on the server and then sending the response. Various file formats can be streamed such as CSV, JSON, XML, ZIP, etc. It really depends on finding a library that supports streaming content. When streaming the response as soon as the request starts, detecting the start of the download won't matter as much because it will start almost right away.
Another option is to just output the download headers up front instead of waiting for all of the content to be generated first. Then generate the content and finally start sending to the client. The user's built-in downloader will patiently wait until the data starts arriving. The downside is that the underlying network connection could timeout waiting for data to start flowing (either on the client or server side).
i use the following to download blobs and revoke the object-url after the download. it works in chrome and firefox!
function download(blob){
var url = URL.createObjectURL(blob);
console.log('create ' + url);
window.addEventListener('focus', window_focus, false);
function window_focus(){
window.removeEventListener('focus', window_focus, false);
URL.revokeObjectURL(url);
console.log('revoke ' + url);
}
location.href = url;
}
after the file download dialog is closed, the window gets her focus back so the focus event is triggered.
This Java/Spring example detects the end of a Download, at which point it hides the "Loading..." indicator.
Approach: On the JS side, set a Cookie with a Max Expiration Age of 2 min, and poll every second for cookie expiration. Then the server-side overrides this cookie with an earlier expiration age -- the completion of the server process. As soon as the cookie expiration is detected in the JS polling, "Loading..." is hidden.
JS Side
function buttonClick() { // Suppose this is the handler for the button that starts
$("#loadingProgressOverlay").show(); // show loading animation
startDownloadChecker("loadingProgressOverlay", 120);
// Here you launch the download URL...
window.location.href = "myapp.com/myapp/download";
}
// This JS function detects the end of a download.
// It does timed polling for a non-expired Cookie, initially set on the
// client-side with a default max age of 2 min.,
// but then overridden on the server-side with an *earlier* expiration age
// (the completion of the server operation) and sent in the response.
// Either the JS timer detects the expired cookie earlier than 2 min.
// (coming from the server), or the initial JS-created cookie expires after 2 min.
function startDownloadChecker(imageId, timeout) {
var cookieName = "ServerProcessCompleteChecker"; // Name of the cookie which is set and later overridden on the server
var downloadTimer = 0; // reference to timer object
// The cookie is initially set on the client-side with a specified default timeout age (2 min. in our application)
// It will be overridden on the server side with a new (earlier) expiration age (the completion of the server operation),
// or auto-expire after 2 min.
setCookie(cookieName, 0, timeout);
// set timer to check for cookie every second
downloadTimer = window.setInterval(function () {
var cookie = getCookie(cookieName);
// If cookie expired (NOTE: this is equivalent to cookie "doesn't exist"), then clear "Loading..." and stop polling
if ((typeof cookie === 'undefined')) {
$("#" + imageId).hide();
window.clearInterval(downloadTimer);
}
}, 1000); // Every second
}
// These are helper JS functions for setting and retrieving a Cookie
function setCookie(name, value, expiresInSeconds) {
var exdate = new Date();
exdate.setTime(exdate.getTime() + expiresInSeconds * 1000);
var c_value = escape(value) + ((expiresInSeconds == null) ? "" : "; expires=" + exdate.toUTCString());
document.cookie = name + "=" + c_value + '; path=/';
}
function getCookie(name) {
var parts = document.cookie.split(name + "=");
if (parts.length == 2 ) {
return parts.pop().split(";").shift();
}
}
Java/Spring Server Side
@RequestMapping("/download")
public String download(HttpServletRequest request, HttpServletResponse response) throws Exception {
//... Some logic for downloading, returning a result ...
// Create a Cookie that will override the JS-created Max-Age-2min Cookie
// with an earlier expiration (same name)
Cookie myCookie = new Cookie("ServerProcessCompleteChecker", "-1");
myCookie.setMaxAge(0); // this is immediate expiration,
// but can also add +3 sec. for any flushing concerns
myCookie.setPath("/");
response.addCookie(myCookie);
//... -- presumably the download is writing to the Output Stream...
return null;
}