Javascript Basics Part 13

By Mark Kahn

Browser compatibility is one of the biggest issues facing web developers, and has been since the original browser wars of Netscape vs IE. The problem stems from the fact that there were initially no standards in place as to how web browsers should render HTML and JavaScript content, and each company had different ideas on how it should be done.

The W3C developed a set of web standards that all browsers were supposed to adhere to, but as we all know, not all browsers adhere to those standards. Any browser you look at is going to have at least a few of its own methods for doing things. Even today there are still quite a few compatibility issues floating around that you should be aware of. This article is going to highlight some of those key points and tell you how to avoid or simply deal with them.

Basic Browser Detection

Now some people will tell you that in order to deal with browser differences you should check to see what browser your user is using and write custom functions from there. Your basic code ends up looking something like:

// this is pseudo-code

// some browser detection code goes here
if(Internet Explorer 5.5 or greater){
	// do stuff for IE5.5+
}else if(Internet Explorer 5 or greater){
	// do stuff for IE5-IE5.5
}else if(Internet Explorer){
	// do stuff for IE below version 5
}else if(Netscape 6 or greater){
	// do stuff for Netscape 6 or greater
} // this keeps going...you get the point

Now this is all fine and dandy, if you don't mind writing it all out and checking for every browser in existance. What happens if a new browser is released, however? If it's a version of IE or Netscape or something else you've taken care of, this code will work fine. If not, well it's probably going to fall into your final else statement which will probably have very little actual functionality. Fortunately JavaScript gives us another very convienient method to do browser detection. We test to see if an object exists, simply write:
if(object){
	...
}
What we are actually testing here is to see if the object is defined or not. If the object is undefined, this statement will return false and our code will not run. Since this code doesn't throw an error if the object doesn't exist, it will become the basis for just about all browser detection in JavaScript. We test all known methods to do pretty much exactly the same thing and we do the one that works. Sounds tedious, doesn't it?

One of the simplest things we do with JavaScript is to get a reference to an object with a specific ID. Slightly older versions of Internet Explorer only did this with the document.all array. Firefox, newer versions of IE, and most other browsers do this with document.getElementByID. Older versions of netscape use document.layers. So:

function getElement(id){
	if(document.getElementById){    // test the most common method first.  Most browsers won't get past this test
		return document.getElementById(id);
	}else if(document.all){         // test older versions of IE
		return document.all[id];
	}else if(document.layers){      // test older versions of Netscape
		return document.layers[id];
	}else{                          // not sure what to do...return null
		return null;
	}
}
Voila, a cross-browser function to get an element by its ID and it's not even complicated. This works great, but it has one flaw: the browser has to run the check each and every time you call the function. This may not seem like a big deal, but if you had to run this function several hundred times, those extra checks can lead up to a significant delay in your script.
if(document.getElementById){
	getElement = function(id){ return document.getElementById(id); }
}else if(document.all){
	getElement = function(id){ return document.all[id]; };
}else if(document.layers){
	getElement = function(id){ return document.layers[id]; };
}else{
	getElement = function() { return null; }
}
This looks a bit different but it has the same effect, and in the long run can run much faster. Instead of defining a function to check what our browser can do, we check what our browser can do first, then define the function to do that and only that. Now each time we call our getElement function our browser already knows exactly what to do and doesn't have to figure it out as in the previous example. The only downside to this is that if you never call the getElement function in your code, our browser has to do the check anyway, but this is an incredibly miniscule problem since it's only running once anyway. If, however, this issue concerns you, there are workarounds: simply leave the code out of your page (it shouldn't be there anyway if it's not being used, right?) or write a self-redefining function.

A self-what? A self-redefining function is exactly what is sounds like. A function that re-defines itself!

function getElement(id){
	if(document.getElementById){
		getElement = function(id){ return document.getElementById(id); }
	}else if(document.all){
		getElement = function(id){ return document.all[id]; };
	}else if(document.layers){
		getElement = function(id){ return document.layers[id]; };
	}else{
		getElement = function() { return null; }
	}

	// When we get here, the getElement function has been replaced.
	// So we return the result of the new function.
	return getElement(id);
}
This time our browser detection code only runs the first time we call our function. If the function is never called, the detection code never runs.

There are hundreds of issues similiar to the document.getElementByID vs document.all issue illustrated here. We're not going to explain each of them here but there is a table at the end of this article that has a list of many of the more common contradictions between browsers. If you need to do any of the things in that table, simply follow exactly the same methods used in this example and you will have cross-browser friendly functions.

There is one thing to look out for: if your detection code tries to detect a property of an object and that object does not exist, you will get an error. Perhaps an example will explain that better:

if(HTMLElement.style.filter.alpha){
	// set the alpha opacity (transparency) in IE
}else if(HTMLElement.style.opacity){
	// set the opacity in Mozilla browsers
}
Internet explorer has its own set of style filters, accessed with item.style.filter. Other browsers don't have these filters so item.style.filter doesn't exist. Since we are checking one level beyond filter in this example, our code will break in all browsers except Internet Explorer. The browser will throw an "object does not exist" error, refering to HTMLElement.style.filter. If you see this error in your browser refering to your detection code, this is likely the cause.

So what do we do here? We simply drop the ".alpha" from the check. Since IE is the only browser to support filters in this manner, and we know that filter.alpha exists if filter does, we don't need this extra bit of information. Optionally we could do the check for Mozilla and any other browsers first, in which case these browsers would never get to the filter check, but that leaves room for browsers that don't support style.opacity to break.

Fixing the Browser

JavaScript is a evolving language and there are often new versions of it that are released. As browsers are updated, they start to support these new versions. At the time of this writing, the newest version of JavaScript is/was 1.6. Most browsers don't support JavaScript 1.6, however. Namely, Internet Explorer doesn't support JS1.6, not even the newest IE7 beta. So if we want to use features from JavaScript 1.6, we're out of luck unless we want to abandon well over half of all internet users, right? Well not quite. If we're willing to put a bit of effort into things we can make most features from JavaScript 1.6 work in any browser.

Some very useful functions have been added to the Array object in JavaScript 1.6, for instance. An "indexOf" function has been added that returns the index # of the first occurance of an item in an array. Previously we would need to write

StringToFind = 'apple';
Fruits = ['orange', 'banana', 'pear', 'apple', 'kiwi'];
var found = -1;

for(var i=0; i<Fruits.length; i++){
	if(Fruits[i]==StringToFind){
		found = i;
		break;
	}
}
Thanks to this new method, however, we can now simply write:
StringToFind = 'apple';
Fruits = ['orange', 'banana', 'pear', 'apple', 'kiwi'];
var found = Fruits.indexOf(StringToFind);
Much simpler, isn't it? But again, Internet Explorer doesn't support .indexOf. So let's force IE (and other browsers that don't already) to support it!
if(!Array.indexOf){
	Array.prototype.indexOf = function(obj){
		for(var i=0; i<this.length; i++){
			if(this[i]==obj){
				return i;
			}
		}
	}
}
If you're unsure as to what a prototype is, refer to Article #8 of this series.

What are we doing here? Well we're re-creating the Array.indexOf function for browsers that don't already support it. For browsers that do already support it, this code won't run.

We need to be careful of two things here. First of all, we need to make sure we perfectly duplicate the functionality of the existing function. Unfortunately we havn't in this case. The Array.indexOf function takes an optional 2nd argument, being the index of the array at which to begin searching. So we re-write:

if(!Array.indexOf){
	Array.prototype.indexOf = function(obj, start){
		for(var i=(start||0); i<this.length; i++){
			if(this[i]==obj){
				return i;
			}
		}
	}
}
Now we have all the functionality of the built-in Array.indexOf function of JavaScript 1.6. If you'll look at our new function the only difference is that we pass a start variable. If it doesn't exist, we start with 0. The simple code "(start||0)" means "if(start){ start } else { 0 }".

We can now find the 2nd occurance of "Apple" if our array was ['apple', 'orange', 'banana', 'pear', 'apple', 'kiwi'].

There is one more thing to look out for in this particular example. By prototyping Array we flat-out break foreach loops dealing with Array. What does this mean? Well take the following example:

var Fruits = ['apple', 'orange', 'banana', 'pear', 'apple', 'kiwi'];

foreach(fruit in Fruits){
	alert(fruit);
}
In this example you would expect 6 alerts, right? "apple", "orange", "banana", "pear", "apple" and "kiwi". Well there's going to be one more now, "indexOf". Why? Because anything that is added as a prototype shows up in a foreach loop.

So we can't do this because it breaks foreach loops, right? Well...that's a matter of debate. In comes down to the fact that foreach loops were never intended to be used with arrays in the first place. You heard right, JavaScript doesn't actually have associative arrays, we just use the language as if it did. Anyway, this is a long drawn out argument and it comes down to the fact that you shouldn't be using foreach loops on Arrays. If you need an associative array, use an Object, not an array. As such, never extend the "Object" prototype in JavaScript; you're just asking for trouble if you do.

We went through an example of how to make IE behave like Firefox, so why not go in the other direction? Why? Well as we know, Firefox and IE differ in some areas and sometimes it's just easier, or more convienient, to make them behave the same than it is to write our own functions and use them. Take the "innerText" property of an element, for example. It happens to be another IE non-standard function, but it is very widely used. It is a read/write function that sets the text value of an HTML element and happens to be rather useful in many circumstances. Firefox doesn't support it, but it does support an identical function, "textValue", which is a standard.

Unforunately in this case we can't make IE behave like Firefox. Why? Because we need to define a "getter" and "setter", which IE doesn't allow us to do. What are these? Well instead of calling a function and passing variables to it, a getter and setter allow us to call functions as if we were reading/writing properties on an object. "object.innerText = 'asdf'" vs "object.innerText('asdf')".

So we define our getter and setter in Firefox and some other browsers:

if(HTMLElement){
	HTMLElement.prototype.__defineGetter__("innerText", function(){
		return this.textContent;
	});
	HTMLElement.prototype.__defineSetter__("innerText", function(v){
		this.textContent = v;
	});
}
__defineGetter__ and __defineSetter__ are used to define getters and setters, exactly as they sound, but not in IE, which is why we have to copy IE's functions and not the standard ones.

Common Differences

You now know how to make the browsers behave the same way in terms of JavaScript, or at least how to make your JavaScript code work in all browsers. As promised, here is a list of some of the more common methods that differ between IE and Mozilla. Other browsers like Safari and Opera should be taken into account when writing code as well. If you need to use any of this functionality in your pages make sure that you "fix" it using one of the above methods of your choice first.

Action IE Mozilla
Get Element by ID Name document.all document.getElementById
Get All Elements on a Page use the document.all array document.getElementById('*')
Get all child elements from a node element.children works in IE to get all HTML elements. element.childNodes works in all browsers but returns all nodes, not just HTML elements.
Make one node the child of another applyElement (appendChild works too) appendChild
Create an XMLHttp Object new ActiveXObject(ObjID);

ObjID can be one of ['Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP.5.0', 'Msxml2.XMLHTTP.4.0', 'Msxml2.XMLHTTP.3.0', 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP'], depending on what the client has installed

new XMLHttpRequest();
Set or read the text value of an element innerText textContent
Read/Set the float property of an element style.cssFloat style.styleFloat
Get the event object in a function window.event must be passed to the function
Get the target of an event event.srcElement event.target
Get the mouse position on screen event.clientX+object.scrollLeft-object.clientLeft
and
event.clientY+object.scrollTop-object.clientTop
(phew!)
event.pageX and event.pageY
Get window size document.body.clientHeight or document.documentElement.clientHeight, depending on the doctype window.innerHeight and window.innerWidth
One way to set position/size of an element pixelLeft, pixelTop, pixelHeight and pixelWidth left, top, height and width

This list is by no means complete, but it should cover many common problems. Good luck with your programming, and we hope you've enjoyed this JavaScript Basics series!

Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •