POSTS
Guide to Developing Chrome Context Menu Applications Part 5
BlogWe’re ready to make this real!
Get Ready to Ship!
Well the things that need to change are few indeed!
- We’ll stop using
stevengharms.com
- We’ll change the selected input fields
- We’ll tighten up the code’s footprint
- ADVANCED We’ll optimize a critical UX bug cased by LinkedIn’s JavaScript-heavy render
From stevengharms.com to linkedin.com
My simple site has served me well enough. Time for the big leagues. I’ll present this only as a diff.
diff --git a/background.js b/background.js
index 0762faf..d68eed9 100644
--- a/background.js
+++ b/background.js
@@ -17,7 +17,7 @@ chrome.webNavigation.onCompleted.addListener(function(details) {
id: MENU_PREFIX + details.tabId,
title: "Canned Response",
contexts: ["editable"],
- documentUrlPatterns: [ "*://stevengharms.com/*" ]
+ documentUrlPatterns: [ "*://*.linkedin.com/*" ]
});
// Create a canned responses menu
@@ -27,13 +27,13 @@ chrome.webNavigation.onCompleted.addListener(function(details) {
parentId: parentMenuId,
id: MENU_PREFIX + details.tabId + i,
contexts: ["editable"],
- documentUrlPatterns: [ "*://stevengharms.com/*" ]
+ documentUrlPatterns: [ "*://*.linkedin.com/*" ]
})
_CANNED_RESPONSE_REGISTRY[id] = response[1];
});
})
}, {
- url: [{ "hostContains": "stevengharms.com" }]
+ url: [{ "hostContains": "linkedin.com" }]
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
diff --git a/manifest.json b/manifest.json
index d585fb3..f4c843c 100644
--- a/manifest.json
+++ b/manifest.json
@@ -9,5 +9,5 @@
"persistent": false
},
- "permissions": ["webNavigation", "*://stevengharms.com/*", "contextMenus"]
+ "permissions": ["webNavigation", "*://*.linkedin.com/*", "contextMenus"]
}
Make the DOM Code Clearer
And then apply a mild change to the event logic:
var _CANNED_RESPONSE_ACTIVE_INPUT;
+var _CANNED_RESPONSE_INPUT_ELEMENT = "textarea";
-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);
+chrome.runtime.onMessage.addListener(message => _CANNED_RESPONSE_ACTIVE_INPUT.value += message.canned_response);
+document.getElementsByTagName(_CANNED_RESPONSE_INPUT_ELEMENT)[0].addEventListener('focus', e => _CANNED_RESPONSE_ACTIVE_INPUT = e.target);
Try Out the App
If you’ve been developing along with me, let’s test this thing out.
- Linked in: Navigate to “Messaging”
- Reload plugin
- Reload Linked In Page
- Click in Reply Field
- Select a Canned response(!)
- See it get inserted multiple times
Not quite what we wanted. Let’s make some tweaks.
Optimization: Limit Event Binding
LinkedIn uses EmberJS and other page-render accelerating technology. As such,
it’s possible for its pages to send the completed
event multiple times. We
should listen for onHistoryStateUpdated
instead of onCompleted
.
Having worked with Ember some years back I recognized that it or some other SPA technology might be confusing the event code. I checked this hypothesis out on Stack Overflow and had my suspicion confirmed.
Go ahead and make that change.
Optimization: Narrow the Event Filter
Let’s tighten the filter to only wake our Event Page only on
the /messaging
path. This goes in background.js
.
url: [{ "hostContains": "linkedin.com", "pathPrefix": "/messaging" }]
Optimization: Only Show the contextMenus
on the Messaging Page
This is a nice constraint. Not much to say:
documentUrlPatterns: ["*://*.linkedin.com/messaging/*"]
UX BUG: Multiple Revisits to /messaging
Make the Insertion Event Repeat
If you “navigate around” Linked in and come back to /messaging
several time
and then use the plugin, you’ll see the insertion code fires once for each time
you visited /messaging
.
I’m going to fix this with a simple guard flag. If there are any better solutions, I’d love to hear it.
var _CANNED_RESPONSE_ACTIVE_INPUT;
var _CANNED_RESPONSE_INPUT_ELEMENT = "textarea";
var _CANNED_RESPONES_RUNTIME_EVENTS_BOUND;
document.getElementsByTagName(_CANNED_RESPONSE_INPUT_ELEMENT)[0].addEventListener('focus', e => _CANNED_RESPONSE_ACTIVE_INPUT = e.target)
if (!_CANNED_RESPONES_RUNTIME_EVENTS_BOUND) {
chrome.runtime.onMessage.addListener(message => _CANNED_RESPONSE_ACTIVE_INPUT.value += message.canned_response);
_CANNED_RESPONES_RUNTIME_EVENTS_BOUND = true;
}
UX Issue: Tell the User It’s Ready
I’d certainly like to know this thing is ready to go. Let’s change the placeholder text.
After the contextMenus
load in background.js
add:
chrome.tabs.sendMessage(details.tabId, { initialize_placeholder: "Canned Responses Ready To Go!" })
In canned_response.js
:
var _CANNED_RESPONSE_ACTIVE_INPUT;
var _CANNED_RESPONSE_INPUT_ELEMENT = "textarea";
var _CANNED_RESPONES_RUNTIME_EVENTS_BOUND;
document.getElementsByTagName(_CANNED_RESPONSE_INPUT_ELEMENT)[0].addEventListener('focus', e => _CANNED_RESPONSE_ACTIVE_INPUT = e.target)
if (!_CANNED_RESPONES_RUNTIME_EVENTS_BOUND) {
chrome.runtime.onMessage.addListener(message => {
if (message.initialize_placeholder) {
document.getElementsByTagName(_CANNED_RESPONSE_INPUT_ELEMENT)[0].setAttribute("placeholder", message.initialize_placeholder);
return;
}
_CANNED_RESPONSE_ACTIVE_INPUT.value += message.canned_response;
});
_CANNED_RESPONES_RUNTIME_EVENTS_BOUND = true;
}
Merge and Call it A Day
Well, we’ve dont pretty well by our user stories (see below). Let’s merge in and call it a day.
git commit -a -m'Finished tweaks for Linked in'; git checkout master; git merge --no-ff prepare-for-linked-in
Check on the User Stories
- ✓ AS A LinkedIn user I WANT to be able to reply with canned responses SO THAT I can more effectively manage my inbox
- ✓ AS A $TOOL user I WANT to be able to add new responses SO THAT I can more effectively manage my inbox
- ✓ AS A $TOOL user I WANT to activate my response by using a drop down from a context menu
- ✓ AS A $TOOL user I want the drop down to have a list of “headings” which
correspond to long-form text responses e.g.
"KISS-OFF" => I would never work for a company as committed to environmental ruin as you. Never contact me again.
Looks like our stakeholder(s) should be pleased!
Code Reflection
Here’s a good time to reflect on the state of the app:
Good
- Shipped a simple, understood bit of code we can start getting feedback on!
- Have a usable template for future page action extensions!
Sub-Optimal
- No nesting in menus. It’d be sweet to have
(registered site) -> [template 1, template 2]
that varies based on the site. It’d pull up templates for Gmail for, uh, Gmail; templates for LinkedIn on LinkedIn - Allow nesting in the registered responses to be nested e.g.
(top-level) -> not interested -> [1,2,3]
and(top-level) -> lets meet -> [1,2,3]
- Expose a configuration page so that we don’t have to edit JavaScript
Array
s orObjects
in the plugin to add / change reponses.
Conclusion
The ecosystem’s barriers are weirdly high due to inconsistent documentation. Once you have a good example, I think you’re really going to enjoy customizing your web experience with extensions. There are a ton more directions in which this work could be taken. I hope you built your own or grabbed the github repo with this code. If this was a help let me know on Twitter (@sgharms) or via email. Please report bugs to the Github repo!