It seems that $('#someIframe').load(function(){...}) won't fire if it is attached after the iframe has finished loading. Is that correct?
What I'd really like is to have a function that is always called once when or after an iframe has loaded. To make this clearer, here are two cases:
Iframe hasn't loaded yet: run a callback function once it loads.
Iframe has already loaded: run the callback immediately.
How can I do this?
I've banged my head against a wall until I found out what's happening here.
Background information
Using .load() isn't possible if the iframe has already been loaded (event will never fire)
Using .ready() on an iframe element isn't supported (reference) and will call the callback immediately even if the iframe isn't loaded yet
Using postMessage or a calling a container function on load inside the iframe is only possible when having control over it
Using $(window).load() on the container would also wait for other assets to load, like images and other iframes. This is not a solution if you want to wait only for a specific iframe
Checking readyState in Chrome for an alredy fired onload event is meaningless, as Chrome initializes every iframe with an "about:blank" empty page. The readyState of this page may be complete, but it's not the readyState of the page you expect (src attribute).
Solution
The following is necessary:
If the iframe is not loaded yet we can observe the .load() event
If the iframe has been loaded already we need to check the readyState
If the readyState is complete, we can normally assume that the iframe has already been loaded. However, because of the above-named behavior of Chrome we furthermore need to check if it's the readyState of an empty page
If so, we need to observe the readyState in an interval to check if the actual document (related to the src attribute) is complete
I've solved this with the following function. It has been (transpiled to ES5) successfully tested in
Chrome 49
Safari 5
Firefox 45
IE 8, 9, 10, 11
Edge 24
iOS 8.0 ("Safari Mobile")
Android 4.0 ("Browser")
Function taken from jquery.mark
/**
* Will wait for an iframe to be ready
* for DOM manipulation. Just listening for
* the load event will only work if the iframe
* is not already loaded. If so, it is necessary
* to observe the readyState. The issue here is
* that Chrome will initialize iframes with
* "about:blank" and set its readyState to complete.
* So it is furthermore necessary to check if it's
* the readyState of the target document property.
* Errors that may occur when trying to access the iframe
* (Same-Origin-Policy) will be catched and the error
* function will be called.
* #param {jquery} $i - The jQuery iframe element
* #param {function} successFn - The callback on success. Will
* receive the jQuery contents of the iframe as a parameter
* #param {function} errorFn - The callback on error
*/
var onIframeReady = function($i, successFn, errorFn) {
try {
const iCon = $i.first()[0].contentWindow,
bl = "about:blank",
compl = "complete";
const callCallback = () => {
try {
const $con = $i.contents();
if($con.length === 0) { // https://git.io/vV8yU
throw new Error("iframe inaccessible");
}
successFn($con);
} catch(e) { // accessing contents failed
errorFn();
}
};
const observeOnload = () => {
$i.on("load.jqueryMark", () => {
try {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href !== bl || src === bl || src === "") {
$i.off("load.jqueryMark");
callCallback();
}
} catch(e) {
errorFn();
}
});
};
if(iCon.document.readyState === compl) {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href === bl && src !== bl && src !== "") {
observeOnload();
} else {
callCallback();
}
} else {
observeOnload();
}
} catch(e) { // accessing contentWindow failed
errorFn();
}
};
Working example
Consisting of two files (index.html and iframe.html):
index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Parent</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
<script>
$(function() {
/**
* Will wait for an iframe to be ready
* for DOM manipulation. Just listening for
* the load event will only work if the iframe
* is not already loaded. If so, it is necessary
* to observe the readyState. The issue here is
* that Chrome will initialize iframes with
* "about:blank" and set its readyState to complete.
* So it is furthermore necessary to check if it's
* the readyState of the target document property.
* Errors that may occur when trying to access the iframe
* (Same-Origin-Policy) will be catched and the error
* function will be called.
* #param {jquery} $i - The jQuery iframe element
* #param {function} successFn - The callback on success. Will
* receive the jQuery contents of the iframe as a parameter
* #param {function} errorFn - The callback on error
*/
var onIframeReady = function($i, successFn, errorFn) {
try {
const iCon = $i.first()[0].contentWindow,
bl = "about:blank",
compl = "complete";
const callCallback = () => {
try {
const $con = $i.contents();
if($con.length === 0) { // https://git.io/vV8yU
throw new Error("iframe inaccessible");
}
successFn($con);
} catch(e) { // accessing contents failed
errorFn();
}
};
const observeOnload = () => {
$i.on("load.jqueryMark", () => {
try {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href !== bl || src === bl || src === "") {
$i.off("load.jqueryMark");
callCallback();
}
} catch(e) {
errorFn();
}
});
};
if(iCon.document.readyState === compl) {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href === bl && src !== bl && src !== "") {
observeOnload();
} else {
callCallback();
}
} else {
observeOnload();
}
} catch(e) { // accessing contentWindow failed
errorFn();
}
};
var $iframe = $("iframe");
onIframeReady($iframe, function($contents) {
console.log("Ready to got");
console.log($contents.find("*"));
}, function() {
console.log("Can not access iframe");
});
});
</script>
<iframe src="iframe.html"></iframe>
</body>
</html>
iframe.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Child</title>
</head>
<body>
<p>Lorem ipsum</p>
</body>
</html>
You can also change the src attribute inside index.html to e.g. "http://example.com/". Just play around with it.
I'd use postMessage. The iframe can assign its own onload event and post to the parent. If there are timing issues just make sure to assign the parent's postMessage handler before creating the iframe.
For this to work the iframe must know the url of the parent, for instance by passing a GET parameter to the iframe.
This function will run your callback function immediately if the iFrame is already loaded or wait until the iFrame is completely loaded before running your callback function.
Just pass in your callback function that you want to run when the iFrame finishes loading and the element to this function:
function iframeReady(callback, iframeElement) {
const iframeWindow = iframeElement.contentWindow;
if ((iframeElement.src == "about:blank" || (iframeElement.src != "about:blank" && iframeWindow.location.href != "about:blank")) && iframeWindow.document.readyState == "complete") {
callback();
} else {
iframeWindow.addEventListener("load", callback);
}
}
This will take care of the most common issues like chrome initializing iframe with about:blank and iFrame not supporting DOMContentLoaded event. See this https://stackoverflow.com/a/69694808/15757382 answer for explanation.
I had the same problem. In my case, I simply checked if the onload function is fired or not.
var iframe = document.getElementById("someIframe");
var loadingStatus = true;
iframe.onload = function () {
loadingStatus = false;
//do whatever you want [in my case I wants to trigger postMessage]
};
if (loadingStatus)
//do whatever you want [in my case I wants to trigger postMessage]
I tried very hard to come to a solution that worked consistently cross browser. IMPORTANT: I was not able to come to such a solution. But here is as far as I got:
// runs a function after an iframe node's content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
// secondary note - this doesn't seem to work for chrome : (
// another note - doesn't seem to work for nodes created dynamically for some reason
function onReady(iframeNode, f) {
var windowDocument = iframeNode[0].contentWindow.document;
var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document;
if(iframeDocument.readyState === 'complete') {
f();
} else {
iframeNode.load(function() {
var i = setInterval(function() {
if(iframeDocument.readyState === 'complete') {
f();
clearInterval(i);
}
}, 10);
});
}
}
and I was using it like this:
onReady($("#theIframe"), function() {
try {
var context = modal[0].contentWindow;
var i = setInterval(function() {
if(context.Utils !== undefined && context.$) { // this mess is to attempt to get it to work in firefox
context.$(function() {
var modalHeight = context.someInnerJavascript();
clearInterval(i);
});
}
}, 10);
} catch(e) { // ignore
console.log(e);
}
});
Note that even this does not solve the problem for me. Here are some problems with this solution:
In onReady, for iframes that were added dynamically, iframeDocument.readyState seems to be stuck at "uninitialized" and thus the callback never fires
The whole setup still doesn't seem to work in firefox for some reason. It almost seems like the setInterval function is cleared externally.
Note that some of these problems only happen when there is a lot of other stuff loading on the page, which makes the timing of these things less deterministic.
So if anyone can improve upon this, it would be much appreciated.
Only when the content inside the iframe is loaded innerDoc is true and fires code inside the if.
window.onload = function(){
function manipulateIframe(iframeId, callback) {
var iframe = document.getElementById(iframeId).contentWindow.document;
callback(iframe);
};
manipulateIframe('IFwinEdit_forms_dr4r3_forms_1371601293572', function (iframe) {
console.log(iframe.body);
});};
example
I think you should try using onreadystatechange event.
http://jsfiddle.net/fk8fc/3/
$(function () {
var innerDoc = ($("#if")[0].contentDocument) ? $("#if")[0].contentDocument : $("#if")[0].contentWindow.document;
console.debug(innerDoc);
$("#if").load( function () {
alert("load");
alert(innerDoc.readyState)
});
innerDoc.onreadystatechange = function () {
alert(innerDoc.readyState)
};
setTimeout(innerDoc.onreadystatechange, 5000);
});
EDIT: the context is not what I think it is. you can just check the readyState of iframe document and everything should be fine.
OP: This is a packaged up function I made from the concepts described above:
// runs a function after an iframe node's content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
onReady: function(iframeNode, f) {
var windowDocument = iframeNode[0].contentWindow.document;
var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document
if(iframeDocument.readyState === 'complete') {
f();
} else {
iframeNode.load(f);
}
}
Related
A site I visit has this script:
<script type="text/javascript">
(function () {
var ca = document.createElement('script');
ca.type = 'text/javascript';
ca.async = true;
var s = document.getElementsByTagName('script')[0];
ca.src = 'http://serve.popads.net/checkInventory.php';
ca.onerror = function () {
setTimeout(function () {
$('.embed-player').attr('src', "/adblock.html")
}, 8000);
}
s.parentNode.insertBefore(ca, s);
})();
</script>
and another one as follows that is probably related:
function adBlockDetected() {
$('#block').modal({
'backdrop': 'static',
'keyboard': false,
})
}
if (window.canRunAds === undefined) {
adBlockDetected()
}
I'm trying to see the content inside an iframe:
<iframe src="https://example.com" frameborder="0" class="embed-player" scrolling="no" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" rel="nofollow"></iframe>
but the src attribute keeps getting updated asynchronously by the first script above - with a warning message asking me to turn off my adblocker if want to use the site.
I wrote this Tampermonkey script to update the page document in my browser:
(function() {
'use strict';
var embedSrc = $('.embed-player').attr('src');
setTimeout(function () {
$('.embed-player').attr('src', embedSrc)
}, 16000);
})();
THIS WORKS! . . . BUT . . .
The problem is that I had to set a very high timeout value of 16000 ms to counteract the 8000 ms value used on the page - because sometimes the page's own async call comes back later than expected.
This leads to the annoyance of having to wait for both async calls to complete before I can view the content of the page.
Is there a more effective approach than the one I'm using?
BTW: I already have Reek's Anti-Adblock Killer Tampermonkey script and UBlock Origin filter installed - but for some reason the script on this site is bypassing that code (I've checked and it works on most other sites). It throws a console error: Execution of script 'Anti-Adblock Killer | Reek' failed! Cannot assign to read only property 'adblock2' of object '#<Window>'
There are several ways to hijack that code before it runs.
For example, you can prevent setting onerror properties on script elements:
Object.defineProperty(HTMLScriptElement.prototype, 'onerror', {
setter: function() {}
});
If you don't want to affect other scripts,
var onerror = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'onerror');
Object.defineProperty(HTMLScriptElement.prototype, 'onerror', {
get: onerror.get,
setter: function() {
if(this.src !== 'http://serve.popads.net/checkInventory.php') {
return onerror.set.apply(this, arguments);
}
}
});
Or you could add a capture event listener which prevents the event from reaching the script
document.addEventListener('error', function(e) {
if(e.target.tagName.toLowerCase() === 'script' &&
e.target.src === 'http://serve.popads.net/checkInventory.php'
) {
e.stopPropagation();
}
}, true);
You could also hijack $.fn.attr to prevent it from changing src to "/adblock.html":
var $attr = $.fn.attr;
$.fn.attr = function(attr, value) {
if(attr === "src" && value === "/adblock.html") {
return this;
}
return $attr.apply(this, arguments);
}
If your code runs after the code you want to hijack, as Bergi says it might be simpler to remove the event handler:
iframe.onerror = null;
Instead of working around the symptom, I'd suggest fixing what's causing it to change the attribute.
The request for "checkInventory.php" appears to merely be used as bait in this case. So what you could do is simply add the exception rule ##||serve.popads.net/checkInventory.php^ to your ad blocker so that the error doesn't occur in the first place.
How would you cycle through every image on a given page and check to see if it was loaded or errored out?
The following are not seeming to work in the phantomjs/casperjs setup yet cycling through and grabbing the src is working.
.load()
and
.error()
If the above does work, could someone show a proper usage that would work in a casperjs script?
The code I used looked similar to the following:
var call_to_page = this.evaluate(function() {
var fail_amount = 0;
var pass_amount = 0;
var pass_img_src = [];
var fail_img_src = [];
var img_src = [];
$("img").each(function(){
$(this).load(function(){
pass_amount++;
pass_img_src.push($(this).attr("src"));
}).error(function(){
fail_amount++;
fail_img_src.push($(this).attr("src"));
});
img_src.push($(this).attr("src"));
});
return [img_src, fail_amount, fail_img_src, pass_amount, pass_img_src];
});
Any help as to why the above code doesnt work for me would be great. The page is properly being arrived upon and I am able to mess with the dom, just not .load or .error. Im thinking its due to the images being done loaded, so Im still looking for alternatives.
CasperJS provides the resourceExists function which can be used to check if a particular ressource was loaded. The ressource is passed into a function, so custom constraints can be raised.
casper.then(function(){
var fail_amount = 0;
var pass_amount = 0;
var pass_img_src = [];
var fail_img_src = [];
var elements = this.getElementsInfo("img");
elements.forEach(function(img){
if (img && img.attributes && casper.resourceExists(function(resource){
return resource.url.match(img.attributes.src) &&
resource.status >= 200 &&
resource.status < 400;
})) {
pass_amount++;
pass_img_src.push(img.attributes.src);
} else {
fail_amount++;
fail_img_src.push(img.attributes.src);
}
});
this.echo(JSON.stringify([fail_amount, fail_img_src, pass_amount, pass_img_src], undefined, 4));
});
This can be done after the page is loaded. So there is no need to preemptively add some code into the page context.
In turn, the problem with you code may be that the callbacks never fire because the images are already loaded of already timed out. So there is no new information.
If you're not sure what kind of errors are counted, you can use a custom resources detection for all available types or errors.
var resources = []; // a resource contains at least 'url', 'status'
casper.on("resource.received", function(resource){
if (resource.stage == "end") {
if (resource.status < 200 || resource.status >= 400) {
resource.errorCode = resource.status;
resource.errorString = resource.statusText;
}
resources.push(resource);
}
});
casper.on("resource.timeout", function(request){
request.status = -1;
resources.push(request);
});
casper.on("resource.error", function(resourceError){
resourceError.status = -2;
resources.push(resourceError);
});
function resourceExists(url){
return resources.filter(function(res){
return res.url.indexOf(url) !== -1;
}).length > 0;
}
casper.start(url, function(){
var elements = this.getElementsInfo("img");
elements.forEach(function(img){
if (img && img.attributes && resourceExists(img.attributes.src) && !resourceExists(img.attributes.src).errorCode) {
// pass
} else {
// fail
}
});
});
I don't have much experience with caperjs, in my observation I identified below points
Note:
jQuery .load( handler ) and .error( handler ) both were deprecated from verson 1.8.
If you're using jQuery 1.8+ then attaching load and error events to img(tags) does nothing.
jQuery Ajax module also has a method named $.load() is shortcut form of $.get(). Which one is fired depends on the set of arguments passed.
Here are Caveats of the load event when used with images from jQuery Docs
A common challenge developers attempt to solve using the .load() shortcut is to execute a function when an image (or collection of images) have completely loaded. There are several known caveats with this that should be noted. These are:
It doesn't work consistently nor reliably cross-browser
It doesn't fire correctly in WebKit if the image src is set to the same src as before
It doesn't correctly bubble up the DOM tree
Can cease to fire for images that already live in the browser's cache
So if you've version 1.8+ of jQuery the below block does nothing.
$(this).load(function(){
pass_amount++;
pass_img_src.push($(this).attr("src"));
}).error(function(){
fail_amount++;
fail_img_src.push($(this).attr("src"));
});
As a result this return [img_src, fail_amount, fail_img_src, pass_amount, pass_img_src]; statement will give us only img_src[with number of imgs as length] array filled with srcs of imgs in page. and other elements fail_amount, fail_img_src, pass_amount, pass_img_src will have same defaults all the time.
In the case of jQuery 1.8 below load and error events attachment with jQuery were meaningful(in your case these events were attached after they were loaded on page, so they won't show any effect with load and error callbacks), but the time where we attach events matters. We should attach these before the img tags or place the events in tag level(as attributes onload & onerror) and definitions of function handlers script should keep before any img tag or in very beginning of the body or in head
There're ways to figure out some are here:
use open-source plug-in like (Use imagesLoaded](https://github.com/desandro/imagesloaded)
Can use ajax call to find out whether the img.src.url were good or not?
Dimension based check like below function IsImageRendered
below I've but its old one not sure the browser support at this time. i recommend to go with above plug in if you can use it
var call_to_page = this.evaluate(function () {
function isImageRendered(img) {
// with 'naturalWidth' and 'naturalHeight' to get true size of the image.
// before we tried width>0 && height>0
if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) {
return false;
}
//good
return true;
}
var pass_img_src = [];
var fail_img_src = [];
var img_src = [];
$("img").each(function () {
var $img = $(this), $srcUrl = $img.attr("src");
img_src.push($srcUrl);
if (!$srcUrl)
fail_img_src.push($srcUrl);
else {
if (isImageRendered($img.get(0))) {
pass_img_src.push($srcUrl);
}
else {
fail_img_src.push($srcUrl);
}
}
});
var fail_count = fail_img_src.length,
pass_count = pass_img_src.length;
return [img_src, fail_count, fail_img_src, pass_count, pass_img_src];
});
I have a javascript tag embedded in the <head> of a webpage. In this script (served via Amazon Cloudfront) it does some processing and then if certain checks are set, appends another script to the document head. Here is an example of that embed:
var js = document.createElement('script');
js.src = 'http://cdn.mysite.com/script.js?cache_bust='+Math.random();
document.getElementsByTagName('head')[0].appendChild(js);
This works properly, however it's non-blocking meaning the webpage continues to load while that script is being appended to the head.
Is there any way to embed a script in a blocking manner so the webpage "hangs" while the new script is setup?
I was thinking about maybe moving the first script to just below the <body> tag and then using document.write() the second script but I'd prefer to keep it in the <head>.
Any ideas? Thanks!
You're right, document.write should work. As Dr.Molle mentioned in the comment, you can use inside the <head> too (the <head> is part of the document too!).
So:
<head>
<script>
var src = 'http://cdn.mysite.com/script.js?cache_bust='+Math.random();
document.write '<script src="">'+ src + '</scr' + 'ipt>';
</script>
</head>
The '</scr' + 'ipt>' part is a precaution, browsers usually think the outer script block is being closed when they find </script>, even if inside a string.
Use something like this script loader I made:
var scriptLoader = [];
/*
* loads a script and defers a callback for when the script finishes loading.
* you can also just stack callbacks on the script load by invoking this method repeatedly.
*
* opts format: {
* url: the url of the target script resource,
* timeout: a timeout in milliseconds after which any callbacks on the script will be dropped, and the script element removed.
* callbacks: an optional array of callbacks to execute after the script completes loading.
* callback: an optional callback to execute after the script completes loading.
* before: an optional callback to execute before the script is loaded, only intended to be ran prior to requesting the script, not multiple times.
* success: an optional callback to execute when the script successfully loads, always remember to call script.complete at the end.
* error: an optional callback to execute when and if the script request fails.
* }
*/
function loadScript(opts) {
if (typeof opts === "string") {
opts = {
url: opts
};
}
var script = scriptLoader[opts.url];
if (script === void 0) {
var complete = function (s) {
s.status = "loaded";
s.executeCallbacks();
};
script = scriptLoader[opts.url] = {
url: opts.url,
status: "loading",
requested: new Date(),
timeout: opts.timeout || 10000,
callbacks: opts.callbacks || [opts.callback || $.noop],
addCallback: function (callback) {
if (!!callback) {
if (script.status !== "loaded") {
script.callbacks.push(callback);
} else {
callback();
}
}
},
executeCallbacks: function () {
$.each(script.callbacks, function () {
this();
});
script.callbacks = [];
},
before: opts.before || $.noop,
success: opts.success || complete,
complete: complete,
error: opts.error || $.noop
};
script.before();
$.ajax(script.url, {
timeout: script.timeout,
success: function () {
script.success(script);
},
error: function () {
script.error(); // .error should remove anything added by .before
scriptLoader[script.url] = void 0; // dereference, no callbacks were executed, no harm is done.
}
});
} else {
script.addCallback(opts.callback);
}
}
loadScript({
url: 'http://fiddle.jshell.net/js/lib/mootools-core-1.4.5-nocompat.js',
callback: function(){
alert('foo');
}
});
Generally speaking, you should be deferring execution, rather than blocking, giving your user an increased perceived page loading speed.
I'm creating a jquery plugin and I want to verify an external script is loaded. This is for an internal web app and I can keep the script name/location consistent(mysscript.js). This is also an ajaxy plugin that can be called on many times on the page.
If I can verify the script is not loaded I'll load it using:
jQuery.getScript()
How can I verify the script is loaded because I don't want the same script loaded on the page more than once? Is this something that I shouldn't need to worry about due to caching of the script?
Update:
I may not have control over who uses this plugin in our organization and may not be able to enforce that the script is not already on the page with or without a specific ID, but the script name will always be in the same place with the same name. I'm hoping I can use the name of the script to verify it's actually loaded.
If the script creates any variables or functions in the global space you can check for their existance:
External JS (in global scope) --
var myCustomFlag = true;
And to check if this has run:
if (typeof window.myCustomFlag == 'undefined') {
//the flag was not found, so the code has not run
$.getScript('<external JS>');
}
Update
You can check for the existence of the <script> tag in question by selecting all of the <script> elements and checking their src attributes:
//get the number of `<script>` elements that have the correct `src` attribute
var len = $('script').filter(function () {
return ($(this).attr('src') == '<external JS>');
}).length;
//if there are no scripts that match, the load it
if (len === 0) {
$.getScript('<external JS>');
}
Or you can just bake this .filter() functionality right into the selector:
var len = $('script[src="<external JS>"]').length;
Few too many answers on this one, but I feel it's worth adding this solution. It combines a few different answers.
Key points for me were
add an #id tag, so it's easy to find, and not duplicate
Use .onload() to wait until the script has finished loading before using it
mounted() {
// First check if the script already exists on the dom
// by searching for an id
let id = 'googleMaps'
if(document.getElementById(id) === null) {
let script = document.createElement('script')
script.setAttribute('src', 'https://maps.googleapis.com/maps/api/js?key=' + apiKey)
script.setAttribute('id', id)
document.body.appendChild(script)
// now wait for it to load...
script.onload = () => {
// script has loaded, you can now use it safely
alert('thank me later')
// ... do something with the newly loaded script
}
}
}
#jasper's answer is totally correct but with modern browsers, a standard Javascript solution could be:
function isScriptLoaded(src)
{
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
UPDATE July 2021:
The accepted solutions above have changed & improved much over time. The scope of my previous answer above was only to detect if the script was inserted in the document to load (and not whether the script has actually finished loading).
To detect if the script has already loaded, I use the following method (in general):
Create a common library function to dynamically load all scripts.
Before loading, it uses the isScriptLoaded(src) function above to check whether the script has already been added (say, by another module).
I use something like the following loadScript() function to load the script that uses callback functions to inform the calling modules if the script finished loading successfully.
I also use additional logic to retry when script loading fails (in case of temporary network issues).
Retry is done by removing the <script> tag from the body and adding it again.
If it still fails to load after configured number of retries, the <script> tag is removed from the body.
I have removed that logic from the following code for simplicity. It should be easy to add.
/**
* Mark/store the script as fully loaded in a global variable.
* #param src URL of the script
*/
function markScriptFullyLoaded(src) {
window.scriptLoadMap[src] = true;
}
/**
* Returns true if the script has been added to the page
* #param src URL of the script
*/
function isScriptAdded(src) {
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
/**
* Returns true if the script has been fully loaded
* #param src URL of the script
*/
function isScriptFullyLoaded(src) {
return src in window.scriptLoadMap && window.scriptLoadMap[src];
}
/**
* Load a script.
* #param src URL of the script
* #param onLoadCallback Callback function when the script is fully loaded
* #param onLoadErrorCallback Callback function when the script fails to load
* #param retryCount How many times retry laoding the script? (Not implimented here. Logic goes into js.onerror function)
*/
function loadScript(src, onLoadCallback, onLoadErrorCallback, retryCount) {
if (!src) return;
// Check if the script is already loaded
if ( isScriptAdded(src) )
{
// If script already loaded successfully, trigger the callback function
if (isScriptFullyLoaded(src)) onLoadCallback();
console.warn("Script already loaded. Skipping: ", src);
return;
}
// Loading the script...
const js = document.createElement('script');
js.setAttribute("async", "");
js.src = src;
js.onload = () => {
markScriptFullyLoaded(src)
// Optional callback on script load
if (onLoadCallback) onLoadCallback();
};
js.onerror = () => {
// Remove the script node (to be able to try again later)
const js2 = document.querySelector('script[src="' + src +'"]');
js2.parentNode.removeChild(js2);
// Optional callback on script load failure
if (onLoadErrorCallback) onLoadErrorCallback();
};
document.head.appendChild(js);
}
This was very simple now that I realize how to do it, thanks to all the answers for leading me to the solution. I had to abandon $.getScript() in order to specify the source of the script...sometimes doing things manually is best.
Solution
//great suggestion #Jasper
var len = $('script[src*="Javascript/MyScript.js"]').length;
if (len === 0) {
alert('script not loaded');
loadScript('Javascript/MyScript.js');
if ($('script[src*="Javascript/MyScript.js"]').length === 0) {
alert('still not loaded');
}
else {
alert('loaded now');
}
}
else {
alert('script loaded');
}
function loadScript(scriptLocationAndName) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptLocationAndName;
head.appendChild(script);
}
Create the script tag with a specific ID and then check if that ID exists?
Alternatively, loop through script tags checking for the script 'src' and make sure those are not already loaded with the same value as the one you want to avoid ?
Edit: following feedback that a code example would be useful:
(function(){
var desiredSource = 'https://sitename.com/js/script.js';
var scripts = document.getElementsByTagName('script');
var alreadyLoaded = false;
if(scripts.length){
for(var scriptIndex in scripts) {
if(!alreadyLoaded && desiredSource === scripts[scriptIndex].src) {
alreadyLoaded = true;
}
}
}
if(!alreadyLoaded){
// Run your code in this block?
}
})();
As mentioned in the comments (https://stackoverflow.com/users/1358777/alwin-kesler), this may be an alternative (not benchmarked):
(function(){
var desiredSource = 'https://sitename.com/js/script.js';
var scripts = document.getElementsByTagName('script');
var alreadyLoaded = false;
for(var scriptIndex in document.scripts) {
if(!alreadyLoaded && desiredSource === scripts[scriptIndex].src) {
alreadyLoaded = true;
}
}
if(!alreadyLoaded){
// Run your code in this block?
}
})();
Simply check if the global variable is available, if not check again. In order to prevent the maximum callstack being exceeded set a 100ms timeout on the check:
function check_script_loaded(glob_var) {
if(typeof(glob_var) !== 'undefined') {
// do your thing
} else {
setTimeout(function() {
check_script_loaded(glob_var)
}, 100)
}
}
Another way to check an external script is loaded or not, you can use data function of jquery and store a validation flag. Example as :
if(!$("body").data("google-map"))
{
console.log("no js");
$.getScript("https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=initilize",function(){
$("body").data("google-map",true);
},function(){
alert("error while loading script");
});
}
}
else
{
console.log("js already loaded");
}
I think it's better to use window.addEventListener('error') to capture the script load error and try to load it again.
It's useful when we load scripts from a CDN server. If we can't load script from the CDN, we can load it from our server.
window.addEventListener('error', function(e) {
if (e.target.nodeName === 'SCRIPT') {
var scriptTag = document.createElement('script');
scriptTag.src = e.target.src.replace('https://static.cdn.com/', '/our-server/static/');
document.head.appendChild(scriptTag);
}
}, true);
Merging several answers from above into an easy to use function
function GetScriptIfNotLoaded(scriptLocationAndName)
{
var len = $('script[src*="' + scriptLocationAndName +'"]').length;
//script already loaded!
if (len > 0)
return;
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptLocationAndName;
head.appendChild(script);
}
My idead is to listen the error log if there is an error on script loading.
const checkSegmentBlocked = (e) => {
if (e.target.nodeName === 'SCRIPT' && e.target.src.includes('analytics.min.js')) {
window.isSegmentBlocked = true;
e.target.removeEventListener(e.type, checkSegmentBlocked);
}
};
window.addEventListener('error', checkSegmentBlocked, true);
Some answers on this page are wrong. They check for the existence of the <script> tag - but that is not enough. That tells you that the tag was inserted into the DOM, not that the script is finished loading.
I assume from the question that there are two parts: the code that inserts the script, and the code that checks whether the script has loaded.
The code that dynamically inserts the script:
let tag = document.createElement('script');
tag.type = 'text/javascript';
tag.id = 'foo';
tag.src = 'https://cdn.example.com/foo.min.js';
tag.onload = () => tag.setAttribute('data-loaded', true); // magic sauce
document.body.appendChild(tag);
Some other code, that checks whether the script has loaded:
let script = document.getElementById('foo');
let isLoaded = script && script.getAttribute('data-loaded') === 'true';
console.log(isLoaded); // true
If the both of those things (inserting and checking) are in the same code block, then you could simplify the above:
tag.onload = () => console.log('loaded');
I found a quick tip before you start diving into code that might save a bit of time. Check devtools on the webpage and click on the network tab. The js scripts are shown if they are loaded as a 200 response from the server.
Ok, I have put together a script which does this:
Make ajax request (via getHTTPObject(), no libraries is used)
Create an iframe with script, src is "blank.html".
use iframe.document.write() to execute scripts (inkluding document.write based scripts) in the iframe.
call parent window's document to clone the iframe content.
Append the content clone to parent body.
Works like a charm in all browsers but IE, where every version - including IE9 beta - hangs on iframeWindow.document.close() with empty cache, leaving the window/tab unresponsible. When I force quit, restart and load the page again it works.
What I've tried already:
Googled.
called the ajax request callback manually with string instead of request.responseText - it works even with empty cache here.
Removed document.close() - resulting in scripts in iframe not executing at all (again, only with empty cache, cached pages works fine).
Tested to make the ajax request synchronous - no difference.
Any ideas?
Live example here:
http://labs.adeprimo.se/~adupanyt/ot/unlimited_scroll/
Here is the code. The install(), finish() and append()-functions manages the iframe.
/*!
* Cross browser unlimited scroll snippet
* Copyright (c) 2010 by Adeprimo.
* Released under the MIT license.
*/
/* Code assumptions:
*
* <div id="unlimited-scroll-wrapper">
*
* ... first content ...
*
* <a id="load-more-unlimited-content" href="/nyheter">
* Ladda mer innehÄll</a>
* </div>
*/
(function(window, document, undefined){
/**
* This snippet has two running modes, and it is the load_on_scroll
* directive which rules it.
*
* true = a scroll event is initiated, which will wait
* until the bottom is reached before it load any new content.
* false = this script will continue loading more content, one piece
* at the time, until there is no more to get.
*
*/
load_on_scroll = false;
var request; // results of getHTTPObject()
var wrapper; // reference to the element which stores the new contents.
var callsCount; // keep the count of ajax calls, helps css targeting.
var attachEvent;// stores result of window.attachEvent for performance
// locks, these are updated by the script.
window.mkt_nothing_more_to_load = false;// true when end of queue reached.
window.mkt_prevent_new_loading = false; // true when ajax in progress.
wrapper = document.getElementById('unlimited-scroll-wrapper');
callsCount = 1;
attachEvent = window.attachEvent;
/**
* Customize this function for your need.
* It is called each time a new document fragment
* shall load.
* In here, you might add some nifty jQuery or Prototype if your app
* has it.
*/
function load_and_append() {
var src; // href attribute of the #load-more-unlimited-content A element
// get the source
src = document.getElementById('load-more-unlimited-content').href;
// mktwebb specific, will only return the content and
// not the header or footer.
src = src.replace(/\?.+?$/, '') + '?segment=content,footer';
if (!request) {
request = getHTTPObject(); // getHTTPObject must be declared
} // separately!
if (request) {
request.onreadystatechange = function() {
if (request.readyState == 4) {
if (request.status == 200 || request.status == 304) {
console.log('append() begin');
append(request.responseText, src);
console.log('append() done');
}
}
};
request.open("GET", src, false);
request.send(null);
}
}
function finish(iframe_window) {
var acc_elm = (function(doc){
return doc.getElementsByTagName('div')[0].cloneNode(true);
})(iframe_window.document);
document.getElementById('unlimited-scroll-wrapper').appendChild(acc_elm);
window.mkt_prevent_new_loading = false; // we are ready to more content.
// we are done with the iframe, let's remove it.
var iframe_container = document.getElementById('mkt_iframe-container');
iframe_container.parentNode.removeChild(iframe_container);
// basically, the script continues as long as it finds a new
// #load-more-unlimited-content element in the newly added content.
// if it can't find the #load-more-unlimited-content,
// the script will stop and unattach itself.
if (document.getElementById('load-more-unlimited-content')) {
// if load_on_scroll, the new content is added under
// the control of the scroll event. There is no need to call
// load_and_append here since the scroll event will manage
// that fine.
// however, when not load_on_scroll,
// new content should be loaded asap.
if (!load_on_scroll) {
window.mkt_prevent_new_loading = true;
// give the browser some time to reflow and rest, then continue.
setTimeout(load_and_append, 2 * 1000);
}
} else {
nothing_more_to_load = true; // tell the scroll event to stop.
// remove scroll event since it is not needed anymore
if (attachEvent) {
window.detachEvent('onscroll',
look_for_trouble); // ie
} else {
window.removeEventListener('scroll',
look_for_trouble, false); // w3c
}
}
};
window['mkt_importFromIframe'] = finish;
// ------------------------------------------------------------------------
// We are now ready to start.
// ------------------------------------------------------------------------
// see head section in this file for how to configure load_on_scroll
function init() {
if (load_on_scroll) {
if (attachEvent) {
window.attachEvent('onscroll', look_for_trouble);
} else {
window.addEventListener('scroll', look_for_trouble, false);
}
} else {
load_and_append();
}
}
// we are using window.onload since we want "everything" to run
// in the first screeen before we continue.
if (attachEvent) {
window.attachEvent('onload', init);
} else {
window.addEventListener('load', init, false);
}
// ------------------------------------------------------------------------
// the script has started. Below functions are supporting it.
// ------------------------------------------------------------------------
// loaded in load_and_append after a successful ajax call.
function append(txt, src) {
// remove previously #load-more-unlimited-content links since
// it i not needed anymore.
(function(elm_to_remove){
elm_to_remove.parentNode.removeChild(elm_to_remove);
})(document.getElementById('load-more-unlimited-content'));
console.log('install() begin');
install(txt, src);
console.log('install() done');
}
/**
* cleaning function to strip unecessary tags(oup) out.
* also attach and execute scripts with the help of an
* handy little snippet.
*/
function install(ajax_result, src) {
var acc_elm; // final wrapper DIV,
var acc_class; // and it's unique class.
// remove the footer since we don't want that in our result.
ajax_result = ajax_result.match(/^([\n\s\S]+?)<div id="mainBottom/im)
|| ['', ajax_result];
// rename #startPageContainer to avoid css conflicts.
ajax_result = ajax_result[1].replace(/startpageContainer/m,
'startPageContainer_'
+ Math.floor(Math.random() * 1000));
ajax_result = ajax_result.replace(/<scr/ig, '\x3Cscr').replace(/<\/scr/ig, '\x3C/scr');
acc_class = 'aCC-' + src.match(/:\/\/.+?\/(.+?)\?/)[1].replace(/[\/.]/g, '_');
acc_class += ' aCC'+ ++callsCount;
acc_class = 'allColumnsContainer ' + acc_class;
// mount ajax response in a temporary iframe
// and send the markup upwards when done. by doing
// this, all scripts in the ajax response is executed
// correctly.
console.log('iframe begin');
(function(iframe_container){
var iframe_window; // reference
var splitted; // for cross browser script execution
iframe_window = getIFrameWindow(document.getElementById('unlimited-loader'));
iframe_window.document.open();
// open wrappers DIVs.
iframe_window.document.write(
'<div class="' + acc_class +
'"><div class="allColumnsContainer-inner">');
// now comes a tricky part: all but IE will execute
// script tags without any complains. To make the
// last stubborn one work with us, we need to split
// strings to mimic the rusty ol':
// '<scr' + 'ipt>doAmazingStuff()</scr' + 'ipt>';
splitted = ajax_result.split('<scr');
// first chunk can be added right away.
iframe_window.document.write(splitted[0]);
// we are done now unless there was a script in
// the ajax response, which we know if we have more
// than one chunk.
if (splitted.length > 1) {
for (var extracted, i = 1, max = splitted.length; i < max; i++) {
// this is necessary since we need to
// split the end tags as well.
extracted = splitted[i].split('</scr');
// now we can put it together
iframe_window.document.write('<scr'
+ extracted[0] + '</scr' + extracted[1]);
}
}
// close wrapper DIVs
iframe_window.document.write('</div></div>');
// finally, we ask the iframe to send the html
// up to the parent.
iframe_window.document.write('<scr');
iframe_window.document.write('ipt>setTimeout(function(){parent.mkt_importFromIframe(this);}, 999);</scr' + 'ipt>');
iframe_window.document.write('<h1>bu!</h1>');
console.log('iframe document.close begin');
console.log('iframe document.close done');
})((function(){
var div = document.createElement('div');
// the iframe should be visually hidden so
// lets add some css for that.
div.style.position = "absolute";
div.style.left = "-9999px";
div.style.top = "-99px";
div.id = 'mkt_iframe-container'; // do not change this!
div.innerHTML = '<iframe id="unlimited-loader" name="unlimited-loader" src="inner.html"></iframe>';
(function(scr){
scr.parentNode.insertBefore(div, scr);
})(document.getElementsByTagName('script')[0]);
return div;
})());
console.log('iframe done');
}
/**
* callback function which is called when we are using
* scrollbased loading and fires a scroll event.
* It makes sure we are not loading anything
* until it is necessary.
*/
function look_for_trouble(e) {
// first, check to see if we should continue.
if (window.mkt_nothing_more_to_load || window.mkt_prevent_new_loading) {
return; // one or more locks is still active, so we wait.
}
// second, we only want to load new content
// if we are at the bottom of the page.
if (getDocHeight() - getScrollTop() <= window.outerHeight) {
window.mkt_prevent_new_loading = true;
load_and_append();
}
};
//
// borrowed functions.
//
// found at http://james.padolsey.com/javascript/get-document-height-cross-browser/
function getDocHeight() {
return Math.max(
Math.max(document.body.scrollHeight,
document.documentElement.scrollHeight),
Math.max(document.body.offsetHeight,
document.documentElement.offsetHeight),
Math.max(document.body.clientHeight,
document.documentElement.clientHeight)
);
}
// found at http://stackoverflow.com/questions/871399/cross-browser-method-for-detecting-the-scrolltop-of-the-browser-window
function getScrollTop(){
if(typeof pageYOffset!= 'undefined'){
//most browsers
return pageYOffset;
}
else{
var B= document.body; //IE 'quirks'
var D= document.documentElement; //IE with doctype
D= (D.clientHeight)? D: B;
return D.scrollTop;
}
}
// http://av5.com/docs/changing-parent-window-s-url-from-iframe-content.html
function getIFrameWindow(iframe) {
return (iframe.contentWindow) ? iframe.contentWindow : (iframe.contentDocument.document) ? iframe.contentDocument.document : iframe.contentDocument;
}
function getHTTPObject() {
var xhr = false;//set to false, so if it fails, do nothing
if(window.XMLHttpRequest) {//detect to see if browser allows this method
var xhr = new XMLHttpRequest();//set var the new request
} else if(window.ActiveXObject) {//detect to see if browser allows this method
try {
var xhr = new ActiveXObject("Msxml2.XMLHTTP");//try this method first
} catch(e) {//if it fails move onto the next
try {
var xhr = new ActiveXObject("Microsoft.XMLHTTP");//try this method next
} catch(e) {//if that also fails return false.
xhr = false;
}
}
}
return xhr;//return the value of xhr
}
})(window, document);
Maybe I'm wrong, but as far as I know in accordance to the DOM-specification it's not possible in MSIE to move nodes between documents.(and that's what you do, regarding to your description)
for testing:
<html>
<head>
<script type="text/javascript">
<!--
function fx(o)
{
var doc=o.contentWindow.document;
doc.body.appendChild(doc.createTextNode('This works with nodes \
from the same document'));
try{
doc.body.appendChild(document.createTextNode(' and also with nodes \
from another document'));
}
catch(e)
{
doc.body.appendChild(doc.createTextNode(' but not with nodes from \
another document=>['+e.description+']'));
}
}
//-->
</script>
</head>
<body>
<iframe onload="fx(this)" src="about:blank"></iframe>
</body>
</html>