Monday, May 31, 2010

Cross-browser compatibility

My first true deep dive into heavy JavaScript programming was working on a project that required adding Firefox support for a web application that only targeted Internet Explorer. The web application was originally written for IE6 and therefore contained some hairy JavaScript.

I experienced firsthand the same rite of passage as millions of web developers around the world. Fortunately, adding support for Firefox also meant that the web app no longer needed to support IE6 (only IE7+) so fixing the JavaScript to be cross-browser compatible became a bit more reasonable and sane.

Now, some snippets collected for these changes in no particular order:

# 1

Replacing all references to the DOM Level 1 function
top.frames['myId']
, which gave Firefox much trouble, with the DOM Level 2 function:
document.getByElementId('myId')


# 2

Replace the function 'removeNode':
if (x)
{
    x.removeNode();
}
with a conditional check that uses the Firefox friendly function 'removeChild' if parentNode is defined (otherwise fallback on 'removeNode'):
if (x)
{                  
    if (x.parentNode) 
        x.parentNode.removeChild(x);  // Firefox                  
    else
        x.removeNode(); // IE
}

# 3

The 'innerText' property does not work in Firefox but it does in IE. Instead, Firefox does recognize 'textContent' serving the same purpose. Again, use another if-else statement checking if the target element exists:
function setText(elem, textValue)
{
    if (elem.textContent || elem.textContent == "")
    {
         elem.textContent = textValue; // Firefox
    }
    else 
    {
         elem.innerText = textValue; // IE
    }
}

# 4

Dot notation for defining functions is another area where JavaScript errors emerge in Firefox:

"...missing ( before formal parameters..."
function window.onDoSomething()
{
 // do some stuff
}
To fix is to swap around the 'function' keyword:
window.onDoSomething = function()
{
 // do some stuff
}

# 5

One of the web pages had a character that used the Webdings (TrueType dingbat) font for an expressive, functional symbol. It rendered incorrectly (and confusingly) in Firefox. Substituting the equivalent Unicode character resolved the discrepancy.


# 6

All AJAX calls involved the following IE6 object:
var xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');
As mentioned, with no need for supporting IE6 (something web developers dream of someday being true for all the of internet) every instance of the previous line of code is fully replaced with:
var xmlHttp = new XMLHttpRequest();
Certainly, one of the more satisfying cross-browser changes.


# 7

Firefox does not support referencing global event objects specifically 'window.event' and when encounters JavaScript code that attempts to do so it responds with this error message:

window.event is not defined.

Instead, it is necessary to pass the event object as an argument via a function's parameter:

    function myFunction(e) // <-- add 'e' as a parameter for the global event object
    {
        if(!e) e = window.event // if e is undefined then set e using IE event object
        // other code
    }

    <button onclick="myFunction(event);">test events</button>

# 8

A web page had a file browser functionality to attach a file for upload to the server. The 'onclick' event handler for this element's tag and type
<input type="file" ...
was coded to be programmatically triggered. The reason for this was to allow for the user to type in and edit the free form text of the file path and then the JavaScript would create the 'input' and then fire the event on the user's behalf.

Firefox does not allow this requiring the user to manually click on the tag since the file path text is read-only and can not be edited. It is considered a potential security flaw hence the restriction. The code needed to be rewritten to have the user directly fire the event and open the file browser.


# 9

An html table on a page contained rows with hidden nested rows functioning as a tree-like grid. These top level rows when clicked toggled between the style of
display:none
when hiding its children rows and then used
display:block 
when showing the rows.

In Firefox, the rows do not align properly when made visible with 'block' display style. The premature solution was to replace 'block' with
display:table-row
which worked for both IE8 and Firefox.

However, I later discovered that this type of display was not supported in IE7. Instead, substituted the equivalent of no display style at all using empty string
rowObject.style.display = ''
to show hidden rows. Apparently, each browser knows by default how to appropriately render the rows without the need to be specific in the html.


# 10

Consider a deeply nested event firing and then needing to prevent it from triggering other event handlers further up the DOM hierarchy. In IE,
window.event.cancelbubble
should take care of this. Firefox, of course, does not recognize this command. Instead, one must use the
event.stopPropagation
function to exercise the same control over the scope of an event.

To ensure coverage across the different browsers, do this:
function doSomething(e)
{
 if (!e) var e = window.event;
 e.cancelBubble = true;
 if (e.stopPropagation) e.stopPropagation();
}

# 11

Some JavaScript code was not executing at all. No clear indications why the function was not defined. It turns out that using the term 'jscript' as part of the 'type' attribute in the 'script' tag:
<script language="javascript" type="text/jscript" ...
is , not surprisingly, recognized only in IE and not in Firefox. All instances of 'jscript' were replaced with 'javascript':
<script type="text/javascript" ...
This cross-browser issue caused so much grief for such a simple fix. I spent way too much time figuring it out. When I read:

"...Nevermind, I think I found it. I inherited the code and just noticed that the original programmer had specified JScript rather than Javascript as the script language..."

I glanced over to my aspx page and my eyes immediately saw that exact error. Unbelievable.


# 12

Another function not defined in Firefox but used in IE:
window.attachEvent
In Firefox, use this instead:
window.addEventListener
The cross-browser code might look like this:
eventName = 'load';

if (window.addEventListener) // Firefox
{
  window.addEventListener(eventName, myFunction, false);
} 
else if (window.attachEvent) // IE
{
  window.attachEvent('on' + eventName, myFunction);
}
(Note that IE requires the prefix "on" for the event name while Firefox does not.)

All of the above applies to IE's
window.detachEvent
For Firefox, use
window.removeEventListener


# 13

Some image icons when hovering over with the mouse were expected to show tooltip text. However, no tooltips shown in Firefox with the following:
<input type="image" disabled="disabled" text="Hi there"...
Instead, replaced the 'input' element tag with an 'img' element:
<img text="Hi there" src="disabled.gif"

# 14

An html table needed to be dynamically resized by changing it's style. The original IE-only code:
tableObject.style.left=400
    tableObject.style.top=400
No effect in Firefox (the size remained the same), so needed to explicitly add the unit of measurement "px":
tableObject.style.left=400 + "px"
    tableObject.style.top=400 + "px"
Also applies to 'height' and 'width':
tableObject.style.height="55px"
    tableObject.style.width="33px"


# 15
Dynamically adding some new html into a page relied on 'insertAdjacentHtml':
document.body.insertAdjacentHTML('AfterBegin', '<div>foo</div>')
Worthless in Firefox (at least until HTML5 is supported) so fall back on 'insertBefore':
elementHtml = '<div>foo</div>';

if (document.body.insertAdjacentHTML)
{
     document.body.insertAdjacentHTML('AfterBegin', elementHtml)
}
else
{
    element = document.createElement("div");
    element.innerHTML = elementHtml;
    document.body.parentNode.insertBefore(elem, document.body);    
}
(Orignally, used
document.body.insertBefore(element, document.body.childNodes[0])
but that seemed to cause the event (specifically the 'onload' event of an image) to fire repeatedly in Firefox so changed it to use the one listed above.)


# 16

The mouse's position was necessary to figure out where to render a dynamically injected image. In IE, to determine the X and Y coordinates relative to the web page document:
window.event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
    window.event.clientY + document.body.scrollTop + document.documentElement.scrollTop
Functions 'client<X|Y>' tell you the "viewport" position of the mouse which is a smaller, overlapping portion of the entire document but not a true subset of it. To obtain the document's actual mouse position, you need to add to these position values the scroll values by using the other functions shown above. Specifically,
document.body.scroll<Left|Top>
are the older (i.e. quirksmode) DOM syntax to retrieve the scroll values while
document.documentElement.scroll<Left|Top>
are the more modern standard approach. Depending on the browser only one of these will have the actual value while the other will equal zero. Therefore, it's safer and relatively harmless to include both.

In stark contrast, Firefox simply uses these:
e.pageX
    e.pageY
For a comprehensive code snippet that works across most major modern browsers:
function doSomething(e) {
 var posx = 0;
 var posy = 0;
 if (!e) var e = window.event;
 if (e.pageX || e.pageY)  {
  posx = e.pageX;
  posy = e.pageY;
 }
 else if (e.clientX || e.clientY)  {
  posx = e.clientX + document.body.scrollLeft
   + document.documentElement.scrollLeft;
  posy = e.clientY + document.body.scrollTop
   + document.documentElement.scrollTop;
 }
 // posx and posy contain the mouse position relative to the document
 // Do something with this information
}


# 17

To cancel an event just for the local scope only but not stop the event from bubbling up to the rest of DOM tree, in IE set
e.returnValue
to false.

For Firefox, use:
e.preventDefault
Cross-browser function:
if(e.preventDefault)
  {
    e.preventDefault();    // Firefox
  } 
  else
  {
    e.returnValue = false;    // IE
  }

One last (thought) snippet

While extremely beneficial being exposed to JavaScript's historical client-side scripting messiness in different browsers, next time when faced with cross-browser quirkiness I'd use a library like jQuery for simplified and easier web development.