Creating My First Google Chrome Extension - Part 2

PART 1 | PART 2 | PART 3

I started writing my first browser extension a couple weeks ago, and though my spare time has been pretty limited I've made some (never as much I'd like) progress.

I left off last time with (finally!) figuring out how to authenticate to the Pinboard API. As with so many things, once I knew the answer I couldn't believe I didn't figure it out sooner. There are a number of reasons it might fail, but in my case I needed to add the API URL to the list of permissions in the manifest.json file.

Here's what I learned this week...

AddEventListener is preferred over inline event handlers

Whenever I dabbled in javascript in the past (it's been awhile, and wasn't much...) I had always attached events inline like this:

<button id="create_button" class="btn button_create_folder btn-sm"  
        onclick="jstree_node_create();">
    <i class="glyphicon glyphicon-asterisk"></i>
    Create
</button>  

Chrome discourages, if not prohibits, online event handlers in its extensions as part of its content security policy:

Inline JavaScript will not be executed. This restriction bans both inline <script> blocks and inline event handlers (e.g. <button onclick="...">).

The inline event handler definitions must be rewritten in terms of addEventListener and extracted into [your script file].

When calling jstree_node_create() in the manner above, Chrome throws an error:

Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

There's a more flexible, more recommended way of doing the same thing, and it's the addEventListener function.

window.addEventListener('load', function load(event){  
    var createButton = document.getElementById('create_button');
    createButton.addEventListener('click', function() { jstree_node_create(); });
}

This code lives in your Javascript file, keeping your html markup cleaner as well as allowing for attaching multiple functions to the same event.

You can read more about the differences between this method and the older methods here.

(As an aside, I nested these listeners so I wouldn't try getting the create_button control before it was created. Not sure if that's necessary...)

jsTree is great, unless you like reinventing wheels

I figured on using a tree control to allow users to create the bookmark structure they desired (the left pane, shown below). After a quick search, I found jsTree, which works great out-of-the-box and is also pretty easy to customize. The documentation on the site, as well as the extensive comments in the script files, is extremely helpful.

first-extension-jstree

There's an ongoing argument in the development community about whether comments are good, necessary or just plain evil. It's about as boring as the tabs vs spaces argument...

Anyway, I think the developers of jsTree struck a good balance here, not commenting on tons of individual lines but making it very clear how each function should be used. Some functions even have sample usages.

Learn more about jsTree, and then browse the source code yourself if you're interested.

GitHub Issues are good for conversation, not so much for simple tasks

I've been using GitHub's "Issues" to track features I want to add, or when I think of some problem I want to address. I used them quite a bit during Hacktoberfest, and it lent itself well to discussions with other developers and linking commits and pull requests with conversations.

But for a single developer, where there are no conversations (or maybe there are, who am I to judge?) or pull requests, it's a bit heavy-handed.

I'm a big fan of Trello, which allows for quickly creating and deleting small tasks. I think on my next project I would just use that, and leave the "Issues" feature for when someone using the project finds a bug or has a topic to discuss. If you haven't used Trello before, check it out. It's free, with some additional features available for purchase.

Viewport units are great for vertical sizing

This has to do with sizing an element vertically so that it takes up a certain portion (or all) of the browser window. Traditionally, this has been somewhat tricky. The last time I tried to get something to take up 100% of the screen height, I remember messing with getting the size of the window, and handling resizing, and I don't know what else. It wasn't very straight-forward, unlike width which seems to just work.

Apparently it's somewhat easier now with a new size unit called viewport units, which are pretty much supported everywhere. Here's how I implemented it on my tag tree (pictured above).

#tagTree {
     height: 100vh;
     width: 300px;
     overflow: auto;
     position: fixed;
}

Storage in Chrome - to sync or not to sync?

Storage is necessary for many extensions, and I happen to be using it for a few things.

There are also two types of storage - stuff that sync's with your Google account and stuff that's local to your machine. The major difference between the two, at least for my concerns, is the size limit.

  • Sync is limited to 100KB total, with chunks no bigger than 8KB.
  • Local is limited to 5MB total.

I happen to be using both. I want the selected tags to be in sync, so that someone could use it to generate bookmarks on multiple machines without having to recreate it on both machines. That'd just be annoying.

But for storing the Pinboard API Token (so it doesn't have to be re-entered) or caching large amounts of data like URLs (because Pinboard limits to one request every 5 minutes), local storage is fine. I have concerns about whether it'll be enough if someone has 20000+ URLs bookmarked with Pinboard, but that's another discussion...

You can read the documentation on using Chrome storage, and check out one of the ways I'm using it below. The first snippet shows data being stored as JSON, while the second snippet shows it being retrieved and applied to the control which expects JSON.

// Storing the tag tree in 'sync' storage under the 'selected_tags' key

var tagTree = $('#tagTree').jstree(true).get_json('#');  
chrome.storage.sync.set({'selected_tags': tagTree}, function() {  
    BookmarkHelper.getAllPostsAndGenerateBookmarks();
});

// Loading it when the options page is refreshed

static loadSelectedTagsFromStorage() {  
    chrome.storage.sync.get('selected_tags', function(result) {
        if (result != undefined &amp;&amp; result.selected_tags != undefined) {
            var data = result.selected_tags;
        } else {
            var data = [ { "id" : ROOT_NODE_ID, "text" : "Pinboard", "icon" : "images/root.gif" } ];
        }

        $('#tagTree').jstree({
            'core' : {
                'animation' : 100,
                'themes' : { 'stripes' : false },
                'multiple' : false,
                'check_callback' : true,
                'data' : data,
            },

Things I'm still investigating...

Extending the jsTree plugin

For example, here's how its "Sort" feature works. Notice the commented-out part?

/**
 * ### Sort plugin
 *
 * Automatically sorts all siblings in the tree according to a sorting function.
 */
    /**
     * the settings function used to sort the nodes.
     * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`.
     * @name $.jstree.defaults.sort
     * @plugin sort
     */
    $.jstree.defaults.sort = function (a, b) {
        // return this.get_type(a) === this.get_type(b)
                //     ? (this.get_text(a) &gt; this.get_text(b) ? 1 : -1)
                //     : this.get_type(a) &gt;= this.get_type(b);
        return this.get_text(a) &gt; this.get_text(b) ? 1 : -1;
    };

By default, it only looks at the name when it sorts, which resulted in folders and files being intermingled. What I needed was for it to look at type as well as name, and the code was already there to do it... sort of.

I uncommented the other lines that take type into account, but once a node had about 10 items in it sorting got seriously messed up. I have no idea why it happens at 10 items, but it was because the last commented out line above isn't returning an integer. Here's the fix. Notice how the last line returns an integer instead of a boolean.

return this.get_type(a) === this.get_type(b)  
    ? (this.get_text(a).toLowerCase() &gt; this.get_text(b).toLowerCase() ? 1 : -1)
    : (this.get_type(a) &gt;= this.get_type(b) ? 1 : -1);
// return this.get_text(a) &gt; this.get_text(b) ? 1 : -1;

After that, sorting works great. On the left, only names are being considered. On the right, folders (type "default") are sorted before bookmarks (type "file").

first-extension-compare-tree-sort

The problem is this. I changed the code in the jsTree.js file itself, so I'll lose my changes if I overwrite it later with a newer copy.

There's probably a way to extend the jsTree instead of modifying the source code, but I need to read through the docs more.

Organizing by namespaces (are something like them)

I previously had everything as a function in one large options.js file, but that got kinda unruly feeling.

When I split it out, I was looking for a way to add functions to classes to organize things better. I'm used to C#, where there are clear lines on what's public vs private, and how things are organized into classes and namespaces. AFAIK, every function in JavaScript is just public. Ugh.

Here's what I've come up with for now, but I'm not sure it's the best way to do this.

class EventRegistration {  
    static registerCheckApiTokenButton() {
        var verifyApiToken = document.getElementById('verifyApiToken');
        verifyApiToken.addEventListener('click', function() {
            var apiToken = document.getElementById('apiToken');
            TokenValidator.verifyApiTokenAndLoadTags(apiToken.value);
        });
    }

    static registerGenerateBookmarksButton() {
        var generateBookmarks = document.getElementById('generateBookmarks');
        generateBookmarks.addEventListener('click', function() {
            chrome.storage.sync.set({'selected_tags': $('#tagTree').jstree(true).get_json('#')}, function() {
                BookmarkHelper.getAllPostsAndGenerateBookmarks();
            });
        });
    }
}

This forces me to call EventRegistration.registerCheckApiTokenButton from other classes, my attempt to bring some order to the chaos.

What's left?

Getting pretty close to getting this thing done. That is, until someone else uses it and starts poking holes in it!

I've got to generate the actual bookmarks which will involve some recursion. I've also got to handle a few edge cases, and I'd like to write some documentation on issues I foresee and what to do about them.

Oh, and somewhat ironically, I mused in my last post about what if Google got hacked. Then it happened... sort of. It seems to mostly affect people who downloaded sketchy/unproven apps from third-party app stores, possibly compounded by phones that did not have up-to-date software patches. Still, how much have you got tied to your single Google account?! Ugh!

Keep that Android option that prevents you from installing apps outside of the official app store selected unless you really, really know what you're doing!

Subscribe to Weekly Updates!

Get an email with the latest posts, once per week...
* indicates required