dev/panel

This article needs a technical review. How you can help.

We are planning to deprecate the use by Firefox add-ons of the techniques described in this document.

Don't use these techniques to develop new add-ons. Use WebExtensions instead.

If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions instead.

Add-ons developed using these techniques might not work with multiprocess Firefox (e10s), which is already the default in Firefox Nightly and Firefox Developer Edition, and will soon be the default in Beta and Release versions of Firefox. We have documentation on making your add-ons multiprocess-compatible, but it will be more future-proof for you to migrate to WebExtensions.

A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.

This module is new in Firefox 34.

Note that at the moment you can't debug remote targets (for example, Firefox OS, the Firefox OS Simulator, or Firefox for Android) using tools developed with this API. We're working on removing this restriction.

Enables you to extend the Firefox Developer Tools. Most of the Firefox Developer Tools are hosted in a UI component called the Toolbox. Individual built-in tools, such as the JavaScript Debugger or the Web Console, occupy "panels" in the toolbox. With the dev/panel module, you can create your own panels in the Toolbox:

The panel gets a tab in the Toolbox toolbar which enables the user to open it:

You specify the panel's content and behavior using HTML, CSS, and JavaScript. When the panel's created, the framework passes it a debuggee: this is a MessagePort object that you can use to exchange JSON messages with the browser that the developer tools are currently debugging. The messages follow the remote debugging protocol.

For a simple walkthrough of using the dev/panel API to add a new tool, see Adding a panel to the toolbox.

Basic usage

Defining the panel constructor

To add a new tool you first need to define a constructor that inherits from the Panel class, and in that constructor you need to supply values for various properties .

You can set the constructor up manually if you like, or you can use the Add-on SDK core/heritage module to simplify the mechanics of inheriting from Panel. You can use the Class utility function:

const { Panel } = require("dev/panel");
const { Class } = require("sdk/core/heritage");

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool",
  icon: "./my-devtool.png",
  url: "./my-devtool.html",
  setup: function(options) {
    // my setup goes here
  },
  dispose: function() {
    // my teardown goes here
  },
  onReady: function() {
    // I can send messages to
    // the panel document here
  }
});

Alternatively, you can use the extend function:

const { extend } = require("sdk/core/heritage");

function MyPanel() {};

MyPanel.prototype = extend(Panel.prototype, {
  label: "My Panel",
  tooltip: "...",
  ....
});

In the constructor definition there are a number of mandatory and optional parameters for you to supply.

Name Type Description  
label String The string to display in the Toolbox toolbar. Mandatory
icon String

The icon to display in the Toolbox toolbar, specified as a resource:// URL pointing to an icon file, typically in your add-on's "data" directory.

You can use the notation "./my-icon.png" as an alias for the URL pointing to "data/my-icon.png".

Mandatory
url String

A resource:// URL pointing to an HTML file, typically in your add-on's "data" directory. This file contains the specification of the panel's user interface.

You can use the notation "./my-file.html" as an alias for the URL pointing to "data/my-file.html".

You can't directly manipulate the panel's content from main.js, but you can exchange messages with scripts running in the panel.

Mandatory
tooltip String A string that will be used as a tooltip in the Toolbox toolbar. Optional
setup Function

A function that will be called when the panel is created. It's passed an options object containing a single property debuggee.

debuggee is a MessagePort object that you can use to exchange messages with the debugger server.

Optional
dispose Function A function that will be called when the panel is about to be destroyed. You can use it to do any cleanup. Optional
onReady Function An event handler that will be called when the document in the panel becomes interactive. It's equivalent to document.readyState === "interactive". At this point you can send the panel document messages. Optional
onLoad Function An event handler that will be called after the document in the panel is fully loaded. It's equivalent to document.readyState === "complete". Optional

Once you've defined the panel's constructor you have to export it so it can be called by the framework.

Creating a Tool

Next, you need to create a new Tool using the dev/toolbox module, initializing it with the newly defined constructor.

Example

Here's a complete main.js:

// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

// define the panel constructor
const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  // when the panel is created,
  // take a reference to the debuggee
  setup: function(options) {
    this.debuggee = options.debuggee;
  },
  dispose: function() {
    this.debuggee = null;
  },
  onReady: function() {
    // in this function you can communicate
    // with the panel document
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

Panel document environment

The panel document loaded from the url property can of course include CSS and JavaScript just like a normal web page:

<html>
  <head>
    <meta charset="utf-8">
    <link href="./my-panel.css"rel="stylesheet"></link>
    <script src="resource://sdk/dev/volcan.js"></script>
  </head>
  <body>
      <div id="content"></div>
  </body>
  <script src="./my-panel.js"></script>
</html>

It doesn't have access to any privileged APIs, including the Add-on SDK APIs. However, it can receive messages from the add-on that created it, and the add-on can pass it the debuggee so it can communicate with the debugger server the developer tools are targeting.

Communicating with the panel document

The main add-on code can't directly access the panel document or any scripts loaded by the panel document. However it can send messages to the panel document using the postMessage method of Panel. This is closely modeled on window.postMessage. You can only send the panel document message after you've received the ready event.

In the main add-on code:

// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  onReady: function() {
    this.postMessage("Message from the add-on");
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

In the panel document script:

// my-panel.js

window.addEventListener("message", function(event) {
  var content = document.getElementById("content");
  content.textContent = event.data;
});

Note that at the moment you have to pass an array of ports into postMessage, even if you don't need to use them:

// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  setup: function(options) {
    this.debuggee = options.debuggee;
  },
  dispose: function() {
    this.debuggee = null;
  },
  onReady: function() {
    this.postMessage("Message from the add-on", [this.debuggee]);
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

This is being tracked as bug 1079540.

Two-way messaging

The panel document does not have an equivalent postMessage method, so if you want your panel document to communicate back to the add-on, you can use channel messaging.

In the add-on side:

  • require the sdk/messaging module
  • create a MessageChannel
  • keep one MessagePort in the add-on side for receiving messages from the panel document
  • pass the other MessagePort to the panel document in the ports argument to postMessage
// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

const { MessageChannel } = require("sdk/messaging");
const channel = new MessageChannel();
const addonSide = channel.port1;
const panelSide = channel.port2;

// messages from the panel arrive here
addonSide.onmessage = function(event) {
  console.log(event.data);
}

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  onReady: function() {
    // send a port to the panel document
    this.postMessage("Message from the add-on", [panelSide]);
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

In the panel document:

  • retrieve the MessagePort from the event, and use it to send messages to the add-on
// my-panel.js

window.addEventListener("message", function(event) {
  // retrieve the port
  var toAddon = event.ports[0];
  toAddon.start();
  toAddon.postMessage("Message from the panel document");
  console.log(toAddon);
});

Communicating with the debugger server

The Firefox developer tools use a client/server protocol: a tool, such as a JavaScript debugger or style editor, is the client, and the program being debugged, such as Firefox, is the server. Clients connect to the server and send it messages to examine and modify the state of the program being debugged. The Remote Debugging Protocol page describes the protocol in detail.

When the user opens a panel and the panel is created, the framework calls the panel's setup method. The setup method will be passed a debuggee object, which is a MessagePort that the add-on can use to exchange messages with the debugger server.

For example, here's a main.js which sends the listTabs message to the debugger server, and logs the response:

// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  setup: function(options) {
    this.debuggee = options.debuggee;
    this.debuggee.start();
    this.debuggee.onmessage = function(event) {
      console.log(event.data);
    }
    this.debuggee.postMessage({
      "to":"root",
      "type":"listTabs"
    });
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

More usefully, you can pass debuggee from the main add-on to the panel document using the ports argument to postMessage. Then the panel document can communicate with the debugger server.

In main.js:

// main.js

// require the SDK modules
const { Panel } = require("dev/panel");
const { Tool } = require("dev/toolbox");
const { Class } = require("sdk/core/heritage");

const MyPanel = Class({
  extends: Panel,
  label: "My Panel",
  tooltip: "My new devtool's panel",
  icon: "./my-icon.png",
  url: "./my-panel.html",
  setup: function(options) {
    this.debuggee = options.debuggee;
  },
  dispose: function() {
    this.debuggee = null;
  },
  onReady: function() {
    this.debuggee.start();
    this.postMessage("port", [this.debuggee]);
  }
});

// export the constructor
exports.MyPanel = MyPanel;

// create a new tool, initialized
// with the new constructor
const myTool = new Tool({
  panels: { myPanel: MyPanel }
});

In my-panel.js:

// my-panel.js

var content = document.getElementById("content");

window.addEventListener("message", function(event) {
  var debuggee = event.ports[0];
  console.log(debuggee);

  debuggee.onmessage = function(event) {
    content.textContent = JSON.stringify(event.data);
  }
 
  debuggee.postMessage({
    "to":"root",
    "type":"listTabs"
  });
});

If you do this, don't forget to call start() on the port before passing it over to the panel document.

Volcan.js: a JavaScript API for the debugging protocol

Communicating with the debugger server by exchanging JSON messages can be cumbersome. So we've also provided a library called volcan.js which gives you a JavaScript API to the remote debugging protocol.

Note that at the moment volcan.js does not support the complete remote debugging protocol. We're still working on documenting exactly which messages are supported by volcan.js.

To use volcan.js, you can just include it from your panel's HTML like this:

<html>
  <head>
    <meta charset="utf-8">
    <link href="./my-panel.css"rel="stylesheet"></link>
    <script src="resource://sdk/dev/volcan.js"></script>
  </head>
  <body>
      <div id = "content"></div>
  </body>
  <script src="./my-panel.js"></script>
</html>

Here's a script that uses volcan.js to get the selected tab and display its URL:

// my-panel.js

var content = document.getElementById("content");

window.addEventListener("message", function(event) {
  var debuggee = event.ports[0];
  volcan.connect(debuggee).
    then(listTabs).
    then(writeTabList);
});

function listTabs(root) {
  return root.listTabs();
}

function writeTabList(tabList) {
  content.textContent = tabList.tabs[tabList.selected].url;
}

We don't have detailed documentation for volcan.js, but it's coming soon. In the meantime, here's a quick guide.

Connecting

Volcan.js provides a global connect() function that takes a MessagePort connected to the debugger server, and returns a promise which is fulfilled with an object representing the root actor:

volcan.connect(debuggee).then(gotRoot);

function gotRoot(root) {
 // can use root actor here
}

Actors

Actors in the Remote Debugging Protocol are volcan.js objects. The messages actors can accept are methods of those objects. The message type becomes the method name. For example, the root actor object has a method listTabs and the stylesheet actor object has a method getStyleSheets. Any additional parameters sent in the message become arguments to the method.

If a message is expected to get a reply, then the method returns a promise which is fulfilled with the reply. If the message reply in the remote debugging protocol would contain an actor ID, then in volcan.js the object that fulfills the promise contains that actor instance. If the message reply contains some JSON object, then in volcan.js the object that fulfills the promise includes a corresponding JSON object.

If an actor can send notifications, as in the request/reply/notify pattern, then the corresponding volcan.js object emits an event, which you can listen for using addEventListener. Any data sent with the notification is available to the event listener in the event.data property.

Document Tags and Contributors

 Contributors to this page: wbamberg, jonaswjs, guidocalvano
 Last updated by: wbamberg,