Opening and closing a new tab when downloading

skittleys picture skittleys · Dec 4, 2012 · Viewed 37.4k times · Source

On many websites (Dropbox being a good example), when you click on a document to download it, it opens a new window/tab, then the download prompt appears, and the tab/window immediately closes itself (while the prompt remains open).

How do I replicate this behavior using javascript?

I think one approach would be to detect the appearance of that download prompt, then use window.close(). However, I'm not sure how to detect that particular prompt.

A cross-browser solution is preferred, but anything that'll work in Firefox is greatly appreciated.

Clarifications

  1. I'm doing this in a Greasemonkey script
  2. The "link" that opens in the new tab is not necessarily a direct link to the document. Sometimes it is a simple page that starts the download in the background. I'm referring to the type of sites that have their special "download" page...the ones that say something like "your download will begin shortly, if it doesn't start, please click here".

More clarification: With regards to the type of website mentioned in clarification 2 above, what I want to do is click on the download link, have that particular download page load in a new window, and have the window close once the download has been initiated.

Answer

Brock Adams picture Brock Adams · Dec 5, 2012

There are three basic parts to what you want:

  1. You must Intercept the download links.
  2. You must send the link to a new tab when it is clicked, and not just modify it with target="_blank". The tab must be opened with javascript, so that javascript will be allowed to close it when the time comes.
  3. Your script must also run-on / handle the "popup" tab, so that it can detect when to close the popup.

For this discussion, refer to this test page at jsFiddle. It is structured like this:

<div id="downloadLinks">
  <ul>
    <li><a class="dwnPageLink" href="http://fiddle.jshell.net/cDTKj/show/">
            Test file, download page at jsFiddle
        </a>
    </li>
    <li><a class="dwnPageLink" href="http://dw.com.com/redir...">
            TextPad (a great text editor), download page at CNET / Download
        </a>
    </li>
  </ul>
</div>

where the a.dwnPageLink links each open a "Download page" -- which automatically starts a file download after a short delay.


Intercept the download links (a.dwnPageLink):

We intercept the links like so:

$("#downloadLinks a.dwnPageLink").each (interceptLink);

function interceptLink (index, node) {
    var jNode   = $(node);
    jNode.click (openInNewTab);
    jNode.addClass ("intercepted");
}

Note that we also add a CSS class, so that we may quickly see which links have been affected.
openInNewTab will be detailed below. It must both open a tab, and it must stop the normal link action.


Send the link to a new tab:

We must use window.open() to handle the link. If the page is not opened via window.open, our script will not be able to close it.

Note that GM_openInTab() cannot be used because it does not cause window.opener to be set properly and otherwise does not provide a mechanism to close the opened tab.

The new tab is launched in openInNewTab, which looks like this:

function openInNewTab (zEvent) {
    //-- Optionally adjust the href here, if needed.
    var targURL     = this.href;
    var newTab      = window.open (targURL, "_blank");

    //--- Stop the link from doing anything else.
    zEvent.preventDefault ();
    zEvent.stopPropagation ();
    return false;
}


Handle the "popup" tab:

It is not possible to monitor for the File dialog from the launching page. So we must set the script to also run on the "popup" tab. Add @include directives accordingly.

The popup portion of our script can detect the File dialog by monitoring the beforeunload event. Browsers will fire the beforeunload event, just before opening the File dialog (and also just before the tab closes, but we can ignore that).

However, we cannot just close the tab when the dialog appears. Doing so will close the dialog too. So we add a small time delay, and a "Confirm" dialog so that the tab will stay open until the File dialog is closed. To clear the Confirm dialog, just hit Enter an extra time (or Click OK).

The code looks like this:

$(window).bind ("beforeunload",  function (zEvent) {
    //-- Allow time for the file dialog to actually open.
    setTimeout ( function () {
            /*-- Since the time it takes for the user to respond
                to the File dialog can vary radically, use a confirm
                to keep the File dialog open long enough for the user 
                to act.
            */
            var doClose = confirm ("Close this window?");
            if (doClose) {
                window.close ();
            }
        },
        444 // 0.444 seconds
    );
} );


Note:

  1. Since the script will be running on both "List" pages and "Download" pages, we can tell which is which by checking window.opener. On a page opened by javascript, this will have a non-null value.
  2. This question did not ask about having the tab load in the background. That can be done, with varying degrees of success, but is a more involved. Ask a new question for that.


Complete script:

This script works on the test page and on CNET / Download pages:

// ==UserScript==
// @name        _Download page, auto closer
// @namespace   _pc
// ******** Includes for "List pages" that have the links we might click...
// @include     http://YOUR_SERVER.COM/YOUR_LIST-PAGE_PATH/*
// @include     http://jsbin.com/ozofom/*
// @include     http://fiddle.jshell.net/qy3NP/*
// @include     http://download.cnet.com/*
// ******** Includes for "Popup pages" that do the actual downloads...
// @include     http://YOUR_SERVER.COM/YOUR_POPUP-PAGE_PATH/*
// @include     http://fiddle.jshell.net/cDTKj/*
// @include     http://dw.com.com/redir?*
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @grant       GM_addStyle
// @grant       GM_openInTab
// ==/UserScript==
/*- Important: The @include or @match directives must for for both the pages
    that list the download links, AND the pages that do the actual downloading.

    The @grant directive is needed to work around a design change
    introduced in GM 1.0.   It restores the sandbox.
*/

var bPageNotOpenedByJavascript = window.opener ? false : true;
if (bPageNotOpenedByJavascript) {
    /***** "Normal" page, which might contain links to special download pages.
    */
    //--- Intercept links to the download pages:
    // For our jsFiddle Test page:
    $("#downloadLinks a.dwnPageLink").each (interceptLink);

    // For CNET/Download:
    $("#downloadLinks div.dlLinkWrapper a").each (interceptLink);

    GM_addStyle ( "                                 \
        a.intercepted {                             \
            background:         lime;               \
        }                                           \
    " );
}
else {
    /***** Page opened by JS in either a popup or new tab.
        This was *most likely* done by us, using window.open.
    */
    $(window).bind ("beforeunload",  function (zEvent) {
        //-- Allow time for the file dialog to actually open.
        setTimeout ( function () {
                /*-- Since the time it takes for the user to respond
                    to the File dialog can vary radically, use a confirm
                    to keep the File dialog open long enough for the user
                    to act.
                */
                var doClose = confirm ("Close this window?");
                if (doClose) {
                    window.close ();
                }
            },
            444 // 0.444 seconds
        );
    } );
}

function interceptLink (index, node) {
    var jNode   = $(node);
    jNode.click (openInNewTab);
    jNode.addClass ("intercepted");
}

function openInNewTab (zEvent) {
    //-- Optionally adjust the href here, if needed.
    var targURL     = this.href;
    var newTab      = window.open (targURL, "_blank");

    //--- Stop the link from doing anything else.
    zEvent.preventDefault ();
    zEvent.stopPropagation ();
    return false;
}