javascript - how to dynamically embed a blocking script? - javascript

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.

Related

How to recognize that one js file loaded?

I try to display progress bar when page loading. I want change width of progress bar after js files loaded .At the final after load all documents set with of progress bar to 100% .Now I need to recognize js files loading with javascript.
Is this possible ? Please advice.
For internal js files loading recognition:
As functions and variables can be accessed from another file you can set the value of global progress variable and display it's value by calling the function
//on page or head js file
var progress = 0;
function displayProgress()
{
//show progress based on 'progress' variable
}
//file1.js
progress += 10;
displayProgress();
...
//file2.js
progress += 20;
displayProgress();
...
For external js files there is good article. The main idea is to periodically check existense of external functions (typeof fixpng =='function') and if it exist - stop checking and display progress.
Here's the JavaScript code to load the external library with a
callback passed in:
function loadExtScript(src, callback) { var s = document.createElement('script'); s.src = src; document.body.appendChild(s); // if loaded...call the callback }
Firefox allows you to listen for the onload event on the script
element:
s.onload = callback;
With Internet Explorer you can wait for a state change on the script
element:
s.onreadystatechange = function() { if ( this.readyState != "loaded"
) return; callback.call(); }
The problem comes with Safari - there's no event change for Safari, so
we can't tell when the script has loaded. This is the solution I came
up with (and this solution should also work with Opera):
function loadExtScript(src, test, callback) { var s =
document.createElement('script'); s.src = src;
document.body.appendChild(s);
var callbackTimer = setInterval(function() {
var call = false;
try {
call = test.call();
} catch (e) {}
if (call) {
clearInterval(callbackTimer);
callback.call();
} }, 100); }
The function takes a test as a parameter. Since you are the designer
of the app, you'll know what successful test is. Once this test is
true, it will execute the callback. A simple test could be to check
whether a function exists, for example:
loadExtScript('/fixpng.js', function() { return (typeof fixpng ==
'function'); }, myCallbackFunction);
If you know at least one defined namespace (almost all libraries and plugins have it: e.g. jQuery, jQuery.ui, jQuery.mobile, toastr, DataTable, etc.) or one global variable name introduced by the script files which are being loaded, then you can do this:
(function(undefined) {
var scriptFilesLoaded = false,
files = [],
timer = setInterval(function(){
try {
files = [
jQuery,
jQuery.mobile,
jQuery.ui,
someGlobalVariableName
];
if(files.indexOf(undefined)<0){
scriptFilesLoaded = true;
clearInterval(timer);
}
}
catch(e) {
console.warn('Preloader in action: Script files not loaded yet.');
}
},200);
})();
It doesn't matter if the script file is remote or local.

Load Javascript into ajax loaded content

I am new to working with AJAX and have some experience with Java/Jquery. I have been looking around for an solution to my problem but i cant seem to find any.
I am trying to build a function in a webshop where the product will appear in a popup window instead of loading a new page.
I got it working by using this code:
$(".product-slot a").live('click', function() {
var myUrl = $(this).attr("href") + " #product-content";
$("#product-overlay-inner").load(myUrl, function() {
});
$("#product-overlay").fadeIn();
return false;
});
product-slot a = Link to the product in the category page.
product-content = the div i want to insert in the popup from the product page.
product-overlay-inner = The popup window.
product-overlay = The popup wrapper.
The problem that i now have is that my Javascript/Jquery isnt working in the productpopup. For example the lightbox for the product image or the button to add product to shoppingcart doesnt work. Is there anyway to make the javascript work inside the loaded content or to load javascript into the popup?
I hope you can understand what my problem is!
Thank you in advance!
EDIT: The platform im using has jquery-ui-1.7.2
I know this is an old thread but I've been working on a similar process with the same script loading problem and thought I'd share my version as another option.
I have a basic route handler for when a user clicks an anchor/button etc that I use to swap out the main content area of the site, in this example it's the ".page" class.
I then use a function to make an ajax call to get the html content as a partial, at the moment they are php files and they do some preliminary rendering server side to build the html but this isn't necessary.
The callback handles placing the new html and as I know what script I need I just append it to the bottom in a script tag created on the fly. If I have an error at the server I pass this back as content which may be just a key word that I can use to trigger a custom js method to print something more meaningful to the page.
here's a basic implementation based on the register route handler:
var register = function(){
$(".page").html("");
// use the getText ajax function to get the page content:
getText('partials/register.php', function(content) {
$(".page").html(content);
var script = document.createElement('script');
script.src = "js/register.js";
$(".page").append(script);
});
};
/******************************************
* Ajax helpers
******************************************/
// Issue a Http GET request for the contents of the specified Url.
// when the response arrives successfully, verify it's plain text
// and if so, pass it to the specified callback function
function getText(url, callback) {
var request = new XMLHttpRequest();
request.open("GET", url);
request.onreadystatechange = function() {
// if the request is complete and was successful -
if (request.readyState === 4 && request.status === 200) {
// check the content type:
var type = request.getResponseHeader("Content-Type");
if (type.match(/^text/)) {
callback(request.responseText);
}
}
};
// send it:
request.send(null); // nothing to send on GET requests.
}
I find this a good way to 'module-ize' my code into partial views and separated JavaScript files that can be swapped in/out of the page easily.
I will be working on a way to make this more dynamic and even cache these 'modules' for repeated use in an SPA scenario.
I'm relatively new to web dev so if you can see any problems with this or a safer/better way to do it I'm all ears :)
Yes you can load Javascript from a dynamic page, but not with load() as load strips any Javascript and inserts the raw HTML.
Solution: pull down raw page with a get and reattach any Javascript blocks.
Apologies that this is in Typescript, but you should get the idea (if anything, strongly-typed TypeScript is easier to read than plain Javascript):
_loadIntoPanel(panel: JQuery, url: string, callback?: { (): void; })
{
// Regular expression to match <script>...</script> block
var re = /<script\b[^>]*>([\s\S]*?)<\/script>/gm;
var scripts: string = "";
var match;
// Do an async AJAX get
$.ajax({
url: url,
type: "get",
success: function (data: string, status: string, xhr)
{
while (match = re.exec(data))
{
if (match[1] != "")
{
// TODO: Any extra work here to eliminate existing scripts from being inserted
scripts += match[0];
}
}
// Replace the contents of the panel
//panel.html(data);
// If you only want part of the loaded view (assuming it is not a partial view)
// using something like
panel.html($(data).find('#product-content'));
// Add the scripts - will evaluate immediately - beware of any onload code
panel.append(scripts);
if (callback) { callback(); }
},
error: function (xhr, status, error)
{
alert(error);
}
});
}
Plain JQuery/Javascript version with hooks:
It will go something like:
var _loadFormIntoPanel = function (panel, url, callback) {
var that = this;
var re = /<script\b[^>]*>([\s\S]*?)<\/script>/gm;
var scripts = "";
var match;
$.ajax({
url: url,
type: "get",
success: function (data, status, xhr) {
while(match = re.exec(data)) {
if(match[1] != "") {
// TODO: Any extra work here to eliminate existing scripts from being inserted
scripts += match[0];
}
}
panel.html(data);
panel.append(scripts);
if(callback) {
callback();
}
},
error: function (xhr, status, error) {
alert(error);
}
});
};
$(".product-slot a").live('click', function() {
var myUrl = $(this).attr("href") + " #product-content";
_loadFormIntoPanel($("#product-overlay-inner"), myUrl, function() {
// Now do extra stuff to loaded panel here
});
$("#product-overlay").fadeIn();
return false;
});

Replace DOM with javascript and run new scripts

I am trying to replace the whole DOM on page load to do a no-js fallback for a user created knockout page.
I have it replacing the DOM, but when I do the scripts included in the new document aren't running. I was wondering if theres any way of forcing them to run.
<!DOCTYPE html>
<html>
<head>
<title>Title1</title>
</head>
<body>
Hello world <!-- No JS enabled content -->
</body>
<script type="text/javascript">
var model = { 'template' : '\u003chtml\u003e\u003chead\u003e\u003ctitle\u003eTitle2\u003c/title\u003e\u003cscript type=\"text/javascript\"\u003ealert(\"test\");\u003c/script\u003e\u003c/head\u003e\u003cbody\u003eHello world2\u003c/body\u003e\u003c/html\u003e' };
document.documentElement.innerHTML = model.template;
</script>
</html>
template contains the following encoded
<html>
<head>
<title>aaa</title>
<script type='text/javascript'>alert('hello world');</script>
</head>
<body>
Hello world <!-- JS enabled content -->
</body>
</html>
how can I get the alert to run?
As you've discovered, the code in the script tags in the text you assign to innerHTML is not executed. Interestingly, though, on every browser I've tried, the script elements are created and placed in the DOM.
This means it's easy to write a function to run them, in order, and without using eval and its weird effect on scope:
function runScripts(element) {
var scripts;
// Get the scripts
scripts = element.getElementsByTagName("script");
// Run them in sequence (remember NodeLists are live)
continueLoading();
function continueLoading() {
var script, newscript;
// While we have a script to load...
while (scripts.length) {
// Get it and remove it from the DOM
script = scripts[0];
script.parentNode.removeChild(script);
// Create a replacement for it
newscript = document.createElement('script');
// External?
if (script.src) {
// Yes, we'll have to wait until it's loaded before continuing
newscript.onerror = continueLoadingOnError;
newscript.onload = continueLoadingOnLoad;
newscript.onreadystatechange = continueLoadingOnReady;
newscript.src = script.src;
}
else {
// No, we can do it right away
newscript.text = script.text;
}
// Start the script
document.documentElement.appendChild(newscript);
// If it's external, wait for callback
if (script.src) {
return;
}
}
// All scripts loaded
newscript = undefined;
// Callback on most browsers when a script is loaded
function continueLoadingOnLoad() {
// Defend against duplicate calls
if (this === newscript) {
continueLoading();
}
}
// Callback on most browsers when a script fails to load
function continueLoadingOnError() {
// Defend against duplicate calls
if (this === newscript) {
continueLoading();
}
}
// Callback on IE when a script's loading status changes
function continueLoadingOnReady() {
// Defend against duplicate calls and check whether the
// script is complete (complete = loaded or error)
if (this === newscript && this.readyState === "complete") {
continueLoading();
}
}
}
}
Naturally the scripts can't use document.write.
Note how we have to create a new script element. Just moving the existing one elsewhere in the document doesn't work, it's been marked by the browser as having been run (even though it wasn't).
The above will work for most people using innerHTML on an element somewhere in the body of the document, but it won't work for you, because you're actually doing this on the document.documentElement. That means the NodeList we get back from this line:
// Get the scripts
scripts = element.getElementsByTagName("script");
...will keep expanding as we add further scripts to the document.documentElement. So in your particular case, you have to turn it into an array first:
var list, scripts, index;
// Get the scripts
list = element.getElementsByTagName("script");
scripts = [];
for (index = 0; index < list.length; ++index) {
scripts[index] = list[index];
}
list = undefined;
...and later in continueLoading, you have to manually remove entries from the array:
// Get it and remove it from the DOM
script = scripts[0];
script.parentNode.removeChild(script);
scripts.splice(0, 1); // <== The new line
Here's a complete example for most people (not you), including the scripts doing things like function declarations (which would be messed up if we used eval): Live Copy | Live Source
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Run Scripts</title>
</head>
<body>
<div id="target">Click me</div>
<script>
document.getElementById("target").onclick = function() {
display("Updating div");
this.innerHTML =
"Updated with script" +
"<div id='sub'>sub-div</div>" +
"<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js'></scr" + "ipt>" +
"<script>" +
"display('Script one run');" +
"function foo(msg) {" +
" display(msg); " +
"}" +
"</scr" + "ipt>" +
"<script>" +
"display('Script two run');" +
"foo('Function declared in script one successfully called from script two');" +
"$('#sub').html('updated via jquery');" +
"</scr" + "ipt>";
runScripts(this);
};
function runScripts(element) {
var scripts;
// Get the scripts
scripts = element.getElementsByTagName("script");
// Run them in sequence (remember NodeLists are live)
continueLoading();
function continueLoading() {
var script, newscript;
// While we have a script to load...
while (scripts.length) {
// Get it and remove it from the DOM
script = scripts[0];
script.parentNode.removeChild(script);
// Create a replacement for it
newscript = document.createElement('script');
// External?
if (script.src) {
// Yes, we'll have to wait until it's loaded before continuing
display("Loading " + script.src + "...");
newscript.onerror = continueLoadingOnError;
newscript.onload = continueLoadingOnLoad;
newscript.onreadystatechange = continueLoadingOnReady;
newscript.src = script.src;
}
else {
// No, we can do it right away
display("Loading inline script...");
newscript.text = script.text;
}
// Start the script
document.documentElement.appendChild(newscript);
// If it's external, wait for callback
if (script.src) {
return;
}
}
// All scripts loaded
newscript = undefined;
// Callback on most browsers when a script is loaded
function continueLoadingOnLoad() {
// Defend against duplicate calls
if (this === newscript) {
display("Load complete, next script");
continueLoading();
}
}
// Callback on most browsers when a script fails to load
function continueLoadingOnError() {
// Defend against duplicate calls
if (this === newscript) {
display("Load error, next script");
continueLoading();
}
}
// Callback on IE when a script's loading status changes
function continueLoadingOnReady() {
// Defend against duplicate calls and check whether the
// script is complete (complete = loaded or error)
if (this === newscript && this.readyState === "complete") {
display("Load ready state is complete, next script");
continueLoading();
}
}
}
}
function display(msg) {
var p = document.createElement('p');
p.innerHTML = String(msg);
document.body.appendChild(p);
}
</script>
</body>
</html>
And here's your fiddle updated to use the above where we turn the NodeList into an array:
HTML:
<body>
Hello world22
</body>
Script:
var model = {
'template': '\t\u003chtml\u003e\r\n\t\t\u003chead\u003e\r\n\t\t\t\u003ctitle\u003eaaa\u003c/title\u003e\r\n\t\t\t\u003cscript src=\"http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.1/jquery.min.js\"\u003e\u003c/script\u003e\r\n\t\t\t\u003cscript type=\u0027text/javascript\u0027\u003ealert($(\u0027body\u0027).html());\u003c/script\u003e\r\n\t\t\u003c/head\u003e\r\n\t\t\u003cbody\u003e\r\n\t\t\tHello world\r\n\t\t\u003c/body\u003e\r\n\t\u003c/html\u003e'
};
document.documentElement.innerHTML = model.template;
function runScripts(element) {
var list, scripts, index;
// Get the scripts
list = element.getElementsByTagName("script");
scripts = [];
for (index = 0; index < list.length; ++index) {
scripts[index] = list[index];
}
list = undefined;
// Run them in sequence
continueLoading();
function continueLoading() {
var script, newscript;
// While we have a script to load...
while (scripts.length) {
// Get it and remove it from the DOM
script = scripts[0];
script.parentNode.removeChild(script);
scripts.splice(0, 1);
// Create a replacement for it
newscript = document.createElement('script');
// External?
if (script.src) {
// Yes, we'll have to wait until it's loaded before continuing
newscript.onerror = continueLoadingOnError;
newscript.onload = continueLoadingOnLoad;
newscript.onreadystatechange = continueLoadingOnReady;
newscript.src = script.src;
} else {
// No, we can do it right away
newscript.text = script.text;
}
// Start the script
document.documentElement.appendChild(newscript);
// If it's external, wait
if (script.src) {
return;
}
}
// All scripts loaded
newscript = undefined;
// Callback on most browsers when a script is loaded
function continueLoadingOnLoad() {
// Defend against duplicate calls
if (this === newscript) {
continueLoading();
}
}
// Callback on most browsers when a script fails to load
function continueLoadingOnError() {
// Defend against duplicate calls
if (this === newscript) {
continueLoading();
}
}
// Callback on IE when a script's loading status changes
function continueLoadingOnReady() {
// Defend against duplicate calls and check whether the
// script is complete (complete = loaded or error)
if (this === newscript && this.readyState === "complete") {
continueLoading();
}
}
}
}
runScripts(document.documentElement);
This approach just occurred to me today when reading your question. I've never seen it used before, but it works in IE6, IE8, Chrome 26, Firefox 20, and Opera 12.15.
To force the scripts the run you can iterate over the script elements and run them using eval or more preferably using Function but it runs outside the scope of the script tag so you will not have access to local variables. (document.currentScript will be null):
document.querySelectorAll('script').forEach(script => {
if (!script.src)
new Function(script.text)()
else {
fetch(script.src).then(response => {
return response.text().then(text => {
new Function(text)()
});
})
}
})

How to detect when an iframe has already been loaded

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);
}
}

Verify External Script Is Loaded

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.

Categories