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.
// 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 pointNow 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.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(!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.
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 |