Oct 14, 2018

An improvement of WebExtensions on Firefox 64 about implicit collaboration of addons

このエントリの日本語版はこちらから読めます。

(Note that this article describes about an improvement on Firefox 64, and Firefox ESR60 is out of target.)

Good news! An old feature proposal filed at the time Mozilla announced that XUL become deprecated and WebExtensions become the next main line has became fixed: Bug 1280347 - Add ability to provide custom HTML elements working as alias of existing Firefox UI items, especially tabs.

Why it is a news for me? Let's look around the short history of addon migration from XUL to WebExtensions.

XUL addons were collaborative with each other implicitly

In old days, all XUL addons worked in the common namespace and they modified UI elements and behaviors of Firefox itself as they like.

(An illustration describing that XUL addons modified Firefox itself.)

Because old TST's tab tree was the native tab bar of Firefox itself, tab related features added by other addons were also available there.

(An illustration describing that XUL-based TST just modified appearance of Firefox's UI instead of adding new UI elements.)

As I described at the migration story of Tree Style Tab, XUL addons were collaborative implicitely. This was a large advantage compared to other similar extension systems. But such a unified namespace also introduced chaos from many numbers of addons, including security concern.

WebExtensions addons are isolated and require special effort to collaborate with each other

On the other hand, WebExtensions APIs are designed to separate namespace of each addon.

(An illustration decribing WebExtensions-based addons are separated in each namespace.)

Extra context menu commands provided by other addons are not callable from TST's context menu, because TST's menu is just an imitation based on HTML and CSS in the sidebar area. The menu is also impossible to spread out of the sidebar frame, so it is too narrow and stressful.

As a workaround for the incompatibility with other addons, I implemented public APIs callable via browser.runtime.sendMessage() from others.

(An illustration describing addons can communicate with custom APIs.)

TST's custom APIs reintroduced collaboration of addons partially, but it forced to add more codes to call TST's APIs explicitely. Some addon developers did that and I also sent pull requests to some addons. But there are too much number of addons - this approach is endless, especially about future addons.

Then, what's the improvement on Firefox 64 for this uncomfortable situation?

Firefox 64 provides ability to show context menus including commands for tabs (and bookmarks) added by other addons, on a popup panel or a sidebar panel. Yes, now WE-based addons who add extra context menu commands work together with each other implicitly, without any special effort like a workaround on TST's APIs!

(An illustration describing the new context menu can work same as Firefox's native context menu.)

How can addons do such an collaboration? Let's see basics of custom context menu on WebExtensions addons.

(An illustration describing relations between the "contextmenu" event and the native context menu.)

When you do right-click (or something other action to open the context menu), a contextmenu DOM event is fired on the target element, and the default context menu for an webpage will open.

(An illustration describing the native context menu is cancellable.)

The event is cancellable by calling its preventDefault() method. When you cancel it, the default context menu is also canceled. After that you can draw any custom menu-like UI - TST's imitated context menu was implemented based on this logic. That was most major way to provide custom context menu by a WebExtensions addon, for a long time. (There is another approach based on HTML5's <menu> and related element types, but it looked unconvenient around addon purpose for me.)

On Firefox 64 and later, a new API browser.menus.overrideContext() is introduced.

(An illustration describing what we should do on Firefox 64 and later.)

It is callable on event handlers for the contextmenu DOM events. You'll call it with suitable context information -

(An illustration describing the context menu is switched for the specified context.)

then the default menu will be switched to a context menu for the specified context. The menus is not a fake, but a really native menu provided by Firefox. Extra commands added by other addons are also available, and it spreads out of the sidebar area or the popup panel. Wow!

How to use the new feature?

There are three requirements to try that.

  • Adding new permission menus.overrideContext
  • Specifying a context by browser.menus.overrideContext()
  • Adding menu items with the viewTypes parameter by browser.menus.create()

There is another article to describe how to implement context menu items for various cases. In this article I describe just general information.

A new permission menus.overrideContext

The new API browser.menus.overrideContext() becomes callable only when the addon has a new permission menus.overrideContext.

You just need to add a new value menus.overrideContext to the list of permissions in the manifest.json, even if your addon still supports Firefox 63 or older. Old Firefox just ignores such an unknown permission, I've confirmed the behavior on Firefox ESR60. So you don't need putting it under optional_permissions and calling browser.permissions.request({ permissions: ['menus.overrideContext'] }) to grant the required permission after the installation.

Specifying a context by browser.menus.overrideContext()

Here is a snippet to open a tab context menu on your custom UI:

document.addEventListener('contextmenu', event => {
  const tab = event.target.closest('.tab');
  if (tab && typeof browser.menus.overrideContext == 'function') {
    // When the context menu is opened on a fake tab element, set the
    // context to "opening a tab context menu on the specified tab".
    browser.menus.overrideContext({
      context: 'tab',
      tabId:   parseInt(tab.dataset.id)
    });
  }
  else {
    // Otherwise, don't show the menu.
    event.preventDefault();
  }
}, { capture: true });

Another snippet to open a bookmark context menu on your custom UI:

document.addEventListener('contextmenu', event => {
  const item = event.target.closest('.bookmark');
  if (item && typeof browser.menus.overrideContext == 'function') {
    // When the context menu is opened on a fake bookmark item, set the
    // context to "opening a bookmark context menu on the specified item".
    browser.menus.overrideContext({
      context:    'bookmark',
      bookmarkId: parseInt(item.dataset.id)
    });
  }
  else {
    // Otherwise, don't show the menu.
    event.preventDefault();
  }
}, { capture: true });

However, you'll see a sorry menu with only extra commands added by other addons.

(A screenshot of the new context menu opened with a specified tab context. There is no default tab context menu commands, and there are only addon's commands.)

Default commands for the context are missing - is that a bug? No, it is by design. The author of the patch told:

Including Firefox's default menu items is out of scope, because the default menu labels don't always make sense. For example, the "Close Tabs to the Right" menu item makes no sense in a vertical tabs-type extension.

If there are menu items that cannot be replicated with the extension APIs, then we can decide on a case-by-case basis for how this should be supported.

Adding menu items with the viewTypes parameter

So you need to implement imitated commands by yourself, if you hope to make the context menu compatible to Firefox's one. (Moreover, you can also add any other top-level custom items to the menu as you like.)

As you know, multiple extra commands added by browser.menus.create() are grouped under a sub menu. But here is an exception - the addon who called browser.menus.overrideContext() can put top-level commands as ungrouped. Thus you can define imitated default items and/or other top-level custom items without any worrying.

And, a new parameter viewTypes for browser.menus.create() will help you to add such commands only for a context menu on a popup panel or a sidebar panel.

browser.menus.create({
  id:        'context_reloadTab',
  title:     browser.i18n.getMessage('context_reloadTab_title'),
  type:      'normal',
  contexts:  ['tab'],
  viewTypes: ['sidebar'], // Important!!
  documentUrlPatterns: [`moz-extension://${location.host}/*`] // Important!!
});

viewTypes is an array of these possible values:

  • popup: a context menu shown on a popup panel
  • sidebar: a context menu shown on the sidebar area
  • tab: a context menu shown on the tab bar on the content area

Please note that there is no value for "the context menu on the tab bar". If you specify viewTypes:["tab","sidebar"] to show the item in the context menu on the tab bar and the sidebar, and hide on popup panels, actually it will be shown in the context menu on the content area and the sidebar, and hidden on popup panels and the tab bar. On such cases you need to omit viewTypes and control visibility of the item by a listener for browser.menus.onShown, based on info.viewType given to the listener. You can show/hide menu item by browser.menus.update() with its visible option.

If you specify both contexts and viewTypes, the item will become visible only when both conditions are satisfied.

The documentUrlPatterns parameter is required to hide your custom top-level items on any sidebar or popup panel provided by other addons. By the bug 1498896 documentUrlPatterns is effective to control visibility of the item based on the URL of the sidebar/popup panel itself.

Do you want to show an item on the menu except in your sidebar panel and popup panel? For example, the Multiple Tab Handler addon does that: commands for selected tabs are shown as top-level items on its popup panel, otherwise those commands are grouped under a "Selected Tabs" submenu. If you don't mind the submenu has a label same to the name of the addon, you don't think seriously - you just define all items without viewTypes option. However, if you want to set a label different to the addon's name to the submenu, you need to define top-level items and submenu items separately and control visibility of them.

Items visible only on your sidebar or popup panel are easily definable with the viewTypes and documentUrlPatterns options. On the other hand, items invisible on your sidebar or popup panel is hard a little. To do that you need to update visibility of such items dynamically via the visible option for browser.menus.update(). How to do that? MTH does by this commit, key points are:

  1. Add codes to track the state of your panel: opened or closed. On the background side:

    let gIsPanelOpen = false;
    browser.runtime.onConnect.addListener(port => {
      if (!/^connection-from-my-panel:/.test(port.name))
        return;
      gIsPanelOpen = true;
      port.onDisconnect.addListener(() => {
        gIsPanelOpen = false;
      });
    });
    

    On the panel side:

    browser.runtime.connect({
      // the connection name must be unique!
      name: `connection-from-my-panel:${Date.now()}`
    });
    
  2. Add codes to update visibility of items to the background side:

    if (typeof browser.menus.overrideContext == 'function') {
      let gLastVisible = true;
      browser.menus.onShown.addListener(async () => {
        const visible = !gIsPanelOpen;
        if (gLastVisible == visible)
          return;
        await browser.menus.update('id-of-the-item-you-want-to-hide-in-your-panel', { visible });
        gLastVisible = visible;
        browser.menus.refresh();
      });
    }
    

Note that both viewTypes and visible are available only on Firefox 64 or later. If you want to keep your addon compatible to Firefox 63 or older, you need to skip operations to register/update such commands on old Firefox when typeof browser.menus.overrideContext == 'function' equals to false, like these snippets.

Imitating default tab context menu commands

Most default tab context menu commands are imitable based on WebExtensions APIs. I think that complete imitation of default commands is painful, but finally I did that...

(A screenshot of new context menu on the sidebar. The context menu has default tab commands imitated by Tree Style Tab itself.)

You need to prepare colorized SVG icons for containers and pack them into the XPI package by yourself. Sadly there is no way to quote Firefox's built-in SVG icons directly as colorized versions to your imitated menu items, due to restrictions from security reasons.

And, one more sad thing: "Send Tab to Device" is still not imitable for now, due to missing WebExtensions APIs.

Conclusion

Things become possible on Firefox 64:

  • Opening custom context menu for tabs or bookmarks, including extra commands added by other addons. Implicitly collaboration of addons is partially back!
  • The addon calling browser.menus.overrideContext() can put multiple commands at the top-level of the context menu. (They won't be grouped automatically.)

Things still impossible:

  • Opening native context menu on demand - for example mouseover. This new way is available only on cases the contextmenu event is fired.
  • Controlling or overriding behaviors of extra commands added by other addons. You still need to do special collaboration via message based custom APIs.
  • Quoting default context menu commands of Firefox itself. You need to re-implement and imitate them by self.

If you want to know what you should do on real cases in the world, see another article describing about various patterns of custom menus.

On the migration from XUL to WebExtensions of Firefox's addon system itself, many advantage were lost and people thought that Firefox became same to Chrome. However, now Firefox get back more customizability step by step. I think you should pay attention on Firefox's future improvements around new WebExtensions APIs - Firefox sometimes try to do unique thing like above.

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能