Creating a Chrome Extension for Diigo, Part 2

Tweet
This entry is part 2 of 3 in the series Chrome Extension for Diigo

Chrome Extension for Diigo

In part 1, we introduced some new concepts, explained how we were going to build the extension and demonstrated the use of the Diigo API. In this part, we'll create most of our helper functions and deal with error handling.

Error handling

When the API returns a response, it's up to us to cover all edge cases and use it adequately. Relying on the request to succeed every time isn't an option – we need to account for not only the ready-state, but also potential failures.

In order to clean up the code somewhat and make background.js more concise, I compressed the Base64 object into a minified string. The background.js file as it is now looks like this. You can start from that one if you're following along with the code.

The xml.readyState === 4 part checks if the request is complete. Once it's complete, we're free to check for the status code. Only 200 means "success", all others mean something went wrong. Using the list of possible responses, we'll modify our code to produce a human readable description of the error that occurred.

var possibleErrors = {
    400: 'Bad Request: Some request parameters are invalid or the API rate limit is exceeded.',
    401: 'Not Authorized: Authentication credentials are missing or invalid.',
    403: 'Forbidden: The request has been refused because of the lack of proper permission.',
    404: 'Not Found: Either you\'re requesting an invalid URI or the resource in question doesn\'t exist (e.g. no such user).',
    500: 'Internal Server Error: Something is broken.',
    502: 'Bad Gateway: Diigo is down or being upgraded.',
    503: 'Service Unavailable: The Diigo servers are too busy to server your request. Please try again later.',
    other: 'Unknown error. Something went wrong.'
};

xml.onreadystatechange = function() {
    if (xml.readyState === 4) {
        if (xml.status === 200) {
            console.log(xml.responseText);
        } else {
            if (possibleErrors !== undefined) {
                console.error(xml.status + ' ' + possibleErrors);
            } else {
                console.error(possibleErrors.other);
            }
        }
    }
};

In the above code, we define a set of error messages and bind each message to a key corresponding to the status code. We then check if the code matches any of the predefined ones and log it in the console. If the request is successful, we output the responseText.

The above error handling is very basic, and not very end-user friendly. Options to improve it are: an alert box when an error occurs, graying out the extension's icon, deactivating the extension, and more. I'll leave that up to you.

We can also wrap the whole shebang into a function, just so it's neatly encapsulated and the global namespace isn't polluted:

var doRequest = function() {

    var xml = new XMLHttpRequest();
    xml.open('GET', url);
    xml.setRequestHeader('Authorization', auth);
    xml.send();

    xml.onreadystatechange = function() {
        if (xml.readyState === 4) {
            if (xml.status === 200) {
                console.log(xml.responseText);
            } else {
                if (possibleErrors !== undefined) {
                    console.error(xml.status + ' ' + possibleErrors);
                } else {
                    console.error(possibleErrors.other);
                }
            }
        }
    };
};

doRequest();

Popup

Now that we have our responseText, we can process it. We first need to turn it into a proper array, because it's useless to us in string form. Replace console.log(xml.responseText); with:

var response = JSON.parse(xml.responseText);
console.log(response);

The above should produce a JavaScript Array of JavaScript Objects when you look at the generated background page's JavaScript console.

I've made a test account called "testerguy" on Diigo, with some sample bookmarks. You should probably make your own to experiment with, since there's no telling what might be going on with this one by the time you're reading this article.

As mentioned in part 1, the structure of the bookmark folder will be: all "bbs-root" tagged bookmarks in the root of the folder, and all tags in subfolders in the "tags" folder. This is so the user can prioritize certain bookmarks by tagging them with "bbs-root" and make sure they appear outside their respective folders for fastest access.

In order to properly make the Bookmark Bar folder, we need to find out all the unique tags, create the root folder, create the subfolder "tags" and create subfolders for each tag we know of, in that order. To make testing for this easier, we'll add a popup to our extension with a Refresh button which repeats the XHR request. Update the manifest.json browser_action block like so:

"browser_action": {
        "default_icon": {
            "19": "icons/19.png",
            "38": "icons/38.png"
        },
        "default_title": "Diigo BBS",
        "default_popup": "popup/popup.html"
    },

and create a folder called popup in the root of your project. Create three more files in that folder: popup.html, popup.js and popup.css with the following content:

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
    <title>BBS popup</title>
    <script src="popup.js"></script>
    <link rel="stylesheet" type="text/css" href="popup.css">
    <link rel="icon" href="../icons/19.png">
</head>
<body>
<button id="refreshButton">Refresh</button>
</body>
</html>
// popup.js
var bg = chrome.extension.getBackgroundPage();

document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('refreshButton').addEventListener('click', function() {
        bg.doRequest();
    });
});
/* popup.css */
#refreshButton {
    margin: 10px;
}

The JS code here does the following: first we fetch the window object of the background.js script's autogenerated page. Popup scripts have direct access to background page code, as opposed to content scripts which have to pass messages. Then, we bind a click handler to the Refresh button's click event which calls our doRequest method from background.js.

If you reload the extension now and keep the generated background page open, you should see repeated outputs of fetched bookmarks as you click the refresh button.

We can now continue coding in background.js.

Processing the response array

We find all tags by iterating through all the fetched bookmarks, storing them in an array, and then removing duplicates. While we're iterating, we can check for all bookmarks containing the tag "bbs-root" and make a note of them in a separate variable. Let's add a process function:

var process = function(response) {
    var iLength = response.length;
    if (iLength) {
        console.info(iLength + " bookmarks were found.");
    } else {
        console.info("Response is empty - there are no bookmarks?");
    }
};

Also, in the function doRequest, let's replace

var response = JSON.parse(xml.responseText);
console.log(response);

with process(JSON.parse(xml.responseText));.

Reloading the extension will print out the number of found bookmarks for the selected user. Let's include a helper function to assist us with filtering out the duplicate tags from the tags array. This function extends the native JavaScript Array, so you can call it as if it had been built in all along. Put it under the Base64 part, near the top of the file:

/**
 * Removes duplicate elements from the array
 */
Array.prototype.unique = function () {
    var result = [];
    var len = this.length;
    while (len--) {
        if (result.indexOf(this[len]) == -1) {
            result.push(this[len]);
        }
    }
    this.length = 0;
    len = result.length;
    while (len--) {
        this.push(result[len]);
    }
};

Now, let's build out the process function.

var process = function(response) {
    var iLength = response.length;
    var allTags = [];
    var rootBookmarks = [];
    if (iLength) {
        console.info(iLength + " bookmarks were found.");
        var i = iLength;
        while (i--) {
            var item = response[i];
            if (item.tags !== undefined && item.tags != "") {
                var tags = item.tags.split(',');
                if (tags.indexOf('bbs-root') > -1) {
                    rootBookmarks.push(item);
                }
                allTags = allTags.concat(tags);
            }
        }
        allTags.unique();
        allTags.sort();
        console.log(allTags);
    } else {
        console.info("Response is empty - there are no bookmarks?");
    }
};

We iterate through all the bookmarks, if any are found, and for each one we turn their "tags" property into an array. This array then gets merged with the allTags array on which we call unique() to remove duplicates, and sorted alphabetically. In the process, we also watch out for bbs-root tagged bookmarks and copy their references to the rootBookmarks array.

We are now ready to manipulate the Bookmarks Bar.

Bookmarks Bar

First, we need to check if "Diigo #BBS" exists as a folder in the bookmarks bar. If not, we create it. Put the following code immediately under allTags.sort();:

var folderName = 'Diigo #BBS';
        chrome.bookmarks.getChildren("1", function(children) {
            var numChildren = children.length;
            var folderId;
            while (numChildren--) {
                if (children[numChildren].title == folderName) {
                    folderId = children[numChildren].id;
                    break;
                }
            }
            if (folderId === undefined) {
                chrome.bookmarks.create({
                    parentId: "1",
                    title: folderName
                }, function(folder) {
                    folderId = folder.id;
                    console.log(folderName + " not found and has been created at ID " + folder.id);
                });
            }

        });

We first get the children of the node with the ID 1, which is the bookmarks bar (you can see that by using getTree). We then iterate through them, and compare their titles to the desired name of our folder. If the folder is found, we save its ID and exit the loop. If it's never found, we create it and save the ID.

Now we need to find out if our folder contains the folder "Tags". Once we do that, we'll need to find out if our "Tags" folder contains a subfolder matching the name of every tag we found. Noticing a pattern here? Looks like we'll need a common function for checking if a bookmark folder contains another folder. We might as well make another helper method to check for actual bookmarks, too. Let's add the following functions to our background.js file (above the process function, for example):

chrome.bookmarks.getFirstChildByTitle = function (id, title, callback) {
    chrome.bookmarks.getChildren(id, function (children) {
        var iLength = children.length;
        while (iLength--) {
            var item = children[iLength];
            if (item.title == title) {
                return callback(item);
            }
        }
        return callback(false);
    });
};

chrome.bookmarks.getFirstChildByUrl = function (id, url, callback) {
    chrome.bookmarks.getChildren(id, function (children) {
        var iLength = children.length;
        while (iLength--) {
            var item = children[iLength];
            if (item.hasOwnProperty('url') && item.url == url) {
                return callback(item);
            }
        }
        return callback(false);
    });
};

These functions are almost identical, though each compares its own property to the provided value. We'll use one for folders, and the other for bookmarks. These methods are asynchronous just like the rest of the chrome.bookmarks namespace, so we'll need to provide callbacks whenever we use them.

You can also merge them into one single method and use a third parameter that tells the method which property we're looking for (title or url), thus respecting the DRY principle a bit more. I'll leave that up to you for now, and come back to it in a followup article that will be focusing on optimizations.

Let's rewrite our process method to use this now.

        chrome.bookmarks.getFirstChildByTitle("1", folderName, function(value) {
            if (value === false) {
                chrome.bookmarks.create({
                    parentId: "1",
                    title: folderName
                }, function (folder) {
                    console.log(folderName + " not found and has been created at ID " + folder.id);
                });
            }
        });

Much more concise, isn't it? When we consider further steps, it's clear we'll need to differentiate between a list of existing tags, and the list of tags we've freshly fetched from the server. For this purpose, we'll add two new helper methods to the native JavaScript Array object: intersect and diff. Let's put them at the top of the file, right where Array.unique() is, and while we're at it, let's move the getFirstChildByTitle and getFirstChildByUrl methods up there as well.

/**
 * Returns an array - the difference between the two provided arrays.
 * If the mirror parameter is undefined or true, returns only left-to-right difference.
 * Otherwise, returns a merge of left-to-right and right-to-left difference.
 * @param array {Array}
 * @param mirror
 * @returns {Array}
 */
Array.prototype.diff = function (array, mirror) {

    var current = this;
    mirror = (mirror === undefined);

    var a = current.filter(function (n) {
        return array.indexOf(n) == -1
    });
    if (mirror) {
        return a.concat(array.filter(function (n) {
            return current.indexOf(n) == -1
        }));
    }
    return a;
};

/**
 * Returns an array of common elements from both arrays
 * @param array
 * @returns {Array}
 */
Array.prototype.intersect = function (array) {
    return this.filter(function (n) {
        return array.indexOf(n) != -1
    });
};

Finally, let's add a helper method for console logging in the same place at the top of the background.js file:

const CONSOLE_LOGGING = true;
function clog(val) {
    if (CONSOLE_LOGGING) {
        console.log(val);
    }
}

You can now replace all your console.log() calls in the code with clog. When you need to turn the logging off, simply switch the CONSOLE_LOGGING constant to false and all output will stop. This is great when moving from development to production – it introduces a very small overhead, but cuts down on preparation time in that you don't need to manually hunt down and comment or remove all your console outputs.

Conclusion of Part 2

In this part, we built several helper functions essential for further work, and added some basic error handling logic. In the next installment of this series, we build the body of the extension. Stay tuned!

Chrome Extension for Diigo

<< Creating a Chrome Extension for Diigo, Part 1Creating a Chrome Extension for Diigo, Part 3 >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments