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.

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);
});