In part 1, we introduced some new concepts and built a skeleton version of our extension, ready for installation and testing. Part 2 then took us through some helper methods and error handling, as well as parsing the result we got from Diigo and filtering out unique tags.
In Part 3 of this series, we’ll write the body of our extension using everything we’ve done so far.
Preparation
I cleaned up the background.js
file we made in the previous parts so go ahead and grab its content from Github. It’s essentially identical, just reformatted and restructured slightly.
Listeners for bookmark events
The first thing we’ll do is add some listeners for bookmark events. Specifically, when a bookmark creation, change or deletion occurs, we want Diigo to know about it.
chrome.bookmarks.onCreated.addListener(function (id, node) {
chrome.bookmarks.get(node.parentId, function (parent) {
if (parent !== false) {
chrome.bookmarks.get(parent[0].parentId, function (grandparent) {
/** @namespace grandparent.title */
if (grandparent[0] !== false && grandparent[0].title == "Tags") {
// Bookmark was created in proper location, send to Diigo
doRequest(node, parent[0].title);
}
});
}
});
});
chrome.bookmarks.onRemoved.addListener(function (id, removeInfo) {
// To be added when API supports it
});
chrome.bookmarks.onChanged.addListener(function (id, changeInfo) {
// To be added when API supports it
});
The bottom two listeners are just placeholders, because Diigo doesn’t support this functionality yet. I’m told their API is soon to be upgraded, though, so we’re putting them there nonetheless.
The onCreated
listener first checks if the created bookmark node has a parent. If it does, then it checks the name of that parent’s parent – and if that name is “Tags” we know we’ve got the right folder and we need to submit to Diigo. Now, this function assumes you have no other double-parent bookmark with “Tags” as the grandparent but, theoretically, it could happen. To check against that, we would need to add yet another parent level check for the Diigo master folder, but I’ll leave that up to you for homework.
We then call doRequest
with two parameters: the actual bookmark node that was created, and the name of the tag folder it was created in. Obviously, we need this data to tell Diigo which bookmark to create and which tag to give it. But why doRequest
? Isn’t that our “GET” function? Yep – but as you’ll see in a moment, we’ll modify it so that it can handle both the POST and GET action of our extension.
What we need to do next is add these params to our doRequest
function, and have it react to their presence or absence, like so:
var doRequest = function (bookmarknode, tag) {
var xml = new XMLHttpRequest();
if (bookmarknode !== undefined) {
if (tag === undefined) {
console.error("Tag not passed in. Unaware of where to store bookmark in Diigo. Nothing done.");
} else {
// Bookmark node was passed in. We're doing a POST for update, create or delete
// Currently only create is supported
var uriPart = encodeURI("url=" + bookmarknode.url + "&title=" + bookmarknode.title + "&tags=" + tag);
xml.open('POST', rootUrl + uriPart);
xml.setRequestHeader('Authorization', auth);
xml.send();
xml.onreadystatechange = function () {
if (xml.readyState === 4) {
if (xml.status === 200) {
clog("Successfully created new bookmark in Diigo");
} else {
if (possibleErrors
!== undefined) {
console.error(xml.status + ' ' + possibleErrors
);
} else {
console.error(possibleErrors.other);
}
}
}
};
}
} else {
xml.open('GET', rootUrl + "&count=100&filter=all&user="+user);
xml.setRequestHeader('Authorization', auth);
xml.send();
xml.onreadystatechange = function () {
if (xml.readyState === 4) {
if (xml.status === 200) {
process(JSON.parse(xml.responseText));
} else {
if (possibleErrors
!== undefined) {
console.error(xml.status + ' ' + possibleErrors
);
} else {
console.error(possibleErrors.other);
console.error(xml.status);
}
}
}
};
}
};
If the bookmarknode
and tag
params are provided and valid, we execute the XHR request almost the same way as we did the original one for GETting the bookmarks, with one crucial difference – this time, we make it a POST request and add the title, tag, and bookmark name into the URL. That’s all that’s needed – now Diigo can accept our POST request and react accordingly. This is the beauty of RESTful API design.
Root bookmarks
Now let’s save all the BBS-root bookmarks. We already have them in an array from the initial looping in the process
function, but we don’t do anything with them. Let’s change that.
In Part 2, we made sure the “Diigo #BBS” folder exists. Once we’re certain that it does, we can initiate the creation of the root bookmarks – they have a home we can put them in at that point.
Rewrite the part of the process
function from this:
var folderName = 'Diigo #BBS';
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);
});
}
});
to
var folderName = 'Diigo #BBS';
chrome.bookmarks.getFirstChildByTitle("1", folderName, function(value) {
if (value === false) {
chrome.bookmarks.create({
parentId: "1",
title: folderName
}, function (folder) {
clog(folderName + " not found and has been created at ID " + folder.id);
processTagsFolder(folder, allTags);
});
} else {
processTagsFolder(value, allTags);
}
});
As you can see, we added a new call to a processTagsFolder
function. This function gets the “Diigo #BBS” folder passed in as the first parameter, and the array of all tags as the second. Seeing as this method executes either way – whether the “Diigo #BBS” folder pre-existed or not, we can put our root-bookmark-creation logic inside it.
/**
* Creates the Tags master folder if it doesn't exist
* Initiates the check for tag subfolders
* Creates ROOT bookmarks
* @param rootNode
* @param tagsArray
*/
function processTagsFolder(rootNode, tagsArray) {
// Get all current root bookmarks, if any
chrome.bookmarks.getChildren(rootNode.id, function (currentRoots) {
var crl = currentRoots.length;
var ignoredUrls = [];
var rootNumOrig = rootBookmarks.length;
if (crl) {
var bAmongThem = false;
var rootNum = rootNumOrig;
// Iterate through all the current items in the root folder
while (crl--) {
// Check if current item is a URL bookmark, not a folder
if (currentRoots[crl].hasOwnProperty('url')) {
// Iterate through downloaded bookmarks to see if it's among them
bAmongThem = false;
while (rootNum--) {
if (rootBookmarks[rootNum].url == currentRoots[crl].url) {
// Found among existing!
bAmongThem = true;
if (rootBookmarks[rootNum].title != currentRoots[crl].title) {
// Does title need updating?
chrome.bookmarks.update(currentRoots[crl].id, {
title: rootBookmarks[rootNum].title
});
}
// Ignore this URL when later adding the downloaded root bookmarks
ignoredUrls.push(rootBookmarks[rootNum].url);
break;
}
}
if (!bAmongThem) {
// Does not exist in downloaded - needs to be deleted from browser
chrome.bookmarks.remove(currentRoots[crl].id);
}
}
}
}
// At this point, we know we removed all the bookmarks that are no longer in our Diigo account
// Now let's add those that are left
while (rootNumOrig--) {
if (ignoredUrls.indexOf(rootBookmarks[rootNumOrig].url) === -1) {
chrome.bookmarks.create({
url: rootBookmarks[rootNumOrig].url,
title: rootBookmarks[rootNumOrig].title,
parentId: rootNode.id
});
}
}
});
}
In short, what we do here is fetch all the current root bookmarks, see if they’re among the freshly downloaded ones and delete them if they’re not (that means they’ve been untagged as bbs-root in Diigo), and finally, we add all the others. If you try it out, this should work wonderfully.
We also need to create the Tags folder if it doesn’t exist. Add the following code right under the last bit:
chrome.bookmarks.getFirstChildByTitle(rootNode.id, 'Tags', function (tagsFolder) {
if (tagsFolder === false) {
chrome.bookmarks.create({
parentId: rootNode.id,
title: "Tags"
}, function (folder) {
processTags(folder, tagsArray);
});
} else {
processTags(tagsFolder, tagsArray);
}
});
Obviously, we’ve made another function that gets called regardless of whether or not the Tags folder pre-existed. Let’s define processTags
.
Processing Tags
/**
* Creates all non-existent tag subfolders.
* Removes all tag subfolders that do not have any bookmarks.
* @param tagsFolder
* @param tagsArray
*/
function processTags(tagsFolder, tagsArray) {
// Remove all unused tag subfolders
chrome.bookmarks.getChildren(tagsFolder.id, function (currentTagSubfolders) {
var numCurrentTags = currentTagSubfolders.length;
if (numCurrentTags > 0) {
var currentTags = [];
var currentTagsIds = {};
var cTag;
while (numCurrentTags--) {
cTag = currentTagSubfolders[numCurrentTags];
currentTags.push(cTag.title);
currentTagsIds[cTag.title] = cTag.id;
}
var diff = currentTags.diff(allTags, false);
var numUnused = diff.length;
if (numUnused) {
while (numUnused--) {
chrome.bookmarks.removeTree(currentTagsIds
]);
}
}
}
});
// Create necessary tag subfolders
var numTags = tagsArray.length;
while (numTags--) {
let title = tagsArray[numTags];
chrome.bookmarks.getFirstChildByTitle(tagsFolder.id, title, function (tagFolder) {
if (tagFolder === false) {
// Needs to be created
chrome.bookmarks.create({
parentId: tagsFolder.id,
title: title
}, function (folder) {
addAllBookmarksWithTag(folder);
});
} else {
addAllBookmarksWithTag(tagFolder);
}
});
}
}
The above function filters out the difference between the AllTags
array (the list of tags we fetched fresh from Diigo and made unique) and the tag sub folders currently present in the “Tags” folder. This difference represents those folders in the Chrome Bookmark Bar which no longer have any members in the user’s Diigo library. Thus, these folders are removed from Chrome.
After the cleanup is done, the function iterates through the list of tags most recently downloaded from Diigo, and creates those sub folders, after which the addAllBookmarksWithTag
function is called.
Adding bookmarks to a tag subfolder
/**
* Adds all bookmarks with given tag to provided folder, if they don't exist.
* Looks at URL for comparison, not title.
* @param folder
*/
function addAllBookmarksWithTag(folder) {
chrome.bookmarks.getChildren(folder.id, function (children) {
var urls = {};
if (children.length > 0) {
var numChildren = children.length;
var subItem;
while (numChildren--) {
subItem = children[numChildren];
urls[subItem.url] = subItem;
}
}
var i = iLength;
var key = false;
while (i--) {
var item = response[i];
var tags = item.tags.split(',');
if (tags.indexOf(folder.title) > -1) {
// Bookmark belongs in folder
if (urls.hasOwnProperty(item.url)) {
key = item.url;
}
if (urls.hasOwnProperty(item.url + "/")) {
key = item.url + "/";
}
if (key) {
// Bookmark already exists in folder
if (urls[key].title != item.title) {
// Title needs an update
clog('Title updated: "' + urls[key].title + '" to "' + item.title + '"');
chrome.bookmarks.update(urls[key].id, {title: item.title});
}
} else {
// Bookmark needs to be created
chrome.bookmarks.create({
parentId: folder.id,
title: item.title,
url: item.url
}, function (bookmarkItem) {
clog("Created Item: " + bookmarkItem.title + " on " + bookmarkItem.url);
});
}
}
}
});
}
Finally, we add the bookmarks to their respective tag folders. We first build an object containing current bookmark URLs from each folder as keys, and the bookmark nodes themselves as values.
The function iterates through the original result set, splits the tags and checks if the bookmark it’s currently dealing with belongs into the current folder. The “/” trick is due to Diigo sometimes pushing random slashes onto URLs. We’ll handle this in a followup “Optimizations” article. If the bookmark belongs into the folder, and is already inside, the function checks if the bookmark title needs updating. If so, it updates it. If the bookmark doesn’t exist in the folder, and it should, then it is created.
Conclusion
At long last, we built the most of our extension. There’s still some quirks to iron out, but most of the work is done. You can download the final version of background.js
from Github.
In Part 4, we’ll focus on letting people log into the extension and use their own account, we’ll allow adding of custom API keys in case of quota problems, and we’ll optimize our code a bit. Stay tuned!
Frequently Asked Questions (FAQs) about Creating a Chrome Extension
How can I create a root-level folder in Chrome bookmarks using JavaScript?
Creating a root-level folder in Chrome bookmarks using JavaScript involves using the Chrome bookmarks API. The API provides the ‘create’ method which you can use to create a new bookmark or folder. Here is a simple example:chrome.bookmarks.create({'parentId': bookmarkBar.id, 'title': 'My Folder'}, function(newFolder) {
console.log("added folder: " + newFolder.title);
});
In this code, ‘parentId’ is the ID of the parent folder under which you want to create the new folder. If you want to create a root-level folder, you can use the ID of the bookmarks bar.
How can I add a bookmark to a specific folder in Chrome using JavaScript?
To add a bookmark to a specific folder in Chrome using JavaScript, you can use the ‘create’ method of the Chrome bookmarks API. You need to specify the ‘parentId’ as the ID of the folder where you want to add the bookmark. Here is an example:chrome.bookmarks.create({'parentId': folderId, 'title': 'My Bookmark', 'url': 'https://www.example.com'}, function(newBookmark) {
console.log("added bookmark: " + newBookmark.title);
});
In this code, ‘folderId’ is the ID of the folder where you want to add the bookmark. ‘title’ is the title of the bookmark, and ‘url’ is the URL that the bookmark should point to.
How can I retrieve all bookmarks in Chrome using JavaScript?
To retrieve all bookmarks in Chrome using JavaScript, you can use the ‘getTree’ method of the Chrome bookmarks API. This method retrieves the entire bookmarks tree. Here is an example:chrome.bookmarks.getTree(function(bookmarkTreeNodes) {
console.log(bookmarkTreeNodes);
});
In this code, ‘bookmarkTreeNodes’ is an array of BookmarkTreeNode objects representing the entire bookmarks tree.
How can I delete a bookmark in Chrome using JavaScript?
To delete a bookmark in Chrome using JavaScript, you can use the ‘remove’ method of the Chrome bookmarks API. You need to specify the ID of the bookmark that you want to delete. Here is an example:chrome.bookmarks.remove(bookmarkId, function() {
console.log("removed bookmark");
});
In this code, ‘bookmarkId’ is the ID of the bookmark that you want to delete.
How can I update a bookmark in Chrome using JavaScript?
To update a bookmark in Chrome using JavaScript, you can use the ‘update’ method of the Chrome bookmarks API. You need to specify the ID of the bookmark that you want to update and the new properties that you want to set. Here is an example:chrome.bookmarks.update(bookmarkId, {'title': 'New Title', 'url': 'https://www.newurl.com'}, function(updatedBookmark) {
console.log("updated bookmark: " + updatedBookmark.title);
});
In this code, ‘bookmarkId’ is the ID of the bookmark that you want to update. ‘title’ and ‘url’ are the new title and URL that you want to set for the bookmark.
How can I search for a bookmark in Chrome using JavaScript?
To search for a bookmark in Chrome using JavaScript, you can use the ‘search’ method of the Chrome bookmarks API. You need to specify the query that you want to search for. Here is an example:chrome.bookmarks.search('My Bookmark', function(results) {
console.log(results);
});
In this code, ‘My Bookmark’ is the query that you want to search for. The ‘search’ method returns an array of BookmarkTreeNode objects representing the bookmarks that match the query.
How can I move a bookmark to a different folder in Chrome using JavaScript?
To move a bookmark to a different folder in Chrome using JavaScript, you can use the ‘move’ method of the Chrome bookmarks API. You need to specify the ID of the bookmark that you want to move and the new parent ID. Here is an example:chrome.bookmarks.move(bookmarkId, {'parentId': newParentId}, function(movedBookmark) {
console.log("moved bookmark: " + movedBookmark.title);
});
In this code, ‘bookmarkId’ is the ID of the bookmark that you want to move. ‘newParentId’ is the ID of the folder where you want to move the bookmark to.
How can I get the children of a bookmark folder in Chrome using JavaScript?
To get the children of a bookmark folder in Chrome using JavaScript, you can use the ‘getChildren’ method of the Chrome bookmarks API. You need to specify the ID of the folder. Here is an example:chrome.bookmarks.getChildren(folderId, function(children) {
console.log(children);
});
In this code, ‘folderId’ is the ID of the folder whose children you want to get. The ‘getChildren’ method returns an array of BookmarkTreeNode objects representing the children of the folder.
How can I get the parent of a bookmark in Chrome using JavaScript?
To get the parent of a bookmark in Chrome using JavaScript, you can use the ‘get’ method of the Chrome bookmarks API. You need to specify the ID of the bookmark. Here is an example:chrome.bookmarks.get(bookmarkId, function(bookmark) {
chrome.bookmarks.get(bookmark.parentId, function(parent) {
console.log("parent of bookmark: " + parent.title);
});
});
In this code, ‘bookmarkId’ is the ID of the bookmark whose parent you want to get. The ‘get’ method returns a BookmarkTreeNode object representing the parent of the bookmark.
How can I get the siblings of a bookmark in Chrome using JavaScript?
To get the siblings of a bookmark in Chrome using JavaScript, you can use the ‘getSiblings’ method of the Chrome bookmarks API. You need to specify the ID of the bookmark. Here is an example:chrome.bookmarks.getSiblings(bookmarkId, function(siblings) {
console.log(siblings);
});
In this code, ‘bookmarkId’ is the ID of the bookmark whose siblings you want to get. The ‘getSiblings’ method returns an array of BookmarkTreeNode objects representing the siblings of the bookmark.
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.