Porting the Library Detector
This example walks through the process of porting a XUL-based add-on to the SDK. It's a very simple add-on and a good candidate for porting because there are suitable SDK APIs for all its features.
The add-on is Paul Bakaus's Library Detector.
The Library Detector tells you which JavaScript frameworks the current
web page is using. It does this by checking whether particular objects
that those libraries add to the global window object are defined.
For example, if window.jQuery
is defined, then the page has loaded
jQuery.
For each library that it finds, the library detector adds an icon representing that library to the status bar. It adds a tooltip to each icon, which contains the library name and version.
You can browse and run the ported version in the SDK's examples
directory.
How the Library Detector Works
All the work is done inside a single file,
librarydetector.xul
This contains:
- a XUL overlay
- a script
The XUL overlay adds a box
element to the browser's status bar:
The script does everything else.
The bulk of the script is an array of test objects, one for each library.
Each test object contains a function called test()
: if the
function finds the library, it defines various additional properties for
the test object, such as a version
property containing the library version.
Each test also contains a chrome://
URL pointing to the icon associated with
its library.
The script listens to gBrowser's
DOMContentLoaded
event. When this is triggered, the testLibraries()
function builds an array of libraries by iterating through the tests and
adding an entry for each library which passes.
Once the list is built, the switchLibraries()
function constructs a XUL
statusbarpanel
element for each library it found, populates it with the
icon at the corresponding chrome://
URL, and adds it to the box.
Finally, it listens to gBrowser's TabSelect
event, to update the contents
of the box for that window.
Content Script Separation
The test objects in the original script need access to the DOM window object, so in the SDK port, they need to run in a content script. In fact, they need access to the un-proxied DOM window, so they can see the objects added by libraries, so we’ll need to use the experimental unsafeWindow object.
The main add-on script, main.js
, will use a
page-mod
to inject the content script into every new page.
The content script, which we'll call library-detector.js
, will keep most of
the logic of the test
functions intact. However, instead of maintaining its
own state by listening for gBrowser
events and updating the
user interface, the content script will just run when it's loaded, collect
the array of library names, and post it back to main.js
:
function testLibraries() {
var win = unsafeWindow;
var libraryList = [];
for(var i in LD_tests) {
var passed = LD_tests[i].test(win);
if (passed) {
var libraryInfo = {
name: i,
version: passed.version
};
libraryList.push(libraryInfo);
}
}
self.postMessage(libraryList);
}
testLibraries();
main.js
responds to that message by fetching the tab
corresponding to that worker using
worker.tab
, and adding
the array of library names to that tab's libraries
property:
pageMod.PageMod({
include: "*",
contentScriptWhen: 'end',
contentScriptFile: (data.url('library-detector.js')),
onAttach: function(worker) {
worker.on('message', function(libraryList) {
if (!worker.tab.libraries) {
worker.tab.libraries = [];
}
libraryList.forEach(function(library) {
if (worker.tab.libraries.indexOf(library) == -1) {
worker.tab.libraries.push(library);
}
});
if (worker.tab == tabs.activeTab) {
updateWidgetView(worker.tab);
}
});
}
});
The content script is executed once for every window.onload
event, so
it will run multiple times when a single page containing multiple iframes
is loaded. So main.js
needs to filter out any duplicates, in case
a page contains more than one iframe, and those iframes use the same library.
Implementing the User Interface
Showing the Library Array
The widget
module is a natural fit
for displaying the library list. We'll specify its content using HTML, so we
can display an array of icons. The widget must be able to display different
content for different windows, so we'll use the
WidgetView
object.
main.js
will create an array of icons corresponding to the array of library
names, and use that to build the widget's HTML content dynamically:
function buildWidgetViewContent(libraryList) {
widgetContent = htmlContentPreamble;
libraryList.forEach(function(library) {
widgetContent += buildIconHtml(icons[library.name],
library.name + "<br>Version: " + library.version);
});
widgetContent += htmlContentPostamble;
return widgetContent;
}
function updateWidgetView(tab) {
var widgetView = widget.getView(tab.window);
if (!tab.libraries) {
tab.libraries = [];
}
widgetView.content = buildWidgetViewContent(tab.libraries);
widgetView.width = tab.libraries.length * ICON_WIDTH;
}
main.js
will
use the tabs
module to update the
widget's content when necessary (for example, when the user switches between
tabs):
tabs.on('activate', function(tab) {
updateWidgetView(tab);
});
tabs.on('ready', function(tab) {
tab.libraries = [];
});
Showing the Library Detail
The XUL library detector displayed the detailed information about each library on mouseover in a tooltip: we can't do this using a widget, so instead will use a panel. This means we'll need two additional content scripts:
- one in the widget's context, which listens for icon mouseover events
and sends a message to
main.js
containing the name of the corresponding library:
function setLibraryInfo(element) {
self.port.emit('setLibraryInfo', element.target.title);
}
var elements = document.getElementsByTagName('img');
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('mouseover', setLibraryInfo, false);
}
- one in the panel, which updates the panel's content with the library information:
self.on("message", function(libraryInfo) {
window.document.body.innerHTML = libraryInfo;
});
Finally main.js
relays the library information from the widget to the panel:
widget.port.on('setLibraryInfo', function(libraryInfo) {
widget.panel.postMessage(libraryInfo);
});