12 Mar, 2014

I have a web app running on my home server to keep track of my bookmarks—it's a little like Delicious, but simpler and with some personal customisations. Currently I save bookmarks to this app via a Javascript bookmarklet: clicking it gets the current page's title and url (and also any selected text, to use as a summary) and sends it to a popup form; submitting that form then saves the bookmark data to the server.

Although this system works well enough, it looks a bit untidy and takes up space in the bookmarks bar. With the advent of Extensions for Chrome, I thought I'd have a go at writing an extension to nicely integrate my custom page bookmarking button into the Chrome browser.

Screen Shot

It's clear from the start that Chrome's extension structure is a lot simpler than that of Firefox extensions. Chrome extensions are just a collection of plain HTML and JavaScript files—no odd folder hierarchies or XUL to deal with here. There are advantages to Mozilla's approach (ease of internationalisation, UI consistency), but I can't help feeling that building Chrome extensions will be much more accessible to amateur developers; I'm betting that this is exactly what Google was aiming for.

So let's get stuck in! First create a new folder for your extension code—it doesn't matter where for now. My basic Chrome extension consists of just a few files:

manifest.json

This is the glue that holds our extension together. It contains the basic meta data about the extension (title, description etc), as well as acting as a pointer to the various files that contain the extension's user interface and JavaScript code. It also defines permissions that specify which browser components and external URLs the extension is allowed to access. The manifest for our extension looks like this:

{
    "manifest_version": 2,
    "name": "Bookmark Extension Example",
    "description": "POST details of the current page to a remote endpoint.",
    "version": "0.1",
    "background": {
        "scripts": ["background.js"],
        "persistent": true
    },
    "browser_action": {
        "default_icon": "icon.png",
        "default_popup": "popup.html"
    },
    "permissions": [
        "tabs", 
        "http://*/", 
        "https://*/"
    ]
}

The background property points to a JavaScript file which contains the logic code for the extension. The browser_action section defines a button with an icon, which the user will click to open the bookmarking dialog, and the popup property points to the file containing the dialog form HTML.

popup.html

This file contains our UI: a basic HTML form with title, url, summary and tag fields (so that we can edit and tag our bookmark before saving it).

<html>
    <head>
        <style>
            body { 
                min-width: 420px; 
                overflow-x: hidden; 
                font-family: Arial, sans-serif; 
                font-size: 12px; 
            }
            input, textarea { 
                width: 420px; 
            }
            input#save { 
                font-weight: bold; width: auto; 
            }
        </style>
        <script src="popup.js"></script>
    </head>
    <body>
        <form id="addbookmark">
            <p><label for="title">Title</label><br />
            <input type="text" id="title" name="title" size="50" value="" /></p>
            <p><label for="url">Url</label><br />
            <input type="text" id="url" name="url" size="50" value="" /></p>
            <p><label for="summary">Summary</label><br />
            <textarea id="summary" name="summary" rows="6" cols="35"></textarea></p>
            <p><label for="tags">Tags</label><br />
            <input type="text" id="tags" name="tags" size="50" value="" /></p>
            <p>
                <input id="save" type="submit" value="Save Bookmark" />
                <span id="status-display"></span>
            </p>
        </form>
    </body>
</html>

popup.js

This file contains JavaScript code to populate and save field values. You can download the complete source here, but for now the important part is the script itself:

// This callback function is called when the content script has been 
// injected and returned its results
function onPageInfo(o)  { 
    document.getElementById('title').value = o.title; 
    document.getElementById('url').value = o.url; 
    document.getElementById('summary').innerText = o.summary; 
}

// Global reference to the status display SPAN
var statusDisplay = null;

// POST the data to the server using XMLHttpRequest
function addBookmark() {
    // Cancel the form submit
    event.preventDefault();

    // The URL to POST our data to
    var postUrl = 'http://post-test.markb.com';

    // Set up an asynchronous AJAX POST request
    var xhr = new XMLHttpRequest();
    xhr.open('POST', postUrl, true);

    // Prepare the data to be POSTed
    var title = encodeURIComponent(document.getElementById('title').value);
    var url = encodeURIComponent(document.getElementById('url').value);
    var summary = encodeURIComponent(document.getElementById('summary').value);
    var tags = encodeURIComponent(document.getElementById('tags').value);

    var params = 'title=' + title + 
                 '&url=' + url + 
                 '&summary=' + summary +
                 '&tags=' + tags;

    // Replace any instances of the URLEncoded space char with +
    params = params.replace(/%20/g, '+');

    // Set correct header for form data 
    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

    // Handle request state change events
    xhr.onreadystatechange = function() { 
        // If the request completed
        if (xhr.readyState == 4) {
            statusDisplay.innerHTML = '';
            if (xhr.status == 200) {
                // If it was a success, close the popup after a short delay
                statusDisplay.innerHTML = 'Saved!';
                window.setTimeout(window.close, 1000);
            } else {
                // Show what went wrong
                statusDisplay.innerHTML = 'Error saving: ' + xhr.statusText;
            }
        }
    };

    // Send the request and set status
    xhr.send(params);
    statusDisplay.innerHTML = 'Saving...';
}

// When the popup HTML has loaded
window.addEventListener('load', function(evt) {
    // Bind our addBookmark function to the form submit event
    document.getElementById('addbookmark').addEventListener('submit', addBookmark);
    // Cache a reference to the status display SPAN
    statusDisplay = document.getElementById('status-display');
    // Call the getPageInfo function in the background page, injecting
    // content_script.js into the current HTML page and passing in our 
    // onPageInfo function as the callback
    chrome.extension.getBackgroundPage().getPageInfo(onPageInfo);
});

This may look a little confusing at first, but hopefully it will make more sense when you see the rest!

background.js

Think of this file as the negotiator between the popup dialog and the content/DOM of the currently loaded web page. getPageInfo is the function we called when our popup loaded, and its parameter is the callback function which sets the values of the form fields in popup.js.

// Array to hold callback functions
var callbacks = [];

// This function is called onload in the popup code
function getPageInfo(callback) { 
    // Add the callback to the queue
    callbacks.push(callback); 
    // Inject the content script into the current page 
    chrome.tabs.executeScript(null, { file: 'content_script.js' }); 
};

// Perform the callback when a request is received from the content script
chrome.extension.onMessage.addListener(function(request)  { 
    // Get the first callback in the callbacks array
    // and remove it from the array
    var callback = callbacks.shift();
    // Call the callback function
    callback(request); 
});

When getPageInfo is called, it pushes the callback function onto a queue and then injects the content script (below) into the DOM of the current web page.

content_script.js

The content script itself is pretty simple: it just gets the title, url and any selected text from the current page and fires them back to the background script.

// Object to hold information about the current page
var pageInfo = {
    "title": document.title,
    "url": window.location.href,
    "summary": window.getSelection().toString()
};

// Send the information back to the extension
chrome.extension.sendRequest(pageInfo);

The background page listener then gets the callback function from the queue (which, if you remember, is the onPageInfo function from the popup page) and calls it, passing in the information about the page so that it can populate the form field values.

To test your extension, open the Chrome Extensions tab (Tools > Extensions), check 'Developer Mode' and click 'Load unpacked extension...'. Browse to your extension's folder and select it: you'll see the icon appear in your browser toolbar. Click it while viewing any normal web page and you should see a popup like the one in the screen shot at the beginning of this article, populated with the data from the current page.

You can download all the source code here and modify it to suit your own purposes, or just use it to learn from.

That's it! I'll explain more about Chrome extensions in future posts, but in the meantime, the Google extension documentation is comprehensive and very useful to learn from. I also picked up a lot of good information from this thread on the Chromium Extensions Google Group.

Originally published on 26 Jan, 2010; updated on 12 Mar, 2014.

comments powered by Disqus