Using YUI in Greasemonkey Scripts

By Carlo ZottmannJanuary 3rd, 2007

About the Author: Carlo Zottmann is a Market Engineer who works for Yahoo! in Munich, Germany. He spends his days integrating feeds from Yahoo!’s European content partners, helping develop new features for de.yahoo.com and fixing things. He’s usually employing Perl, PHP, Python or Javascript, or whatever the job requires.

Carlo has been blogging since 2001 and he blogs (mostly in English) at http://carlo.zottmann.org/; his blog is called “tail -f carlo.log”.

I love Greasemonkey. I like how much power it gives me when it comes to bending other peoples’ websites to my will, how I can add features to or or ditch them from a website, how I can use Greasemonkey scripts to pull data from all over the net to spice up the very page I am looking at. It makes my daily life as Yahoo! engineer easier.

Also, I love the Yahoo! UI library. YUI contains JavaScript and CSS components that allow anyone to quickly build some pretty amazing things.

Wouldn’t it be great if we could bring Greasemonkey and YUI together? How nice would it be to use YUI components anywhere, to have Greasemonkey dynamically load the libraries when needed and to attach YUI-powered thingamajigs to any page we like? For example to add autocompletion to form fields, or to make use of the advanced event management in your Greasemonkey scripts! The mind boggles.

In this brief article, I’ll share with you my own effort to reach that goal — a Greasemonkey script that adds calls to external JavaScript libraries and CSS files to a given page and, once they are loaded, passes the YAHOO global object to the code inside the Greasemonkey script. (All YUI components reside within this single single global variable, YAHOO — so, for example, you access the YUI Event Utility by referencing YAHOO.util.Event.) I’m sure that this approach is neither the perfect nor the only solution to achieving YUI/Greasemonkey integration, so suggestions and ideas are welcome! Please sound off in the comments and let me know what approaches you’ve taken to this problem in your own projects.

An Example Greasemonkey Script Implementing a YUI Loader and Using YUI Components

What I am interested in sharing with you here, primarily, is the mechanism by which you can include and invoke YUI from within a Greasemonkey script while reusing (and not disturbing) existing YUI components already present in the document. I’ll do that by exploring a simple Greasemonkey sample script that translates selected text on YUIBlog.com, Yahoo! News, or my personal blog using Yahoo! Babelfish; with the script installed, you can highlight any passage of text on one of those sites and, if you hold down the shift key while releasing the mouse, a YUI Panel with a German Babelfish translation will pop up. (If you want to install and test the script, you can download it from http://carlo.zottmann.org/code/yuigm_example_yuiblog_babelfish.user.js; the script is configured to operate only on http://*yuiblog.com/*, http://news.yahoo.com/*, and http://carlo.zottmann.org/* URIs).

In case you’re not using Firefox, here’s a quick example of the script in action. Click the screencapture below to see a 10-second QuickTime movie of the interaction:

The example script in action: The Greasemonkey script loads YUI components, sends selected text to Yahoo! Babelfish for translation, then displays the results in a YUI Panel Control.

Key Objects in the Script

We have four key objects in the script: GM_YUILOADER, GM_YUILOADER_CONFIG, YBFLOOKUP, and of course the YAHOO global object.

  1. GM_YUILOADER holds all the logic to inject the necessary JavaScript and CSS files, makes sure they are loaded and triggers execution of the main part of the script (the “payload”).
  2. GM_YUILOADER_CONFIG contains the configuration parameters for our YUI usage, including the list of YUI JavaScript libraries and/or CSS files we want to load, the maximum time to wait for for said files to complete loading, the frequency with which to check for completion, and information about which callback function to fire once everything is loaded.
  3. YBFLOOKUP is the “payload”, containing the code where we use YUI to achieve our goals. In our example this is the Babelfish YUI Panel (hence the name of the object) which will display a German translation for the English text on the page that was marked by the user.
  4. YAHOO is what you would expect — the YAHOO global object. It is avaible once GM_YUILOADER has triggered execution of the main part of the script.

The Loader

The loader is the most critical component of the script, and it’s the part that you are most likely to want to adapt in creating your own YUI-based Greasemonkey implementations. Here is the general workflow of the GM_YUILOADER technique.

  1. Greasemonkey triggers script execution.
  2. GM_YUILOADER.loader() is called and…
    • adds a GM_YUILOADER_DOC property to Greasemonkey’s unsafeWindow.document which (among other things) holds a counter, a so-called trigger variable and a function (which increments aformentioned counter; if the counter reaches the number of included <script/> tags, the trigger variable is set to true)
    • adds new <script src="..."/> and/or <link rel="stylesheet" type="text/css" href="..."/> tags to the page (unless that YUI component is already included in the page, which is determined using object detection)
    • adds onLoad event handlers to above <script/> tags (which call the
      function inside unsafeWindow.document.GM_YUILOADER_DOC)
  3. GM_YUILOADER.loaderCheck() is run periodically, checking the status of the trigger variable, until one of two things happens: either the variable is true, in which case the payload logic is invoked (i.e. YBFLOOKUP.run()) after making the YAHOO global object available to the Greasemonkey script, or the maximum loading time is reached, which will cause the script to abort.

Let’s take a look at some of the loader-specific code in the sample script. First, you specify the YUI components on which your Greasemonkey script will rely. You do so in an structured object — the assets member of the GM_YUILOADER_CONFIG object:

// Settings used by the loader "engine"
var GM_YUILOADER_CONFIG = {
    // List of JS libraries and CSS files to load. obj is used for the object
    // detection used in the loader. Basically, if the object already exists,
    // the script is not injected in the page.
    assets: [
        { type: 'css', url: 'http://developer.yahoo.com/yui/build/container/assets/container.css' },
        { type: 'js', obj: 'YAHOO', url: 'http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/yahoo_2.1.0.js' },
        { type: 'js', obj: 'YAHOO.util.Event', url: 'http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/event_2.1.0.js' },
        { type: 'js', obj: 'YAHOO.util.Dom', url: 'http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/dom_2.1.0.js' },
        { type: 'js', obj: 'YAHOO.util.Anim', url: 'http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/animation_2.1.0.js' },
        { type: 'js', obj: 'YAHOO.widget.Panel', url: 'http://us.js2.yimg.com/us.js.yimg.com/lib/common/widgets/2/container/container_2.1.0.js' }
    ],

By comparing this list with the YUI objects that may already be present in the YAHOO global object, the script creates a “to-do” list of needed-but-missing components. It can then loop through the needed assets and include them on the page. Here’s the underlying code for that part of the loader:

// Now let's add the extra tags to the page that'll load the libraries and
    // CSS files.

    var numAssets = GM_YUILOADER_CONFIG.assets.length;

    for (var a = 0; a < numAssets; a++) {
        var tag;
        var asset = GM_YUILOADER_CONFIG.assets[a];

	switch (asset.type) {
		// CSS file
		case 'css':
			tag = document.createElement('link');
			tag.href = asset.url;
			tag.type = 'text/css';
			tag.rel = 'stylesheet';
			break;

		// Javascript library.
		case 'js':
			var injectScript = true;

			// Object detection
			try {
				injectScript = eval('window.' + asset.obj + ' === undefined');
			}
			catch (e) {}

			if (injectScript) {
				tag = document.createElement('script');
				tag.src = asset.url;

				// The crucial part: triggering document.GM_YUILOADER.countLoaded()
				// means keeping track whether all scripts are loaded yet.

				tag.setAttribute('onload', 'document.GM_YUILOADER_DOC.countLoaded();');

				// How many JS libraries are we dealing with again? Let's keep
				// track.

				ud.GM_YUILOADER_DOC.numberTotal++;
			}
			break;
	}

	document.body.appendChild(tag);
}

There are other details taken care of in the loader portion of the script, but this is the heart of the logic & and the code above captures the essence of this approach to marrying YUI with Greasemonkey.

The Payload

The practical part of the script (a.k.a. the "payload") is pretty straightforward: a simple, invisible YUI Panel is built, a mouseup event handler is attached to the document body. Once triggered, it'll check if text was selected and if the shift key is still pressed; if so, it'll grab the German translation for the text from Babelfish, put it in the body of the Panel and invoke the Panel's show() method.

At the heart of the payload is an event listener listening for the mouseup event on the window object. Here's the beginning of that event handler:

// Event handler for mouseUp events
YBFLOOKUP.subscriberSelect = function(e) {
var selection = window.getSelection();
var selectionText = selection.toString();

// Shift key pressed? Anything selected?
if (!e.shiftKey || selectionText == '') { return; }
YBFLOOKUP.panel.setBody('Loading Babelfish EN-DE translation, just a second...');
YBFLOOKUP.panel.cfg.setProperty('x', e.clientX + 20);
YBFLOOKUP.panel.cfg.setProperty('y', e.pageY + 20);
YBFLOOKUP.panel.show();

From there, the script proceeds to make a call to Greasemonkey's built in facility for loading external pages (GM_xmlhttpRequest), loads the translation from Yahoo! Babelfish, and shows the result in the Panel.

Closing Words

The ability to use YUI in Greasemonkey scripts can be quite beneficial to Greasemonkey developers. I know from personal experience that YUI brings a lot of new options and tools to the Greasemonkey playing field that, without a library, you would have to build on your own. Also I like the idea of playing with new YUI-powered gimmicks on a live site; for instance, it is rather easy now for me to to inject autocompletion into a form field on a live Yahoo! page just to see what it would look and how it would behave — without running the risk of destroying things and without having to set up a dedicated development environment, do exhaustive QA testing, or ask anyone for permission. Greasemonkey captures the essence of hacking and it opens up wonderful creative opportunities.

For me personally, YUI and Greasemonkey are a perfect fit, and I'd like to use this opportunity to thank both the Greasemonkey developers and the YUI crew for their ingenuity and willingness to share the love with us.

19 Comments

  1. [...] The aforementioned tech article is available now on YUIblog.com! It’s explaining a way to use the excellent YUI library in Greasemonkey scripts. I’m sure that this approach is neither the perfect nor the only solution to achieving YUI/Greasemonkey integration, but it works for me and I thought sharing can’t hurt. [...]

  2. Wow! This is really quite an impressive feat, and it opens the doors for a lot of new scripts.
    Does the date in your screencast imply you’ve been holding this from us for over seven months?!

  3. Thanks, Paul.

    “Does the date in your screencast imply you’ve been holding this from us for over seven months?!”

    Yes, we’re quite secretive. ;) But no, this screenshot (and the movie) were taken on a dated-back mock-up page.

  4. @Paul: Yes, we’re excited too about putting Carlo’s work to use. As for the screen grab, that was me — and, no, we would never hold out on you for seven months. Just mocked up the piece in an old template, that’s all. Regards, Eric

  5. This is brilliant work, Carlo. You’ve put together an elegant mechanism for bringing these two powerful tools together.

    Beyond Greasemonkey, it strikes me that this sort of technique might be used to create JavaScript packages that could be easily integrated into a preexisting website. It’s more or less an `include_once` framework for JavaScript.

  6. Peter Herrmann said:
    January 3, 2007 at 6:07 pm

    Does this mean that you, Yahoo are OK with me and my users using the yui js files via the URLs you posted e.g http://us.js2.yimg.com/us.js.yimg.com/lib/common/utils/2/yahoo_2.1.0.js ?

    Is there a doc on these URLs covering updates/versions etc?

  7. Peter,

    No, the fact that Carlo used Yahoo-hosted versions of the files in his example does not mean that Yahoo is opening those files up for general use. You can certainly use them in Carlo’s script, of course, but in your own projects you should host YUI assets on your own server and use those filepaths as you adapt Carlo’s script.

    Regards,
    Eric

  8. Peter Herrmann said:
    January 3, 2007 at 7:11 pm

    @eric… That kind of goes ‘against the grain’ for greasemonkey scripts where typically a developer writes a script for use against a 3rd party site (hardly ever their own).

    You should think hard about hosting YUI like AOL is doing for dojo. The growing cost of doing so would be commensurate with the growing benefit that Yahoo gains from the standardization on YUI as a platform library.

  9. Peter,

    Thanks for the comment. Opening up hosting of YUI would benefit lots of developers — not just Greasemonkey developers — but it would be especially nice in the GM context, absolutely. I hear you loud and clear.

    -Eric

  10. [...] You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your ownsite. [...]

  11. I had a go at this a while back and got distracted apparently. The issue I was grappling with was the use of the unsafeWindow – which as I understand it potentially opens a hole allowing a page author to gain access to the greater privileges Greasemonkey has on your machine.

    I went down a rat-hole loading the files via GM_xmlhttprequest and trying to eval them (in the protected userscript scope). But I see now that even if that could work, the need to load CSS into the document puts you back where you (I) started.

    I think the power of loading in a library like YUI (or Dojo via AOL’s CDN) is /huge/ and should drive the design of future versions of Greasemonkey if there’s not a safe and elegant way to do it now. I know the topic has been aired in the Greasemonkey mailing list.

    Btw. I’m right there with you on prototyping and testing out new ideas on applications and pages. Even though I might in theory have access to those apps’ code, in practice its much easier (and cooler) this way.

  12. For those reading this post Feb 2007, Yahoo does now host the yui files:
    http://developer.yahoo.com/yui/articles/hosting/

  13. [...] Using YUI in Greasemonkey Scripts (Yahoo! User Interface Blog) Title says it all, interesting! (tags: api code webdev YUI yahoo javascript greasemonkey) [...]

  14. Hi,

    I wanted to create a more generic version of this script (GM_includeOnce for example) which was able to be more secure (not use unsafeWindow) if required. Also I wanted to remove what I believe is un-necessary polling in this script.

    Here is what I came up with (please tear it to bits/make it better):

    try {test=GM_includeOnce;}
    catch (err) {
    GM_log(‘adding GM_includeOnce function’);
    function GM_includeOnce(assets, callbackOnComplete, callbackOnTimeout, timeout, context) {
    /*
    GM_includeOnce(assets[, callbackOnComplete[, callbackOnTimeout[, timeout[, context]]]])
    assets = [{url:'', existenceTest:func[, type: typeEnum]}] type is calculated from extension if not included
    no callback is made without callbackOnComplete
    default timeout is 10000 (10 seconds)
    context is the object that the scripts are run on, defaults to this
    callbackOnComplete is sent a closure style function as its only parameter which allows access to the variables
    created in the scripts
    */

    //private functions
    var numAssets=assets.length, numJSAssets=0, numJSLoaded=0;
    var timeoutTimer=setTimeout(callbackOnTimeout, timeout||10000);
    var allScript=”, allCSS=”, importScript=”;
    var asset, type, styleLink;

    function accessFunc(localVar) {try{return eval(localVar);} catch (err) {}}

    function allScriptsLoaded() {
    GM_log(‘allScriptsLoaded’);
    clearTimeout(timeoutTimer);
    //it may be better to add these scripts immediately rather than compiling the big string in some circumstances
    GM_log(‘compiling scripts’);
    for (var a=0;a

  15. Hi,

    The code in the last post didn’t come out too well! Here is the address of a GreaseMonkey script that uses the GM_IncludeOnce loader:

    http://dharmafly.com/hackhud/hackhud.user.js

    Enjoy!

    Annesley

  16. [...] Dive Into Greasemonkey, which is really his Greasemonkey Hacks book online.  Next step: Using YUI in GreaseMonkey Scripts.  Then the outside-world website hacking will begin!!Powered by [...]

  17. This does not work for me. Has something new in Greasemonkey or YUI broken this? I just get a regular text, but no panel formatting whatsoever.

    Thanks.

  18. eric, that might be due to the latest version of the GM extension having tightened its security up. it no longer lets you call the GM_ built in functions like gm_xmlhttprequest from within the unsafewindow scope.

  19. Here is the correct URL for the sample code: https://gist.github.com/297364