Why does document's readystate change from "complete" to "loading"? - javascript

Please take a look at the following code:
this.detachedWin = window.open(window.origin + "/#/tab","Detached","menubar=0,toolbar=0,personalbar=0,status=0");
this.doc = this.detachedWin.document;
setInterval(() => {
console.log(this.doc === this.detachedWin.document,this.detachedWin.document.readyState);
this.doc = this.detachedWin.document;
}, 10);
JSFiddle
If you take a look at your console log, you see something like this:
I don't underestand why the ready state of document is "complete" at the first time and what is the reason it changes to loading after that?
I expect that the ready state to be "loading" for the first time.

First "complete" happens with blank window, and then it changes location to provided URL.
Note that remote URLs won't load immediately. When window.open()
returns, the window always contains about:blank. The actual fetching
of the URL is deferred and starts after the current script block
finishes executing. The window creation and the loading of the
referenced resource are done asynchronously.
developer.mozilla.org
Simplified window opening flow
window.open()
Immediately loaded, because nothing to load: "loading"
Immediately become interactive, because it's empty: "interactive"
Blank window is ready: "completed"
Set window.location explicitly or implicitly
Redirect to URL: "loading"
Window become interactive: "interactive"
Window is ready: "completed"
Full flow with all steps for window opening you can find in specification.
To see how it works, check example:
const openMe = () => {
this.detachedWin = window.open("", "Detached","menubar=0,toolbar=0,personalbar=0,status=0");
setTimeout(() => {
console.log("loading happen now")
this.detachedWin.location = window.origin + "/#/tab"
}, 1000);
this.doc = this.detachedWin.document;
setInterval(() => {
console.log(this.doc === this.detachedWin.document, this.detachedWin.document.readyState);
this.doc = this.detachedWin.document;
}, 10);
}
<button onclick="openMe()">Open window</button>

Related

Javascript wait for window to close before opening another

I need to open a series of popup windows, each window must be closed before the next one can be opened.
function openWindows() {
var urls = getListOfUrls();
for (let url of urls) {
openWindow(url);
// Wait for window to close before continuing.
}
}
The only way I found to make sure that a window is closed is to use a setInterval which, as I understand it, causes the function behave asynchronously.
Any idea on how I can achieve this?
Potential alternate suggestion
Without more information, it sounds like what you're trying to accomplish can be completely automated using puppeteer and in-page scripting. If the URLs that you are visiting aren't all in the same origin, then this is the only method which will work (the solution below will not apply).
Interpretation of question
However, let's say that you need to manually perform some tasks on each page in order (one at a time) for whatever reason (maybe the pages you're retrieving often change their DOM in a way that keeps breaking your scripts), but you want to skip the rigor of serially opening the URLs in new tabs, so that you can just focus on the manual tasks.
Solution
JavaScript web APIs don't provide a way to check for the closure of a window (a script would no longer be running at that point), but the last event that you can respond to is the unload event, and using it would look something like this:
References:
Window
Window: unload event
Window.open()
Same-origin policy
async function openEachWindowAfterThePreviousUnloads (urls) {
for (const url of urls) {
console.log(`Opening URL: "${url}"`);
const target = '_blank';
const initialTime = performance.now();
const windowProxy = window.open(url, target);
if (!windowProxy) {
throw new Error(`Could not get window proxy for URL: "${url}"`);
}
await new Promise(resolve => {
windowProxy.addEventListener('unload', ev => {
const delta = performance.now() - initialTime;
const thresholdMs = 1000;
if (delta < thresholdMs) return;
resolve();
});
});
}
}
const tags = [
'javascript',
'puppeteer',
];
const urls = tags.map(tag => `https://stackoverflow.com/questions/tagged/${tag}`);
openEachWindowAfterThePreviousUnloads(urls);
Code in TypeScript Playground
Caveats:
The script will fail if any of the following is not true:
Every URL is in the same origin as that of the invoking window
If your browser blocks pop-ups, the page where you run the script is allowed to create pop-ups. Example error:
You can try the code above in your browser JS console on this page, and (as long as https://stackoverflow.com is allowed to create popups) it should work.

Wail until iframe contents finish reloading

I have an <iframe> with content designed (and allowed) to be modified by JS code.
At some point I want to clear iframe's content (i.e. load its HTML from the document pointed by src attribute) and start operating on a new DOM as soon as it's loaded.
It seems very convenient to use the iframe.contentWindow.location.reload() function. It reloads the entire inner document and replaces iframe.contentWindow and iframe.contentDocument objects with new ones.
The problem is I can't listen to DOMContentLoaded event on a newly created document. If I add a listener to iframe.contentDocument right after calling reload(), it seems to attach to the old document object which is about to be destroyed. If I wait for some time before adding listener, I have a chance to set it after the event has fired and miss it.
Observing the readyState property of the document doesn't help because it can be set to "complete" when the document hasn't reloaded yet as well as when it has reloaded and finished loading its content.
The best solution I could come up with as far is quite ugly and requires active polling:
function reloadIframe(iframe) {
iframe.contentWindow.dirty = true;
iframe.contentWindow.location.reload();
}
function documentReady(iframe) {
return new Promise(resolve => {
setInterval(() => {
if (
!iframe.contentWindow.dirty && // check that this is not the old window instance
iframe.contentDocument.readyState != "loading"
) {
resolve();
}
}, CHECK_READY_INTERVAL);
});
}
Is the a simpler way to get notified when a window has finished reloading?
Note: I am able to remove an <iframe> element itself and create the new one, but this approach had even more problems.
Due to cross-origin limitations, this will only work assuming you are running the source and parent frames from the same domain, however, you should be able to use:
function documentReady(iframe) {
var doc = iframe.contentDocument || iframe.contentWindow.document;
if ( doc.readyState == 'complete' ) {
iframe.contentWindow.onload = function() {
return true;
};
} else {
return false;
}
}
This will essentially get the iframe document and check whether the document and all sub-resources have finished loading. If they have, it will then call onload to verify that it has finished loading. The function will return true or false depending on whether the iframe has successfully loaded. If wanted, you could also run the function in a loop after requesting the iframe to reload, until the iframe has completed loading.

jQuery deferred promises does not seem to work for opening new window

I am currently trying to achieve the following, with the work flow in sequence:
1. Retrieve a URL which contains a redirect URI/URL if successful.
2. Open new window with retrieved URL, which redirects basically back to the same page but now, the URL has 'code=randomCode' appended to the back of the URL.
3. Parse out the randomCode to use as input for something else.
Problem:
At this moment, I'm using jQuery promises to first $.get(number 1), .then(do number 2), .then(do number 3).
The issue is that this whole function only appears to work after one failure. i.e every single time I refresh, the first time the new window will open, but the other parts will not succeed, i.e the randomCode will not be succcessfully parsed out because during that stage, somehow jQuery cannot detect the opened window, even though it should because it's using .then.
However, after the first failure, once the window is opened, every subsequent attempt succeeds at retrieving everything correctly.
What is going on? Can someone help? My code sample is below:
var retrievedURL;
var desiredString;
function getURL() {
$(document).ready(function() {
$.get(firstURL) // assume valid URL
.then(function(response) {
$('#url').html('Click this');
$('#url-link').attr("href",response.url);
retrievedURL = response.url;
})
.then(function() {
newWindow = window.open(retrievedURL ,'test');
})
.then(function() {
desiredString = newWindow.location.search.substr(6);
})
})
}
So that's the gist of the function. I of course then go on to do something with desiredString, but somehow, every single time, the first run through, desiredString is null.
If the new window does a redirect then you aren't accounting for the time for it to do such. You need a load event listener on new window and then get the url
The last then() is not going to wait for that new window to load it will get called almost instantly. You would need to return a promise from the second then() to have the last then() fire after that promise was resolved
Try
$.get(firstURL) // assume valid URL
.then(function(response) {
$('#url').html('Click this');
$('#url-link').attr("href",response.url);
retrievedURL = response.url;
})
.then(function() {
newWindow = window.open(retrievedURL ,'test');
// listen for window to load
newWindow.onload = function(){
desiredString = newWindow.location.search.substr(6);
// do something with desiredString
}
})
remove the getURL function. this is preventing your code from being bound to document.ready
try this:
var retrievedURL;
var desiredString;
$(document).ready(function() {
$.get(firstURL) // assume valid URL
.then(function(response) {
$('#url').html('Click this');
$('#url-link').attr("href",response.url);
retrievedURL = response.url; })
.then(function() {
newWindow = window.open(retrievedURL ,'test');
})
.then(function() {
desiredString = newWindow.location.search.substr(6);
})
});

Removing setTimeout on click

I'm getting some odd behavior in a part of my js code.
I have some notifications which appear in a bar on top of the page and then disappear after a certain amount of time. I have used a simple setTimeout() to acheive this.
Sometimes, a notification will appear as a result of a particular url query string when the page loads but then a new one would need to be displayed when the user clicks on a button. I want the old one to disappear and the new one to appear. I'm using a variable to keep a reference to the setTimeout() in order to cancel it. However, when I try to do this I manage to create a loop that eventually crashes my chrome tab.
I have put together a jsfiddle illustrating my problem - http://jsfiddle.net/5Nm4c/
Clicking on show notification while another is visible will crash the browser tab. If you click on it when nothing is shown, it is fine.
Here is my js:
var Notification = {
// close main notification bar
close: function (callback) {
$('#notification-bar').fadeOut(250, function () {
// reset its position and fade it back in so it is ready to go again
$(this).css('top', -100).fadeIn(1);
// check if a callback function has been passed in
if (typeof callback === 'function') {
callback();
}
});
},
// open notification bar with the appropriate css class and message
open: function (message) {
// if the notification bar is already visisble
if (verge.inViewport($('#notification-bar'))) {
// hide and then show it with the new message
window.clearTimeout(Notification.timeout);
Notification.close(Notification.open(message));
return false;
}
$('#notification-bar').html(message);
$('#notification-bar').animate({
'top': 0
}, 250, function () {
Notification.timeout = window.setTimeout(function () { Notification.close() }, 1500);
});
},
timeout: null
}
Notification.open('hello');
$('#button').click(function(e){
e.preventDefault();
Notification.open('link clicked');
});
I'm using https://github.com/ryanve/verge/ as it has some nice methods to check if elements are visible in the viewport.
Could someone please tell me where my error is?
I think the error Uncaught RangeError: Maximum call stack size exceededcomes from jsfiddle itself, so I am not able to test it.
I see what you did there:
var Notification = {
open: function (message) {
Notification.close(Notification.open(message)); //Here you create the loop!!
}
}
Another problem I see in your code is, that when Notification.open is called while a animation is running Notification.timeout is not actuell. Try a $('#notification-bar').stop(true, true); to stop the actuell animation befor you call window.clearTimeout(Notification.timeout);. Maybe it would be even better to use $('#notification-bar').stop(true, false);, so the "old" setTimeout will not even be called.

GMail getElementById('canvas_frame') returning null in Firefox

I have written the following javascript function which hangs up because it never seems to be able to find the canvas_frame element on a loaded GMail page (the compose page). This is begin called via the XUL of a Firefox add-on. Any thoughts on what might be going on?
init : function () {
var frame, interval;
frame = document.getElementById('canvas_frame');
interval = setInterval(function() {
if (frame) {
if (frame.contentDocument) {
clearInterval(interval);
GLOBALS.doc = frame.contentDocument;
onContentReady();
}
}
}, 500);
}
You should prefer to wait for a load event on the frame, rather than polling. But my guess is that the canvas_frame element hasn't been created yet, so you need to fetch it each time inside the polling loop. Otherwise the frame variable is always null.

Categories