Replace DOM with javascript and run new scripts - javascript

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

Related

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.

load scripts asynchronously

I am using several plugins, custom widgets and some other libraries from JQuery. as a result I have several .js and .css files. I need to create a loader for my site because it takes some time to load. it will be nice if I can display the loader before importing all the:
<script type="text/javascript" src="js/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="js/myFunctions.js"></script>
<link type="text/css" href="css/main.css" rel="stylesheet" />
...
....
etc
I have found several tutorials that enable me to import a JavaScript library asynchronously. for example I can do something like:
(function () {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'js/jquery-ui-1.8.16.custom.min.js';
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
})();
for some reason when I do the same thing for all my files the pages does not work. I have been trying for so long to try to find where the problem is but I just cannot find it. First I thought that it was probably because some javascript functions depended on the others. but I loaded them in the right order using the time out function when one completed I proceeded with the next and the page still behaves weird. for example I am not able to click on links etc... animations still work though..
Anyways
Here is what I have been thinking... I believe browsers have a cache that's why it takes a long time to load the page for the first time and the next time it is quick. so what I am thinking of doing is replacing my index.html page with a page that loads all this files asynchronously. when ajax is done loading all those files redirect to the page that I plan on using. when using that page it should not take long to load since the files should alredy be included on the cache of the browser. on my index page (page where .js and .css file get loaded asynchronously) I don't care of getting errors. I will just be displaying a loader and redirecting the page when done...
Is this idea a good alternative? or should I keep trying on implementing the asynchronously methods?
EDIT
the way I load everything async is like:
importScripts();
function importScripts()
{
//import: jquery-ui-1.8.16.custom.min.js
getContent("js/jquery-1.6.2.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
//s.async = true;
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext1,1);
});
//import: jquery-ui-1.8.16.custom.min.js
function insertNext1()
{
getContent("js/jquery-ui-1.8.16.custom.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext2,1);
});
}
//import: jquery-ui-1.8.16.custom.css
function insertNext2()
{
getContent("css/custom-theme/jquery-ui-1.8.16.custom.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext3,1);
});
}
//import: main.css
function insertNext3()
{
getContent("css/main.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext4,1);
});
}
//import: jquery.imgpreload.min.js
function insertNext4()
{
getContent("js/farinspace/jquery.imgpreload.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext5,1);
});
}
//import: marquee.js
function insertNext5()
{
getContent("js/marquee.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext6,1);
});
}
//import: marquee.css
function insertNext6()
{
getContent("css/marquee.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext,1);
});
}
function insertNext()
{
setTimeout(pageReadyMan,10);
}
}
// get the content of url and pass that content to specified function
function getContent( url, callBackFunction )
{
// attempt to create the XMLHttpRequest and make the request
try
{
var asyncRequest; // variable to hold XMLHttpRequest object
asyncRequest = new XMLHttpRequest(); // create request object
// register event handler
asyncRequest.onreadystatechange = function(){
stateChange(asyncRequest, callBackFunction);
}
asyncRequest.open( 'GET', url, true ); // prepare the request
asyncRequest.send( null ); // send the request
} // end try
catch ( exception )
{
alert( 'Request failed.' );
} // end catch
} // end function getContent
// call function whith content when ready
function stateChange(asyncRequest, callBackFunction)
{
if ( asyncRequest.readyState == 4 && asyncRequest.status == 200 )
{
callBackFunction(asyncRequest.responseText);
} // end if
} // end function stateChange
and the weird part is that all the style's work plus all the javascript functions. the page is frozen for some reason though...
A couple solutions for async loading:
//this function will work cross-browser for loading scripts asynchronously
function loadScript(src, callback)
{
var s,
r,
t;
r = false;
s = document.createElement('script');
s.type = 'text/javascript';
s.src = src;
s.onload = s.onreadystatechange = function() {
//console.log( this.readyState ); //uncomment this line to see which ready states are called.
if ( !r && (!this.readyState || this.readyState == 'complete') )
{
r = true;
callback();
}
};
t = document.getElementsByTagName('script')[0];
t.parentNode.insertBefore(s, t);
}
If you've already got jQuery on the page, just use:
$.getScript(url, successCallback)*
Additionally, it's possible that your scripts are being loaded/executed before the document is done loading, meaning that you'd need to wait for document.ready before events can be bound to the elements.
It's not possible to tell specifically what your issue is without seeing the code.
The simplest solution is to keep all of your scripts inline at the bottom of the page, that way they don't block the loading of HTML content while they execute. It also avoids the issue of having to asynchronously load each required script.
If you have a particularly fancy interaction that isn't always used that requires a larger script of some sort, it could be useful to avoid loading that particular script until it's needed (lazy loading).
* scripts loaded with $.getScript will likely not be cached
For anyone who can use modern features such as the Promise object, the loadScript function has become significantly simpler:
function loadScript(src) {
return new Promise(function (resolve, reject) {
var s;
s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
Be aware that this version no longer accepts a callback argument as the returned promise will handle callback. What previously would have been loadScript(src, callback) would now be loadScript(src).then(callback).
This has the added bonus of being able to detect and handle failures, for example one could call...
loadScript(cdnSource)
.catch(loadScript.bind(null, localSource))
.then(successCallback, failureCallback);
...and it would handle CDN outages gracefully.
HTML5's new 'async' attribute is supposed to do the trick. 'defer' is also supported in most browsers if you care about IE.
async - The HTML
<script async src="siteScript.js" onload="myInit()"></script>
defer - The HTML
<script defer src="siteScript.js" onload="myInit()"></script>
While analyzing the new adsense ad unit code I noticed the attribute and a search lead me here: http://davidwalsh.name/html5-async
I loaded the scripts asynchronously (html 5 has that feature) when all the scripts where done loading I redirected the page to index2.html where index2.html uses the same libraries. Because browsers have a cache once the page redirects to index2.html, index2.html loads in less than a second because it has all it needs to load the page. In my index.html page I also load the images that I plan on using so that the browser place those images on the cache. so my index.html looks like:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Project Management</title>
<!-- the purpose of this page is to load all the scripts on the browsers cache so that pages can load fast from now on -->
<script type="text/javascript">
function stylesheet(url) {
var s = document.createElement('link');
s.type = 'text/css';
s.async = true;
s.src = url;
var x = document.getElementsByTagName('head')[0];
x.appendChild(s);
}
function script(url) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = url;
var x = document.getElementsByTagName('head')[0];
x.appendChild(s);
}
//load scritps to the catche of browser
(function () {
stylesheet('css/custom-theme/jquery-ui-1.8.16.custom.css');
stylesheet('css/main.css');
stylesheet('css/marquee.css');
stylesheet('css/mainTable.css');
script('js/jquery-ui-1.8.16.custom.min.js');
script('js/jquery-1.6.2.min.js');
script('js/myFunctions.js');
script('js/farinspace/jquery.imgpreload.min.js');
script('js/marquee.js');
})();
</script>
<script type="text/javascript">
// once the page is loaded go to index2.html
window.onload = function () {
document.location = "index2.html";
}
</script>
</head>
<body>
<div id="cover" style="position:fixed; left:0px; top:0px; width:100%; height:100%; background-color:Black; z-index:100;">Loading</div>
<img src="images/home/background.png" />
<img src="images/home/3.png"/>
<img src="images/home/6.jpg"/>
<img src="images/home/4.png"/>
<img src="images/home/5.png"/>
<img src="images/home/8.jpg"/>
<img src="images/home/9.jpg"/>
<img src="images/logo.png"/>
<img src="images/logo.png"/>
<img src="images/theme/contentBorder.png"/>
</body>
</html>
another nice thing about this is that I may place a loader in the page and when the page is done loading the loader will go away and in a matte of milliseconds the new page will be running.
Example from google
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js?onload=onLoadCallback';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
Several notes:
s.async = true is not very correct for HTML5 doctype, correct is s.async = 'async' (actually using true is correct, thanks to amn who pointed it out in the comment just below)
Using timeouts to control the order is not very good and safe, and you also make the loading time much larger, to equal the sum of all timeouts!
Since there is a recent reason to load files asynchronously, but in order, I'd recommend a bit more functional-driven way over your example (remove console.log for production use :) ):
(function() {
var prot = ("https:"===document.location.protocol?"https://":"http://");
var scripts = [
"path/to/first.js",
"path/to/second.js",
"path/to/third.js"
];
function completed() { console.log('completed'); } // FIXME: remove logs
function checkStateAndCall(path, callback) {
var _success = false;
return function() {
if (!_success && (!this.readyState || (this.readyState == 'complete'))) {
_success = true;
console.log(path, 'is ready'); // FIXME: remove logs
callback();
}
};
}
function asyncLoadScripts(files) {
function loadNext() { // chain element
if (!files.length) completed();
var path = files.shift();
var scriptElm = document.createElement('script');
scriptElm.type = 'text/javascript';
scriptElm.async = true;
scriptElm.src = prot+path;
scriptElm.onload = scriptElm.onreadystatechange = \
checkStateAndCall(path, loadNext); // load next file in chain when
// this one will be ready
var headElm = document.head || document.getElementsByTagName('head')[0];
headElm.appendChild(scriptElm);
}
loadNext(); // start a chain
}
asyncLoadScripts(scripts);
})();
Thanks to HTML5, you can now declare the scripts that you want to load asynchronously by adding "async" in the tag:
<script async>...</script>
Note: The async attribute is only for external scripts (and should only be used if the src attribute is present).
Note: There are several ways an external script can be executed:
If async is present: The script is executed asynchronously with the rest of the page (the script will be executed while the page continues the parsing)
If async is not present and defer is present: The script is executed when the page has finished parsing
If neither async or defer is present: The script is fetched and executed immediately, before the browser continues parsing the page
See this: http://www.w3schools.com/tags/att_script_async.asp
I wrote a little post to help out with this, you can read more here https://timber.io/snippets/asynchronously-load-a-script-in-the-browser-with-javascript/, but I've attached the helper class below. It will automatically wait for a script to load and return a specified window attribute once it does.
export default class ScriptLoader {
constructor (options) {
const { src, global, protocol = document.location.protocol } = options
this.src = src
this.global = global
this.protocol = protocol
this.isLoaded = false
}
loadScript () {
return new Promise((resolve, reject) => {
// Create script element and set attributes
const script = document.createElement('script')
script.type = 'text/javascript'
script.async = true
script.src = `${this.protocol}//${this.src}`
// Append the script to the DOM
const el = document.getElementsByTagName('script')[0]
el.parentNode.insertBefore(script, el)
// Resolve the promise once the script is loaded
script.addEventListener('load', () => {
this.isLoaded = true
resolve(script)
})
// Catch any errors while loading the script
script.addEventListener('error', () => {
reject(new Error(`${this.src} failed to load.`))
})
})
}
load () {
return new Promise(async (resolve, reject) => {
if (!this.isLoaded) {
try {
await this.loadScript()
resolve(window[this.global])
} catch (e) {
reject(e)
}
} else {
resolve(window[this.global])
}
})
}
}
Usage is like this:
const loader = new Loader({
src: 'cdn.segment.com/analytics.js',
global: 'Segment',
})
// scriptToLoad will now be a reference to `window.Segment`
const scriptToLoad = await loader.load()
I would complete zzzzBov's answer with a check for the presence of callback and allow passing of arguments:
function loadScript(src, callback, args) {
var s, r, t;
r = false;
s = document.createElement('script');
s.type = 'text/javascript';
s.src = src;
if (typeof(callback) === 'function') {
s.onload = s.onreadystatechange = function() {
if (!r && (!this.readyState || this.readyState === 'complete')) {
r = true;
callback.apply(args);
}
};
};
t = document.getElementsByTagName('script')[0];
t.parent.insertBefore(s, t);
}
Here is a great contemporary solution to the asynchronous script loading though it only address the js script with async false.
There is a great article written in www.html5rocks.com - Deep dive into the murky waters of script loading .
After considering many possible solutions, the author concluded that adding js scripts to the end of body element is the best possible way to avoid blocking page rendering by js scripts.
In the mean time, the author added another good alternate solution for those people who are desperate to load and execute scripts asynchronously.
Considering you've four scripts named script1.js, script2.js, script3.js, script4.js then you can do it with applying async = false:
[
'script1.js',
'script2.js',
'script3.js',
'script4.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Now, Spec says: Download together, execute in order as soon as all download.
Firefox < 3.6, Opera says: I have no idea what this “async” thing is, but it just so happens I execute scripts added via JS in the order they’re added.
Safari 5.0 says: I understand “async”, but don’t understand setting it to “false” with JS. I’ll execute your scripts as soon as they land, in whatever order.
IE < 10 says: No idea about “async”, but there is a workaround using “onreadystatechange”.
Everything else says: I’m your friend, we’re going to do this by the book.
Now, the full code with IE < 10 workaround:
var scripts = [
'script1.js',
'script2.js',
'script3.js',
'script4.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && ( pendingScripts[0].readyState == 'loaded' || pendingScripts[0].readyState == 'complete' ) ) {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we’ll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
for HTML5, you can use the 'prefetch'
<link rel="prefetch" href="/style.css" as="style" />
have a look at 'preload' for js.
<link rel="preload" href="used-later.js" as="script">
One reason why your scripts could be loading so slowly is if you were running all of your scripts while loading the page, like this:
callMyFunctions();
instead of:
$(window).load(function() {
callMyFunctions();
});
This second bit of script waits until the browser has completely loaded all of your Javascript code before it starts executing any of your scripts, making it appear to the user that the page has loaded faster.
If you're looking to enhance the user's experience by decreasing the loading time, I wouldn't go for the "loading screen" option. In my opinion that would be much more annoying than just having the page load more slowly.
I would suggest you take a look at Modernizr. Its a small light weight library that you can asynchronously load your javascript with features that allow you to check if the file is loaded and execute the script in the other you specify.
Here is an example of loading jquery:
Modernizr.load([
{
load: '//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.js',
complete: function () {
if ( !window.jQuery ) {
Modernizr.load('js/libs/jquery-1.6.1.min.js');
}
}
},
{
// This will wait for the fallback to load and
// execute if it needs to.
load: 'needs-jQuery.js'
}
]);
You might find this wiki article interesting : http://ajaxpatterns.org/On-Demand_Javascript
It explains how and when to use such technique.
Well, x.parentNode returns the HEAD element, so you are inserting the script just before the head tag. Maybe that's the problem.
Try x.parentNode.appendChild() instead.
Check out this https://github.com/stephen-lazarionok/async-resource-loader. It has an example that shows how to load JS, CSS and multiple files with one shot.
Have you considered using Fetch Injection? I rolled an open source library called fetch-inject to handle cases like these. Here's what your loader might look like using the lib:
fetcInject([
'js/jquery-1.6.2.min.js',
'js/marquee.js',
'css/marquee.css',
'css/custom-theme/jquery-ui-1.8.16.custom.css',
'css/main.css'
]).then(() => {
'js/jquery-ui-1.8.16.custom.min.js',
'js/farinspace/jquery.imgpreload.min.js'
})
For backwards compatibility leverage feature detection and fall-back to XHR Injection or Script DOM Elements, or simply inline the tags into the page using document.write.
Here is my custom solution to eliminate render-blocking JavaScript:
// put all your JS files here, in correct order
const libs = {
"jquery": "https://code.jquery.com/jquery-2.1.4.min.js",
"bxSlider": "https://cdnjs.cloudflare.com/ajax/libs/bxslider/4.2.5/jquery.bxslider.min.js",
"angular": "https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-beta.2/angular.min.js",
"ngAnimate": "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.2/angular-animate.min.js"
}
const loadedLibs = {}
let counter = 0
const loadAsync = function(lib) {
var http = new XMLHttpRequest()
http.open("GET", libs[lib], true)
http.onload = () => {
loadedLibs[lib] = http.responseText
if (++counter == Object.keys(libs).length) startScripts()
}
http.send()
}
const startScripts = function() {
for (var lib in libs) eval(loadedLibs[lib])
console.log("allLoaded")
}
for (var lib in libs) loadAsync(lib)
In short, it loads all your scripts asynchronously, and then executes them consequently.
Github repo: https://github.com/mudroljub/js-async-loader
Here a little ES6 function if somebody wants to use it in React for example
import {uniqueId} from 'lodash' // optional
/**
* #param {String} file The path of the file you want to load.
* #param {Function} callback (optional) The function to call when the script loads.
* #param {String} id (optional) The unique id of the file you want to load.
*/
export const loadAsyncScript = (file, callback, id) => {
const d = document
if (!id) { id = uniqueId('async_script') } // optional
if (!d.getElementById(id)) {
const tag = 'script'
let newScript = d.createElement(tag)
let firstScript = d.getElementsByTagName(tag)[0]
newScript.id = id
newScript.async = true
newScript.src = file
if (callback) {
// IE support
newScript.onreadystatechange = () => {
if (newScript.readyState === 'loaded' || newScript.readyState === 'complete') {
newScript.onreadystatechange = null
callback(file)
}
}
// Other (non-IE) browsers support
newScript.onload = () => {
callback(file)
}
}
firstScript.parentNode.insertBefore(newScript, firstScript)
} else {
console.error(`The script with id ${id} is already loaded`)
}
}
A concise answer, the explanations in the code
function loadScript(src) {
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = src;
//
// script.async = false; // uncomment this line and scripts will be executed in the document order, like 'defer' option
//
// script.defer = true; // uncomment this line when scripts need whole DOM and/or relative order execution is important
//
// the script starts loading as it's append to the document and dynamic script behave as “async” by default
// other scripts don’t wait for 'async' scripts, and 'async' scripts don’t wait for them
// scripts that loads first – runs first (“load-first” order)
document.body.append(script);
}
loadScript('js/example01.js');
loadScript('js/example02.js');
/*
the 'defer' attribute tells the browser not to wait for the script
the 'async' attribute make script to load in the background and run when ready
the 'async' and 'defer' attribute are only for external scripts
'defer' is used for scripts that need the whole DOM and/or their relative execution order is important
'async' is used for independent scripts, like counters or ads, when their relative execution order does not matter
More: https://javascript.info/script-async-defer
*/
You can use LABJS or RequreJS
Script loaders like LABJS, RequireJS will improve the speed and quality of your code.
const dynamicScriptLoading = async (src) =>
{
const response = await fetch(src);
const dataResponse = await response.text();
eval.apply(null, [dataResponse]);
}
I would suggest looking into minifying the files first and see if that gives you a big enough speed boost. If your host is slow, could try putting that static content on a CDN.

How do I ensure server request is complete before page redirect?

EDIT: Just to clarify, my main question boils down to this: if you issue an AJAX request to some server (in this case Google's), but then the client leaves the page before the server completes the request, is it possible (or likely) that the server would abort the request as well, or would the server try to complete the request (in spite of having no one to respond to any more)?
Feel free to read the rest for all the specifics, if you want.
I am using Google Analytics on a page that is meant to immediately redirect. Since the Google javascript file ga.js is loaded asynchronously, I need to make sure that all the script tags that are added dynamically by the javascript are tracked and that the page redirect only happens after those scripts complete. I already handled that part.
The file ga.js seems to make a request to a __utm.gif file with parameters, which is what performs the actual tracking. That request obviously isn't being made from a file I can control, so here are my questions:
First of all, is the request asynchronous (I suspect it is)? Secondly, how can I make sure that that request has time to complete before I redirect the page (I'd prefer not to simply redirect after "enough time" has passed)? Would the request still complete if the originating page redirected before the request was finished (after all, I don't need any data back from Google)?
EDIT: Here's my code:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Redirecting...</title>
<script type="text/javascript">
/* <![CDATA[ */
// Holds a value for each script. When all the values contained in this array are true, all loading has completed.
var scripts = [];
function scriptDetectLoaded(script)
{
scripts.push({ loaded: false, element: script });
var index = scripts.length - 1;
// Will set this script as loaded
var callback = function ()
{
//console.log("Script with index " + index + " finished loading");
scripts[index].loaded = true;
};
// For most browsers
script.onload = callback;
// For IE
script.onreadystatechange = function ()
{
if (this.readyState == "complete")
callback();
};
return index;
}
/* ]]> */
</script>
<!-- Google analytics code -->
<script type="text/javascript">
/* <![CDATA[ */
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-MY_ID-1']);
_gaq.push(['_trackPageview']);
(function ()
{
//debugger;
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
scriptDetectLoaded(ga); var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
/* ]]> */
</script>
</head>
<body>
<noscript>
<p>Please enable JavaScript and refresh this page. Our site won't work correctly without it.</p>
<p>How do I enable JavaScript?</p>
</noscript>
<!-- Google Code for New Account Conversion Page -->
<script type="text/javascript">
/* <![CDATA[ */
//debugger;
var google_conversion_id = SOME_ID;
var google_conversion_language = "en";
var google_conversion_format = "2";
var google_conversion_color = "ffffff";
var google_conversion_label = "SOME_LABEL";
var google_conversion_value = 0;
(function () {
var gad = document.createElement('script'); gad.type = 'text/javascript'; gad.async = true;
gad.src = document.location.protocol + "//www.googleadservices.com/pagead/conversion.js";
scriptDetectLoaded(gad); var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gad, s);
})();
/* ]]> */
</script>
<noscript>
<div style="display:inline;">
<img height="1" width="1" style="border-style:none;" alt="" src="http://www.googleadservices.com/pagead/conversion/1056222251/?label=keCSCNO9qAIQq9jS9wM&guid=ON&script=0"/>
</div>
</noscript>
<!-- Redirect Page -->
<script type="text/javascript">
/* <![CDATA[ */
//debugger;
var url = '/console';
var interval = 100 /*milliseconds*/;
var timeout = 5000 /*milliseconds*/;
var count = 0;
// Set a repeating function that checks to ensure all the scripts have loaded.
// Once all the scripts have loaded, redirect the page.
var id = setInterval(function ()
{
count += interval;
// Check for timeout
if (count > timeout)
{
//console.log("Timed out.");
redirect();
}
// Check to make sure all scripts have loaded
for (var i = 0, len = scripts.length; i < len; i++)
{
if (!scripts[i].loaded)
return;
}
// If we make it here, redirect
redirect();
}, interval);
function redirect()
{
location.replace(url);
// Run this in case the page doesn't redirect properly.
clearInterval(id);
var a = document.createElement("a");
a.href = url;
a.innerHTML = "Please click here to proceed";
var p = document.getElementById("message");
p.innerHTML = "";
p.appendChild(a);
}
/* ]]> */
</script>
<p id="message">Please wait as we redirect the page.</p>
</body>
</html>
If you redirect before the request completes, it will be cancelled on the client side. meaning, the server may have gotten the post and acted on it, but you will not get the results. if these items are added dynamically you cannot use any onload type of functions. i would imagine the google files your adding will have some type of callback you can use that will redirect you.
edit: if you want to load the script in a way that you will know exactly when it is done loading so you can redirect to another page i would use jquery's $.getScript function. it allows for a callback once the file has loaded then you can redirect instantly api.jquery.com/jQuery.getScript
For the tracking that depends on __utm.gif, the request shouldn't need to complete for the metrics to work. Once the browser sends the request (with all the params), GA has what it needs. So you just need to make sure that the image request fires.
If this is an interstitial page with no real content, you may be able to do something like check the length of the document's images collection; when length > 0, then you know GA has created the IMG and set its src (thereby triggering the request). Not sure about the details of that collection, but I know stuff like that used to exist; maybe it still does. In that case, a simple setInterval to perform the test and kick off the redirect should suffice.
Edit: Since the GA stuff adds scripts to the document, you can check the DOM structure to see if scripts with the proper src value exist. That may get you closer to chaining the redirect from the right kind of event.
For the current state of ga.js you can check tracking completing using this code:
(function (global) {
var listeners = []
, ImageReworked
, ImageNative = Image
;
function stringMatch(haystack, needle) {
if (typeof needle.valueOf() === 'string' && haystack.indexOf(needle) !== -1) {
return true;
} else if (needle instanceof RegExp && haystack.match(needle)) {
return true;
}
}
global.addImageListener = function (url, handler) {
if (!listeners.length) {
Image = ImageReworked;
}
listeners.push([url, handler]);
};
global.removeImageListener = function (url, handler) {
var newListeners = [];
listeners.forEach(function (el) {
if (url.constructor !== el[0].constructor //this will not work for object in different windows
|| el[0].toString() !== url.toString()
|| (handler && handler !== el[1])) {
newListeners.push(el);
}
});
listeners = newListeners;
if (!listeners.length) {
Image = ImageNative;
}
};
ImageReworked = function(w, h) {
var i = new ImageNative(w, h)
;
function handler() {
listeners.forEach(function (el) {
var url = el[0]
, handler = el[1]
;
if (stringMatch(i.src, url)) {
handler(i.src);
}
});
}
if (i.addEventListener) {
i.addEventListener('load', handler, false);
} else if (i.attachEvent) {
i.attachEvent('onload', handler);
}
return i;
};
})(window);
Usage example:
addImageListener(/__utm/, function (src) {
var parameters = {};
window.removeImageListener(new RegExp('__utm'));//see, regexp can be created by new RegExp!
(foundParameters = src.match(/(\?|&)(.*?\=[^?&]*)/g)) && foundParameters.forEach(function (parameter) {
var pair = parameter.replace(/^(\?|&)/, '').split('=');
parameters[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
console.log(parameters);
});
_gaq.push(['_setAccount', gaid], ['_trackPageview']);
But you should always keep in mind that if GA changes their tracking logic, this code will be broken.
You also should add timeout (what if load-event will never occure).
And don't use i.onload, because GA-script override this attribute.

Script in Internet Explorer (IE) hangs and becomes unresponsible with empty cache (works fine with cache)

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>

Load javascript async, then check DOM loaded before executing callback

Problem:
Load js files asynchronously, then check to see if the dom is loaded before the callback from loading the files is executed.
edit: We do not use jQuery; we use Prototype.
edit: added more comments to the code example.
I am trying to load all of my js files asynchronously so as to keep them from blocking the rest of the page. But when the scripts load and the callback is called, I need to know if the DOM has been loaded or not, so I know how to structure the callback. See below:
//load asynchronously
(function(){
var e = document.createElement('script');
e.type = "text/javascript";
e.async = true;
e.src = srcstr;
// a little magic to make the callback happen
if(navigator.userAgent.indexOf("Opera")){
e.text = "initPage();";
}else if(navigator.userAgent.indexOf("MSIE")){
e.onreadystatechange = initPage;
}else{
e.innerHTML = "initPage();";
}
// attach the file to the document
document.getElementsByTagName('head')[0].appendChild(e);
})();
initPageHelper = function(){
//requires DOM be loaded
}
initPage = function(){
if(domLoaded){ // if dom is already loaded, just call the function
initPageHelper();
}else{ //if dom is not loaded, attach the function to be run when it does load
document.observe("dom:loaded", initPageHelper);
}
}
The callback gets called properly due to some magic behind the scenes that you can learn about from this Google talk: http://www.youtube.com/watch?v=52gL93S3usU&feature=related
What's the easiest, cross-browser method for asking if the DOM has loaded already?
EDIT
Here's the full solution I went with.
I included prototype and the asynchronous script loader using the normal method. Life is just so much easier with prototype, so I'm willing to block for that script.
<script type="text/javascript" src="prototype/prototype.js"></script>
<script type="text/javascript" src="asyncLoader.js"></script>
And actually, in my code I minified the two files above and put them together into one file to minimize transfer time and http requests.
Then I define what I want to run when the DOM loads, and then call the function to load the other scripts.
<script type="text/javascript">
initPage = function(){
...
}
</script>
<script type="text/javascript">
loadScriptAsync("scriptaculous/scriptaculous.js", initPage);
loadScriptAsync("scriptaculous/effects.js", initPage);
loadScriptAsync("scriptaculous/controls.js", initPage);
...
loadScriptAsync("mypage.js", initPage);
</script>
Likewise, the requests above are actually compressed into one httpRequest using a minifier. They are left separate here for readability. There is a snippet at the bottom of this post showing what the code looks like with the minifier.
The code for asyncLoader.js is the following:
/**
* Allows you to load js files asynchronously, with a callback that can be
* called immediately after the script loads, OR after the script loads and
* after the DOM is loaded.
*
* Prototype.js must be loaded first.
*
* For best results, create a regular script tag that calls a minified, combined
* file that contains Prototype.js, and this file. Then all subsequent scripts
* should be loaded using this function.
*
*/
var onload_queue = [];
var dom_loaded = false;
function loadScriptAsync(src, callback, run_immediately) {
var script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.src = src;
if("undefined" != typeof callback){
script.onload = function() {
if (dom_loaded || run_immediately)
callback();
else
onload_queue.push(callback);
// clean up for IE and Opera
script.onload = null;
script.onreadystatechange = null;
};
script.onreadystatechange = function() {
if (script.readyState == 'complete'){
if (dom_loaded || run_immediately)
callback();
else
onload_queue.push(callback);
// clean up for IE and Opera
script.onload = null;
script.onreadystatechange = null;
}else if(script.readyState == 'loaded'){
eval(script);
if (dom_loaded || run_immediately)
callback();
else
onload_queue.push(callback);
// clean up for IE and Opera
script.onload = null;
script.onreadystatechange = null;
}
};
}
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
}
document.observe("dom:loaded", function(){
dom_loaded = true;
var len = onload_queue.length;
for (var i = 0; i < len; i++) {
onload_queue[i]();
}
onload_queue = null;
});
I added the option to run a script immediately, if you have scripts that don't rely on the page DOM being fully loaded.
The minified requests actually look like:
<script type="text/javascript" src="/min/?b=javascript/lib&f=prototype/prototype.js,asyncLoader.js"></script>
<script type="text/javascript"> initPage = function(e){...}</script>
<script type="text/javascript">
srcstr = "/min/?f=<?=implode(',', $js_files)?>";
loadScriptAsync(srcstr, initPage);
</script>
They are using the plugin from: [http://code.google.com/p/minify/][1]
What you need is a simple queue of onload functions. Also please avoid browser sniffing as it is unstable and not future proof. For full source code see the [Demo]
var onload_queue = [];
var dom_loaded = false;
function loadScriptAsync(src, callback) {
var script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.src = src;
script.onload = script.onreadystatechange = function() {
if (dom_loaded)
callback();
else
onload_queue.push(callback);
// clean up for IE and Opera
script.onload = null;
script.onreadystatechange = null;
};
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
}
function domLoaded() {
dom_loaded = true;
var len = onload_queue.length;
for (var i = 0; i < len; i++) {
onload_queue[i]();
}
onload_queue = null;
};
// Dean's dom:loaded code goes here
// do stuff
domLoaded();
Test usage
loadScriptAsync(
"http://code.jquery.com/jquery-1.4.4.js",
function() {
alert("script has been loaded");
}
);
You can always put your initial loader script at the bottom, right before the closing body tag.

Categories