MathJax

SyntaxHighlighter

Highlight

Custom CSS

Thursday, April 22, 2010

Considerations for using jQuery 1.4.2 in a Firefox 3.5 extension

Due to certain project requirements, I found myself using jQuery within a Firefox 3.5 extension, and boy was it a doozy. The problem is that most Firefox extensions just dump a bunch of code inside the script tag of an overlay on top of browser.xul (see more discussion here). The ramifications of this are that any var you declare is global to the browser and to any other extension the user has installed. Some extensions, like Firebug, get around this by prefixing every global variable with FB_, but this solution will not work with jQuery.

If I decide to include jQuery in my overlay like so:

<script src="chrome://myextension/content/jquery-1.4.2.js"></script>

...this leads to a variety of problems. Firstly, jQuery will automatically set the window.jQuery and window.$ variables in the chrome window, overriding the values that were already there. This is not a terrible problem for $ (which jQuery is nice enough to give it back to you via a call to jQuery.noConflict()), but the jQuery variable is overridden as well. This poses a problem for other not-so-nice extensions that actually use jQuery in this rude manner. For example, if I included jQuery 1.4.2 in my extension and you included jQuery 1.3.2, it would be a crapshoot to determine whose jQuery gets loaded (depends on the overlay loading order).

A second problem for jQuery 1.4.2 is that it actually manipulates the DOM to test for the availability of certain features, which is a big no-no for overlays. The DOM isn't actually available in a XUL document until after all of the overlays have loaded (after all, the overlays themselves dictate elements that should be in the DOM). If you try to access the DOM in an overlay, your overlay will silently not be loaded. Incidentally, all JavaScript in overlays should be executed by an event handler to avoid this problem:

window.addEventListener(
  "load",
  function () {
    // Do stuff after the overlay has loaded
  },
  false
);

One horrific solution to this is to load jQuery in a load handler, and edit the actual jQuery source to stop it from installing itself onto window. For the sadists out there, here's what it would look like:

window.addEventListener(
  "load",     
  function () {
    Components.classes["@mozilla.org/moz/jssubscript-loader;1"].
      getService(Components.interfaces.mozIJSSubScriptLoader).
      loadSubScript("chrome://myextension/content/jquery.js");
  },
  false
);

Of course, chrome://myextension/content/jquery.js would have to be a modified version of jQuery that simply sets the jQuery and $ in the current scope, rather than on window itself. The details of such a hack are omitted (though it's simply a 2 line change within the jQuery source). This solution; however, still allows jQuery itself to run free and wild, doing who-knows-what to the DOM after it has loaded, which is fine and dandy for a webpage but potentially ... explosive for a chrome window.

Another solution is to get jQuery loading within a Firefox code module. Code modules are "sort of" like Google Chrome's content scripts in that any code in a code module will execute in its own context (thus it is unable to stomp on code in overlays unless you explicitly tell it to), but Firefox code modules do not give you access to any sort of DOM, so loading jQuery naively will cause exceptions to be thrown.

So far, I have not found any acceptable solution to this problem. The "cleanest" possible solution may be a jsdom style fake DOM (taking advantage of the Firefox internals to provide implementations such as nsIDOMDocument, and nsIXMLHttpRequest) to fool jQuery, but that's a lot of work.

A poster on the jQuery forums, chewie1024, suggests giving jQuery a real live browser window within a code module by simply giving it a reference to the main browser window. This solution is nice and simple:

var EXPORTED_SYMBOLS = ["jQuery", "$"];

var windowMediator = Components
  .classes["@mozilla.org/appshell/window-mediator;1"]
  .getService(Components.interfaces.nsIWindowMediator);            
var enumerator = windowMediator.getEnumerator("navigator:browser");
if (enumerator.hasMoreElements()) { 
  var window = enumerator.getNext();
  var location = window.location;
  var document = window.document;
  break;
}

// jQuery source follows here...

...however, you'll still have to hack jQuery to make it not install itself on the window object, and you'll have to live with the fact that jQuery will muck around with the DOM on that window. On the flip side, you'll have jQuery in a code module that you can import into any overlay, in any scope you like.

One thing to note is that in the above solution, jQuery will keep a reference to that main window, and anything you do with jQuery will be done on that window by default. That is, make sure that you pass in a context to jQuery or it will be operating on the wrong thing. Another thing to note is that the window that jQuery keeps a reference to can be closed and some of its members could be garbage collected. Unknown behavior will result if that is the case, so keep an eye out if you use this solution (because the symptoms will be seemingly random).

In conclusion, I have no satisfactory solutions to this problem, so if anybody knows of one, please send me a message.
Post a Comment