Saturday, January 8, 2011

Adding Cross-browser support to the Gravey Framework

In order to get back into the Web 2.0 development world, I have recently taken on the project of upgrading the Gravey framework to run on browsers other than Internet Explorer. This post is a chronicle of what I encountered/learned getting its sample applications to also run on iPad 4.2, Safari 5.0, Chrome 8.0, Firefox 3.6, SeaMonkey 2.0, and IE 8.0 (while not breaking IE6 on Win2K). [Note that the issues listed herein may have an IE-centric viewpoint, but it is only because the original code base naively used circa-2005 IE-only techniques, and therefore the news for Gravey is how everybody else does it now.]
The Gravey framework (gravey.org) is a JavaScript-only set of code that supports building Rich-Internet-Apps (RIA) using AJAX where the logic completely resides in the browser. The server-side need only provide a REST-style API for data persistence (which can be a simple as the file:: protocol accessing local xml files). The framework includes GUI widgets, automated domain object persistence, in-browser XML/XSL processing, and complete undo/redo capability.  In short, it was intended to allow RIA development in the style of "fat GUI" development in Java without requiring any particular server side technology (e.g. J2EE, .Net, LAMP, whatever).  The new version 2.5 of Gravey now runs on several browsers (including Safari on the iPad) without requiring any particular client-side technology (e.g. jQuery, Prototype, whatever).
Because it was back in 2005, when I was learning DHTML/OO-JavaScript/AJAX/CSS/REST on the fly, while building Gravey for use in several internal applications for a top-5-in-the-USA bank, and because their requirements only mandated (and their schedule only allowed) Internet Explorer 5.5 as the target browser, Gravey v1.0/v2.0 required IE5+, and were only tested on IE5/IE6 in Win2K/WinXP.

Since Gravey is built in layers, I had originally intended to merely retrofit the bottom layer to use jQuery to make short work of cross-browser compatibility.  However, I immediately encountered enough problems with jQuery that I was required to really learn the issues anyway, and so I ultimately fixed all of the problems in Gravey directly, and as a result, it still has no dependence on any other frameworks.

• AJAX / XML / XSL Issues

Issue:  jQuery doesn't cover in-browser XSL processing
Gravey and its example apps use in-browser processing of the XML returned from AJAX calls via XSL (also loaded from the server).  It turns out that the XSL processing API is browser-specific, and because jQuery didn't handle it, I was going to have to learn how to fix it directly anyway...

Issue:  IE doesn't do XSL processing like everyone else
In order to support older IE browsers, the existing Gravey code that uses ActiveX is required, but other browsers need to use the standard DOM APIs.
  • IE really wants XSL files to be loaded via Msxml2.DOMDocument even though XML files can be loaded via Msxml2.XMLHTTP. So, once the XML DOM has been loaded via AJAX, the IE XSL processing looks like...
    var xmlDom = XHR.responseXML;
    var xslDom = new ActiveXObject("Msxml2.DOMDocument");
        xslDom.async = false;
        xslDom.load( xslURL );
    if (xslDom.parseError.errorCode != 0)
        alert("Error loading["+xslURL+"]:" + xmlDom.parseError.reason);
    var resultStr = xmlDom.transformNode( xslDom );
  • The other browsers accomplish this via the standard API...
    var xmlDom  = XHR1.responseXML;
    var xslDom  = XHR2.responseXML;
    var xslProc = new XSLTProcessor();
        xslProc.importStylesheet( xslDOM );
    var resultStr = 
xslProc.transformToDocument( xmlDOM ).documentElement.textContent;
  • BTW, when I used an XMLSerializer to generate the result string in the above code, it gave me quotes around the result, so using the .textContent was simpler and did not include extraneous quotes.
  • IE also requires the XML file to be loaded via Msxml2.DOMDocument when the files are local. In other words, when the .html page is loaded from local disk via the "file::" protocol instead of via "http::" from a web server, IE fails attempting to load the xml unless it is via Msxml2.DOMDocument. Otherwise, for local files, the AJAX callback will be given readyState==4 and status==0, instead of status==200. At this point in Firefox/others, the file has loaded ok, but in IE it will have failed.
Issue:  Some browsers can't handle <xsl:import>
While good ole IE6 can handle an XSL file including another one via the <xsl:import> directive, new browsers like Safari and Chrome can't.  I never did find a reasonable fix, so I bit the bullet and just copied the imported XSL directly into the top level XSL file. Not pretty but it works.  If I had a larger system to worry about, I would preprocess, on the server side, the XSL file to include the imports before sending it back to the browser's AJAX request.

Issue:  Firefox doesn't call your AJAX callback on synchronous requests
Parts of Gravey performed AJAX requests with async=false with no problem until Firefox came along.  It turns out FF doesn't call your callback function like everyone else on synchronous requests. For a fix, I simply changed to async=true because I was already processing the responses via callback instead of inline code anyway! [ Back in 2005, I had thought that synchronous requests might be faster, or higher priority, than async requests, but experience has taught me otherwise.]

••• DOM Issues •••

Issue:  jQuery doesn't like funny characters in your element IDs.
This problem I ran into immediately because Gravey uses all sorts of special characters (e.g. ".", "#", "_", ":", etc), and it took a while to track down a fix.  By that time, I had taken to fixing Gravey directly.  But there is a work-around...
Given that ID="tar.foo.bar", replace the following...
    $(ID).whatever...
with...
    $( jq(ID) ).whatever...
where...
    function jq(ID){ return '#' + ID.replace(/(:|\.)/g,'\\$1'); }

Issue:  Only IE looks up Names as well as IDs in getElementById()
Gravey originally created HMTL elements identified by the "name" attribute because it worked just fine in IE when doing document.getElementById("foo").  Other browsers don't, SO, I changed all those .name references to .id references.

Issue:  innerHTML doesn't equal what it was just set to
Early on, I came across speculation that browser security might strip away inline JavaScript when setting the .innerHTML of some element. Being paranoid, I put in code to check if the .innerHTML I just set, was different than what I just set it to. To my surprise, it was quite often literally different, but not functionally different. In other words, the before and after strings did not match because the quotes might have been changed, the attributes might be in different orders, and the upper/lowercase of the tags might have been switched.  I never caught browser security stripping off anything, but the .toString() of the innerHTML could be completely reworked compared to my original HTML string.

Issue:  Saving data between page loads in window or navigator properties is no longer reliable
In the olden days of IE6 on Win2K, one could create properties on the navigator object that would allow communications between page loads.  With modern browsers this isn't reliable.  Gravey used this ability to save a reference to data between the time an AJAX request was made and when the callback was later invoked on the reply event. Luckily, I was only doing this because I didn't understand JavaScript "closures" enough to use one instead.  The Gravey 2.5 code uses a closure to save data in the callback function to be accessed when that function is later invoked on the reply event.

If I really needed client-side storage of data in between page loads or between pages, I would hope to wait until the near future when HTML5 is supposed to have local storage to share fat data between pages. There is an article here that tests this new feature out on a the usual suspect browsers.

Issue:  Some browsers change the placement of span and legend tags
In tracking down all the event handling problems, I came across a bug in some browsers (e.g. Firefox/Safari/SeaMonkey, but not Chrome/IE) where I created a <legend> tag inside of a <span> tag (via setting innerHTML), but in Firefox (via Firebug) I could see that it put the legend outside and after the span!  Luckily, I could live with the <span> being inside of the <legend>, and all the browsers seem to handle that ok.

Issue:  IE8 changes behavior depending on where a page is served up from
I was trying to use a simple trick to determine if the browser is IE (and which version of IE) via conditional compilation as shown here at quirksmode.org. Unfortunately, IE8 on WinXP would compile the comments and define the flags when the page was loaded from my test server on my LAN.  The exact same code would NOT compile when served up from my Internet web site.  Go figure. I tried adding a DOCTYPE to invoke quirks mode (which I had not specified before), but that made no difference. I wound up using the code below to distinguish between the cases that I needed to determine: pre-IE7, IE-in-general, IE-style-AJAX, Firefox-style-AJAX.
In the home web page...
    <!--[if lte IE 6]>
    <script type="text/javascript">
      //NOTE: THIS LOOKS COMMENTED OUT BUT IT IS ACTIVE IN IE !!!
      var Pre7IE = true;
    </script>
    <![endif]-->

In the javascript...
    function IE_Pre7(){ return (typeof(Pre7IE)=="undefined") ? false : Pre7IE; }
    function FF_AJAX(){ return document.implementation
 
                            && document.implementation.createDocument; }
    function IE_AJAX(){ return IE_Pre7() || !FF_AJAX(); }
    function Is_IE  (){ return '\v'=='v'; }


••• Display and Styling Issues •••

Issue:  Handling of partial escape sequences is browser-specific
A bug in Gravey went undetected because IE quietly ignored partial escape sequences like "&nb". The bug generated partial escape sequences in element text (e.g. <div>foo&nb</div>) and browsers like Safari and IE quietly display "foo".  Other browsers like Firefox and Chrome display "foo&nb". I fixed the bug so that only full sequences like "&nbsp;" would be generated.

Issue:  Special character escape sequences are browser and OS-specific
Originally, Gravey used special characters via the Microsoft "symbol" font, so I had a goal to replace them with standard HTML characters. E.G. replace...
    '<font face="Symbol">&#101;</font>'
with... 
    "&epsilon;"

The set of graphic characters like arrows, and bullets, and greek letters, etc can not be all rendered by all browsers.  Unfortunately, the intuitive idea that if an older browser can render a character, its newer brethren would also, is alas not true.  Most browsers, including IE6 on Win2000, can render the black star (&#9734;) and white star (&#9733;) characters, but IE8 on WinXP can't!  Some characters can be displayed when given the hex charcode, but not the name (e.g. &rArr;). And some characters that you would think are part of a set are not all implemented together. E.G. Of the up/down/right/left arrow set (see shapes), IE6 can render the right arrow but not the down arrow.

Issue:  Fieldset Legend rendering is browser-specific
Given the same style settings, the Legends of Fieldsets render differently on different browsers. Gravey had used the attribute "text-align:center" which didn't do anything on IE, but it shifts the entire legend display to the center (instead of the left that I want) on non-IE browsers. I removed the attribute. Unfortunately, the "vertical-align: middle" attribute DOES do something I want in IE but not the other browsers.  It was needed to get the gif images (being used for expanded/collapsed icons) to line up with the title text inside the legend box.  After a lot of fiddling with transparent pixels around the icons and legend padding attributes, I gave up and switched to graphic arrow characters (ala Google results).  Unfortunately, I ran into all of the special character problems listed earlier, and still had to specify a different set of characters (wingdings) for IE6 than specified for the other browsers.

Issue:  CSS doesn't easily replace all HTML presentational attributes
While the original Gravey used some external CSS, there was still a lot of inline style specs, and old-time HTML presentational markup and "spacer" gifs. I set out to replace them all with CSS, but since I was using IE specific attributes like cellspacing, cellpadding, bordercolordark and bordercolorlight it was not trivial. Sadly, some IE attributes like table "cellspacing" can't be replaced with CSS. Others like cellpadding, bordercolordark and bordercolorlight can but with great effort. Luckily, Gravey used a cellspacing and cellpadding of zero, some of which can be specified with "border-collapse:collapse". Also, the bordercolorlight/dark colors being used created an effect similar to { border-style: outset }.

Issue:  Background color gradients are browser-specific
The gradient coloring Gravey used was IE specific. Other browser specific style settings had to be added. E.G. the following...
    body {
      filter:progid:DXImageTransform.Microsoft.Gradient
        (GradientType=0,StartColorStr='#f8f8f8',EndColorStr='#802222');
    }
gets replaced with...
    body {
      background: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#802222));
      background: -moz-linear-gradient(top, #f8f8f8, #802222) no-repeat;
      filter:progid:DXImageTransform.Microsoft.Gradient
        (GradientType=0,StartColorStr='#f8f8f8',EndColorStr='#802222');
    }

Issue:  Tooltip rendering is browser-specific
Tooltips are used extensively by Gravey, and for images it used the .alt text. It turns out that non-IE browsers don't render image .alt text, only .title text, so I changed all the alts to titles.
There is also a known bug in Firefox where tooltips are not displayed on disabled buttons. As a workaround, the "readOnly" attribute can be used, however, when setting the readonly attribute rather than the disabled attribute, I could never get the CSS styling to kick in, even though I tried the selectors ".fooClass[readonly]" and "input[readonly]" and "input:read-only". So, I wound up dynamically setting (via JavaScript) the style for both readonly/not-readonly explicitly via classnames (ala .foo and .fooRO).

Issue:  Cursor styles are browser-specific
Gravey extensively used the "hand" and "not-allowed" cursor shapes which turn out to be IE-specific.  The "hand" cursor can be changed to the standard "pointer" to get the same effect. But while many browsers implement the non-standard "not-allowed", Firefox on the Mac does not and has no real equivalent. Because there is a visual difference between the default cursor and the "pointer" on Firefox, I live with it as is.

Issue:  Disabled button styles are browser-specific
Gravey expects the rendering of GUI widgets to reflect their enabled/disabled status. Some browsers stop showing disabled buttons differently if their foreground color is set to black (as Gravey was doing and IE not caring).  I was able to simply stop setting the foreground color since black was the default color anyway).

Issue:  Chrome displays popup menu items in a strange order
There is a strange (but known?) bug in Chrome where it displays items in a pop-up menu in a weird order that is different than the order shown in all the other browsers. I give up and document it as a known Chrome bug.

Issue:  Cross-browser manipulation of element visibility is complicated
Gravey originally got away with making chunks of HTML visible or not via...

    e.style.visibility = visibleFlag ? "visible"  : "hidden";
    e.style.position   = visibleFlag ? "relative" : "absolute";
    e.style.display    = visibleFlag ? "inline"   : "none";

However, to do this with other browsers the original visible state needs to be saved and reused later when restoring visibility.  Code like the following is needed...
    if (visibleFlag) showElem(e); else hideElem(e);

    function hideElem( e )
    {
      if ( e.style.display === "none" ) return;
      e.oldDisplay = e.style.display;
      e.style.display = "none";
      e.style.visibility = "hidden";
    }
    function showElem( e )
    {
      e.style.visibility = "visible";
      if ( e.oldDisplay ) e.style.display = e.oldDisplay;
      else if ( e.style.display==="none") e.style.display = "";
    }

Also, some browsers have a problem unless the HTML is rendered visible before making it hidden.

Issue:  Pop-up windows are no longer reliable
In the early IE5 days, the cursor would not reliably change to the "please wait" icon when told to, so as a workaround, Gravey used a little "please wait" popup window that was launched and later killed.  Unfortunately, modern browsers suppress popup windows, so, that code won't reliably work anymore. If the cursor STILL can not be reliably set in this modern age, then a replacement "please wait" popup window would have to be done via fancy CSS floating DIVs as I've seen elsewhere.

••• Event Handling Issues •••

Issue:  Event object location is browser-specific
In IE, the event object may be accessed in event handler functions by the global variable "event". Most other browsers pass the event object as a parameter to the event handler function and it is not available as a global variable.
  • Thus, code like the following...
    <body onKeyPress="onKey()">
    ...
    function onKey(){ alert(event...); }
    must be changed to... 
    <script> 
      document.onkeypress = onKey;
      function onKey(e){ e = e || window.event; alert(e...); }
    </script> 
  • Another consequence of event handlers being passed the event as a parameter is that the calling signature has to be changed when passing your own parameters to the event handler. Even if you don't care about the event arg itself, the other parameters need to be shifted down otherwise the formal parameter list won't match the actual parameter list. For example, Gravey generates new HTML elements by building a string and setting the innerHTML.
    So, change code like this...
        e.innerHTML = "<select onchange='return foo("+myArg+")'>";
        ...
        function foo( myArg ){ alert(myArg); }

    to this...
        e.innerHTML = "<select onchange='return foo(event,"+myArg+")'>";
        ...
        function foo( e, myArg )
    { alert(myArg); }
Issue: Not all body tag event handlers are attached to the "window" object

When moving the specification of event handlers from inline in the <body> tag to being set in JavaScript, one might think that they all go on the window event, but not so.  Thus, this tag...
    <body onKeyPress="onKey()" onBeforeUnload="onBUL()" onLoad="onLoad()">
gets replaced with...
    <script>
        document.onkeypress   = onKey;
        window.onbeforeunload = onBUL;
        window.onload         = onLoad;
    </script>

Issue:  Keyboard event objects are browser-specific
In keyboard-related events, some browsers supply the key pressed via the event's "keyCode" attribute, whereas others via the "which" attribute.
  • Thus code that reads keys...
      function onKeyPress(){ alert( event.keyCode ); }
    must be changed to...
      function onKeyPress( e ){
     
       e = e || window.event;
        var code = e.keyCode || e.which;
       
    alert( code );
      }

     
  • Thus code that sets keys (like keystroke filtering)...
      event.keyCode = char;
    must be changed to...
      e = e || window.event;
      if (e.keyCode) e.keyCode = char; else e.which = char;
     
  • Secondarily, some browsers supply a code that directly reflects the use of control keys, etc, whereas others supply a code for the basic key and require also looking at other attributes that flag the use of any modifier keys.  Thus, to detect a control-z key, the following...
      function onKeyPress(){ if ( event.keyCode==26 ) ... }

    must be changed to...
      function onKeyPress( e ){
     
       e = e || window.event;
        var code = e.keyCode || e.which;
        if ( code==26 || code==122 && e.ctrlKey ) ...

      }
Even then, Chrome on the Mac will not generate an event at all when control-y is pressed, even though control-z works fine.  So, after not finding any info about this Chrome quirk, I gave up and documented the use of shift-control-y as a workaround.

Issue:  Modifier keys on mouse click are browser-specific
Gravey and its example apps use "control-clicks" (ie. holding down the control key while clicking) in several places. Unfortunately, on the Mac, control-click activates the context menu (ala back-click). So, for it, allow the "command" key to be used as an alternate. Unfortunately, the DOM3 standard indicator for this key is the event.metaKey flag and Internet Exploder does not even implement that attribute, so references to it will be undefined. So, this code...
  function onClick(){
    var specialFlag = event.ctrlKey;
  }
has to be changed to...
  function onClick(e){
    e = e || window.event;
    var cmdKey = (typeof e.metaKey=="undefined") ? false : e.metaKey;
    var specialFlag = e.ctrlKey || cmdKey;
  }

Issue:  Some "are you sure you want to leave?" techniques are browser-specific
In order to ask if the user really wants to leave the web page, only some browsers recognize setting the event object's "returnValue" attribute to the desired message.  However all browsers tested (except for a known bug on the iPad) will recognize returning the string message from the onbeforeunload event handler.  Thus, code like this...
    function onBeforeUnLoad(){
      if (unsavedData) event.returnValue = "Are you sure?";
    }
gets replaced with code like this...
    function onBeforeUnLoad(){
      if (unsavedData) return "Are you sure?";
    }

Issue: Getting a reference to the event's target element is browser-specific
For example, to blur the element that is the target of a keyboard event, the original code...
    event.srcElement.blur();
has to be changed to (according to here in quirksmode.org)...
    var targ = e.target || e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
        targ = targ.parentNode;
    targ.blur();

Issue:  Focus and Blur and Change events are browser-specific
There is a known problem: "Safari and Chrome do not fire focus/blur events when the user uses the mouse to access checkboxes, radios or buttons. Text fields, textareas and select boxes work correctly."
Hmmm...this is difficult because the entire architecture of Gravey is based on blur events triggering datamodel updates. Lets see if I can fake it for the offending browsers with the change event (but only the offending ones since onchange is apparently buggy on IE).  Actually because of what I am doing on blur is idempotent, I can just make both onchange and onblur invoke the same event handler (and also because of idempotency I dont have to worry about the unpredictable ordering of blur and change events in different browsers).

No comments: