OMG: jQuery tag selectors are case sensitive

… if you use Firefox and client-side XSLT transformations. This is the scenario:

If you use tags in jQuery selectors, for example $(".pulldownMenu P A").click(handler), the tag names are case insensitive if you use IE or server-side transformation with Firefox but become case sensitive if Firefox performs client-side transformation regardless of xsl:output settings. To ensure your jQuery code works in all cases, write exactly the same tag names in jQuery selectors and XSLT stylesheet.

Detect whether your browser has a working XSLT implementation

After I’ve finally managed to persuade IE7, FF and Transform jQuery plugin to work with my XSL documents, I’ve started testing the other two browsers I have: Opera and Chrome. The latest release of Opera might occasionally work. Chrome fails (as expected) as it doesn’t support xsl:import. Welcome to the next round of browser incompatibilities.

I’ve decided to ignore Chrome until the great minds @ Google decide to implement XSLT properly. Visitors using it will have reduced experience … but of course I have to detect whether I should use XSLT or not.

Here’s a small Christmas gift if you have similar issues: a jQuery extension that checks whether the current visitor can use XSLT transformations.

jQuery.xslt = {
  need: function(success,url,xml) {
    var componentCount = 0;
    var xslt;
    var el;
    var debug=1;
    
    function tryTransform() {
      if (typeof(xslt) != 'object') return; // XSL did not parse into XML object
      var html = $.transform({xmlstr: xml, xslobj: xslt, async: false});
      alert("html="+html);
      if (!html) return;
    }

    function gotScript() {

      function transformFail(html,xsl,xml,obj,ex) { 
        if(debug) alert ("$.xslt.need failed:"+ex.message); }
    
      function transfromDone(html,xsl,xml,obj) { 
        $.xslt.has = html.search($.xslt.expectedResult) >= 0;
        if ($.xslt.has && success) success();
      }
      
      el = $("<div>");
      el.transform({xmlstr: xml, xsl: url, 
        success: transfromDone, error: transformFail});
    }
    
    url = url ? url : $.xslt.defaultURL;
    xml = xml ? xml : $.xslt.defaultXML;
    $.getScript($.xslt.transformScript,gotScript);
  },
  has: false,
  
  defaultURL: "/forms/xml/static.xsl",
  defaultXML: "<section />",
  transformScript: "/common/js/jquery.transform.packed.js",
  expectedResult:  /table/i
}

The extension implements a simple function and a property: $.xslt.need() and $.xslt.has. You can pass the URL to the sample XSL document and the sample XML markup to the need function; it also supports a success callback to tell you whether you should install XSL-related action handlers.

The need function loads the jQuery Transform plugin (reducing the load time if the page does not need the XSLT functionality) and tries to transform a sample XSL document. The sample transformation should be as complex as possible: you should use xsl:import, xsl:include or the document() function if you use them in other transformations.

You could change the default parameters in the source code or write a setup function.

IE7, xsl:import and jQuery Transform plugin

After exhausting all other (more convenient from my perspective) approaches, I’ve had to admit that there’s only a single solution that allows you to include/import XSL stylesheets with the jQuery Transform plugin if you want to:

  • Use XSL documents from another directory on the Web server.
  • Use xsl:import or xsl:include in the XSL documents.
  • Have a reliable cross-browser implementation.

The solution, as you might have guessed, is to use the URL pointing to the XSL document in the call to $.transform (the xsl parameter). And just in case you’re wondering why I had to try everything else: I wanted to use the $.transform() call to get the transformed HTML without the delay incurred by setting the async parameter to false.

Xsl:import fails when using XSL object in $.transform

After the initial set of xsl:import related problems I’ve encountered running jQuery Transform plugin in IE7, I’ve tried to pass the XML document as string (using the xmlstr parameter) and XSL as parsed object (using the xslobj parameter) returned by the $.get call. Works perfectly in FF, fails miserably in IE7. The error message indicates that the imports requested with xsl:import don’t work at all. Back to the drawing board …

Import/include problems in jQuery Transform plugin

I started testing the jQuery Transform plugin with a set of pretty convoluted XSLT documents that I use on the target web site, which relies almost exclusively on browser-side XSLT transformations. All server responses are initially encoded as XML and transformed on the server only if the browser does not support XSLT (or if a spider has come to visit us). Not surprisingly, the XSLTs are heavily loaded with xsl:import and xsl:include statements.

Initially, I tried to control as much as possible: read XML and XSLT into strings and perform the transformation when everything has been collected. This approach fails miserably in IE7. The reason is simple: if I pass XSLT source or parsed XSLT object to the $.transform routine, the routine tries to fix xsl:import and xsl:include references using the current web page’s URL as the base reference, which is wrong if the stylesheet has been loaded from another directory.

Conclusion: if you read XSL documents from another directory and pass them as string to the $.transform routine, make sure you use absolute references in import/include statements.

jQuery Transform plugin

Finally I found some time to start working on XSLT transformations in jQuery framework. I may be completely wrong, but I decided to go with the Transform plugin. If anyone has found a better plugin, I would highly appreciate your feedback :).

Refactoring a menu: remove inline CSS

If you’re new to this series, read the “Refactoring a simple menu” and “Changing the background images” first.

The next step in HTML/CSS refactoring was to clean up the very ugly HTML code … I don’t know what I was thinking at that time; the menu full of repetitive inline CSS is plainly stupid.

Looking at the HTML code, it’s obvious that:

  • The row buttons are independent DIV elements;
  • Each row button has a P child and potentially a DIV child (holding the pull-down menu box).
  • The pull-down menu box has P children that are styled identically to the P elements in the row buttons.

So here’s a very obvious CSS solution:

  • Mark the row button DIV elements with a class (rowMenu).
  • Define styling for P, A and DIV children of the DIV.rowMenu elements.

The resulting CSS is pretty simple …

DIV.rowMenu { float: left; position: relative; z-index: 100; }
DIV.rowMenu DIV { position: absolute; display: none; }
DIV.rowMenu P { … original btx170 definition … }
DIV.rowMenu P.down { … btx170_down definition … }
DIV.rowMenu A { color: #000; text-decoration: none; }

… and so is the cleaned-up HTML code:

<div class="rowMenu" id="IDAHYJQB">
  <p><a href="/climbing/myClimbs/myClimbs.asp">First page</a></p>
</div>
<div class="rowMenu" id="IDAKYJQB">
  <script>menuRegister('IDAKYJQB')</script>
  <p id="IDAKYJQB_main"><a href="javascript:menuClick('IDAKYJQB')">Add ...</a></p>
  <div id="IDAKYJQB_sub">
    <p><a onclick="menuSelect('IDAKYJQB')" 
        href="/climbing/myClimbs/myClimbs_add.asp">New entry </a></p>
    <p><a onclick="menuSelect('IDAKYJQB')" 
        href="/climbing/myClimbs/myClimbs_editWall.asp?a=add">Edit</a></p>
    … more …
  </div>
</div>

Next task: get rid of superfluous element IDs.

Refactoring a menu: Changing the background images to borders

Before reading this: read the “refactoring a simple menu” post to get the background information.

One of the stupidities I did in the original menu implementation was to use CSS background images for simple buttons that could be implemented equally well with CSS borders. It would be hard(er) to get rid of background images if I would have had rounded corners or shaded button background, but I had none of those. So here’s the new CSS definition, replacing images with borders.

The new CSS has another great side-effect: the button sizes are specified in EMs, not in pixels, so the buttons get resized automatically if you change points-to-pixels ratio.

.btx170,.btx170_down {
  overflow: hidden; cursor: pointer;
  padding: 0 0 2px 0; margin: 0 0; text-align: center;
  font-family: Verdana, Arial, Helvetica, sans-serif; 
  font-weight: 700; font-size: 8pt; }
  
.btx170, .btx170_down { width: 16em; }
.btx170 { background-color: #FFCE63; border: 2px outset #FFCE63; }
.btx170_down, { background-color: rgb(255,156,0); 
  border: 2px inset rgb(255,156,0); }

Next task: Remove inline CSS

Refactoring a simple menu

Years ago I had to implement a drop-down menu. Nothing fancy; no open-on-hover magic, but a simple line of buttons with drop-down boxes that would open on clicking the button.

There were just a few annoying details: clicking a top-row button should obviously open a drop-down box, but also change state the button’s state to “depressed” and close any other open drop-down box (and change the state of corresponding buttons).

Here is my five-year-old HTML code …

<div style="float: left; position: relative; z-index: 100;" id="IDAHYJQB">
  <p class="btn170">
    <a href="/climbing/myClimbs/myClimbs.asp">First page</a>
  </p>
</div>
<div style="float: left; position: relative; z-index: 100;" id="IDAKYJQB">
  <script>menuRegister('IDAKYJQB')</script>
  <p class="btn170" id="IDAKYJQB_main">
    <a href="javascript:menuClick('IDAKYJQB')">Add ...</a>
  </p>
  <div style="position: absolute; display: none;" id="IDAKYJQB_sub">
    <p class="btn170">
      <a onclick="menuSelect('IDAKYJQB')" 
        href="/climbing/myClimbs/myClimbs_add.asp">New entry </a>
    </p>
    <p class="btn170">
      <a onclick="menuSelect('IDAKYJQB')" 
        href="/climbing/myClimbs/myClimbs_editWall.asp?a=add">Edit</a>
    </p>
    … more …
  </div>
</div>

… the corresponding CSS …

.btn170, .btn170_down { background-repeat: no-repeat; 
    width: 170px; height: 20px; line-height: 18px; overflow: hidden; 
    padding: 0 0; margin: 0 0; text-align: center;
    font-family: Verdana, Arial, Helvetica, sans-serif; 
    font-weight: 700; font-size: 11px; }

.btn170
  { background-image: url('images/button_170.gif');  }   
.btn170_down
  { background-image: url('images/button_down_170.gif'); }

… and JavaScript code …

var topMenuItems = [] ;

function addClass(id,sfx) {
  var se = getElement(id) ;
  if (se.className.indexOf(sfx) < 0) se.className = se.className + sfx ;
}
function removeClass(id,sfx) {
  var se = getElement(id) ;
  var i = se.className.indexOf(sfx) ;
  if (i > 0) se.className = se.className.substr(0,i) ;
}

function menuShow(id) {
  var se = getElement(id) ; se.menuActive = true ; 
  showElement(id + "_sub") ; addClass(id+"_main","_down"); }

function menuHide(id) { 
  var se = getElement(id) ; se.menuActive = false ; 
  hideElement(id + "_sub") ; removeClass(id+"_main","_down"); }

function menuGo(id,l) { menuHide(id); location.href = l; }
function menuSelect(id) { menuHide(id) ; }

function menuClick(id) {
  var i,se ;
  se = getElement(id) ;
  if (se.menuActive) {
    menuHide(id) ; return ;
  }
  for (i = 0 ; i < topMenuItems.length ; i++) {
    if (topMenuItems[i] != id) menuHide(topMenuItems[i]);
  }
  menuShow(id) ;
}

function menuRegister(id) { 
  topMenuItems[topMenuItems.length] = id ;
}

I will not try to explain what this code does, as it’s way too painful. As I’ll walk through the refactoring process, I’ll show you the changes I’ve made and the stupidities in the original code.