How to Dynamically Load Javascript File into HTML - javascript

I'm a bit confused on what's required to dynamically load a JS file into the DOM.
When I include in my HTML file, example.js will run normally.
When I include it will add to the DOM but not run it.
I previously believed that I had to recreate , then append() it to the tag. I feel as if I am missing a crucial step, I just don't know what that step is.
example.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="example.js"></script><!-- working -->
<script src="add-example-dynamically.js"></script><!-- not working -->
</head>
<body>
<script>
execute( anyScriptElement ); // not working
</script>
</body>
</html>
</body>
</html>
add-example-dynamically.js
function toExecutable( tagElement ){
// Duplicate the provided tag as a new element in order for all tags to run the 'src' attribute after adding it to the DOM
// Required to run: <script src=""></script>
var newTag = document.createElement( tagElement.tagName );
if( tagElement.hasAttributes() ){
// Check if the tag has attributes
for( var countAttributes = 0; countAttributes < tagElement.attributes.length; ++countAttributes ){
var name = tagElement.attributes[ countAttributes ].name;
var value = tagElement.attributes[ countAttributes ].value;
newTag.setAttribute( name, value );
}
}
if( tagElement.textContent ){
// Check if the tag has content within it
newTag.textContent = tagElement.textContent;
}
return newTag;
}
function execute( anyScriptElement ){
var tag = toExecutable( anyScriptElement );
document.getElementsByTagName( 'head' )[ 0 ].append( tag );
}
var theScript = document.createElement( 'script' );
theScript.src = 'example.js';
execute( theScript ); // not working
Things I've tried (or a variation of)
error loading javascript files dynamically
I've also been adding .onload and .onreadystatechange to various objects without success.
Things I don't quite yet understand
Dynamically load a JavaScript file
How do you import multiple javascript files in HTML index file without the bloat?
Dynamically load a JavaScript file
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://cleverbeagle.com/blog/articles/tutorial-how-to-load-third-party-scripts-dynamically-in-javascript
https://humanwhocodes.com/blog/2009/07/28/the-best-way-to-load-external-javascript/
Things I don't think solve my problem
http://www.javascriptkit.com/javatutors/loadjavascriptcss.shtml
https://gomakethings.com/a-better-way-to-load-scripts-with-javascript-or-why-document-write-sucks/
Thoughts
I have a feeling that the right solution doesn't involve XMLHttpRequest or Promises but I'm not certain.
My repository in question: Widgets
If someone could point me in the right direction, that would help me figure out what I need to look into.
FYI
Native JS ideal, not interested in JQuery.
I only care about support for Chrome, Firefox, Opera, Safari (desktop / mobile)

So I found the issue was with the order that I was resolving code. It took forever to find because there was nothing inherently wrong with my code, but the sequence was wrong.
I was calling everything in the correct order, but the order that things were resolving in my network panel were incorrect.
Once I fixed the sequence that things were being loaded into the DOM, everything worked as expected.
Fix #1
Because my XMLHttpReqests should be asynchronous, I put all the calls into a single Javascript file so they would run synchronously.
I needed Javascript files to be loaded in the tag before loading function calls that reference those files.
The function calls I wrapped in window.onload = function(){}.
Basically my final solution was for any <script>code</script> that I was dynamically placing in example.html I would wrap in window.onload = function(){}.
i.e. <script>window.onload = function(){ code }</script>
Fix #2
I was using the onload wrapper window.onload = function(){} in a location that did not make sense. Also it may have been nested within another window.onload function at one point while debugging, which probably didn't help.

Related

Getting the ID of current script programmatically [duplicate]

How can I reference the script element that loaded the javascript that is currently running?
Here's the situation. I have a "master" script being loaded high in the page, first thing under the HEAD tag.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="scripts.js"></script>
There is a script in "scripts.js" which needs to be able to do on-demand loading of other scripts. The normal method doesn't quite work for me because I need to add new scripts without referencing the HEAD tag, because the HEAD element hasn't finished rendering:
document.getElementsByTagName('head')[0].appendChild(v);
What I want to do is reference the script element that loaded the current script so that I can then append my new dynamically loaded script tags into the DOM after it.
<script type="text/javascript" src="scripts.js"></script>
loaded by scripts.js--><script type="text/javascript" src="new_script1.js"></script>
loaded by scripts.js --><script type="text/javascript" src="new_script2.js"></script>
How to get the current script element:
1. Use document.currentScript
document.currentScript will return the <script> element whose script is currently being processed.
<script>
var me = document.currentScript;
</script>
Benefits
Simple and explicit. Reliable.
Don't need to modify the script tag
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Will not work in older browsers and IE.
Does not work with modules <script type="module">
2. Select script by id
Giving the script an id attribute will let you easily select it by id from within using document.getElementById().
<script id="myscript">
var me = document.getElementById('myscript');
</script>
Benefits
Simple and explicit. Reliable.
Almost universally supported
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Requires adding a custom attribute to the script tag
id attribute may cause weird behaviour for scripts in some browsers for some edge cases
3. Select the script using a data-* attribute
Giving the script a data-* attribute will let you easily select it from within.
<script data-name="myscript">
var me = document.querySelector('script[data-name="myscript"]');
</script>
This has few benefits over the previous option.
Benefits
Simple and explicit.
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Requires adding a custom attribute to the script tag
HTML5, and querySelector() not compliant in all browsers
Less widely supported than using the id attribute
Will get around <script> with id edge cases.
May get confused if another element has the same data attribute and value on the page.
4. Select the script by src
Instead of using the data attributes, you can use the selector to choose the script by source:
<script src="//example.com/embed.js"></script>
In embed.js:
var me = document.querySelector('script[src="//example.com/embed.js"]');
Benefits
Reliable
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
No custom attributes or id needed
Problems
Does not work for local scripts
Will cause problems in different environments, like Development and Production
Static and fragile. Changing the location of the script file will require modifying the script
Less widely supported than using the id attribute
Will cause problems if you load the same script twice
5. Loop over all scripts to find the one you want
We can also loop over every script element and check each individually to select the one we want:
<script>
var me = null;
var scripts = document.getElementsByTagName("script")
for (var i = 0; i < scripts.length; ++i) {
if( isMe(scripts[i])){
me = scripts[i];
}
}
</script>
This lets us use both previous techniques in older browsers that don't support querySelector() well with attributes. For example:
function isMe(scriptElem){
return scriptElem.getAttribute('src') === "//example.com/embed.js";
}
This inherits the benefits and problems of whatever approach is taken, but does not rely on querySelector() so will work in older browsers.
6. Get the last executed script
Since the scripts are executed sequentially, the last script element will very often be the currently running script:
<script>
var scripts = document.getElementsByTagName( 'script' );
var me = scripts[ scripts.length - 1 ];
</script>
Benefits
Simple.
Almost universally supported
No custom attributes or id needed
Problems
Does not work with asynchronous scripts (defer & async)
Does not work with scripts inserted dynamically
Since scripts are executed sequentially, the currently executed script tag is always the last script tag on the page until then. So, to get the script tag, you can do:
var scripts = document.getElementsByTagName( 'script' );
var thisScriptTag = scripts[ scripts.length - 1 ];
Probably the easiest thing to do would be to give your scrip tag an id attribute.
Here's a bit of a polyfill that leverages document.CurrentScript if it exists and falls back to finding the script by ID.
<script id="uniqueScriptId">
(function () {
var thisScript = document.CurrentScript || document.getElementByID('uniqueScriptId');
// your code referencing thisScript here
());
</script>
If you include this at the top of every script tag I believe you'll be able to consistently know which script tag is being fired, and you'll also be able to reference the script tag in the context of an asynchronous callback.
Untested, so leave feedback for others if you try it.
Script are executed sequentially only if they do not have either a "defer" or an "async" attribute. Knowing one of the possible ID/SRC/TITLE attributes of the script tag could work also in those cases. So both Greg and Justin suggestions are correct.
There is already a proposal for a document.currentScript on the WHATWG lists.
EDIT: Firefox > 4 already implement this very useful property but it is not available in IE11 last I checked and only available in Chrome 29 and Safari 8.
EDIT: Nobody mentioned the "document.scripts" collection but I believe that the following may be a good cross browser alternative to get the currently running script:
var me = document.scripts[document.scripts.length -1];
It must works at page load and when an script tag is added with javascript (ex. with ajax)
<script id="currentScript">
var $this = document.getElementById("currentScript");
$this.setAttribute("id","");
//...
</script>
To get the script, that currently loaded the script you can use
var thisScript = document.currentScript;
You need to keep a reference at the beginning of your script, so you can call later
var url = thisScript.src
An approach for dealing with async & deferred scripts is to leverage the onload handler- set an onload handler for all script tags and the first one which executes should be yours.
function getCurrentScript(callback) {
if (document.currentScript) {
callback(document.currentScript);
return;
}
var scripts = document.scripts;
function onLoad() {
for (var i = 0; i < scripts.length; ++i) {
scripts[i].removeEventListener('load', onLoad, false);
}
callback(event.target);
}
for (var i = 0; i < scripts.length; ++i) {
scripts[i].addEventListener('load', onLoad, false);
}
}
getCurrentScript(function(currentScript) {
window.console.log(currentScript.src);
});
Follow these simple steps to obtain reference to current executing script block:
Put some random unique string within the script block (must be unique / different in each script block)
Iterate result of document.getElementsByTagName('script'), looking the unique string from each of their content (obtained from innerText/textContent property).
Example (ABCDE345678 is the unique ID):
<script type="text/javascript">
var A=document.getElementsByTagName('script'),i=count(A),thi$;
for(;i;thi$=A[--i])
if((thi$.innerText||thi$.textContent).indexOf('ABCDE345678'))break;
// Now thi$ is refer to current script block
</script>
btw, for your case, you can simply use old fashioned document.write() method to include another script.
As you mentioned that DOM is not rendered yet, you can take advantage from the fact that browser always execute script in linear sequence (except for deferred one that will be rendered later), so the rest of your document is still "not exists".
Anything you write through document.write() will be placed right after the caller script.
Example of original HTML page:
<!doctype html>
<html><head>
<script src="script.js"></script>
<script src="otherscript.js"></script>
<body>anything</body></html>
Content of script.js:
document.write('<script src="inserted.js"></script>');
After rendered, the DOM structure will become:
HEAD
SCRIPT script.js
SCRIPT inserted.js
SCRIPT otherscript.js
BODY
Consider this algorithm. When your script loads (if there are multiple identical scripts), look through document.scripts, find the first script with the correct "src" attribute, and save it and mark it as 'visited' with a data-attribute or unique className.
When the next script loads, scan through document.scripts again, passing over any script already marked as visited. Take the first unvisited instance of that script.
This assumes that identical scripts will likely execute in the order in which they are loaded, from head to body, from top to bottom, from synchronous to asynchronous.
(function () {
var scripts = document.scripts;
// Scan for this data-* attribute
var dataAttr = 'data-your-attribute-here';
var i = 0;
var script;
while (i < scripts.length) {
script = scripts[i];
if (/your_script_here\.js/i.test(script.src)
&& !script.hasAttribute(dataAttr)) {
// A good match will break the loop before
// script is set to null.
break;
}
// If we exit the loop through a while condition failure,
// a check for null will reveal there are no matches.
script = null;
++i;
}
/**
* This specific your_script_here.js script tag.
* #type {Element|Node}
*/
var yourScriptVariable = null;
// Mark the script an pass it on.
if (script) {
script.setAttribute(dataAttr, '');
yourScriptVariable = script;
}
})();
This will scan through all the script for the first matching script that isn't marked with the special attribute.
Then mark that node, if found, with a data-attribute so subsequent scans won't choose it. This is similar to graph traversal BFS and DFS algorithms where nodes may be marked as 'visited' to prevent revisitng.
I've got this, which is working in FF3, IE6 & 7. The methods in the on-demand loaded scripts aren't available until page load is complete, but this is still very useful.
//handle on-demand loading of javascripts
makescript = function(url){
var v = document.createElement('script');
v.src=url;
v.type='text/javascript';
//insertAfter. Get last <script> tag in DOM
d=document.getElementsByTagName('script')[(document.getElementsByTagName('script').length-1)];
d.parentNode.insertBefore( v, d.nextSibling );
}
I was inserting script tags dynamically with this usual alternative to eval and simply set a global property currentComponentScript right before adding to the DOM.
const old = el.querySelector("script")[0];
const replacement = document.createElement("script");
replacement.setAttribute("type", "module");
replacement.appendChild(document.createTextNode(old.innerHTML));
window.currentComponentScript = replacement;
old.replaceWith(replacement);
Doesn't work in a loop though. The DOM doesn't run the scripts until the next macrotask so a batch of them will only see the last value set. You'd have to setTimeout the whole paragraph, and then setTimeout the next one after the previous finishes. I.e. chain the setTimeouts, not just call setTimeout multiple times in a row from a loop.
If you can assume the file name of the script, you can find it. I've only really tested the following function in Firefox so far.
function findMe(tag, attr, file) {
var tags = document.getElementsByTagName(tag);
var r = new RegExp(file + '$');
for (var i = 0;i < tags.length;i++) {
if (r.exec(tags[i][attr])) {
return tags[i][attr];
}
}
};
var element = findMe('script', 'src', 'scripts.js');
I have found the following code to be the most consistent, performant, and simple.
var scripts = document.getElementsByTagName('script');
var thisScript = null;
var i = scripts.length;
while (i--) {
if (scripts[i].src && (scripts[i].src.indexOf('yourscript.js') !== -1)) {
thisScript = scripts[i];
break;
}
}
console.log(thisScript);

How to get the surrounding DOM element from Javascript located inside [duplicate]

How can I reference the script element that loaded the javascript that is currently running?
Here's the situation. I have a "master" script being loaded high in the page, first thing under the HEAD tag.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script type="text/javascript" src="scripts.js"></script>
There is a script in "scripts.js" which needs to be able to do on-demand loading of other scripts. The normal method doesn't quite work for me because I need to add new scripts without referencing the HEAD tag, because the HEAD element hasn't finished rendering:
document.getElementsByTagName('head')[0].appendChild(v);
What I want to do is reference the script element that loaded the current script so that I can then append my new dynamically loaded script tags into the DOM after it.
<script type="text/javascript" src="scripts.js"></script>
loaded by scripts.js--><script type="text/javascript" src="new_script1.js"></script>
loaded by scripts.js --><script type="text/javascript" src="new_script2.js"></script>
How to get the current script element:
1. Use document.currentScript
document.currentScript will return the <script> element whose script is currently being processed.
<script>
var me = document.currentScript;
</script>
Benefits
Simple and explicit. Reliable.
Don't need to modify the script tag
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Will not work in older browsers and IE.
Does not work with modules <script type="module">
2. Select script by id
Giving the script an id attribute will let you easily select it by id from within using document.getElementById().
<script id="myscript">
var me = document.getElementById('myscript');
</script>
Benefits
Simple and explicit. Reliable.
Almost universally supported
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Requires adding a custom attribute to the script tag
id attribute may cause weird behaviour for scripts in some browsers for some edge cases
3. Select the script using a data-* attribute
Giving the script a data-* attribute will let you easily select it from within.
<script data-name="myscript">
var me = document.querySelector('script[data-name="myscript"]');
</script>
This has few benefits over the previous option.
Benefits
Simple and explicit.
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
Problems
Requires adding a custom attribute to the script tag
HTML5, and querySelector() not compliant in all browsers
Less widely supported than using the id attribute
Will get around <script> with id edge cases.
May get confused if another element has the same data attribute and value on the page.
4. Select the script by src
Instead of using the data attributes, you can use the selector to choose the script by source:
<script src="//example.com/embed.js"></script>
In embed.js:
var me = document.querySelector('script[src="//example.com/embed.js"]');
Benefits
Reliable
Works with asynchronous scripts (defer & async)
Works with scripts inserted dynamically
No custom attributes or id needed
Problems
Does not work for local scripts
Will cause problems in different environments, like Development and Production
Static and fragile. Changing the location of the script file will require modifying the script
Less widely supported than using the id attribute
Will cause problems if you load the same script twice
5. Loop over all scripts to find the one you want
We can also loop over every script element and check each individually to select the one we want:
<script>
var me = null;
var scripts = document.getElementsByTagName("script")
for (var i = 0; i < scripts.length; ++i) {
if( isMe(scripts[i])){
me = scripts[i];
}
}
</script>
This lets us use both previous techniques in older browsers that don't support querySelector() well with attributes. For example:
function isMe(scriptElem){
return scriptElem.getAttribute('src') === "//example.com/embed.js";
}
This inherits the benefits and problems of whatever approach is taken, but does not rely on querySelector() so will work in older browsers.
6. Get the last executed script
Since the scripts are executed sequentially, the last script element will very often be the currently running script:
<script>
var scripts = document.getElementsByTagName( 'script' );
var me = scripts[ scripts.length - 1 ];
</script>
Benefits
Simple.
Almost universally supported
No custom attributes or id needed
Problems
Does not work with asynchronous scripts (defer & async)
Does not work with scripts inserted dynamically
Since scripts are executed sequentially, the currently executed script tag is always the last script tag on the page until then. So, to get the script tag, you can do:
var scripts = document.getElementsByTagName( 'script' );
var thisScriptTag = scripts[ scripts.length - 1 ];
Probably the easiest thing to do would be to give your scrip tag an id attribute.
Here's a bit of a polyfill that leverages document.CurrentScript if it exists and falls back to finding the script by ID.
<script id="uniqueScriptId">
(function () {
var thisScript = document.CurrentScript || document.getElementByID('uniqueScriptId');
// your code referencing thisScript here
());
</script>
If you include this at the top of every script tag I believe you'll be able to consistently know which script tag is being fired, and you'll also be able to reference the script tag in the context of an asynchronous callback.
Untested, so leave feedback for others if you try it.
Script are executed sequentially only if they do not have either a "defer" or an "async" attribute. Knowing one of the possible ID/SRC/TITLE attributes of the script tag could work also in those cases. So both Greg and Justin suggestions are correct.
There is already a proposal for a document.currentScript on the WHATWG lists.
EDIT: Firefox > 4 already implement this very useful property but it is not available in IE11 last I checked and only available in Chrome 29 and Safari 8.
EDIT: Nobody mentioned the "document.scripts" collection but I believe that the following may be a good cross browser alternative to get the currently running script:
var me = document.scripts[document.scripts.length -1];
It must works at page load and when an script tag is added with javascript (ex. with ajax)
<script id="currentScript">
var $this = document.getElementById("currentScript");
$this.setAttribute("id","");
//...
</script>
To get the script, that currently loaded the script you can use
var thisScript = document.currentScript;
You need to keep a reference at the beginning of your script, so you can call later
var url = thisScript.src
An approach for dealing with async & deferred scripts is to leverage the onload handler- set an onload handler for all script tags and the first one which executes should be yours.
function getCurrentScript(callback) {
if (document.currentScript) {
callback(document.currentScript);
return;
}
var scripts = document.scripts;
function onLoad() {
for (var i = 0; i < scripts.length; ++i) {
scripts[i].removeEventListener('load', onLoad, false);
}
callback(event.target);
}
for (var i = 0; i < scripts.length; ++i) {
scripts[i].addEventListener('load', onLoad, false);
}
}
getCurrentScript(function(currentScript) {
window.console.log(currentScript.src);
});
Follow these simple steps to obtain reference to current executing script block:
Put some random unique string within the script block (must be unique / different in each script block)
Iterate result of document.getElementsByTagName('script'), looking the unique string from each of their content (obtained from innerText/textContent property).
Example (ABCDE345678 is the unique ID):
<script type="text/javascript">
var A=document.getElementsByTagName('script'),i=count(A),thi$;
for(;i;thi$=A[--i])
if((thi$.innerText||thi$.textContent).indexOf('ABCDE345678'))break;
// Now thi$ is refer to current script block
</script>
btw, for your case, you can simply use old fashioned document.write() method to include another script.
As you mentioned that DOM is not rendered yet, you can take advantage from the fact that browser always execute script in linear sequence (except for deferred one that will be rendered later), so the rest of your document is still "not exists".
Anything you write through document.write() will be placed right after the caller script.
Example of original HTML page:
<!doctype html>
<html><head>
<script src="script.js"></script>
<script src="otherscript.js"></script>
<body>anything</body></html>
Content of script.js:
document.write('<script src="inserted.js"></script>');
After rendered, the DOM structure will become:
HEAD
SCRIPT script.js
SCRIPT inserted.js
SCRIPT otherscript.js
BODY
Consider this algorithm. When your script loads (if there are multiple identical scripts), look through document.scripts, find the first script with the correct "src" attribute, and save it and mark it as 'visited' with a data-attribute or unique className.
When the next script loads, scan through document.scripts again, passing over any script already marked as visited. Take the first unvisited instance of that script.
This assumes that identical scripts will likely execute in the order in which they are loaded, from head to body, from top to bottom, from synchronous to asynchronous.
(function () {
var scripts = document.scripts;
// Scan for this data-* attribute
var dataAttr = 'data-your-attribute-here';
var i = 0;
var script;
while (i < scripts.length) {
script = scripts[i];
if (/your_script_here\.js/i.test(script.src)
&& !script.hasAttribute(dataAttr)) {
// A good match will break the loop before
// script is set to null.
break;
}
// If we exit the loop through a while condition failure,
// a check for null will reveal there are no matches.
script = null;
++i;
}
/**
* This specific your_script_here.js script tag.
* #type {Element|Node}
*/
var yourScriptVariable = null;
// Mark the script an pass it on.
if (script) {
script.setAttribute(dataAttr, '');
yourScriptVariable = script;
}
})();
This will scan through all the script for the first matching script that isn't marked with the special attribute.
Then mark that node, if found, with a data-attribute so subsequent scans won't choose it. This is similar to graph traversal BFS and DFS algorithms where nodes may be marked as 'visited' to prevent revisitng.
I've got this, which is working in FF3, IE6 & 7. The methods in the on-demand loaded scripts aren't available until page load is complete, but this is still very useful.
//handle on-demand loading of javascripts
makescript = function(url){
var v = document.createElement('script');
v.src=url;
v.type='text/javascript';
//insertAfter. Get last <script> tag in DOM
d=document.getElementsByTagName('script')[(document.getElementsByTagName('script').length-1)];
d.parentNode.insertBefore( v, d.nextSibling );
}
I was inserting script tags dynamically with this usual alternative to eval and simply set a global property currentComponentScript right before adding to the DOM.
const old = el.querySelector("script")[0];
const replacement = document.createElement("script");
replacement.setAttribute("type", "module");
replacement.appendChild(document.createTextNode(old.innerHTML));
window.currentComponentScript = replacement;
old.replaceWith(replacement);
Doesn't work in a loop though. The DOM doesn't run the scripts until the next macrotask so a batch of them will only see the last value set. You'd have to setTimeout the whole paragraph, and then setTimeout the next one after the previous finishes. I.e. chain the setTimeouts, not just call setTimeout multiple times in a row from a loop.
If you can assume the file name of the script, you can find it. I've only really tested the following function in Firefox so far.
function findMe(tag, attr, file) {
var tags = document.getElementsByTagName(tag);
var r = new RegExp(file + '$');
for (var i = 0;i < tags.length;i++) {
if (r.exec(tags[i][attr])) {
return tags[i][attr];
}
}
};
var element = findMe('script', 'src', 'scripts.js');
I have found the following code to be the most consistent, performant, and simple.
var scripts = document.getElementsByTagName('script');
var thisScript = null;
var i = scripts.length;
while (i--) {
if (scripts[i].src && (scripts[i].src.indexOf('yourscript.js') !== -1)) {
thisScript = scripts[i];
break;
}
}
console.log(thisScript);

Edit a <script> tag in the <head> before it loads and runs the script

I am on an e-commerce platform where I can edit the <head>, however some things that are injected into the head are out of reach for users. So even though we can edit the <head>, there are injections which are out of reach and therefore unremovable via the traditional method.
PS: I can put script before or after these injected JS script tags, which are generated and populated along with my scripts. And so my script would run before the injected tags if I place my script before their "tag injection line."
The Problem
The problem is, this platform started injecting analytics and spam into the head, basically jacking our customers info and selling it to third parties. So I want to disable their crappy scripts.
<script type="text/javascript" async="" src="/some.JS.file.min.js"></script>
<script type="text/javascript" async="" src="/another.JS.file.min.js"></script>
The Question
Is it possible with javascript or jquery to write a script that will edit tags before they run? I can insert this custom script before the tags are in injected. I was wrong -- the unwanted <script> tags are always PREpended to the first non-commented <script> tag, and so no javascript will work to hack up the tags before they run.
What I Have Tried So Far
I found this incomplete and not working answer from this SO question.
When I run the full script with the right details entered for my own site, I get so many errors it's difficult to know where to begin as I have no idea what all the XHR stuff is for or what it does, and some of the errors are ones I've never even seen before.
When I run just this part, which I somewhat understand:
doc = document.implementation.createHTMLDocument(""+(document.title || ""));
scripts = doc.getElementsByTagName("script");
//Modify scripts as you please
[].forEach.call( scripts, function( script ) {
if(script.getAttribute("src") == "/some.JS.file.min.js"
|| script.getAttribute("src") == "/another.JS.file.min.js") {
script.removeAttribute("src");
}
});
EDIT UPDATE:
Their script is inserted AFTER my scripts. That is, I can insert the script into the <head> before their script tags or after. We are looking into new platforms now but I still need to solve this in the meantime as it will be months before we switch. I was hoping g there is some JavaScript I am not aware of that can edit HTML script tags before they run, if this script runs before they do.
EDIT 2:
Nit's answer window.bcanalytics = function () {}; works great and breaks most of it by breaking window.bcanalytics.push but somehow some of it still survives.
In this block:
<script type="text/javascript">
(function() {
window.bcanalytics || (window.bcanalytics = []), window.bcanalytics.methods = ["debug", "identify", "track",
"trackLink", "trackForm", "trackClick", "trackSubmit", "page", "pageview", "ab", "alias", "ready", "group",
"on", "once", "off", "initialize"], window.bcanalytics.factory = function(a) {
return function()
{
var b = Array.prototype.slice.call(arguments);
return b.unshift(a), window.bcanalytics.push(b),
window.bcanalytics
}
};
for (var i = 0; i < window.bcanalytics.methods.length; i++)
{
var method = window.bcanalytics.methods[i];
window.bcanalytics[method] = window.bcanalytics.factory(method)
}
window.bcanalytics.load = function() {
var a = document.createElement("script");
a.type = "text/javascript",
a.async = !0, a.src = "http://cdn5.bigcommerce.com/r-2b2d3f12176a8a1ca3cbd41bddc9621d2657d707/app/assets/js/vendor/bigcommerce/analytics.min.js";
var b = document.getElementsByTagName("script")[0];
// This line still runs and loads analytics.min.js
// This line still runs and loads analytics.min.js
// This line still runs and loads analytics.min.js
b.parentNode.insertBefore(a, b)
// ^^^ This line still runs and loads analytics.min.js
// This line still runs and loads analytics.min.js
// This line still runs and loads analytics.min.js
}, window.bcanalytics.SNIPPET_VERSION = "2.0.8", window.bcanalytics.load();
bcanalytics.initialize({"Fornax": {"host": "https:\/\/analytics.bigcommerce.com","cdn": "http:\/\/cdn5.bigcommerce.com\/r-2b2d3f12176a8a1ca3cbd41bddc9621d2657d707\/app\/assets\/js\/vendor\/bigcommerce\/fornax.min.js","defaultEventProperties": {"storeId": 729188,"experiments": {"shipping.eldorado.ng-shipment.recharge-postage": "on","shipping.eldorado.label_method": "on","cp2.lightsaber": "on","PMO-272.cp1_new_product_options": "on","cart.limit_number_of_unique_items": "control","cart.auto_remove_items_over_limit": "control","BIG-15465.limit_flash_messages": "control","BIG-15230.sunset_design_mode": "control","bigpay.checkout_authorizenet.live": "on","bigpay.checkout_authorizenet.live.employee.store": "control","bigpay.checkout_authorizenet.test": "on","bigpay.checkout_authorizenet.test.employee.store": "control","bigpay.checkout_stripe.live": "on","bigpay.checkout_stripe.live.employee.store": "control","bigpay.checkout_stripe.test": "on","bigpay.checkout_stripe.test.employee.store": "control","sessions.flexible_storage": "on","PMO-439.ng_payments.phase1": "control","PMO-515.ng_payments.phase2": "control","PROJECT-331.pos_manager": "control","PROJECT-453.enterprise_apps": "control","shopping.checkout.cart_to_paid": "legacy_ui","onboarding.initial_user_flow.autoprovision": "on","faceted_search.enabled": "off","faceted_search.displayed": "off","themes.previewer": "enabled"}},"defaultContext": {"source": "Bigcommerce Storefront"},"anonymousId": "24a35a36-7153-447e-b784-c3203670f644"}});
})();
</script>
window.bcanalytics.load manages to survive and loads analytics.min.js (according to the Network tab), though I can't tell if the script then runs or doesn't.
Also, I've figured out that these pesky HTML lines:
<script type="text/javascript" defer="" async="" src="http://tracker.boostable.com/boost.bigcommerce.js"></script>
<script type="text/javascript" async="" defer="" src="http://cdn5.bigcommerce.com/r-2b2d3f12176a8a1ca3cbd41bddc9621d2657d707/javascript/jirafe/beacon_api.js"></script>
<script type="text/javascript" async="" src="http://cdn5.bigcommerce.com/r-2b2d3f12176a8a1ca3cbd41bddc9621d2657d707/app/assets/js/vendor/bigcommerce/analytics.min.js"></script>
<script type="text/javascript" async="" src="http://www.google-analytics.com/plugins/ua/ecommerce.js"></script>
are Always PREpended to the first non-commented <script> opening tag, so unfortunately, none of the creatively destructive methods below will work, as any script I try to insert ahead of these tags will automatically find the pesky unwanted lines appended before it.
Assuming the offending code is similar to that of the question you linked to, I would simply try to break the offending code so it fails to execute.
From hereon the answer relies on code from the other question since you didn't provide any.
The offending code relies on analytics, which is ensured on the page at the beginning of the script:
(function(){
window.analytics||(window.analytics=[]),window.analytics.methods=["debug","identify","track","trackLink","trackForm","trackClick","trackSubmit","page","pageview","ab","alias","ready","group","on","once","off","initialize"],window.analytics.factory=function(a){return function(){var b=Array.prototype.slice.call(arguments);return b.unshift(a),window.analytics.push(b),window.analytics}};for(var i=0;i<window.analytics.methods.length;i++){var method=window.analytics.methods[i];window.analytics[method]=window.analytics.factory(method)}window.analytics.load=function(){var a=document.createElement("script");a.type="text/javascript",a.async=!0,a.src="http://cdn2.bigcommerce.com/r6cb05f0157ab6c6a38c325c12cfb4eb064cc3d6f/app/assets/js/analytics.min.js";var b=document.getElementsByTagName("script")[0];b.parentNode.insertBefore(a,b)},window.analytics.SNIPPET_VERSION="2.0.8",window.analytics.load();
//The rest of the script
})();
To break the whole script and prevent it from running you should simply assign window.analytics a value that will conflict with the methods that are used.
So, for example, you could run a script before the offending script that simply assigns the following:
window.analytics = function () {};
Which will result in the offending script failing due to a type error.
If you know you can at least get your scripts to run first, one (albeit hacky) solution is to just absolutely "trash" the JS environment for the next script, so it has some problems. For example:
//trash it
document.getElementById=null;
document.querySelector=null;
document.querySelectorAll=null;
window.console=null;
window.alert=null;
document.getElementsByTagName=null;
document.getElementsByClassName=null;
As soon as the enemy script tries using one of those functions, it will just crap out. Those are just some common methods off the top of my head... find out which ones its using, and nuke those. Of course, nuking anything you need for events on your own page could be an issue.
How are the scripts being injected? If it's through something like document.createElement, you could attempt to hijack that function and disable it if the element name is script:
var origCreate = document.createElement;
document.createElement = function (name) {
if (name.toLowerCase() !== 'script') {
origCreate.call(document, name);
}
};
Since the scripts are being inserted server-side, you won't be able to disable the running of the scripts in your JavaScript. However, if you're able to inject any arbitrary text before and after the scripts being inserted, you could try commenting out the script tags by inserting this first:
<!--
...then this after:
-->
If the scripts get injected between these, it will hopefully cause the HTML parser to ignore the scripts.
Update:
Sounds like you need to disable just some of this content, so commenting everything out won't work. However, if before/after hijacking works, you could potentially wrap the injected scripts in a DOM element, parse that content, strip out the scripts you don't want, and inject the scripts so they run:
Inject something like this before:
<style id="hijack" type="text/html">
...and this after:
</style>
<script>
var hijackedWrapper = document.getElementById('hijack');
var scripts = hijackedWrapper.textContent;
scripts = scripts.replace('<script src="http://some.domain.com/foo.js"></s' + 'cript>', '');
document.write(scripts); // There's better ways to do this, but is just an illustration
</script>
Like the others, I would suggest sabotaging the js environment for the hostile script, and then recovering it back once you need it.
For example, if the script relies on document.getElementById, you can do this
var restore = {
getElementById: document.getElementById
};
document.getElementById = null;
and then if you have a need to use document.getElementById later, you can restore it back:
document.getElementById = restore.getElementById;
I also wanted to note that removing the actual script tags, as far as I can tell, is not possible:
If you put in a script before the hostile scripts, then they will not be loaded in the DOM yet, so it can't see anything to remove.
If you put in a script after the hostile scripts, the hostile scripts will already be loaded.

Cross-browser onload event in a static script tag

Is there a cross-browser way to associate an onload event to a static script tag, in a html document?
The following will not work in IE 7 and IE 8:
<script onload="DoThat" type="text/javascript" src="..."></script>
Some background
I have found a way to accomplish this with a dynamic script tag and if statements. It is for example explained in this MSDN article.
My issue is that I need to locate the current script tag, because I am building widgets that insert DOM elements in-place. In the past I have found some workarounds to do this, but they all have their downside. I am hoping that using the "this" keyword on a script onload event will help.
As far as I understand, you need your code to know what script element it belongs to. You would then insert necessary HTML widget content right next to this script or append to the script parent, etc. If so then I would propose you another approach.
Inside you widget javascript file you place next line of code:
var scripts = document.getElementsByTagName('script'),
thisScript = scripts[scripts.length - 1];
thisScript is the DOM element you are looking for, current script element this code located in. So simple.
Why it works.
Detail explanation http://feather.elektrum.org/book/src.html. Just an abstract:
When a script with a src is loaded in a page, the rest of the page is not yet written. The implication is that no matter how many scripts a page contains, the one currently starting to execute is the last one
This is fairly simple and effective technic, although but not so many people know about this possibility. It's reliable and 100% browser compatible. By the way, Google uses this code to render +1 buttons:
<script type="text/javascript" src="https://apis.google.com/js/plusone.js">
{lang: 'dk'}
</script>
So how do think how the api script could get this hash parameters? In my example it would be thisScript.innerHTML
I'm not sure if I caught your needs.
Anyway to locate the current script you can try this example
<script src="jquery-1.7.1.min.js" varTest="valueTest" id="myID"></script>
<script>
var scripts = document.getElementsByTagName('script');
var pattern = 'jquery-1.7.1.min.js';
var i = undefined;
for (var x=0; x<scripts.length; x++)
{
if (scripts[x].src.match(pattern)) { i = x; break; }
}
if (typeof(i) != 'undefined')
{
// Do any association needed here
var current_script = scripts[i];
// Just for test
console.log(current_script.attributes);
}
</script>

Use JavaScript to prevent a later `<script>` tag from being evaluated?

This is a bit of an oddball use case, but I have my reasons:
I'd like to be able to write
<script type="text/javascript" src="first.js"></script>
<script type="text/javascript" src="second.js"></script>
in my markup and, using the code in first.js, prevent or delay the execution of second.js. Is this possible, in any browser? What if the contents of first.js are inlined? (If it helps, assume that the second script tag has an id attribute.)
Since I've gotten a couple of answers that missed what I'm getting at, I should clarify:
The solution must be entirely within first.js. Anything that require changes to the original HTML of the page, or to second.js, is not acceptable.
It is acceptable to load second.js via Ajax and execute it using eval. That's the easy part. The hard part is preventing the immediate execution of second.js.
Assume that you don't know what's in second.js. So, you can't just replace each global function called by second.js with a no-op function. (Plus, this would almost certainly lead to errors.)
If you know of a solution that works in some browsers but not in others, I'd love to hear it.
Example: To make this a little more concrete, let's say that the code
<script type="text/javascript">
function func() {
window.meaningOfLife = 42;
window.loadSecond();
};
setTimeout(func, 10);
</script>
precedes the two script includes, and that second.js contains the line
if (window.meaningOfLife !== 42) {throw new Error();}
first.js should be able to prevent this error by delaying second.js from executing until window.loadSecond is run. (Assume the implementation of window.loadSecond is also in first.js.) It is not allowed to touch window.meaningOfLife.
Update: Alohci's answer meets these requirements, but only on the condition that the second script tag comes immediately after the first, with nothing but whitespace in between. If someone could extend his hack to avoid that requirement, without introducing other unwanted consequences, that would be amazing...
Given your specific requirements set, this is actually quite simple and should work completely cross-browser. It does require however, that first.js immediately precedes second.js without anything between them except white space.
First, let's assume that the HTML looks like this:
<!DOCTYPE html>
<html>
<head>
<title>Test Case</title>
<meta charset="UTF-8" />
<script type="text/javascript">
function func() {
window.meaningOfLife = 42;
window.loadSecond();
};
</script>
<script type="text/javascript" src="first.js"></script>
<script type="text/javascript" src="second.js"></script>
</head>
<body>
<p>Lorem ipsum dolor sit amet ...</p>
Run Func()
</body>
</html>
I've removed the setTimeout because that can cause func() to run before start.js runs causing a "loadSecond is not defined" error. Instead, I've provided an anchor to be clicked on to run func().
Second, let's assume that second.js looks like this:
document.body.appendChild(document.createTextNode("second.js has run. "));
if (window.meaningOfLife !== 42) {throw new Error();}
Here, I've just added a line to append some text to the document body, so that it is easier to see when second.js actually runs.
Then the solution for first.js is this:
function loadSecond()
{
var runSecond = document.createElement("script");
runSecond.setAttribute("src", "second.js");
document.body.appendChild(runSecond);
}
document.write("<script type='application/x-suppress'>");
The loadSecond function is just there to run second.js when func() runs.
The key to the solution is the document.write line. It will inject into the HTML <script type='application/x-suppress'> between the close script tag of first.js and the open script tag of second.js.
The parser will see this and start a new script element. Because the type attribute has a value which is not one that the browser recognises as being JavaScript, it will not attempt to run its content. (So there are an infinite number of possible type attribute values you could use here, but you must include a type attribute, as in its absence, the browser will assume that the script's content is JavaScript.)
The second.js script's opening tag will then be parsed as text content of the new script element and not executed. Finally the second.js script's closing tag will be re-purposed to close the new script element instead, which means that the remainder of the HTML is parsed correctly.
You can see a working version at http://www.alohci.net/static/jsprevent/jsprevent.htm
In first.js, set var shouldILoad = true
Then, load second.js this way:
<script>
if (shouldILoad) {
(function() {
var myscript = document.createElement('script');
myscript.type = 'text/javascript';
myscript.src = ('second.js');
var s = document.getElementById('myscript');
s.parentNode.insertBefore(myscript, s);
})();
}
</script>
(where 'myscript' is the ID of some element before which you'd like to insert the new Script element)
As far as I know, you can't. If the markup looks like
<script type="text/javascript" src="first.js"></script>
<script type="text/javascript" src="second.js"></script>
you can't access the second script element from within first.js, as it hasn't been added to the DOM at the moment the first script runs (even not if you assign an id to the second element). It doesn't matter whether the code of second.js is put inline or in an external file.
The only thing I don't understand is your second point. First you say that you can't control the markup of the document, but then you state it is possible to load second.js dynamically (using AJAX).
Following article describes the way you could block (3-rd party) scripts loading/execution from your script (including the both tag in the page head and dynamically added tags).
To handle existing tags on a page:
Use a MutationObserver to observe script elements insertion and inside the MutationObserver callback backup the script (to enable/insert it later) and change the script type to "javascript/blocked" (not works in IE, Edge, Firefox). Also you could handle deprecated (but working) beforescriptexecute event in Firefox to prevent script load.
Manually set type "javascript/blocked" (works everywhere including IE and Edge) like
<script type="text/javascript" type="javascript/blocked" src="second.js"></script>, then backup it in MutationObserver callback and re-add it later.
To handle dynamically added tags
Monkey-patch the document.createElement.
Override ‘src’ and ‘type’ descriptors on the HTMLScriptElement prototype.
Also this guys provide a yett library with the approach described in the article.
All <script> tags have their own execution context, which makes it nearly impossible to interfere with each other. Of course you've got the (infamous) global object (referenced by window in browsers).
Preventing the execution of second.js is rather simple: break it!
Assuming that second.js tries to call document.getElementById for example:
Working example of breaking jQuery, then loading later (with dependecies).
Tested on: IE 6+, FF 3.6+, Chrome
end of first.js
var execute;
// saving our position
var scripts = document.getElementsByTagName("script");
var i = scripts.length;
// breaking getElementById
var byId = document.getElementById;
document.getElementById = null;
var interval = setInterval(function () {
if (i != scripts.length) {
var second = scripts[i];
// stop polling
clearInterval(interval);
// fix getElementById
document.getElementById = byId;
// set the delayed callback
execute = function (onload) {
var script = document.createElement("script");
script.src = second.src;
script.onload = script.onreadystatechange = onload;
document.getElementsByTagName("head")[0].appendChild(script);
};
}
}, 100);
anytime you wanna execute second.js
execute(function(){
// second.js dependant code goes here...
});
Note: the onload parameter for execute is optional.
Just to see if this was possible, I had first.js send a synchronous XHR to a PHP file, and had the PHP file delete second.js. When the readyState reached '4', I had the JS alert something, to stop the thread. Then I went and checked the server... Yeah, second.js was deleted. And yet, it wouldn't work. I'd close the alert box, and the code that was in second.js would still be executed, despite the fact that the file was gone.
I don't really know what this means, but the answer to your question is probably, "No, it's not possible."
you may use setTimeout() to delay the execution of some code

Categories

Resources