Oct 26, 2018

Various "custom context menu" usages in Firefox 64

As I described at the previous article, you can provide more useful and usable context menu for your addon on Firefox 64 and later, if it is focused to control tabs or bookmarks. The previous article described basics of new APIs, but it looked too complex because there are various usecases. So this article aims to describe how to provide context menu simply for different cases:

  1. Extra context menu items for custom commands, when your addon has no sidebar/popup panel
  2. Extra context menu items for custom commands, grouped under a submenu with a custom label
  3. Context menu dedicated to custom commands on your sidebar/popup panel, and expose them as a submenu on other situations
  4. Context menu dedicated to custom commands only on your sidebar/popup panel
  5. Context menu dedicated to custom commands on your sidebar/popup panel, and expose them as a submenu on other situations, with a custom label
  6. Imitated context menu compatible to Firefox's one on tabs or bookmarks, only on your sidebar/popup panel

All following examples assume that your addon named "Bucket" provides ability to send tabs to an online bucket, like the "Pocket".

Case 1: Extra context menu items for custom commands, when your addon has no sidebar/popup panel

On this case there is no change from Firefox 63 or older. You simply need to create context menu items with the contexts option.

browser.menus.create({
  id:       'add-to-bucket',
  title:    'Add to Bucket',
  contexts: ['tab'] // or 'bookmark'
});
browser.menus.create({
  id:       'remove-from-bucket',
  title:    'Remove from Bucket',
  contexts: ['tab'] // or 'bookmark'
});

// Handling of click on those items
browser.menus.onClick((info, tab) => {
  switch (info.menuItemId) {
    case 'add-to-bucket':
      ...
      break;

    case 'remove-from-bucket':
      ...
      break;
  }
});

Such multiple custom commands are automatically grouped under a submenu with the name of the addon itself (in this case, it wille be "Bucket".) Moreover, it will appear at context menus on other addon's special sidebar/popup panel, like Tree Style Tab's vertical tab bar.

(Screenshot of menu items added in this way.)

Case 2: Extra context menu items for custom commands, grouped under a submenu with a custom label

Do you want to put those commands under a submenu with a label different to the name of the addon?

(Screenshot of a submenu with custom label.)

Then, you need to use the parentId option.

// The parent item
browser.menus.create({
  id:       'bucket',
  title:    'Manage Bucket',
  contexts: ['tab']
});
// Items in the submenu
browser.menus.create({
  parentId: 'bucket', // Important!
  id:       'add-to-bucket',
  title:    'Add to Bucket'
});
browser.menus.create({
  parentId: 'bucket', // Important!
  id:       'remove-from-bucket',
  title:    'Remove from Bucket'
});

browser.menus.onClick((info, tab) => {
  ...
});

The submenu will appear at context menus on other addon's special sidebar/popup panel.

Case 3: Context menu dedicated to custom commands on your sidebar/popup panel, and expose them as a submenu on other situations

(Screenshot of top-level custom context menu items.) They appear in the native tab context/bookmark context menu under a submenu.

You may want to show your commands as top-level items like above, if your addon has a custom popup (or sidebar) panel.

In such a situation you don't need any extra operation to define menu items. Just same to the case 1, you simply define menu items for specific context.

browser.menus.create({
  id:       'add-to-bucket',
  title:    'Add to Bucket',
  contexts: ['tab']
});
browser.menus.create({
  id:       'remove-from-bucket',
  title:    'Remove from Bucket',
  contexts: ['tab']
});

browser.menus.onClick((info, tab) => {
  ...
});

Those items will appear also in the native context menu and custom context menu provided by other addons, same as the case 1.

On the other hand, you need to call browser.menus.overrideContext() on your sidebar/popup panel, like:

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 element for a tab, set the
    // context to "opening a tab context menu on the real tab".
    browser.menus.overrideContext({
      context: 'tab',
      tabId:   parseInt(tab.dataset.id)
    });
  }
  else {
    // Otherwise, don't show the menu.
    event.preventDefault();
  }
}, { capture: true });

Case 4: Context menu dedicated to custom commands only on your sidebar/popup panel

If you don't want to expose your custom menu items to the native tab (or bookmark) context menu, you need to restrict them only for your sidebar/popup panel, by the combination of viewTypes and documentUrlPatterns options.

if (typeof browser.menus.overrideContext == 'function') {
  // Top-level items on your custom panel
  const CUSOTM_PANEL_PATTERN = [`moz-extension://${location.host}/*`];
  browser.menus.create({
    id:        'add-to-bucket',
    title:     'Add to Bucket',
    contexts:  ['tab'],
    viewTypes: ['sidebar', 'popup'], // Important!
    documentUrlPatterns: CUSOTM_PANEL_PATTERN // Important!
  });
  browser.menus.create({
    id:        'remove-from-bucket',
    title:     'Remove from Bucket',
    contexts:  ['tab'],
    viewTypes: ['sidebar', 'popup'], // Important!
    documentUrlPatterns: CUSOTM_PANEL_PATTERN // Important!
  });
}

The combination of viewTypes and documentUrlPatterns for a moz-extension: URL makes the menu item visible only on panels matching to the pattern, and they items won't appear on general situations - native tab context menu and custom tab context menu on other addons.

Please remind that put those codes under a block runnable only on Firefox 64 and later. Both the viewTypes option and a support for moz-extension: URL pattern are introduced on Firefox 64, and they cause errors on Firefox 63 or older.

Those items become inaccessible on context menu for a sidebar/bookmark panel provided by other addons, like Tree Style Tab.

Of course you still need to call browser.menus.overrideContext() on your sidebar/popup panel, same as the previous case.

Case 5: Context menu dedicated to custom commands on your sidebar/popup panel, and expose them as a submenu on other situations, with a custom label

In this case there are top-level custom context menu items on your custom panel. On the other hand, there is a submenu with custom label for other situations.

This is complex a little. For general situations, you need to define menu items same as the case 2.

// The parent item on general situations
browser.menus.create({
  id:       'bucket',
  title:    'Manage Bucket',
  contexts: ['tab']
});
// Items in the submenu
browser.menus.create({
  parentId: 'bucket',
  id:       'add-to-bucket',
  title:    'Add to Bucket'
});
browser.menus.create({
  parentId: 'bucket',
  id:       'remove-from-bucket',
  title:    'Remove from Bucket'
});

And you need to define top-level items separately for your sidebar/popup panel like as the previous case. They also must have unique ID different from regular versions:

if (typeof browser.menus.overrideContext == 'function') {
  // Top-level items on your custom panel
  const CUSOTM_PANEL_PATTERN = [`moz-extension://${location.host}/*`];
  browser.menus.create({
               // Note that there is a prefix part
    id:        'top-level:add-to-bucket',
    title:     'Add to Bucket',
    contexts:  ['tab'],
    viewTypes: ['sidebar', 'popup'], // Important!
    documentUrlPatterns: CUSOTM_PANEL_PATTERN // Important!
  });
  browser.menus.create({
               // Note that there is a prefix part
    id:        'top-level:remove-from-bucket',
    title:     'Remove from Bucket',
    contexts:  ['tab'],
    viewTypes: ['sidebar', 'popup'], // Important!
    documentUrlPatterns: CUSOTM_PANEL_PATTERN // Important!
  });
}

Moreover, you need to update handling of clicking on those menu items:

// Handling of click on those items
browser.menus.onClick((info, tab) => {
  // Important!
  const normalizedId = info.menuItemId.replace(/^top-level:/, '');
  switch (normalizedId) {
    case 'add-to-bucket':
      ...
      break;

    case 'remove-from-bucket':
      ...
      break;
  }
});

Did you pay attention that IDs of extra top-level items were defined with a part same to regular versions and a common previx? By this trick you just need to remove the prefix part, then you can share implementations for menu commands to both versions.

But here is just a half on the way. Because general version items have no viewTypes option, they also appear on your custom panel together with your special top-level items. (Please remind that you should not add viewTypes: ['tab'] or something value for compatibility with other addons, because such an option will hide your item unexpectedly in a context menu on any sidebar/popup panel provided by other addons.) So you need to hide it only on your custom panel.

Here is an example to track the state of your panel: shown or hidden. Note that they are placed on a background script:

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

// And update visibility of the menu item based on the tracked state.
// Because the "visible" option is also introduced on Firefox 63,
// this section must be skipped on old Firefox.
if (typeof browser.menus.overrideContext == 'function') {
  let lastVisible = true;
  browser.menus.onShown.addListener(async () => {
    const visible = !isPanelOpen;
    if (lastVisible == visible)
      return;
    await browser.menus.update('bucket', { visible });
    lastVisible = visible;
    browser.menus.refresh();
  });
}

And finally you need to add a code to notify the panel is shown.

document.addEventListener('contextmenu', event => {
  ...
}, { capture: true });

// Establish connection with the background page.
// When the panel is hidden, it will be disconnected automatically.
browser.runtime.connect({
  // the connection name must be unique!
  name: `connection-from-my-panel:${Date.now()}`
});

Case 6: Imitated context menu compatible to Firefox's one on tabs or bookmarks, only on your sidebar/popup panel

(Screenshot of imitated default tab commands on Tree Style Tab sidebar.)

This is a variation of the case 4.

Those imitated menu items should appear only on your custom panel, so you need to define them with viewTypes and documentUrlPatterns options, like:

if (typeof browser.menus.overrideContext == 'function') {
  const CUSOTM_PANEL_PATTERN = [`moz-extension://${location.host}/*`];
  browser.menus.create({
    id:        'imitated:reloadTab',
    title:     'Reload Tab',
    contexts:  ['tab'],
    viewTypes: ['sidebar', 'popup'], // Important!
    documentUrlPatterns: CUSOTM_PANEL_PATTERN // Important!
  });
  ...
}

Most default tab context menu items are imitable based on WebExtensions APIs. There is an example implementation on Tree Style Tab addon.

The tab context menu has "Reopen in Container" items with colorized icons for each container. You need to prepare 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

For those different cases, you need to define context menu items carefully.

Context menu items defined with suitable options become well compatible to other addons. Other-addons-friendly addon will improved user experiences much more.

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能