31

Is there an easy hook for detecting that a window opened by a script has finished loading? Basically, I want the equivalent of the onLoad() hook, but I can't set it directly -- assume that the child document is a given and I can't actually put any code of my own in it.

For instance, say I have the following two files:

parent.html:

<html>
  <head>
    <title>Parent</title>
  </head>
  <script type="text/javascript">
    var w;
    function loadChild() {
      w = window.open();
      w.location.href="child.html";
      // block until child has finished loading... how?
      w.doSomething();
    } 
  </script>
</html>
<body>
  I am a parent window. <a href="javascript:loadChild()">Click me</a>.
</body>

child.html:

<html>
  <head>
    <title>Child</title>
  </head>
  <script type="text/javascript">
    function doSomething() {
      alert("Hi there");
    }
  </script>
</html>
<body>
  I am a child window
</body>

Since setting location.href is non-blocking, w.doSomething() isn't defined yet and the doSomething() call blows up. How can I detect that the child has finished loading?

David Moles
  • 48,006
  • 27
  • 136
  • 235

3 Answers3

28

This works if the location of the newly opened window is same-origin:

var w = window.open('child.html')
w.addEventListener('load', w.doSomething, true); 
  • 1
    Can you clarify "same-origin"? It doesn't seem to work with `file:///` URLs on my local box, but maybe that's a security thing? – David Moles Sep 03 '09 at 08:38
  • 1
    Yes, probably. In the browser, same-origin basically means that the target must be on the same _host_ and same _protocol_. Are you mixing it with `http://`, perhaps? – Øystein Riiser Gundersen Sep 03 '09 at 08:43
  • I forgot to add that `addEventLister` is not properly supported in IE, so using jQuery like gabtub suggests (or another JS framework which fixes browser incompatibilities for event handling) is probably a good idea... – Øystein Riiser Gundersen Sep 03 '09 at 08:48
  • Not mixing, I guess Chrome just doesn't consider two `file:///` URLs the same origin? I can't get IE7 to run scripts on local files at all... Probably I should be trying this with a running server. – David Moles Sep 07 '09 at 09:42
  • That could be the case, yes. I believe the security policy for `file:///` URLs in browsers is _very_ restrictive (and in some cases, simply broken). Try it with a proper web server and see if this fixes the problem. Also, try checking the console in IE/Chrome for any error messages. – Øystein Riiser Gundersen Sep 07 '09 at 13:09
  • This doesn't work if window is already opened, there is no event fired in that case – Johan Danforth Jun 05 '20 at 08:24
10

The accepted answer does not solve the original problem:

  w = window.open();
  w.location.href="child.html";
  // block until child has finished loading... how?
  w.doSomething();

In order to solve this we need to know a little bit more about how page loading goes in the background. Actually it is something asynchronous, so when you write w.location.href="child.html"; and call the w.doSomething(); immediately after that, it won't wait until the new page is loaded. While browser developers solved the loading of the iframes pretty well, the same is not true for child windows. There is no API for that, so all you can do is writing some kind of workaround. What you can do is using the proper defer function from the unload event listener of the child window:

w.addEventListener("unload", function (){
    defer(function (){
        w.doSomething();
    });
});

As usual by client side js development, each of the browsers work completely differently.

enter image description here

The best solution is using a defer function, which calls the callback when the document of the child window is in a loading readyState, where you can add a load handler. If it calls the callback before that, then in the current browsers, the new document is not yet created, so the load handler will be added to the old document, which will be later replaced by the new document, and hence the load handler will be lost. The only exception is Firefox, because that browser keeps the load handlers added to the window. If the browser calls the defer callback after the loading readyState, then in some of the current browsers there can be even more than 2500 msec delay after the page is actually loaded. I have no clue why some of the browsers have such huge delays by some of the defers by child window document loading. I searched for a while, but did not find any answer. Some of the browsers don't have any "loading" defer, so by those all you can do is using a "delayed" defer, and hope for the best. According to my test results a MessageChannel based solution is the best multi-browser "loading" defer:

function defer (callback) {
    var channel = new MessageChannel();
    channel.port1.onmessage = function (e) {
        callback();
    };
    channel.port2.postMessage(null);
}

So you can do something like:

w.addEventListener("unload", function (){
    // note: Safari supports pagehide only
    defer(function (){
        if (w.document.readyState === "loading")
            w.addEventListener("load", function (){
                w.doSomething();
            });
        else
            w.doSomething();
    });
});

If you want to support Safari, then you should use pagehide instead of unload. The pagehide event is supported from IE 11, so if you want to support even older browsers, then you must use both unload and pagehide and start the defer only with one of them if both are available.

var awaitLoad = function (win, cb){
    var wasCalled = false;
    function unloadListener(){
        if (wasCalled)
            return;
        wasCalled = true;
        win.removeEventListener("unload", unloadListener);
        win.removeEventListener("pagehide", unloadListener);
        // Firefox keeps window event listeners for multiple page loads
        defer(function (){
            win.document.readyState;
            // IE sometimes throws security error if not accessed 2 times
            if (win.document.readyState === "loading")
                win.addEventListener("load", function loadListener(){
                    win.removeEventListener("load", loadListener);
                    cb();
                });
            else
                cb();
        });
    };
    win.addEventListener("unload", unloadListener);
    win.addEventListener("pagehide", unloadListener);
    // Safari does not support unload
});


w = window.open();
w.location.href="child.html";
awaitLoad(w, function (){
    w.doSomething();
});

If Promises and async functions are supported, then you can use something like the following:

w = window.open();
await load(w, "child.html");
w.doSomething();

but that is a different story...

inf3rno
  • 24,976
  • 11
  • 115
  • 197
  • You saved my day thanks ! I was searching for a way to close a popup window from an event raised in the popup, but i always got the warning saying something like "Only the script that created the window can close it". But this code allow me to put an eventListener from the opener ! So, to everyone searching a way to do that, this is the way to go – Nenri Apr 04 '19 at 09:00
  • @Nenri yw ............ :-) Be aware that you cannot access browser error pages, like Chrome error pages by giving a bad address. They are in a different origin than the opener page. There is no workaround afaik. I was working on an e2e testing project using window.open and Karma, but I had to stop partially because of this. https://github.com/inf3rno/e2e I might start it again later. – inf3rno Apr 04 '19 at 13:44
  • @Nenri The same thing is true for every cross-frame cross-origin request. So for example if `www.example.com` opens `sub.example.com` in a new window, then you won't be able to access that window and await page load. Only disabling browser security and isolation solves that, but that is a Chromium only feature, and it is for testing, not for production. Normally all you can do is using `postMessage` and asking the child window to close itself. So I would recommend that in your case. – inf3rno Apr 04 '19 at 13:53
  • In my case, the opened popup is from an other origin but is static and will never change to something i can't edit so i've not this problem, but thank you for telling me those :-) I'm waiting for tommorow to give this answer a few bounty rep. – Nenri Apr 04 '19 at 14:31
  • 1
    @Nenri Try it in different browsers, especially in recent Chrome. You cannot access the `childWindow.document` if you open a different origin in the child window. That should give a security error. Note that I am talking about this kind of origin: https://en.wikipedia.org/wiki/Same-origin_policy If you don't want to access it, just close it, then a simple defer is ok, but don't use the `document.readyState` and the event listeners in that case. – inf3rno Apr 04 '19 at 15:00
  • Ok actually i've a problem with it, it works fine from a site with HTTP protocol but as my project doesn't have a SSL certificate,the popup (loading in HTTP) is blocked from websites in HTTPS. Is there a way to prevent it without having to migrate to HTTPS ? – Nenri Apr 05 '19 at 07:25
  • @Nenri I don't think so. This is a secruity feature in modern browsers, I doubt you can do anything about it. – inf3rno Apr 05 '19 at 07:31
  • Ok thanks anyway, and kudos for the great answer and explanations ^^ I haven't a certificate on dev environment but i'll try to setup one on the production version of it – Nenri Apr 05 '19 at 07:39
  • So, today, how would you use Promises? – Mark Apr 18 '23 at 15:28
6

how about

parent.html:

<html>
<head>
<title>Parent</title>
</head>
<script type="text/javascript">
  var w;
  function loadChild() {
    w = window.open();
    w.location.href="child.html";
    // like this (with jquery)
    $(w).ready(function()
    {
      w.doSomething();
    });
  } 
</script>
</html>
<body>
  I am a parent window. <a href="javascript:loadChild()">Click me</a>.
</body>
gabtub
  • 1,460
  • 2
  • 18
  • 21
  • 2
    Doesn't seem to do anything. :( I haven't seen the `$` operator before (I spend most of my time in Java); what's it supposed to do? The Chrome JavaScript debugger gives me `uncaught exception ReferenceError: $ is not defined`. – David Moles Sep 03 '09 at 08:27
  • Oops, cross-comment. Yeah, jQuery, that would explain why I haven't seen it. It does seem like there ought to be some way to do it just with the DOM out of the box. – David Moles Sep 03 '09 at 08:29
  • 1
    without using a JS framework, it's probably best to use gunderwonder's solution. As he said "addEventListener" is not supported in IE, but you can do a browsercheck and, in case of IE, use microsofts implementation of adding event listener => "w.attachEvent('onload',doSomething);" – gabtub Sep 03 '09 at 08:57