Hello 👋 Welcome to my corner of the internet. I write here about the different challenges I encounter, and the projects I work on. Find out more about me.
The extension I’ve built allows you to directly upload pictures (right click an image) to an API endpoint of your choice with the ability to add tags and adjust a pre-filled source. I made it as an fun add-on to a bigger project my studio is currently working on: Baubauhaus. It already had a plugin to upload images and I was curious to see how I could improve it.
This post is going to be as modular as possible: I’ll describe the envisioned workflow of the extension, the general design, how to set up your project so that the chrome browser works with it and then an array of mini tasks I use in the extension to ‘build’ its’ functionalities. You’re free to recycle and use anything I post here!
I will publish how I realised the following usage steps, part-for-part. The first two parts will be included in this first blog post. The following parts will have their own dedicated blog posts.
This is my first Chrome plugin so I’ve tried to keep things as simple as possible. To accomplish all that I wanted, I figured out I need 3 types of pages.
First a page that pops up when you click the extension icon, this is called popup.html
. Then there is a background page called background.html
, which is always running but not visible as a tab (it allows persistence of data). Lastly there is an options.html
page, which allows the extension user to set and adjust data that you can consequently use in the extension ‘backend’.
For chrome extensions, you have to declare all settings in a file called manifest.json
. In here you have describe the plugin, the earliest compatible version of Chrome, the version number, the permissions, the plugin-button icon, what you background page is called, what browser actions you allow and what you options page is called. See the chrome documentation for more information.
My manifest.json
looks like this:
{
"manifest_version": 2,
"name": "Baubauhaus Uploader",
"short_name": "Baubauhaus",
"description": "Uploads images to Baubauhaus",
"version": "0.0.2",
"minimum_chrome_version": "38",
"permissions": [
"contextMenus",
"storage",
"http://api.baubauhaus.com/*",
"http://localhost:3000/api/*"
],
"icons": {
"16": "assets/baubauhaus.png"
},
"background": {"page": "background.html"},
"browser_action": {
"default_title": "Pending uploads to Baubauhaus",
"default_popup": "popup.html"
},
"options_ui": {
"page": "options.html",
"chrome_style": true
}
}
There are a few conventions here: mostly the options_ui
key, see here for more information on that. Your permissions you manage in the permissions array. The particular expression to add per ‘permission’ differs per action you’re trying to do and the chrome extension documentation will generally guide you well in what permission you have to add in order to allow what type of behavior.
Also, you set the background page (the html page). This allows nice decoupling of js / html but I recommend naming the js pages the same as the html pages just for clarities’ sake. I did it differently because stackoverflow told me but in the end had to change it to be consistent. It’s much more clear now.
Background.html needs javascript to be functional as a background ‘script’. I don’t think the html is actually visible anywhere so the html is very straightforward:
<!DOCTYPE html>
<html>
<body>
<script src=“background.js”></script>
</body>
</html>
Right clicking on an object in the chrome browser opens the so-called ‘context menu’. To allow you to adjust the contextmenu, in your manifest.json
file, add a permission for "contextMenus”
(see my gives manifest.json).
Then I added in my background.js
:
var images = [];
chrome.contextMenus.create({
title: "Baubauhaus",
contexts:["image"],
onclick: function(targetImage) {
images.push(targetImage.srcUrl);
}
});
Images
is simply an array that will contain the urls of the images I’d like to keep in a list. Because it’s on the background page, this array will persist across your browser session. When you fully quit the browser, this array will be lost.
A little explenation: The chrome.contextMenus.create
function makes the menu option. The contexts
specifies on which type of content the menu is active (images in this case). The all I do is, on click, push the image on the images array.
The next step is to setup popup.html, the page that loads when you click the extension icon. I’ve added JQuery since I’m not a javascript magician.. yet. The ‘field’ that shows up when you click the extension icon behaves like a regular html page, so you can do regular things like styling through linked CSS.
<html>
<head>
<title>Get that URL</title>
<script src="jquery.min.js"></script>
<script src="popup.js"></script>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>
<body>
<messages></messages>
<uploads></uploads>
</body>
</html>
Now popup.js
needs to have a list of the images, so it can iteratively display each of them. Chrome has a very handy way of dealing with that: you call the background page in a variable and you can then extract all that you want. The call is var bg = chrome.extension.getBackgroundPage();
. To get the images array, you simply call bg.images
and you’re good to go. Thanks chrome, awesome.
At this point, popup.js
looks like this:
document.addEventListener('DOMContentLoaded', function () {
var bg = chrome.extension.getBackgroundPage();
var images = bg.images;
// a bunch of code to iterate over the images and display them within the forms
The addEventListener(‘DOMContentLoaded’)
is simply there to get the images once the page is ready.
After I have the array, I iterate over it and create a small hierachy of XML elements and DIV’s to be able to structure and style the relevant elements.
This step involves mostly javascript to construct XML and HTML elements.
The popup.js file retrieves the array of image urls from the background.js page by way of:
var bg = chrome.extension.getBackgroundPage();
var images = bg.images;
Then I iterate over the array with the jquery .each
function. For each image, I create an upload
XML element by calling the ‘uploadMaker’ function and then appending that upload
element the the container uploads
element.
$.each(images, function(index, value) {
var upload = uploadMaker(index, value);
$('uploads').append(upload);
});
My goal is to create the follow hierarchy, per image.
<upload>
<div class=“imgwrap”> <!— image-url as background —> </div>
<div class=“textwrap”>
<form>
<div class=“field”></div>
<div class=“field”></div>
<input type=“hidden” name=“imageurl” value=“some_url.com”>
<input type=“hidden” name=“apiKey” value=“fizzbuzz”>
<button type=“removeImage”> Remove image </button>
</form>
</div>
</upload>
To achieve this, I made a 100-line javascript div factory (monster). It’s not pretty, but it works. The uploadMaker
function accepts the two values the jquery .each
function extracts per object it is iterating over (index and the value). The value is the image-url.
// DIV FACTORY AREA
function uploadMaker(nr, url) {
var container = document.createElement('upload');
var img = document.createElement('div');
img.className = 'imgwrap';
$(img).attr('style', ('background-image: url(' + url + ')'));
var textContainer = document.createElement('div');
textContainer.className = 'textwrap';
var formContainer = document.createElement('form');
$(formContainer).attr('action', 'api.some_api_endpoint.com');
$(formContainer).attr('method', 'post');
formContainer.id = 'form-' + nr
formContainer.appendChild( tagsInputMaker('tags', 'TAGS', "") );
formContainer.appendChild( inputMaker('source', 'SOURCE', url) );
formContainer.appendChild( hiddenInputMaker('imageurl', url) );
formContainer.appendChild( hiddenInputMaker('api_key', 'fizzbuzz') );
formContainer.appendChild( submitButtonMaker('Send', 'submit'));
formContainer.appendChild( buttonMaker('Remove this image from list', 'removeImage'));
textContainer.appendChild( formContainer );
container.appendChild(img);
container.appendChild(textContainer);
return container;
};
function tagsInputMaker(name, label, content) {
var inputWrap = document.createElement('div');
inputWrap.className = 'field';
var inputLabel = document.createElement('label');
inputLabel.innerHTML = label;
var mostUsedTags = ['poster', 'graphic design', 'black and white'].split(",");
var i;
var tagsArray = [];
for (i = 0; i < mostUsedTags.length; ++i) {
var value = mostUsedTags[i];
var tag = document.createElement('span');
tag.innerHTML = value;
tag.className = "clickable-tag";
tag.id = "clickable-tag";
tagsArray.push(tag.outerHTML);
}
var tags = document.createElement('tags');
tags.innerHTML = tagsArray.join("");
var inputField = document.createElement('input');
inputField.value = content;
$(inputField).attr('name', name);
$(inputField).attr('type', 'text');
inputWrap.appendChild(inputLabel);
inputWrap.appendChild(tags);
inputWrap.appendChild(inputField);
return inputWrap;
};
function buttonMaker(buttonText, type) {
var button = document.createElement('button');
button.innerHTML = buttonText;
$(button).attr('type', type);
return button;
};
function submitButtonMaker(value, type) {
var inputSubmit = document.createElement('input');
$(inputSubmit).attr('value', value);
$(inputSubmit).attr('type', type);
return inputSubmit;
};
function hiddenInputMaker(name, url) {
var inputField = document.createElement('input');
$(inputField).attr('type', 'hidden');
$(inputField).attr('name', name);
$(inputField).attr('value', url);
return inputField;
};
function inputMaker(name, label, content) {
var inputWrap = document.createElement('div');
inputWrap.className = 'field';
var inputLabel = document.createElement('label');
inputLabel.innerHTML = label;
var inputField = document.createElement('input');
inputField.value = content;
$(inputField).attr('name', name);
$(inputField).attr('type', 'text');
inputWrap.appendChild(inputLabel);
inputWrap.appendChild(inputField);
return inputWrap;
};
What makes it a little bit more complex is the fact that there is a ‘tags’ maker. The tags field needed to have some clickable ‘most used’ tags, which I’ve hardcoded in the above example. In the real extension, this is configurable by the user. I will explain that later.
When you combine the above pieces of code, you should end up with a neat list of the images you’ve previously collected using the right-click mechanism on images. You should see two input fields, one for ‘tags’ and one for ‘source’. There should be two hidden input fields and two buttons, one to send the form (not functioning right now) and one to remove the image from the array.
To make the ‘remove’ button work, we need to understand that in order to fully remove the image, we need to figure out the relevant image-url, then remove that one from the images array in popup.js, from the images array in background.js and lastly remove the relevant
I do this in two steps. First, find the image-url, then, consequently remove from all relevant places. And third, display message that removal was successfull or not.
The button is embedded in a form so it’s easy to pass the form-object to a function to find the image-url. That function is called findImageUrlFromForm
.
function findImageUrlFromForm(form) {
var imageUrl = form
.serializeArray()
.filter(function(x) { return x.name === "imageurl"})[0].value;
return imageUrl;
};
Then the task is to remove the image-url from the images array on background.js. I do that by sending it (background.js) a message with the relevant url.
function removeImageFromBackground(url) {
// send removal message to main.js
chrome.runtime.sendMessage({urlToRemove: url}, function(response) {
// append the returned message at the top of popup.html
displayMessage(response.message, 'fadeOut');
});
};
Note: By splitting the functions up like this, I can re-use the same function when a submittal to the API endpoint has been successful.
In background.js you listen to a message like so:
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// remove the url from the list
images = images.filter(function(x) { return x != request.urlToRemove; });
// send confirmation of removal
sendResponse({message: "Success."});
});
The last step is to add the onclick action to the button.
// on pressing button[type="removeImage"], remove image from popup.html and background.js images-array
$('button[type="removeImage"]').click( function(e) {
e.preventDefault();
// remove from popup.html
$(this.parentNode).parent().parent().remove();
// remove from background.js
var imageUrl = findImageUrlFromForm( $(this.parentNode) );
removeImageFromBackground(imageUrl);
});
Now clicking the ‘remove this image’ button should consistantly remove the image from the foreground and background.