Background
I am working on a project that runs in an embedded web browser in a small device with limited resources. The browser itself is a bit dated and has limits to its capabilities (HTML 4.01†, W3C DOM Level 2†, JavaScript 1.4). I have no documentation on the browser, so what I do know comes from trial and error.
The point is to retrieve dynamic content from a server so that only a minimal amount of inflexible code needs to be embedded into the device running the web browser. The browser does not support the XMLHTTPRequest object, so AJAX is out. Working with I do have, I wrote a bit of test code to dynamically insert JavaScript.
† Minor portions of these standards not supported
EDIT
While I cannot actually confirm it, I believe that this site may list the DOM support for the embedded browser because I see "Mozilla/4.0 (compatible; EBSWebC 2.6; Windows NT 5.1)" as the user agent in the server log.
<html>
<head>
</head>
<body onload="init()">
<div id="root"></div>
<script type="text/javascript">
<!--
function init() {
// Add a div element to the page.
var div = document.createElement("div");
div.id = "testDiv";
document.getElementById("root").appendChild(div);
// Set a timeout to insert the JavaScript after 2 seconds.
setTimeout("dynamicJS()", 2000);
}
function dynamicJS() {
...
}
//-->
</script>
</body>
</html>
Method 1
I initially implemented the dynamicJS function using Method 1 and found that while the code executes as expected in Chrome, IE8, and FireFox 3.5, the JavaScript is not actually retrieved by the embedded browser when the element is appended.
function dynamicJS() {
var js = document.createElement("script");
js.type = "text/javascript";
js.src = "js/test.js";
document.getElementById("root").appendChild(js);
}
Method 2
Looking for a work around, I implemented Method 2. This method actually works in the embedded browser as the JavaScript is retrieved and executed, but it does not work in other modern web browsers's I tested against (Chrome, IE8, FireFox 3.5).
function dynamicJS() {
var js= '<script type="text/javascript" src="js/test.js"> </s' + 'cript>';
document.getElementById("testDiv").innerHTML = js;
}
Question
I'm new to JavaScript and web programming in general, so I'm hoping one (or more) of the experts here can shed some light on this for me.
Is there anything technically wrong with Method 2 and if not, why doesn't it work in modern web browsers?
There is nothing technically wrong with method 2 but most modern browsers have very loose HTML parsers that tend to get caught up in the code that you're sending. Specifically they parse the </script> in your JavaScript string literal as an end tag. This manifests itself in two ways:
You'll see an "Unterminated String Literal" error.
All code after the </script> text will be rendered as text on the page.
A common workaround for this problem is to split the </script>. You can do this with the following code. Yes, I know its a hack, but it works around the problem.
function dynamicJS() {
var js= '<script type="text/javascript" src="js/test.js"></s' + 'cript>';
document.getElementById("testDiv").innerHTML = js;
}
Realistically though, you should be able to use your first approach strictly using the DOM APIs. I've found that some browsers can be really picky about loading scripts added by script in that they will only load them if they are placed as a child of the <head> element. This is how the YUILoader works, so I'd be surprised if it didn't work in all browsers.
Here's an example, you'll want to check this to make sure that it works in all browsers, and add some error checking around the assumption that there will be a <head> element but it give you the general idea.
if (!document.getElementsByTagName) {
document.getElementsByTagName = function(name) {
var nodes = [];
var queue = [document.documentElement];
while (queue.length > 0) {
var node = queue.shift();
if (node.tagName && node.tagName.toLowerCase() === name) {
nodes.push(node);
}
if (node.childNodes && node.childNodes.length > 0) {
for (var i=0; i<node.childNodes.length; i++) {
if (node.childNodes[i].nodeType === 1 /* element */) {
queue.push(node.childNodes[i]);
}
}
}
}
return nodes;
};
}
function dynamicJS() {
var js = document.createElement("script");
js.setAttribute('type', 'text/javascript');
js.setAttribute('src', 'js/test.js');
var head = document.getElementsByTagName('head')[0];
head.appendChild(js);
}
The innerHTML property has not yet actually been standardized, though all modern browsers support it, and the draft standard of HTML5 includes a definition of how it should work. According to the HTML5 specification:
When inserted using the document.write() method, script elements execute (typically synchronously), but when inserted using innerHTML and outerHTML attributes, they do not execute at all.
innerHTML was first introduced in Microsoft Internet Explorer 4, and due to its popularity among authors, has been adopted by all of the other browsers, which is what led to its inclusion in HTML5. So, let's check Microsoft's documentation:
When using innerHTML to insert script, you must include the DEFER attribute in the script element.
So apparently, in IE you can get scripts inserted via innerHTML to execute, but only if you add a defer attribute (I do not have IE in front of me to test this). defer is another feature that was first added to IE; it was included in HTML 4.01, but not picked up by any of the other browsers for quite a while. HTML5 includes a much more detailed description of how <script defer> should work, though it appears to be slightly incompatible with how it works in IE, as it does not allow execution of scripts added via innerHTML. The HTML5 definition of <script defer> appears to be implemented in Firefox 3.5 and Safari 4.
In summary, innerHTML hasn't really been standardized yet, but instead simply implemented by all of the browser vendors in slightly different ways. In IE, the original implementation, it didn't support execution of scripts except with a defer attribute, and defer hasn't been supported in other browsers until just recently, and so the other browsers simply don't support execution of scripts added using innerHTML. This behavior is what HTML5 is standardizing on, so unless Microsoft objects, is probably going to be what goes into the standard.
It sounds like the browser you are working with didn't do as good a job of implementing a compatible innerHTML, as it executes scripts added using innerHTML no matter what. This is unsurprising, as the behavior isn't standardized and so needs to be either reverse engineered or gleaned from reading the documentation of other browsers (which may not have included this fact in the past). One of the main goals of HTML5 is to actually write down all of these unwritten assumptions and undocumented behaviors, so that in the future, someone implementing a browser can do so without being misled by a spec that doesn't match reality, or without having to do the effort of reverse engineering the existing browsers.
It looks to me that you may have to use Method 2 on your embedded browser, and Method 1 if you want to run on the common desktop browsers. It would probably be a good idea to try Method 1 first, and fall back to Method 2 if that does not work, and then error out (or silently fail, depending on your needs) if neither one works.
A long shot but does the embedded browser support iframes?
And if so would you be able to use that to load in whatever additional JS you needed and access it via the iframe?
Related
I'm having a JavaScript debugging question. I would like to know, how it would be possible to find out in which file/line a new script is loaded and called. My website has several scripts which are appended via document.write(), and I would like to find a way to find the function call in all attached scripts of the website.
I would prefer either Firebug or Chrome Dev tools.
Thanks!
Neither Firebug nor the Firefox or the Chrome DevTools currently allow to debug code inserted via document.write(). I created bug 1122222 for the Firefox DevTools and issue 449269 for the Chrome DevTools requesting to be able to debug such scripts. As upcoming Firebug versions will be based on the Firefox DevTools, it will offer this feature once the Firefox bug is fixed, so there's no need to create a separate issue for it.
Until the above bugs are fixed you need to use another method to inject your script in order to be able to debug it within the browser.
Method 1: using eval()
You can use the eval() function to evaluate arbitrary code dynamically. Note that the eval() only evaluates JavaScript code, it must not be surrounded by any HTML.
Example:
eval("console.log('Hi!')");
Method 2: injecting a <script> tag
You can add a <script> tag to the page and then add contents to it.
Example:
var script = document.createElement("script");
script.textContent = "console.log('Hi!');";
document.body.appendChild(script);
Method 3: using new Function()
You can create a new function via the Function constructor.
Example:
(new Function("console.log('Hi!');"))();
Note that JavaScript won't be executed using innerHTML or insertAdjacentHTML() due to security reasons.
I am sure I don't fully understand this problem, but it seems that we are seeing strange behavior on IE9 on my project, somehow related to out-of-order execution of JavaScript that has been injected via calls to document.write, e.g.:
document.write('<scr'+'ipt type="text/javascript" src="'+file1+'"></src'+'ipt>');
document.write('<scr'+'ipt type="text/javascript" src="'+file2+'"></src'+'ipt>');
document.write('<scr'+'ipt type="text/javascript" src="'+file3+'"></src'+'ipt>');
My limited Google research suggests that IE9 will execute scripts injected in this manner in a different order from other browsers (notably, Firefox and Chrome). Is there a better way to achieve what we're going for here, which will ensure the same execution order by all browsers?
I take that back: we don't really care about all browsers, just Chrome and IE9.
Use a script loader (like the one I wrote: LABjs), which will normalize all the different quirks of loading across the various browsers. And bonus: it doesn't use that god-awful document.write(). LABjs will let you load all your scripts asynchronously (in parallel), but make sure they execute in the proper order. Sounds like basically exactly what you want.
I guess you could chain the onload event of one to start the load of another:
var newJS= document.createElement('script');
newJS.onload=function() {alert("done")} //or call next load function
newJS.src="..."
document.body.appendChild(newJS)
So the advantage of writing script tags this way is that they are loaded asynchronously. I don't know about browser nuances about exactly how this is done but I would have thought they would be executed when they're downloaded, in no specific order. Similar to the behaviour of the HTML5 async attribute.
There's another HTML5 attribute defer which instead makes scripts execute in order, but in a non blocking way. You could try adding that into your generated <script> tags. IE9 partially honours it.
I have made a little script, exactly for this purpose:
https://github.com/mudroljub/js-async-loader
In short, it loads all your scripts asynchronously, and then executes them consequently. It looks something like this:
for (var lib in libs) {
loadAsync(lib);
}
And you don't need document.write();
Do browser vendors optimize against jQuery?
I know this sounds absurd anti-standard, but I can imagine browser vendor has optimization against jQuery code built into their JS compiler/interpreter.
For example, lets say the JS compiler/interpreter sees, $('.blah > p'), the browser can say, hmm, I see that user is trying to grab an element, instead of letting jQuery do all the browser detection, it could just take the [actual DOM object], and return $([actual DOM object]) right away.
No guessing what $ does would break any scripts that use $ for different purposes. Consider a page like this (this is actually a pretty weak example relative to what could be found in the wild, but I think it still demonstrates the difficulty well enough):
<div class="bar">
<div class="foo">
<div class="bar">
How will the browser know to select this div with the selector '.bar .foo .bar', without actually running this script the way it is designed?
</div>
</div>
</div>
<script>
window.onload = function(){
var x = $('.foo');
console.log(x);
console.log(x.selector) // '.bar .foo .bar'
};
</script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
var $ = function(){
return arguments[0];
};
$ = (function($){
return function(selector){
return jQuery('.bar ' + selector + ' ' + $('.bar'));
}
})($);
</script>
It would be beyond an optimization for the browser to know which div to select in advance. In fact the browser has to run the scripts the way it is designed to in order to select the correct div.
However many browsers to compile Javascript to a slightly lower level language such as Java or C++ code. Then if the browser downloads and caches jQuery once it will be cached it a compiled form on the users computer. This is not a jQuery specific optimization since it will happen with any cached script, but it is more significant of an optimization for large scripts like jQuery.
No browser vendor announced such a feature at the moment.
It is possible, however. You can easily store for reference parsed Javascript (IL/bytecode, whatever you use in your engine) for most often used versions of jQuery inside JS engine and when incoming function matches that signature, replace it with native version.
I guess the only thing that you have to take in consideration is how much time you'd spend on implementing it vs. speed gains against modern JIT engines. Because some of them already compile code to some native form, at least partially, it is not quite clear if there will be any substantial gains or not.
I know this is crazy, but IE can drive one to do crazy things.
Here's the deal: we have a SharePoint 2007 site with Content Editor Web Parts. We have a contingent of users with Internet Explorer 8. We want to keep the site in IE8/IE8 Standards modes for better rendering of content. However, this configuration breaks the ability to open the Rich Text Editor window from the C.E. web part.
If we force IE8 into IE7 document mode or quirks mode, it works. Indeed, other sources online have suggested doing just that to fix the problem. But we'd really rather have everything run in standards mode.
Through some debugging, we found the source of the problem to be the use of document.all.index("<web_part_id>") JavaScript when retrieving a web part object on the page. In IE8 standards, this returns an object with most of the properties either empty, null or undefined; most notably, the id property is not set. If you were to use document.getElementById to retrieve the same ID, you get back a fully populated object. Likewise if IE8 is not in standards mode, you get back a mostly (but not completely) populated object -- but populated enough to avoid the script error, anyway.
All this code looks like it is injected dynamically into the SharePoint page, so that rules out simply replacing references to document.all. However, we got the crazy idea of redefining the document.all.item method to actually invoke document.getElementById. Our attempts so far to do this don't work, so perhaps someone might shed some light on what we're doing wrong (okay, there's lots wrong about this, but it's IE and SharePoint, right?).
Our initial attempt at an override looks like this:
<script type="text/javascript">
document.all.item = function(id) { return document.getElementById(id); }
</script>
This code is in the document HEAD, above any other script references (SharePoint or otherwise), but it does not appear to supersede the native code.
Thoughts, ideas, suggestions and criticisms welcome!
document.all is a HTMLCollection, so you may use HTMLCollection.prototype to change the behaviour:
if(document.all
&& !window.opera
&& typeof HTMLDocument=='object'
&& typeof HTMLCollection=='object')//filter IE8
{
HTMLCollection.prototype.item=
function(id)
{
return((this==document.all)
? document.getElementById(id)//document.all
: this[id]//maintain native behaviour for other collections
);
}
}
So you're saying you can't change the references to document.all.item because SP directly injects it to the html? So couldn't you use the DOM to replace it?
BTW, I feel your SharePoint pain. I've built SP workflows, and those are enough of a pain for me!
I have just been altered to the fact that a user of my website is using a very old browser which does not run jquery (in this case Safari 1.x) and as a result can not access the login panel which uses jquery's slideToggle function.
Can anyone think of a fix which detects whether a browser is able to use jquery - and if not make that link go to a different page rather than showing the login panel?
You could a little conditional check like
if(!'jQuery' in window) {
// jQuery is not available
}
or, if Safari 1.x doesn't know about the IN operator (I'm not sure) use
if(!window.jQuery) {
}
I think there are alternative answers to this, but for me, I would have to weigh up the time it will take you to support his obsolete browser (I'm sure there may be other things inside the site), versus the payback to you...
In the plain HTML source code for the the href= of the login link, set that to a plain HTML login page.
Using jQuery, attach the click handler to the link, if this part fails, thats ok, the browser will just follow the href in the link to the plain login page, allowing your old-browser-user to login still.
$(document).ready(function(){
$('#login_link_id').click(function(){
// Your code here
});
});
If you use javascript/jQuery you should ALWAYS ensure your site works perfectly without it. In this case if you have a login popup box; you probably assign a click event assigned after the DOM has loaded.
What you should do is ensure that if jQuery isn't present the link loads a "normal" login webpage as opposed to the popupbox. I use something similar to this:
Log in
<script>
if(!'jQuery' in window) {
$(document).ready(function(){
//assign on click event to loginlink
});
}
</script>
If jQuery doesn't exist then login.html will be opened normally.
Wow, seriously?! Safari 1.x?? Anyhow, try this...
var isJQSupported = false;
$(function() { //shorthand for document.ready
isJQSupported = true;
//your usual code
});
if (!isJQSupported) {
window.location = "http://www.apple.com/safari/download/";
}
To me it sounds like safari 1.X has problems with jQuery internally. Which means simple checks like whether $ exists in the global space or whether $(function) does anything are not going to help.
The most likely root cause will be that javascript throws an error in loading of jQuery itself which will then stop the rest of your javascript code from execution.
There are four viable options here.
Either make the website work with noscript. Replace your login control with pure HTML and postbacks and ask the user to turn javascript off. This option is useful since you won't be fixing the issue for safari 1.x problems specifically.
You can make javascript check for safari 1.X and other non-supported browsers and only load jQuery through script tag injection or ajax if your user is using a supported browser. If the user is using a browser not compatible with jQuery then you can instead use plain javascript.
Get a copy of safari 1.x and see why jQuery breaks. Then fix it and ask for it to pulled into the release of jQuery 1.5. This relies on the fix being something that does can be done without hacking and that the jQuery team agrees is worth adding in.
Ask the user to use a compliant browser.
There might be some more options. I would personally lean towards asking the user to use a compliant browser because supporting Safari 1.x is ridiculous.
This seems like a case where progressive enhancement is needed.
You have to do multiple checks
see if $ exists
see if $.fn exists
[not sure if needed] check if $.support is a function
check for feature support as needed with $.support() http://api.jquery.com/jQuery.support/
At the end of the check, when jQuery reports that features you need are present - the rest of the script can run.
If you're not sure which features mentioned in the support you use, then this might need a single test on Safari 1.x to see what are the values returned by $.support(), but that is what your nasty old-browser-user can do for you (if you prepare code and publish) and report the resulting text. Then you compare the list with other [old] browsers that are accessible and determine features that are required.
The easy way would be to require everything and cancel all scripts if suport for any feature is missing. This will also rule out IE6 and IE7 and opera below 9.something and firefox below 2.0 or including - I'm not sure.
Use a server side language to detect if it's the old safari based on user-agent and load a different javascript file