Build Your Own Chrome Extension: Google Docs Word Count Tool
Like a lot of people these days, I have abandoned desktop word processors in favor of the free and simple Google Docs. One thing I miss, however, is a status bar with a persistent word count which helped me keep an eye out for word limits in articles I was working on. To enhance my Google Docs experience, I decided to build one just like it, and here’s how.
Welcome to part one of a three-part series on building and optimising an extension for Google Chrome. Knowledge of HTML, CSS and jQuery is recommended for understanding this tutorial. Also, please make sure you have the latest version of Google Chrome in Developer Mode (under Tools -> Extensions; a checkbox at the top of the screen).
Create a folder anywhere on your hard drive, and give it a name — for example, GDWC. The first step to every extension is to build its manifest file: a bootstrap which tells Chrome what kind of extension it is, which context it operates in, what it requires, what it does, and when it does it. Create a file called manifest.json with the following content:
{
"name": "GDWC",
"version": "0.1",
"description": "Word count statusbar for Google Docs!",
"background" : { "scripts": ["background.js"] },
"page_action" :
{
"default_icon" : "icon.png",
"default_title" : "GDWC statusbar is active"
},
"content_scripts": [
{
"matches": ["https://docs.google.com/document/*"],
"js": ["jq.js", "main.js"],
"run_at": "document_idle"
}
],
"icons": {
"48": "icon48.png",
"128": "icon128.png"
}
}
The first few values like name, version and description are self-explanatory. Then, there’s background which dictates which script to load in the background. Background pages and scripts serve a special purpose (the explanation of which does not fit into the scope of this article), but our extension needs it merely to activate the icon in the address bar. While we’re at it, create a background.js file and place this content into it:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
chrome.pageAction.show(sender.tab.id);
sendResponse({});
}
);
All this does is tell the extension: “show the icon when called”, nothing more.
Let’s continue looking at manifest.json. The existence of the page_action block tells Chrome this extension pertains to a specific page (or some pages), not the entire browser. An extension which is a Page Action can have an icon in the address bar of the browser (the default_icon value). In contrast, a Browser Action adds icons to the right of the address bar, outside it. An extension can either be one or the other, not both. Since we’ve touched on icons, let’s add some right now and get that (and the last manifest block, icons) out of the way. I made a couple of sample ones; the download link for them is at the end of this post. The two larger icons (icon48 and icon128) are for the Chrome Web Store, so your extension looks nice and pretty when others are viewing it there.
Now let’s look at the content_scripts block.
- matches is used to activate the extension on certain web pages. In our case, the goal is to activate it whenever a document in Google Docs is open, thus the URL https://docs.google.com/document/*
- js tells the extension which script to run when the “matches” part is triggered. In our case, we need jq.js (which is a minified version of jQuery) and our main.js script which contains the counting logic.
- run_at tells it when to load those scripts – in our case when the page loads and the document becomes idle.
Now that our bootstrapping is done, we should build the HTML we intend to add. Create a statusbar.html file, and paste in the following content:
<html>
<body>
<style type="text/css">
div#GDWC_statusBar {
width: 100%;
height: 18px;
background-color: #ebebeb;
border-top: 1px solid silver;
color: black;
position: fixed;
bottom: 0;
z-index: 25000;
text-align: right;
}
span.GDWC_statusBarCounter {
padding: 0 10px 0 10px;
line-height: 18px;
font-family: verdana;
font-size: 10pt;
}
span.GDWC_statusBarCounter a {
text-decoration: none;
color: black;
}
span.GDWC_statusBarSeparator {
border-left: 1px solid silver;
border-right: 1px solid silver;
margin: 0 1px;
display: inline-block;
width: 1px;
height: 10px;
}
</style>
<div id='GDWC_statusBar'>
<span class='GDWC_statusBarCounter'>
<a href='http://about.me/bruno.skvorc'>GDWC</a>
</span>
<span class='GDWC_statusBarSeparator'>
</span>
<span class='GDWC_statusBarCounter' id='GDWC_wordsTotal'>Warming up...</span>
</div>
</body>
</html>
As you can see, if you just open the file in a browser, it’s nothing more than a gray bar at the bottom of the screen with a GDWC link to sitepoint.com and the text “Warming up…”. This text will be replaced with the word count through JavaScript, and this is where our next file, main.js, comes in.
First, download the jq.js file into the extension folder (again, the link for this download appears at the end of the post), or head on over to jquery.com and grab a fresh one. Next, create the file main.js and paste in this content:
$.get(chrome.extension.getURL("statusbar.html"), {}, function(data) {$('body').append(data);}, 'html');
You might recognize this is an Ajax call to a local file. This line tells the extension to grab the content of the statusbar.html file and inject it right before the ending body tag in the document. Since the CSS in the .html file dictates the status bar position on the page (fixed, above everything and on the bottom), this basically finishes the status bar implementation from a visual perspective. You can give it a try now and see how it looks by going to Tools -> Extensions, and selecting “Load an unpacked extension”. Pick the GDWC folder, click “OK”, and refresh a new Google Document to see it in action.
Let’s add more code now. Append the following to main.js:
chrome.extension.sendRequest({}, function(response) {});
This line calls up the background.js script and tells it to show the icon. Go ahead and click “Reload” under the loaded extension in Tools -> Extensions, then refresh the tab you were testing it on. The icon should appear in the address bar.
Now we need to actually count the words, so append the following code:
$(document).ready(function(){
countWords();
});
function countWords() {
var number = 0;
$('span.kix-lineview-text-block').each(function(i, obj){
number += $(obj).text().split(/s+/).length;
});
$('span#GDWC_wordsTotal').text(number + ' total words');
timeout = setTimeout('countWords()', 5000);
}
As you can see, we’ve told the document to run the countWords function once the DOM is ready. I would have used keypress events to count the words on every change of document content, but Google has hijacked all default events (with event.preventDefault) and this is behavior which cannot be circumvented easily. I’ve thus opted for timeout, telling countWords() to basically re-run itself every 5 seconds (see the last line of the countWords() function).
We then initialize a local variable number and loop through kix-lineview-text-block spans. If you inspect the elements of the Google Docs site, you’ll notice these spans are actual lines in the document — every span has the width of the document, and every new line is contained within another kix-lineview-text-block span. We add the number of words (we count words using a regex split) from each span to the total number. You might wonder why we don’t just grab the text of the entire document and split that. Well, since every line of text is a new span, grabbing the total text() of the entire document would concatenate the lines. So if a line ended in “word” and the next line began with “count”, you would actually get “wordcount” which counts as a single word, thus offsetting the correct total number of words by the number of rows in the entire document.
Finally, we replace the “Warming up…” message with the actual word count.
If you reload the extension now, you’ll notice that you have now built a fully functional Chrome extension. To distribute it to others, simply zip the folder and send it around. To put it up on the Chrome Web Store (please don’t do that, though), consult the official guides on finishing touches.
Conclusion
As you can see, creating extensions for Chrome can be pretty simple. What we’ve built here is neither production-worthy (the looped count will slow down your workflow on very large files) nor optimized (it all could have been done without jQuery, reducing the filesize somewhat, and the count should be done page by page, recalculating only from the currently focused page onwards), but it’s a solid introduction into the components that make an extension tick. In the next part of the series, we’ll look at some of the necessary optimisations and add some more functionality.
The full source code and associated images can be downloaded from github.