Creating a Chrome Extension for Diigo, Part 2

Share this article

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!

Frequently Asked Questions (FAQs) about Creating a Chrome Extension with Diigo

How do I install the Diigo Chrome extension?

To install the Diigo Chrome extension, first, open your Google Chrome browser. Then, navigate to the Chrome Web Store and search for ‘Diigo Web Collector’. Click on the ‘Add to Chrome’ button next to the extension. A pop-up window will appear asking for confirmation. Click ‘Add extension’ to complete the installation. Once installed, the Diigo icon will appear in your browser’s toolbar.

What features does the Diigo Chrome extension offer?

The Diigo Chrome extension offers a variety of features to enhance your browsing experience. These include the ability to highlight text on web pages, add sticky notes, save pages for offline reading, and bookmark pages for easy access later. You can also organize your bookmarks into lists or tags, and share them with others.

How do I use the Diigo Chrome extension to highlight text?

To highlight text using the Diigo Chrome extension, first, select the text you want to highlight. Then, right-click and choose ‘Diigo: Highlight’ from the context menu. The selected text will be highlighted in yellow. You can also change the highlight color by clicking on the Diigo icon in your toolbar and selecting ‘Highlight’ > ‘Color’.

How do I add a sticky note using the Diigo Chrome extension?

To add a sticky note, first, select the text where you want to add the note. Right-click and choose ‘Diigo: Add Sticky Note’. A pop-up window will appear where you can type your note. Click ‘Post’ to save the note. The note will appear as a small icon next to the selected text. Hover over the icon to view the note.

How do I save a page for offline reading using the Diigo Chrome extension?

To save a page for offline reading, click on the Diigo icon in your toolbar and select ‘Read Later’. The page will be saved to your Diigo library and can be accessed even when you’re offline.

How do I bookmark a page using the Diigo Chrome extension?

To bookmark a page, click on the Diigo icon in your toolbar and select ‘Bookmark’. A pop-up window will appear where you can add a description, tags, and choose a list to add the bookmark to. Click ‘Save’ to save the bookmark.

How do I organize my bookmarks using the Diigo Chrome extension?

You can organize your bookmarks into lists or tags. To add a bookmark to a list, click on the Diigo icon in your toolbar and select ‘Bookmark’. In the pop-up window, choose a list from the ‘Add to List’ dropdown menu. To add a tag, type the tag into the ‘Tags’ field.

How do I share my bookmarks using the Diigo Chrome extension?

To share a bookmark, click on the Diigo icon in your toolbar and select ‘Bookmark’. In the pop-up window, click on the ‘Share’ tab. You can choose to share the bookmark via email, or copy the link to share it in other ways.

How do I create a hotkey to open Diigo in Chrome?

To create a hotkey, navigate to ‘chrome://extensions/shortcuts’ in your Chrome browser. Find the Diigo extension and click on the ‘Keyboard shortcuts’ link. In the pop-up window, you can set a hotkey to open Diigo.

How do I update the Diigo Chrome extension?

To update the Diigo Chrome extension, navigate to ‘chrome://extensions’ in your Chrome browser. Enable ‘Developer mode’ by clicking the toggle switch at the top right. Then, click on the ‘Update’ button. If an update is available, it will be downloaded and installed automatically.

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week