Skip to content

Instantly share code, notes, and snippets.

@kentbrew

kentbrew/_index.md

Last active Dec 30, 2017
Embed
What would you like to do?
Toggling Global Features in Browser Extensions

Toggling Global Features in Browser Extensions

Developers wanting to add context menus to browser extensions face an all-or-nothing situation: context menu items, being global features, will be present on all pages once created. Adding a context menu item is easy, but if we want it to show only on certain Internet domains we will need to:

  • keep an eye on the active tab
  • check its domain whenever it changes
  • show or hide the context menu depending on the domain of the page in the tab

APIs we'll be using:

  • tabs: observe tabs as they are opened and changed
  • sendMessage and onMessage: communicate between our background and content scripts
  • contextMenus: add our new item (stick with contextMenus and not Mozilla's menus API here; the latter is not supported outside Firefox)

Manifest

Access tabs and context menus on all pages from the background, run background.js as a persistent background process, and inject content.js into all pages.

{
  "version": "0.0.1",
  "manifest_version": 2,
  "name": "Toggle Global Features",
  "description": "Show global browser features on a limited subset of domains.",
  "permissions": [
    "*://*/*",
    "tabs",
    "contextMenus"
  ],
  "background": {
    "scripts": [
      "background.js" 
    ]
  },
  "content_scripts": [ {
    "js": [ "content.js" ],
    "matches": [ "*://*/*" ]
  } ]
}

We need our content and background scripts to run on all pages because we're going to show or hide our context menu when tabs are switched.

Background

Start with a little bit of housekeeping to make our extension compatible with all WebExtensions browsers:

let browser = chrome || browser;

Set up a special block of functions that wait for requests from our content script. Here, function refreshContextMenus fires when requested by the content script, checking the tab's URL against a pattern that matches GitHub.

let runRequestFromContent = {
  refreshContextMenus: o => {
    browser.contextMenus.removeAll();
    if (o.url.match(/^https?:\/\/(.*\.)?github\.com\//)) {
      console.log('On target domain.');
      try {
        browser.contextMenus.create({
          id: 'doStuff',
          title: 'Do Stuff',
          contexts: ['link'],
          onclick: () => {
            alert('Doing stuff!');
          }
        });
        console.log('Context menu create WIN.');
      } catch (err) {
        console.log('Context menu create FAIL.');
      }
    } else {
      console.log('Not on target domain.');
    }
  }
};

Add a listener for messages. If we get one with an act key that matches a function under runRequestFromContent, run it:

browser.runtime.onMessage.addListener( r => {
   if (r.act && typeof runRequestFromContent[r.act] === 'function') {
     runRequestFromContent[r.act](r);
   }
});

Listen for tabs.onActivated and send a message to the content tab, saying "You're in view. Please tell me your URL so I can decide if you get the context menu or not."

browser.tabs.onActivated.addListener(r => {
  browser.tabs.sendMessage(r.tabId, {act: 'echoUrl'}, () => {});
});

Content

Same houskeeping here as before:

let browser = chrome || browser;

Set up a special block of functions that wait for requests from the background script. This one contains one function, echoUrl, which will send a message to the background process with our current URL.

let runRequestFromBackground = {
  echoUrl: () => {
    browser.runtime.sendMessage({
      act: 'refreshGlobalThings', 
      url: document.URL || null
    });
  }
}

Add a listener for messages. If we get one with an act key that matches a function under runRequestFromBackground, run it.

browser.runtime.onMessage.addListener(r => {
  if (r.act && typeof runRequestFromBackground[r.act] === 'function') {
    runRequestFromBackground[r.act](r);
  }
});

Try It Out

Save background.js, content.js, and manifest.json to a new directory.

For Chrome or Opera: open chrome://extensions, make sure Developer Mode is checked, and drag your new folder into the window. When Drop to Install appears, drop your directory. Click the background page link next to Inspect Views and bring up the console tab.

For Forefox: see Temporary Installation in Firefox.

In a new tab, open a GitHub link. (You may also reload this page, if it is already open in a tab. All tabs that are already open when the extension is installed will need to be reloaded to inject content.js.) If all goes well you'll see this in your background console:

On target domain.
Context menu create WIN.

Right-click on any link on the GitHub page and note that a new menu option, Do Stuff, has appeared. Select it and your alert box should pop up.

Visit a page on any other domain and you should see this in your console:

Not on target domain.

Directions for Further Development

Now that you know how to hide or show a context menu depending on the domain the reader is visiting, how would you go about enabling or disabling other global features, such as the browser toolbar button?

let browser = chrome || browser;
let runRequestFromContent = {
refreshGlobalThings: o => {
browser.contextMenus.removeAll();
if (o.url.match(/^https?:\/\/(.*\.)?github\.com\//)) {
console.log('On target domain.');
try {
browser.contextMenus.create({
id: 'doStuff',
title: 'Do Stuff',
contexts: ['link'],
onclick: () => {
alert('Doing stuff!');
}
});
console.log('Context menu create WIN.');
} catch (err) {
console.log('Context menu create FAIL.');
}
} else {
console.log('Not on target domain.');
}
}
};
browser.runtime.onMessage.addListener( r => {
if (r.act && typeof runRequestFromContent[r.act] === 'function') {
runRequestFromContent[r.act](r);
}
});
browser.tabs.onActivated.addListener(r => {
browser.tabs.sendMessage(r.tabId, {act: 'echoUrl'}, () => {});
});
let browser = chrome || browser;
let runRequestFromBackground = {
echoUrl: () => {
browser.runtime.sendMessage({
act: 'refreshGlobalThings',
url: document.URL || null
});
}
}
browser.runtime.onMessage.addListener(r => {
if (r.act && typeof runRequestFromBackground[r.act] === 'function') {
runRequestFromBackground[r.act](r);
}
});
{
"version": "0.0.1",
"manifest_version": 2,
"name": "Toggle Global Features",
"description": "Show global browser features on a limited subset of domains.",
"permissions": [
"*://*/*",
"tabs",
"contextMenus"
],
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [ {
"js": [ "content.js" ],
"matches": [ "*://*/*" ]
} ]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment