An improvement of WebExtensions on Firefox 64 about implicit collaboration of addons - Oct 14, 2018

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

(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.

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.

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.

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.

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!

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

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.

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.

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

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.

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

: a context menu shown on a popup panel sidebar : a context menu shown on the sidebar area

: 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:

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()}` }); 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...

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.

. This new way is available only on cases the 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.