Amateur Topologist

Everything but topology.

Tag: javascript

Settings in Chrome Extension Content Scripts

Google Chrome, like most modern browsers, allows you to write extensions for it. And those extensions can inject JavaScript into pages that the end user views, so that you can write extensions to do things such as customize the interface of some site, theme it, etc. Simple extensions that don’t need to be configured by the user can get away with being simple Greasemonkey-like scripts that the user installs by navigating to and then forgets. The upside of this is that it’ll also work in Firefox, since Chrome’s native content script support is a strict subset of Greasemonkey’s (it doesn’t have @require or the ability to set/get data, among a few other things). The downside is that there’s no way to do any kind of updating short of your users manually installing a new version, and, more importantly, your extension cannot have any state or user configuration. In order to have stateful extensions, you have to go full-blown Chrome.

The basics of Chrome extension development are adequately covered by Google’s own Getting Started tutorial. The interesting part comes when you want your content scripts to be configurable by the user. For example, Tumblr Hate (which I’m the author of, and which has source that you can view here) adds ‘hate’ links to tumblr posts that allows the user to hide them. The user can configure the text of the ‘hate’ links, as well as what shows up when a post is ‘hated’ (i.e., hidden). Fortunately, Chrome has built-in support for Extension options pages, which have a localStorage object that you can use as a key/value store (although values are serialized to strings). Unfortunately, the localStorage object is not shared between your options page and your content scripts. I assume the reason for this is because of sandboxing, but it creates a problem: how do your extension’s content scripts get the settings?

Use chrome.extensions.sendRequest

You have your options page do whatever it does, saving settings to localStorage. You then create a ‘background’ page that installs a listener using chrome.extension.onEvent.addListener; the exact form of a request and how it should be responded to is obviously up to you, but here’s what I use: A request looks something like  {type: "get", name: ["settingName", "otherSettingName"]}, which is responded to with an object that looks something like {settingName: 1, otherSettingName: true}, or {type: "set", name: "settingName", value: "5"}, which sets the setting settingName to 5. You can even special-case get requests so that if the name is a single string, the response is just the value and not an object. If your extension does things other than inject content scripts, you’ll need other request types to do things like open tabs or whatever, but this is good enough for simply mimicing a key/value store. The code to do this is simple:

function(request, sender, sendResponse) {
    switch (request.type) {
    case "get":
        var data = {};
        if (request.name.constructor == Array) {
            for (var i = 0; i < request.name.length; i++) {
                data[request.name[i]] = localStorage[request.name[i]];
            sendResponse(data);
        } else {
            sendResponse(localStorage[request.name]);
        break;
    case "set":
        localStorage[request.name] = request.value
        break;
    default:
        console.log("invalid request " + request);
        break;
    }
}

The next issue, however, is trickier: chrome.extension.sendRequest is asynchronous. In particular, instead of returning the response, it accepts a callback that takes the response as an argument, and returns nothing useful. So what do you do?

Do your work in the callback

If you just use the callback to set some globals and then continue on, you have a race condition. Consider the following code:

var setting;
chrome.extension.sendRequest({type: "get", name: "settingName"}, function (resp) {
    setting = resp;
}
);
console.log(setting);

This will fail, because sendRequest is asynchronous, and sending a request and receiving the response very well might happen after the setting has been used! The correct thing to do here is this:

var setting;
chrome.extension.sendRequest({type: "get", name: "settingName"}, function (resp) {
    setting = resp;
    doStuff();
}
);

function doStuff() {
    console.log(setting);
}

Here, you can guarantee that doStuff won’t be called unless the setting has been properly set, so you can safely rely on its value.

Note that I didn’t pass setting as an argument to doStuff; I very well could have. But in the case of Tumblr Hate, for instance, I didn’t feel like threading the relevant variables through function calls, so I simply declared the variables at the top so they’d be global. (Well, no I didn’t, I set the response’s variables as properties of a global ‘root’ object, but that’s basically the same thing.)

URL bar marquees!

Remember the heady days of the late 1990s, when every Geocities webpage would have a script that displayed a marquee in the status bar? Well, thanks to the magic of HTML5, you can revisit them with a twist: instead of marqueeing in the status bar, you can do it in the URL bar without the annoyance of having your history be clogged with a bunch of shit. How does it work? If you’ve ever browsed someone’s YouTube channel, you’ve probably noticed that the URL that you get in the address bar changes each time you click a video. But the page doesn’t refresh; this is because the part that’s changing is actually the anchor element, which comes after the #. This has the downside of potentially interacting poorly with your browser history or other things. HTML5 instead provides pushState and replaceState, which basically allow you to rewrite the displayed URL to any page on the same domain without actually forcing a page reload or whatever. They also allow you to carry around titles for the various states (different from the <title> part of an HTML document) and actual state data. Flickr uses this to great effect; if you go to, say, this photo and click the ‘This photo also appears in’ tabs, you’ll see the URL being rewritten without a page load. Note that as of now this only works in Firefox 4, Safari, and Chrome.

But that’s not what this post is about. It’s about marquees! If you use javascript, you can create a marquee in the URL bar. It has the disadvantage of everything being URL-encoded, so no fancy Unicode characters, or even spaces. It also means that unless your webserver is set up right, hitting refresh will take the user to a non-existant page. It’ll also annoy the ever-loving fuck out of your users since they won’t be able to type in the address bar before your code rewrites it. But who needs that anyway when you have glorious scrolling text?