Sunday, May 9, 2010

Firing Up My Jetpack

Jetpack is the new style (still in early development) of firefox extensions. Mozilla developers have made their in-progress work on this available for some time, however, and I finally took a look at the "jetpacks" available at the Jetpack Gallery. I was pretty impressed with what I found: there's already jetpack versions of some of the my favorite existing firefox extensions, like for flash blocking (ClickToFlash), capturing the colors used on a web page ("eyedropper" style: JetColorPicker), and taking screen-grabs (JetShot).

While I think I'll stick with the conventional firefox-extension versions of each of these tools for now, I was really impressed with how short and sweet the code to many of these jetpacks are. I can see why mozilla is planning to make this the future of extension building: there are tons and tons of web developers who are familiar enough with javascript and dom-manipulation to be able to whip these things out, and the jetpack-management UI has that same "view source" concept built into it that helped fuel the original web 1.0 explosion in the 90s.

But anyway, while I was looking at some of the jetpacks, I got the itch to built a few of my own. And the experience was both glorious and frustrating. Glorious because jetpacks are so wickedly easy to build and install; frustrating because there isn't yet much documentation for building them.

The "retired" jetpack documentation at the Mozilla Developer Center (and API Reference on your own installed jetpack page) are the only docs relevant to the current jetpack extension. The in-progress Jetpack SDK is the second cut at the "jetpack" methodology, and is designed to work with a forthcoming version of firefox — and not the current version of the jetpack extension (the distinction between these two different forms of "jetpack" are not called out clearly anywhere, which makes it confusing to figure out where to get started).

But enough ranting for now. If you want to learn how to build jetpacks, you pretty much just have to do it the same way you learned to build web pages — by viewing the source of other jetpacks. That's what I did, and here are the first few jetpacks I built:

Lookup in Wikipedia

I actually built the Lookup in Wikipedia jetpack second, but it's even simpler than the first, so I'll show it off first. It adds a "Lookup in Wikipedia" menu item to the right-click popup menu, so that when you select some text and then right-click and select this menu item, it'll open up a new tab with the wikipedia page for the text you selected:

// ==UserScript== // @name Lookup in Wikipedia // @description Adds a context menu item to lookup the selected text in wikipedia. // @copyright 2010 Justin Ludwig (http://jetpackgallery.mozillalabs.com/contributors/justinludwig) // @license The MIT License; http://www.opensource.org/licenses/mit-license.php // @version 1.0 // ==/UserScript== jetpack.future.import("menu"); jetpack.future.import("selection"); // add page context-menu item jetpack.menu.context.page.add({ label: "Lookup in Wikipedia", icon: "http://en.wikipedia.org/favicon.ico", command: function() { // lookup the current document's language var docLang = jetpack.tabs.focused.contentDocument.getElementsByTagName("html")[0].lang; // lookup the browser's preferred language var userLang = jetpack.tabs.focused.contentWindow.navigator.language; // ignore everything in the lang tag except for the general language var lang = (docLang || userLang).toLowerCase().replace(/([a-z]+).*/, "$1"); // when the user selects this menu-item, open a new tab with the wikipedia page for the text on the page that the user had selected jetpack.tabs.open("http://" + lang + ".wikipedia.org/wiki/" + encodeURIComponent(jetpack.selection.text)); } });

If it didn't have a little fancy code for trying to figure out what language to use, it would be a one-liner. For the language, you can see I justed used the standard browser dom of the current tab (jetpack.tabs.focused.contentDocument for what we normally refer to just as document and jetpack.tabs.focused.contentWindow for window) to get the current document language, or the browser's preferred language. Opening a new tab with the selected text is a breeze with the jetpack api (jetpack.tabs.open(url) to open the tab, and jetpack.selection.text to get the currently selected text). The jetpack.tabs object is part of the core jetpack api, but accessing jetpack.menu and jetpack.selection requires the jetpack.future.import() commands at the top of the script.

My Last Modified

I totally ripped off the idea for My Last Modified jetpack from azu_re's lastModified jetpack, but I made the display a little more customizable so it plays nice with Vimperator (Vimperator unfortunately seems to break the display of most of the jetpacks which show stuff in the status bar — I wish in fact Vimperator would just leave the status bar alone entirely, because I can see with time I'm going to want to give it over entirely to jetpacks).

Both mine and azu_re's displays the last-modified date for the current page in a little block on the status bar. The difference with mine is that the date format and css style of the block in the status bar is fully customizable:

// ==UserScript== // @name My Last Modified // @description Displays page's last-modified date. // @copyright 2010 Justin Ludwig (http://jetpackgallery.mozillalabs.com/contributors/justinludwig) // @license The MIT License; http://www.opensource.org/licenses/mit-license.php // @attribution azu_re (http://jetpackgallery.mozillalabs.com/jetpacks/367) // @version 1.0 // ==/UserScript== var manifest = { settings: [ // date/time format (using strftime) { name: "dateFormat", type: "text", label: "Date Format", "default": "%m/%d/%y %H:%M" }, // css style for status bar display { name: "statusStyle", type: "text", label: "Status Bar CSS Style", "default": "color:white; background-color:black; font-size:9px; width:75px; padding:2px;" } ] }; jetpack.future.import("storage.settings"); /** * Formats a date using the specified format. * @param format Format string. * @param date JS date object. * @return Formatted string. */ function strftime(format, date) { ... } jetpack.statusBar.append({ // add display div to status bar html: "<div id='my-last-modified' style='" + jetpack.storage.settings.statusStyle + "'></div>", onReady: function(widget) { // update display when tab focused or loaded function update() { var currentDocument = jetpack.tabs.focused.contentDocument; // get last-modified date of current tab var date = new Date(currentDocument.lastModified); // format date per user preference var status = strftime(jetpack.storage.settings.dateFormat, date); // update display $(widget).find('#my-last-modified').text(status); }; jetpack.tabs.onFocus(update); jetpack.tabs.onReady(update); } });

(I omitted the source to the strftime function from this listing, just because it's lengthy and has nothing jetpack-specific in it. You can get the full code for it from the My Last Modified jetpack's page in the jetpack gallery, though.)

The jetpack.statusBar.append() function is a super-easy way of adding some content to the browser's status bar — all you have to do is specify the initial html container, via the html option, and a function that's called when the html has been added to the status bar, via the onReady option. Since in this case the content changes whenever the active tab changes, my onReady function has an update() function in it that it registers to be called when a different tab is focused (via jetpack.tabs.onFocus()) or created (via jetpack.tabs.onReady()). The last line of the update() function uses jquery, conveniently included by default into each jetpack, to lookup the html container and update its text.

This jetpack also makes use of jetpack's super-easy way of exposing and persisting per-jetpack preferences: all you have to do is add a manifest global with a settings array property in it; each item in that array represents a separate preference. Users can access the preferences of any jetpack by typing "about:jetpack" in firefox's address bar, clicking the "Installed Features" link on the resulting page, and then clicking the "settings" link next to the listing for the jetpack. In the source code, you can access each setting as a property on the jetpack.storage.settings object (ie jetpack.storage.settings.dateFormat for a custom preference named dateFormat). You just have to remember to import the storage.settings "package" at the top of your jetpack to make this all work.

Log Open Ajax Events

The Log Open Ajax Events jetpack is the only really original jetpack I've built so far. It's a little more complicated (and took some trial and error to figure out which APIs to use), but still really simple. If the current page has included the Open Ajax Hub (which does the product I work on at work), this jetpack logs any events published to it to the Firebug console.

This is something for which I had already built a bookmarklet; but in jetpack form I don't have to turn it on every time I reload or view a different page — I can just turn it on when I want to debug something, and continue getting the events until I'm completely finished with the problem.

// ==UserScript== // @name Log OpenAjax Events // @description Logs configured openajax events to firebug. // @copyright 2010 Justin Ludwig (http://jetpackgallery.mozillalabs.com/contributors/justinludwig) // @license The MIT License; http://www.opensource.org/licenses/mit-license.php // @version 1.0 // ==/UserScript== var manifest = { settings: [ // comma-separated list of events to which to subscribe (* and ** wildcards allowed per OpenAjax spec) // clear this value completely to disable logging of any events { name: "eventName", type: "text", label: "Event Name", "default": "**" }, ] }; jetpack.future.import("storage.settings"); function subscribe() { // "window" context object of the current page var w = jetpack.tabs.focused.contentWindow.wrappedJSObject; // no firebug console or no openajax on this page if (!w.console || !w.OpenAjax) return; // lazy init our custom data object on each page var name = jetpack.storage.settings.eventName || ""; var data = w._logOpenAjaxEventsData; if (!data) data = w._logOpenAjaxEventsData = { name: "", subs: [] }; // already subscribed to this page (or no events to subscribe for) if (name == data.name) return; // remove existing subscriptions for (var a = data.subs, i = a.length - 1; i >= 0; i--) w.OpenAjax.hub.unsubscribe(a[i]); data.subs = []; // add new subscriptions if (name) for (var a = name.split(","), i = a.length - 1; i >= 0; i--) data.subs.push(w.OpenAjax.hub.subscribe(a[i], function(name, pubData, subData) { // log event (in a form that's collapsable so as not to pollute the log too badly) var o = {}; o["OpenAjax event: " + name] = pubData; w.console.dir(o); })); // remember event names we subscribed to (to check if they changed later) data.name = name; } // re-subscribe as necessary on every page focus or load jetpack.tabs.onFocus(subscribe); jetpack.tabs.onReady(subscribe);

I have it try to re-subscribe to the Open Ajax Hub every time a tab is focused or created (jetpack.tabs.onFocus() and jetpack.tabs.onReady() again). It stores its own custom _logOpenAjaxEventsData in each tab to remember if it's already subscribed, though, so it only actually re-subscribes if the user has updated the jetpack's eventName preference in the interim. The trick with this jetpack (that I had to find by combing through the source of a bunch of other jetpacks) was how to access the javascript "context" object of the current tab. Usually when scripting an html page, we access both the browser dom and the javascript context of the current page (the thing that holds the "global" objects) via the window property. In jetpack, you have to access these things through two distinct objects: the browser dom through jetpack.tabs.focused.contentWindow, and the javascript context via jetpack.tabs.focused.contentWindow.wrappedJSObject.

The only other tricky thing in this jetpack is just the way the Open Ajax events are logged; to make them display nicely in the firebug console (so that each event is displayed initially on a single line, but you can still drill into all the event properties) I created a new simple object for each event, and set the only property of that object to the event data, dynamically creating this one property's name with the event name in it as a friendly label for the event data object (ie "OpenAjax event: CAF.Update" in the screenshot above).

Log Window Events

I also created a Log Window Events jetpack that works pretty much the same as the Log Open Ajax Events jetpack. The difference is that it listens for standard window events (like onclick, onkeypress, etc.) instead of Open Ajax events. I won't bother including the source to it here, but like all jetpacks in the jetpack gallery, you can view the full code from its jetpack page (which, like I mentioned at the top, is probably the best innovation of this innovative extension style).

Let 'Er Rip!

So I love what the jetpack team has done so far, and I'm looking forward to the day when jetpack is included as part of the default firefox install. And hopefully as work on the new jetpack SDK progresses, many of the documentation holes will be filled in. But even if not, as long as this thing has "view source", I predict it will be a smashing success.

No comments:

Post a Comment