Chrome Extension – Insert Blurb

Project management is part of my job, and I often find myself repeatedly say the same thing to different people, e.g. please use this link <link> to initiate your request; please update the project status; please contact our product manager <email> for prioritization. Copy/paste would work, but I still need a place to manage all those pre-defined text blurbs. I need a browser extension. So let’s build one!

The final product is published to the store already! See the screenshots:

Screen Shot 2016-08-29 at 9.46.53 PM

Screen Shot 2016-08-29 at 10.10.53 PM

As always, the official document is our first stop. Looks like we need the following items:

  • manifest.json that defines the extension metadata.
  • popup.html and popup.js that displays the extension pop-up widget.
  • blurb.js that runs in the background and serves as the controller.
  • content.js that runs after any page is loaded, like what Grease Monkey does.

First, manifest.json. After reading the document, there are a few important fields: permissions, browser_action, background, and content_scripts.

manifest.json:

{
  "manifest_version": 2,
  "name": "Insert Blurb",
  "description": "Insert a pre-written response or paragraph",
  "version": "1.1",
  "permissions": [
    "contextMenus",
    "storage",
    "activeTab",
    "clipboardRead",
    "clipboardWrite"
  ],
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "browser_action": {
   "default_icon": "icon19.png",
   "default_popup": "popup.html"
  },
  "background": {
    "persistent": false,
    "scripts": ["blurb.js"]
  },
  "content_scripts": [
      {
        "matches": [
          "http://*/*",
          "https://*/*"
        ],
        "js": [
          "content.js"
        ],
        "run_at": "document_end",
        "all_frames": false
      }
  ]
}

Then we need to write the widget that popups up when clicking the icon. It is easier to use jQuery when manipulate HTML elements so let’s include that. Note that jquery-3.1.0.min.js is downloaded and included in the extension package, so that users do not need to make a HTTP call to acquire the script while using the extension. The UI is very simple.

<!doctype html>
<html>
  <head>
    <title>Insert New Blurb</title>
    <script src="jquery-3.1.0.min.js"></script>
    <script src="popup.js"></script>
<style type="text/css">
      body {width:200; height:300;}
	</style>

  </head>
  <body>

Create a new blurb:

    Name:

    <input type="text" id="blurbName">

    Text:

    <textarea rows="5" cols="50" id="blurbText"></textarea>

    <button id="createBlurb">Create</button>

Available blurbs:
<ul id="blurbs"></ul>
</body>
</html>

In the Javascript, we are using a couple of features:

  • Storage, which allows us to save objects by key. Note that in the get API, passing null will retrieve the entire storage. See the document for more details.
  • Message, which sends a signal to the background script to update the context menu.

popup.js:

$(document).ready(function() {

  displayAllBlurbs();

  $("#createBlurb").click(function(){
    var name = $("#blurbName").val();
    var text = $("#blurbText").val();
    if (name && text) {
      var obj = {};
      obj[name] = text;
      chrome.storage.local.set(obj, function() {
        $("#blurbName").val("");
        $("#blurbText").val("");
        updateContextMenu();
        displayAllBlurbs();
      });
    }
  });

});

function displayAllBlurbs() {
  $("#blurbs").empty();
  chrome.storage.local.get(null, function(result) {
    for (var key in result) {
      var title = result[key].replace("\"", "'");
      var element = "
	<li><span title=\"" + title + "\">" + key + "</span>"
          + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='#' class='delete'>X</a>"
          + "</li>
"
      $("#blurbs").append(element);
    }

    $(".delete").click(function(){
      var element = $(this).parent();
      var key = element.find("span").text();
      chrome.storage.local.remove(key);
      element.remove();
      updateContextMenu();
    });
  });
}

function updateContextMenu() {
  chrome.runtime.sendMessage({
      request: "updateContextMenu"
  });
}

The background script does two things: a) construct the context menu; b) process click event to the content script.

blurb.js:

chrome.runtime.onInstalled.addListener(function() {
  createParentContextMenu();
});

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  removeAllContextMenu();
  createParentContextMenu();
  createChildContextMenu();
});

function createParentContextMenu() {
  chrome.contextMenus.create({
    title: "Insert Blurb",
    contexts: ["editable"],
    id: "menuParent"
  });
}

function removeAllContextMenu() {
  chrome.contextMenus.removeAll();
}

function createChildContextMenu() {
  chrome.storage.local.get(null, function(result) {
    for (var k in result) {
      chrome.contextMenus.create({
        title: k,
        contexts: ["all"],
        parentId: "menuParent",
        id: k
      });
    }
  });
}

chrome.contextMenus.onClicked.addListener(onClickHandler);

function onClickHandler(info) {
  var id = info.menuItemId;
  chrome.storage.local.get(null, function(result) {
    for (var k in result) {
      if (k == id) {
        chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
          chrome.tabs.sendMessage(tabs[0].id, {data: result[k]});
        });
        break;
      }
    }
  });
}

Lastly, the content script, which runs after each page is loaded, will process the click event, and append the blurb at the mouse cursor.

content.js:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.data) {
    insertTextAtCursor(request.data);
  }
});

function insertTextAtCursor(text) {
  var el = document.activeElement;
  var val = el.value;
  var endIndex;
  var range;
  var doc = el.ownerDocument;
  console.log(doc);
  if (typeof el.selectionStart === "number" &&
    typeof el.selectionEnd === "number") {
    endIndex = el.selectionEnd;
    el.value = val.slice(0, endIndex) + text + val.slice(endIndex);
    el.selectionStart = el.selectionEnd = endIndex + text.length;
  } else if (doc.selection && doc.selection !== "undefined" && doc.selection.createRange) {
    el.focus();
    range = doc.selection.createRange();
    range.collapse(false);
    range.text = text;
    range.select();
  } else {
    // Copy to clipboard
    var temp = document.createElement("textarea");
    temp.innerText = text;
    document.body.appendChild(temp);
    temp.select();
    document.execCommand("copy");
    el.focus();
    document.execCommand("paste");
    temp.remove();
  }
}

Limitations: this extension works well with textbox and textarea, but not so much on customized HTML elements. Many websites such as Gmail, Facebook are not using standard textarea for message composer, therefore the active element does not have “selection” or “selectionStart”, which makes the “paste” function not working at all. I am still looking for a solution.

Advertisements