POSTS

Guide to Developing Chrome Context Menu Applications Part 5

Blog

We’re ready to make this real!

Canned Responses menu in LinkedIn Canned Responses content inserted into LinkedIn

Get Ready to Ship!

Well the things that need to change are few indeed!

  1. We’ll stop using stevengharms.com
  2. We’ll change the selected input fields
  3. We’ll tighten up the code’s footprint
  4. 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.

  1. Linked in: Navigate to “Messaging”
  2. Reload plugin
  3. Reload Linked In Page
  4. Click in Reply Field
  5. Select a Canned response(!)
  6. 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 Arrays or Objects 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!