Writing A Google Chrome Extension | Part 1: Proof-of-Concept

Engineering Jul 31, 2020

We, at Deskera, use Atlasssian Jira as issue tracking and Agile workflow tool internally. Sometimes, we communicate the Jira ticket numbers via email or chat. To open the ticket in Jira, I need to copy the ticket number, open Jira, paste the ticket number in either Jira search box or browser address bar. This is tedious if I have to do this many times a day.

As I had been using Brave as my primary browser - which was compatible with Google Chrome Extensions, I decided to write a Chrome Extension.

Chrome Developer Portal has a comprehensive "Getting Started" tutorial, which covers the basics of developing a Chrome Extension.

Proof-of-Concept (PoC)

Our Chrome Extension PoC has two components:

Manifest file - manifest.json which has the metadata of the extension

{
  "name": "Extend Context Menu",
  "version": "0.1",
  "description": "A Chrome Extension to add context menu items",
  "permissions": ["contextMenus"],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "manifest_version": 2
}

Background Script - script for background tasks

const CONTEXT_MENU_ID = "CUSTOM_CONTEXT_MENU_" + Date.now();
function contextMenuHandler(info,tab) {
  if (info.menuItemId !== CONTEXT_MENU_ID) {
    return;
  }
  console.log("Word " + info.selectionText + " was clicked.");
  chrome.tabs.create({  
    url: "https://jira.deskera.com/browse/" + info.selectionText
  });
}
console.log("CONTEXT_MENU_ID: " + CONTEXT_MENU_ID);
chrome.contextMenus.create({
  title: "Open \"%s\" in Jira", 
  contexts:["selection"],
  id: CONTEXT_MENU_ID
});
chrome.contextMenus.onClicked.addListener(contextMenuHandler);

Let's load it in Chrome

  1. Navigate to chrome://extensions/,
  2. make sure "Developer Mode" is enabled
  3. Click on "Load Unpacked" and select the folder containing these extension files

Now let's test it

While this works for my personal single-purpose use-case, it's still crude - the target URL is hardcoded, my context menu entry shows up no matter which phrase I select, and an ugly icon shows up for the extension and the context menu.

(Note: The source code for version 0.1: Browse, Zip)

Showing Context Menu on Selection

The contextMenus API which defines context menu entries - does not need to be called every time the context menu is opened. Instead, we will  create the context menu entry once, and update it on the selectionchange event.

We will introduce content_scripts to capture selectionchange. Let's add the following lines to manifest.json

  "content_scripts": [{
    "matches": ["*://*/*"],
    "js": ["content_script.js"]
  }],

which will run content_script.js on all the sites.

Let's add content_script.js as well.

document.addEventListener('selectionchange', function() {
  var selection = window.getSelection().toString().trim();
  chrome.runtime.sendMessage({
    request: 'updateContextMenu',
    selection: selection
  });
});

Now, we will update our background.js to create/update/delete context menu entries on selectionchange event.

...
var cmid;
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
  if (msg.request === 'updateContextMenu') {
      if (msg.selection == '') {
          if (cmid != null) {
              chrome.contextMenus.remove(cmid);
              cmid = null;
          }
      } else {
          var options = {
              title: "Open \"%s\" in Jira",
              contexts: ['selection']
          };
          if (cmid != null) {
              chrome.contextMenus.update(CONTEXT_MENU_ID, options);
          } else {
              options.id = CONTEXT_MENU_ID;
              chrome.contextMenus.create(options);
              cmid = CONTEXT_MENU_ID;
          }
      }
  }
});
...

This is better, but my context menu entry is still showing on all the selections.

Showing Context Menu Only On Ticket Selection

We will match the selected string with regex pattern for a typical Jira ticket - <Project Key>-<Issue number>,

const selectionMatchingPattern = /\w+-\d+/g;

and show our context menu entry only if matches the pattern

...    
    if (msg.selection.match(selectionMatchingPattern)) {
      var options = {
        title: "Open \"%s\" in " + target,
        contexts: ['selection']
      };
      if (cmid != null) {
        chrome.contextMenus.update(CONTEXT_MENU_ID, options);
      } else {
        options.id = CONTEXT_MENU_ID;
        chrome.contextMenus.create(options);
        cmid = CONTEXT_MENU_ID;
      }
    } else {
      if (cmid != null) {
        chrome.contextMenus.remove(cmid);
        cmid = null;
      }
    }
...

Our updated background.js looks like this:

const target = "Jira";
const targetUrl = "https://jira.deskera.com/browse/";
const selectionMatchingPattern = /\w+-\d+/g;

const CONTEXT_MENU_ID = "CUSTOM_CONTEXT_MENU_" + Date.now();

function contextMenuHandler(info,tab) {
  if (info.menuItemId !== CONTEXT_MENU_ID) {
    return;
  }
  console.log(info.selectionText + " was clicked.");
  chrome.tabs.create({  
    url: targetUrl + info.selectionText
  });
}
console.log("CONTEXT_MENU_ID: " + CONTEXT_MENU_ID);

var cmid;
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
  if (msg.request === 'updateContextMenu') {
    if (msg.selection.match(selectionMatchingPattern)) {
      var options = {
        title: "Open \"%s\" in " + target,
        contexts: ['selection']
      };
      if (cmid != null) {
        chrome.contextMenus.update(CONTEXT_MENU_ID, options);
      } else {
        options.id = CONTEXT_MENU_ID;
        chrome.contextMenus.create(options);
        cmid = CONTEXT_MENU_ID;
      }
    } else {
      if (cmid != null) {
        chrome.contextMenus.remove(cmid);
        cmid = null;
      }
    }
  }
});
chrome.contextMenus.onClicked.addListener(contextMenuHandler);

Adding Extension Icons

Our extension is showing an unimpressive default icon right now. Let's add a fancy icon. I downloaded my free icon from Vecteezy.com, which I resized into 4 sizes and referenced in manifest.json

  "icons": {
    "16": "images/extcm16.png",
    "32": "images/extcm32.png",
    "48": "images/extcm48.png",
    "128": "images/extcm128.png"
  },

Let's reload the extension

Looking good!

(Note: The source code for version 0.2: Browse, Zip, Diff)

Next up - Part 2: Making it Configurable.

Brajesh Sachan

Brajesh, drives direction of Deskera’s future technology and shapes Deskera as the technology leader. With his expertise and over 15 years of experience, he has significantly contributed to Deskera

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.