The function of the popup is to enable the user to choose one of three beasts.
Create a new directory called "popup" under the extension root. This is where we'll keep the code for the popup. The popup will consist of three files:
choose_beast.html
defines the content of the panel
choose_beast.css
styles the content
choose_beast.js
handles the user's choice by running a content script in the active tab
mkdir popup
cd popup
touch choose_beast.html choose_beast.css choose_beast.js
choose_beast.html
The HTML file looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="choose_beast.css" />
</head>
<body>
<div id="popup-content">
<button>Frog</button>
<button>Turtle</button>
<button>Snake</button>
<button type="reset">Reset</button>
</div>
<div id="error-content" class="hidden">
<p>Can't beastify this web page.</p>
<p>Try a different page.</p>
</div>
<script src="choose_beast.js"></script>
</body>
</html>
We have a <div>
element with an ID of "popup-content"
that contains a button for each animal choice and a reset button. We have another <div>
with an ID of "error-content"
and a class "hidden"
. We'll use that in case there's a problem initializing the popup.
Note that we include the CSS and JS files from this file, just like a web page.
choose_beast.css
The CSS fixes the size of the popup, ensures that the three choices fill the space, and gives them some basic styling. It also hides elements with class="hidden"
: this means that our <div id="error-content"...
element will be hidden by default.
html, body {
width: 100px;
}
.hidden {
display: none;
}
button {
border: none;
width: 100%;
margin: 3% auto;
padding: 4px;
text-align: center;
font-size: 1.5em;
cursor: pointer;
background-color: #E5F2F2;
}
button:hover {
background-color: #CFF2F2;
}
button[type="reset"] {
background-color: #FBFBC9;
}
button[type="reset"]:hover {
background-color: #EAEA9D;
}
choose_beast.js
Here's the JavaScript for the popup:
const hidePage = `body > :not(.beastify-image) {
display: none;
}`;
function listenForClicks() {
document.addEventListener("click", (e) => {
function beastNameToURL(beastName) {
switch (beastName) {
case "Frog":
return browser.runtime.getURL("beasts/frog.jpg");
case "Snake":
return browser.runtime.getURL("beasts/snake.jpg");
case "Turtle":
return browser.runtime.getURL("beasts/turtle.jpg");
}
}
function beastify(tabs) {
browser.tabs.insertCSS({ code: hidePage }).then(() => {
const url = beastNameToURL(e.target.textContent);
browser.tabs.sendMessage(tabs[0].id, {
command: "beastify",
beastURL: url
});
});
}
function reset(tabs) {
browser.tabs.removeCSS({ code: hidePage }).then(() => {
browser.tabs.sendMessage(tabs[0].id, {
command: "reset",
});
});
}
function reportError(error) {
console.error(`Could not beastify: ${error}`);
}
if (e.target.tagName !== "BUTTON" || !e.target.closest("#popup-content")) {
return;
}
if (e.target.type === "reset") {
browser.tabs.query({active: true, currentWindow: true})
.then(reset)
.catch(reportError);
} else {
browser.tabs.query({active: true, currentWindow: true})
.then(beastify)
.catch(reportError);
}
});
}
function reportExecuteScriptError(error) {
document.querySelector("#popup-content").classList.add("hidden");
document.querySelector("#error-content").classList.remove("hidden");
console.error(`Failed to execute beastify content script: ${error.message}`);
}
browser.tabs
.executeScript({ file: "/content_scripts/beastify.js" })
.then(listenForClicks)
.catch(reportExecuteScriptError);
The place to start here is line 99. The popup script executes a content script in the active tab as soon as the popup is loaded, using the browser.tabs.executeScript()
API. If executing the content script is successful, then the content script will stay loaded in the page until the tab is closed or the user navigates to a different page.
A common reason the browser.tabs.executeScript()
call might fail is that you can't execute content scripts in all pages. For example, you can't execute them in privileged browser pages like about:debugging, and you can't execute them on pages in the addons.mozilla.org domain. If it does fail, reportExecuteScriptError()
will hide the <div id="popup-content">
element, show the <div id="error-content"...
element, and log an error to the console.
If executing the content script is successful, we call listenForClicks()
. This listens for clicks on the popup.
- If the click was not on a button in the popup, we ignore it and do nothing.
- If the click was on a button with
type="reset"
, then we call reset()
.
- If the click was on any other button (i.e. the beast buttons), then we call
beastify()
.
The beastify()
function does three things:
- map the button clicked to a URL pointing to an image of a particular beast
- hide the page's whole content by injecting some CSS, using the
browser.tabs.insertCSS()
API
- send a "beastify" message to the content script using the
browser.tabs.sendMessage()
API, asking it to beastify the page, and passing it the URL to the beast image.
The reset()
function essentially undoes a beastify:
- remove the CSS we added, using the
browser.tabs.removeCSS()
API
- send a "reset" message to the content script asking it to reset the page.