POSTS
Guide to Developing Chrome Context Menu Applications Part 4
BlogInterlude
In the previous section we set up our communications conduit, and wrestled with the basics of the Chrome Extension API. We’re now going to do a pass where we change the “debuggy” messages from being noise into being canned responses.
Let’s create a new branch called: boilerplate-pass-through
. Our goal is to
get our canned responses passed through from background to content scripts.
Build Context Menu UI
Per our user stories, we want to add a contextMenu element. Let’s add something trivial to verify it works. You should now see the pattern that many APIs require an additional permission. To use the contextMenus API you must add the contextMenus permission.
manifest.json
.
"permissions": ["webNavigation", "*://stevengharms.com/*", "contextMenus"]
Now, let’s use our background script to create a menu item:
background.js
chrome.webNavigation.onCompleted.addListener(function(details) {
chrome.tabs.executeScript(details.tabId, {
file: "canned_response.js"
}, () => {
chrome.contextMenus.create({
id: String(details.tabId),
title: "Canned Response",
documentUrlPatterns: [ "*://stevengharms.com/*" ]
});
})
}, {
url: [{ "hostContains": "stevengharms.com" }]
});
Refresh the plugin, reload the page. When you Right-click on the page you
should see a new entry: “Canned Response.” Furthermore, you will only see it on
stevengharms.com
sited pages. Cool, eh?
Now you might think, “Sweet, I bet I can add a callback for when this menu is
clicked.” You’re right! Let’s do it and look at onclick
in the docs:
Read that text twice:
A function that will be called back when the menu item is clicked. Event pages
cannot use this; instead, they should register a listener for
chrome.contextMenus.onClicked.
Well, heck, we’re an event page, so we need to follow this advice.
Let’s do what the docs say. I’m going to do the simplest thing possible and add
this to background.js
:
chrome.contextMenus.onClicked.addListener(function() {
debugger
})
…and reload the plugin, refresh, etc. Summon the context menu and click.
We hit the debugger! Awesome! We’re in the right spot. The arguments
hold a
number of data that we can use to do our work.
- The 0th element is information about the menu that was clicked
- The 1st element is about the tab
As such, a more self-documenting implementation of the callback might be:
chrome.contextMenus.onClicked.addListener(function(menuInfo, tabInfo) {
debugger
})
At this point, I’m confident that we know how to set up a menu and handle an event and we probably have the required data to handle any decision-making logic. Let’s correct our menu to hold canned responses.
Build Canned Responses Into Menu UI
We’ll keep the responses simple for ease of debugging and store them in the
global _CANNED_RESPONSES
Let’s build up the menus:
background.js
const MENU_PREFIX = "CANNED_REPONSE_CONTEXT_MENU-";
var _CANNED_RESPONSE_REGISTRY = {
}
var _CANNED_RESPONSES = [
["Alpha", "Alpha body"],
["Beta", "Beta body"],
];
chrome.webNavigation.onCompleted.addListener(function(details) {
chrome.tabs.executeScript(details.tabId, {
file: "canned_response.js"
}, () => {
// Create top-level menu
var parentMenuId = chrome.contextMenus.create({
id: MENU_PREFIX + details.tabId,
title: "Canned Response",
documentUrlPatterns: [ "*://stevengharms.com/*" ]
});
// Create a canned responses menu
_CANNED_RESPONSES.forEach((response, i) => {
var id = chrome.contextMenus.create({
title: response[0],
parentId: parentMenuId,
id: MENU_PREFIX + details.tabId + i,
documentUrlPatterns: [ "*://stevengharms.com/*" ]
})
_CANNED_RESPONSE_REGISTRY[id] = response[1];
});
})
}, {
url: [{ "hostContains": "stevengharms.com" }]
});
chrome.contextMenus.onClicked.addListener(function() {
debugger
});
As a quick note, Event Pages are required to give their contextMenus id
s.
It’s something Chrome will complain about if they’re not present. They’re used
to make menu loading more efficient.
Now, in the code above, the new code performs the population of the
_CANNED_RESPONSE_REGISTRY
based on iterating the _CANNED_RESPONSES
. The
goal is simple: we iterate the Array
of Arrays
and add new menu items under
our “top-level” menu item. Items created with a parentId
become sub-menus.
Additionally we populate a registry that associates menuIds (the GUI element) with the associated response body. This will serve as a lookup table to fetch the response we need and which we will pass to the content script.
Recall in the onClicked handler we knew about the menuId as an attribute on the object passed in as the 0th argument. This table will be our way to look up what the appropriate response is.
So let’s update menu click handler:
chrome.contextMenus.onClicked.addListener((info, tab) => {
var boilerplate = _CANNED_RESPONSE_REGISTRY[info.menuItemId];
if (boilerplate) {
// "Sends a single message to the content script(s) in the specified tab,
// with an optional callback to run when a response is sent back."
chrome.tabs.sendMessage(tab.id, { canned_response: boilerplate })
}
});
and let’s update it’s partner message update handler in the content script,
canned_response.js
chrome.runtime.onMessage.addListener( (message) => {
_CANNED_RESPONSE_ACTIVE_INPUT.value = message.canned_response;
});
Be sure and notice that I stopped referring to message.magic_word
as we want
our naming to be more descriptive of what’s coming in.
Track Selected Input Field for Updating
Let’s add code in “DOM land” or the content script so that when the input is activated, we track it.
var _CANNED_RESPONSE_ACTIVE_INPUT;
// ...
document.querySelector("input[type='text']").addEventListener('focus', e => _CANNED_RESPONSE_ACTIVE_INPUT = e.target)
Only Present the Menu When In an Editing Place
We’ll also change the context of the menus to appear only in editable fields.
Add contexts: ["editable"]
to the contextMenus.create
calls. A full diff
will appear below with them.
OK, let’s reload the plugin, reload the page, click in the search field on
stevengharms.com
, right-click for a contextMenu and have our extension
paste in the text!
Whew! That was a lot of work, but we now have our system working, albeit on the wrong site. From here on out it’s just going to be minor tweaks! Here’s the diff:
diff --git a/background.js b/background.js
index b42c5e2..0762faf 100644
--- a/background.js
+++ b/background.js
@@ -1,7 +1,46 @@
+const MENU_PREFIX = "CANNED_REPONSE_CONTEXT_MENU-";
+
+var _CANNED_RESPONSE_REGISTRY = {
+}
+
+var _CANNED_RESPONSES = [
+ ["Alpha", "Alpha body"],
+ ["Beta", "Beta body"],
+];
+
chrome.webNavigation.onCompleted.addListener(function(details) {
chrome.tabs.executeScript(details.tabId, {
- file: "canned_response.js"
- }, () => chrome.tabs.sendMessage(details.tabId, { magic_word: "ROMY ZOMIE" }, txt => alert("called me back: " + txt)) );
+ file: "canned_response.js"
+ }, () => {
+ // Create top-level menu
+ var parentMenuId = chrome.contextMenus.create({
+ id: MENU_PREFIX + details.tabId,
+ title: "Canned Response",
+ contexts: ["editable"],
+ documentUrlPatterns: [ "*://stevengharms.com/*" ]
+ });
+
+ // Create a canned responses menu
+ _CANNED_RESPONSES.forEach((response, i) => {
+ var id = chrome.contextMenus.create({
+ title: response[0],
+ parentId: parentMenuId,
+ id: MENU_PREFIX + details.tabId + i,
+ contexts: ["editable"],
+ documentUrlPatterns: [ "*://stevengharms.com/*" ]
+ })
+ _CANNED_RESPONSE_REGISTRY[id] = response[1];
+ });
+ })
}, {
url: [{ "hostContains": "stevengharms.com" }]
});
+
+chrome.contextMenus.onClicked.addListener((info, tab) => {
+ var boilerplate = _CANNED_RESPONSE_REGISTRY[info.menuItemId];
+ if (boilerplate) {
+ // "Sends a single message to the content script(s) in the specified tab,
+ // with an optional callback to run when a response is sent back."
+ chrome.tabs.sendMessage(tab.id, { canned_response: boilerplate })
+ }
+});
diff --git a/canned_response.js b/canned_response.js
index 634d501..3f30afb 100644
--- a/canned_response.js
+++ b/canned_response.js
@@ -1,4 +1,8 @@
-chrome.runtime.onMessage.addListener( (message, sender, cb) => {
- alert(message.magic_word);
- cb(document.querySelector("h1.post-title").innerText);
+console.log("running");
+var _CANNED_RESPONSE_ACTIVE_INPUT;
+
+chrome.runtime.onMessage.addListener( (message) => {
+ _CANNED_RESPONSE_ACTIVE_INPUT.value = message.canned_response;
});
+
+document.querySelector("input[type='text']").addEventListener('focus', e => _CANNED_RESPONSE_ACTIVE_INPUT = e.target);
diff --git a/manifest.json b/manifest.json
index d2cb2b5..d585fb3 100644
--- a/manifest.json
+++ b/manifest.json
@@ -9,5 +9,5 @@
"persistent": false
},
- "permissions": ["webNavigation", "*://stevengharms.com/*"]
+ "permissions": ["webNavigation", "*://stevengharms.com/*", "contextMenus"]
}
Let’s commit and merge!
git commit -avm 'Can pass canned responses on stevengharms.com'; gco master; git merge --no-ff boilerplate-pass-through
This is a major moment. From here on out we’re going to our deployment target.
Create a new branch called prepare-for-linked-in
and I’ll see you in the
next installment.