The concept of a JavaScript code module in the Gecko layout engine was first introduced in Gecko 1.9. This post discusses how such code modules can be used to simplify preference and add-on management in Firefox 4 which uses Gecko 2.0 and JavaScript 1.8.5. It uses a simple Firefox add-on called HTML5toggle as an example of how to modify existing code to use Javascript code modules.
A JavaScript code module is simply some JavaScript code located in a registered (well-known) location. JavaScript code modules are primarily used to share code between different privileged scopes. They can also be used to create global JavaScript singletons that previously required using JavaScript XPCOM objects.
Here is a list of the current JavaScript code modules in Firefox 4.0:
Name | Purpose |
---|---|
AddonManager.jsm | Provides routines to install, manage, and uninstall add-ons. |
AddonRepository.jsm | Allows searching of the add-ons repository. |
ctypes.jsm | Interface that allows JavaScript code to call native libraries without requiring the development of an XPCOM component. |
DownloadLastDir.jsm | Provides the path to the directory into which the last download occurred. |
Geometry.jsm | Provides routines for performing basic geometric operations on points and rectangles. |
ISO8601DateUtils.jsm | Provides routines to convert between JavaScript Date objects and ISO 8601 date strings. |
NetUtil.jsm | Provides networking utility routines, including the ability to easily copy data from an input stream to an output stream asynchronously. |
openLocationLastURL.jsm | Provides access to the last URL opened using the "Open Location" option in the File menu. |
PerfMeasurement.jsm | Provides access to low-level hardware and OS performance measurement tools. |
PluralForm.jsm | Provides an easy way to get the correct plural forms for the current locale, as well as ways to localize to a specific plural rule. |
PopupNotifications.jsm | Provides an easy way to present non-modal notifications to users. |
Services.jsm | Provides getters for conveniently obtaining access to commonly-used services. |
XPCOMUtils.jsm | Contains utilities for JavaScript components loaded by the JavaScript component loader. |
The Components.utils.import method is used to import a JavaScript code module into a specific JavaScript scope. A module must be imported before any functionality in the module is used. Modules are cached when loaded and subsequent imports do not reload a new version of the module, but instead use the previously cached version. Thus a given module is shared when imported multiple times.
One of the most commonly used modules is the Services.jsm module which offers a wide assortment of lazy getters that simplify the process of obtaining references to commonly used services. A lazy getter is a getter function that checks the item which it is to retrieve (get) and initializes it if it is not set.
Here is a list of the available services in Services.jsm:
Service Accessor | Service Interface | Service Name |
---|---|---|
appinfo | nsIXULAppInfo nsIXULRuntime | Application information service |
console | nsIConsoleService | Error console service |
dirsvc | nsIDirectoryService nsIProperties | Directory service |
droppedLinkHandler | nsIDroppedLinkHandler | Dropped link handler service |
io | nsIIOService nsIIOService2 | I/O Service |
locale | nsILocaleService | Locale service |
obs | nsIObserverService | Observer service |
perms | nsIPermissionManager | Permission manager service |
prefs | nsIPrefBranch nsIPrefBranch2 nsIPrefService | Preferences service |
prompt | nsIPromptService | Prompt service |
scriptloader | mozIJSSubScriptLoader | JavaScript subscript loader service |
search | nsIBrowserSearchService | Browser search service |
storage | mozIStorageService | Storage API service |
strings | nsIStringBundleService | String bundle service |
tm | nsIThreadManager | Thread Manager service |
vc | nsIVersionComparator | Version comparator service |
wm | nsIWindowMediator | Window mediator service |
ww | nsIWindowWatcher | Window watcher service |
For Firefox 4 the add-on manager was extensively reworked. It is now based on a JavaScript code module called AddonManager.jsm. See the source code for AddonManagerInternal in …/mozapps/extensions/AddonManager.jsm for full details. Visually it is implemented as a tab instead of a separate window and uses icons that are 64×64 pixels instead of 32×32 pixels. Add-on metadata is now stored in an SQLite database instead of RDF-based storage.
Here is a list of the available methods in AddonManager.jsm:
METHOD | PURPOSE |
---|---|
getInstallForURL | Asynchronously gets an AddonInstall for a URL |
getInstallForFile | Asynchronously gets an AddonInstall for an nsIFile |
getAddonByID | Asynchronously gets an add-on with a specific ID |
getAddonsByIDs | Asynchronously gets an array of add-ons |
getAddonsWithOperationsByTypes | Asynchronously gets add-ons that have operations waiting for an application restart to complete |
getAddonsByTypes | Asynchronously gets add-ons of specific types |
getAllAddons | Asynchronously gets all installed add-ons |
getInstallsByTypes | Asynchronously gets all current AddonInstalls optionally limiting to a list of types |
getAllInstalls | Asynchronously gets all current AddonInstalls |
isInstallEnabled | Check whether installation is enabled for a particular mimetype |
isInstallAllowed | Checks whether a particular source is allowed to install add-ons of a given mimetype |
installAddonsFromWebpage | Starts installation of an array of AddonInstalls notifying the registered web install listener of blocked or started installs |
addInstallListener | Adds a new InstallListener if the listener is not already registered |
removeInstallListener | Removes an InstallListener if the listener is registered |
addAddonListener | Adds a new AddonListener if the listener is not already registered |
removeAddonListener | Removes an AddonListener if the listener is registered |
Note that all of the above methods are asynchronous which mean that a callback function is required. The callback may well only be called after the method returns.
In earlier versions of Firefox, you have to observe em-action-requested to determine whether an add-on should be uninstalled or not. For example, the following code could be used in Firefox 3 to detect when to cleanup up the preferences for HTML5toggle during add-on removal.
var uninstall = NULL, observe: function(subject, topic, data) { if (topic == "em-action-requested") { subject.QueryInterface(Components.interfaces.nsIUpdateItem); if (subject.id == MY_EXTENSION_UUID) { if (data == "item-uninstalled") { this.uninstall = true; } else if (data == "item-cancel-action") { this.uninstall = false; } } } if (topic == "quit-application-granted") { if (this.uninstall) { var prefService = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); var prefBranch = prefService.getBranch('extensions.html5toggle.'); prefBranch.deleteBranch(""); } this.unregister(); } }, register: function() { var observerService = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); observerService.addObserver(this, "em-action-requested", false); observerService.addObserver(this, "quit-application-requested", false); observerService.addObserver(this, "quit-application-granted", false); }, unregister : function() { var observerService = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); observerService.removeObserver(this, "em-action-requested"); observerService.removeObserver(this, "quit-application-granted"); observerService.removeObserver(this, "quit-application-requested"); }
This code no longer works in Firefox 4. Instead, an add-on needs to handle two events, onUninstalling and onOperationCancelled, as shown in below.
Here is the source code for overlay.js prior to being upgraded to using JavaScript code modules. It is from HTML5toggle v1.02.
const MY_EXTENSION_UUID = "{16f32596-71ca-4053-a4f3-cfc8b157f541}"; var html5toggle = { beingUninstalled: null, onLoad: function() { var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService).getBranch("html5."); var value = prefs.getBoolPref("enable"); // save the current value of html5.enable preference in our own branch var myprefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); var mybranch = myprefs.getBranch('extensions.html5toggle.'); mybranch.setBoolPref('enable', value); myprefs.savePrefFile(null); this.register(); this.setTBbutton(value); this.setMIchecked(value); }, setMIchecked: function (value) { var menuitem = document.getElementById('html5toggle-menuitem'); if (value) { menuitem.setAttribute("checked", "false"); } else { menuitem.setAttribute("checked", "true"); } }, setTBbutton: function (value) { var tbButton = document.getElementById('html5toggle-toolbar-button'); var stringsBundle = document.getElementById('html5toggle-string-bundle'); if (tbButton) { // button might still be on Toolbar Palette if (value) { tbButton.tooltipText = stringsBundle.getString('onString'); tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5on16N.png"); } else { tbButton.tooltipText = stringsBundle.getString('offString'); tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5off16N.png"); } } }, onToolbarButton: function(e) { var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService).getBranch("html5."); var value = prefs.getBoolPref("enable"); value = !value; prefs.setBoolPref("enable", value); this.setTBbutton(value); this.setMIchecked(value); }, onMenuItem: function(e) { var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService).getBranch("html5."); var value = prefs.getBoolPref("enable"); var menuitem = document.getElementById('html5toggle-menuitem'); value = menuitem.getAttribute("checked") == "true" ? false : true; prefs.setBoolPref("enable", value); this.setTBbutton(value); }, observe: function(subject, topic, data) { if (topic == "quit-application-granted") { dump("\nhtml5toggle:quit-application-granted ...."); if (this.beingUninstalled) { // restore initial value of html5.enable preference var myprefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); var mybranch = myprefs.getBranch('extensions.html5toggle.'); var value = mybranch.getBoolPref("enable"); mybranch.deleteBranch(""); var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); var branch = prefs.getBranch('html5.'); branch.setBoolPref('enable', value); prefs.savePrefFile(null); } this.unregister(); } }, onUninstalling: function(addon) { if (addon.id == MY_EXTENSION_UUID) { this.beingUninstalled = true; } }, onOperationCancelled: function(addon) { if (addon.id == MY_EXTENSION_UUID) { this.beingUninstalled = (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) != 0; } }, register: function() { dump("\nhtml5toggle:register ...."); var observerService = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); observerService.addObserver(this, "quit-application-granted", false); Components.utils.import("resource://gre/modules/AddonManager.jsm"); AddonManager.addAddonListener(this); }, unregister: function() { dump("\nhtml5toggle:unregister ...."); var observerService = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); observerService.removeObserver(this, "quit-application-granted"); AddonManager.removeAddonListener(this); } }; window.addEventListener("load", function () { html5toggle.onLoad(); }, false);
Here is the same source file after being modified to use JavaScript code modules. It is from HTML5toggle v1.03.
const MY_EXTENSION_UUID = "{16f32596-71ca-4053-a4f3-cfc8b157f541}"; const PREF_BRANCH_HTML5 = Services.prefs.getBranch("html5."); const PREF_BRANCH_HTML5TOGGLE = Services.prefs.getBranch("extensions.html5toggle."); const OBSERVER = Services.obs; const ADDONMANAGER = AddonManager; var html5toggle = { beingUninstalled: null, onLoad: function() { let value = PREF_BRANCH_HTML5.getBoolPref("enable"); // save the current value of html5.enable preference in our own branch PREF_BRANCH_HTML5TOGGLE.setBoolPref('enable', value); Services.prefs.savePrefFile(null); this.register(); this.setTBbutton(value); this.setMIchecked(value); }, setMIchecked: function (value) { let menuitem = document.getElementById('html5toggle-menuitem'); if (value) { menuitem.setAttribute("checked", "false"); } else { menuitem.setAttribute("checked", "true"); } }, setTBbutton: function (value) { let tbButton = document.getElementById('html5toggle-toolbar-button'); let stringsBundle = document.getElementById('html5toggle-string-bundle'); if (tbButton) { // button might still be on Toolbar Palette if (value) { tbButton.tooltipText = stringsBundle.getString('onString'); tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5on16N.png"); } else { tbButton.tooltipText = stringsBundle.getString('offString'); tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5off16N.png"); } } }, onToolbarButton: function(e) { let value = PREF_BRANCH_HTML5.getBoolPref("enable"); PREF_BRANCH_HTML5.setBoolPref("enable", !value); this.setTBbutton(value); this.setMIchecked(value); }, onMenuItem: function(e) { let menuitem = document.getElementById('html5toggle-menuitem'); let value = menuitem.getAttribute("checked") == "true" ? false : true; PREF_BRANCH_HTML5.setBoolPref("enable", value); this.setTBbutton(value); }, observe: function(subject, topic, data) { if (topic == "quit-application-granted") { dump("\nhtml5toggle:quit-application-granted ...."); if (this.beingUninstalled) { // restore initial value of html5.enable preference let value = PREF_BRANCH_HTML5TOGGLE.getBoolPref("enable"); PREF_BRANCH_HTML5TOGGLE.deleteBranch(""); PREF_BRANCH_HTML5.setBoolPref('enable', value); Service.prefs.savePrefFile(null); } this.unregister(); } }, onUninstalling: function(addon) { if (addon.id == MY_EXTENSION_UUID) { this.beingUninstalled = true; } }, onOperationCancelled: function(addon) { if (addon.id == MY_EXTENSION_UUID) { this.beingUninstalled = (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) != 0; } }, register: function() { dump("\nhtml5toggle:register ...."); OBSERVER.addObserver(this, "quit-application-granted", false); ADDONMANAGER.addAddonListener(this); }, unregister: function() { dump("\nhtml5toggle:unregister ...."); OBSERVER.removeObserver(this, "quit-application-granted"); ADDONMANAGER.removeAddonListener(this); } }; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/AddonManager.jsm"); window.addEventListener("load", function () { html5toggle.onLoad(); }, false);
As you can see if you study the above source code, using JavaScript code modules reduces the number of lines of code and makes the source code more readable. I assume you are familiar with JavaScript in particular and Mozilla add-ons in general if you are reading this post so I am not going to try and explain the above code in any detail.
You should also have noticed that I updated the source code to use the JavaScript let keyword instead of var where possible. This keyword was introduced in JavaScript 1.7 which is a Mozilla only extension. Variables declared by let have local scope, i.e. their scope (visibility) is the block in which they are defined as well as in any sub-blocks in which they are not redefined. Contrast that with variables declared by var which have as their scope the entire enclosing function.
Consider the following short JavaScript example:
var i = 0; for ( let i = i; i < 2 ; i++ ) print ( "> " + i ); print ( i );
If you used the var keyword in the for statement, the variable would be visible within the whole function containing the loop. To reduce the visibility of the variable to just the scope of the for loop, we use the let keyword. Using the Tracemonkey JavaScript engine (which supports up to Javascript v1.85) shell, the output from this code snippet is:
> 0 > 1 > 2 0
There is an enormous amount of useful functionality in the current set of JavaScript code modules. You need to read the appropriate documentation for each code module to gain an understanding of what is available in each module. Unfortunately the documentation is written in the usual terse style adopted by the Mozilla developers and use case examples are scarce.
For example, if you examine the Services.jsm documentation page, you will notice a prompt service accessor. This provides access to a number of useful methods including alert which shows an alert dialog with an OK button. It works the same way as window.alert but accepts a title for the dialog.
Components.utils.import("resource://gre/modules/Services.jsm"); .... Services.prompt.alert(null, "My blog", "Hello there, dear reader of my blog");
The current set of JavaScript code modules does not include string localization. I would like to see somebody develop a good JavaScript code module for string localization as that would simplify the coding of restartless add-ons.
Well, that is all that your should need to know to get you started on updating your Firefox add-ons to take advantage of JavaScript code modules. One thing to watch out for with JavaScript code modules is that you do not break backwards compatibility if your add-on is expected to work with both Firefox 3 and Firefox 4.Good luck!
P.S. The full source code for HTML5toggle versions 1.02 and 1.03 is available on my website.