A webextension with browser action popups

This is the second post in a series . In this post, we are diving straight into building Webextensions.

A minimal project

Webextensions can be built using vanilla HTML, CSS and JS. We will build a real (like, 30% useful) webextension in the easiest way possible. To keep things simple, we will not use any transpilers, bundlers or preprocessors.
Let’s begin by looking at a webextension project scaffold.

├── icons
│   ├── icon-48.png
│   └── icon-96.png
├── manifest.json
└── popup
    ├── index.html
    ├── main.js
    └── styles.css

The manifest file#

Webextension projects are identified by a manifest.json. The manifest file:

  • Must be present in all webextension projects.
  • Contains a few important properties along with a bunch of random ones about the project. This includes name, description, version, icons, etc.
  • The properties manifest_version, name, and version are required.
  • The most important task of the manifest file is to define “entrypoints”.

Entrypoints (not an officially documented name) are properties that tell the browser how, when, and where to load the extension.
For example:
The browser_action property is used to add a button on the browser’s toolbar. We can program this button to perform an action when clicked.
The background property is used to start background scripts.
Let’s look at a simple manifest.json file.

{
  "manifest_version": 2,
  "name": "Timekeeper",
  "description": "A simple stopwatch.",
  "version": "0.0.1",
  "icons": {
    "48": "icons/icon-48.png",
    "96": "icons/icon-96.png"
  },
  "browser_action": {
    "default_title": "Timekeeper",
    "default_popup": "popup/index.html"
  }
}

Popups#

One way an extension can let the user interact with itself is by using a popup. This popup may contain buttons, inputs or any valid HTML.

When a browser_action is specified in the manifest, a button for the extension is added to the browser’s toolbar.
This button can be programmed to:

  • Open a popup (we’re doing that now!)
  • Dispatch a click event to a background script (will be covered in a later post)

We have specified two properties for browser_action:

  • default_title: Name of the browser action; You see this when you hover over the extension icon on the toolbar.
  • default_popup: Relative path to an HTML file. The content of this page is loaded in the popup.

Popups are created by the HTML file referenced in default_popup. This HTML file can use <link> and <script> tags for including CSS and JS just like a normal HTML file. Popups are probably the coolest part about webextensions.

  • Popups are very easy to build. The process is similar to writing a simple HTML page.
  • While most of the entry points work in the shadows, popups provide a visible surface to the user.
  • Popups get a bad rep as they are mostly used for advertisements and auth walls on websites but none of that is applicable here.

WebExtension Icons#

When webextension icons are provided, they are used for toolbar buttons and are displayed on the extensions page as a logo. The extensions page is an internal webpage in the browser where all the installed extensions are listed.

  • about:addons in Firefox
  • chrome://extensions/ in Chrome

Icons are specified by the icons property in the manifest. The keys are icon sizes (48x48 and 96x96 in our case) and values are paths to the icon file.

Enter! The Timekeeper!!

The extension we are building is a stopwatch inside a popup window. We have picked a classic beginner project so that we can focus on the webextension side of things, not the HTML/JS side.

Our extension will have the following features:

  • A main display for total elapsed time.
  • A smaller display for current lap time.
  • An even smaller display for each lap.
  • Start, stop, resume, lap and reset buttons.

Steps:

  • Create a directory.
  • Create a manifest.json file in that directory. Copy the contents from above.
  • Copy the icons from the Github repo (link below) or add your own.
  • Create a directory called ‘popup’ in this directory. This will contain an HTML file along with some JavaScript and CSS. The complete code for these can be found at the following links:
    index.html
    styles.css
    main.js

You may clone this Github repo and look in the 01-popup folder for the source.

index.html#

This will contain empty HTML elements along with some buttons.

  • Heading tags for timer displays
  <h1 class="text-black" id="total-time">00 : 00 : 00</h1>
  <h2 class="text-yellow" id="lap-time">00 : 00 : 00</h2>
  • An empty div for individual lap times
  <div id="lap-list"></div>
  • Buttons for starting/stopping the timer.
  <div class="buttons" id="initial-buttons">
    <button class="bg-yellow" id="btn-start">START</button>
  </div>
  <div class="buttons" id="running-buttons">
    <button class="bg-pink" id="btn-stop">STOP</button>
    <button class="bg-yellow" id="btn-lap">LAP</button>
  </div>
  <div class="buttons" id="stopped-buttons">
    <button class="bg-pink" id="btn-resume">RESUME</button>
    <button class="bg-black" id="btn-reset">RESET</button>
  </div>
  • Our script file is included at the end.
  <script src="./main.js"></script>

main.js#

This will make the timer tick.

  • Get references to DOM elements.
const btnStart = document.querySelector('#btn-start')
const btnStop = document.querySelector('#btn-stop')
const btnLap = document.querySelector('#btn-lap')
const btnResume = document.querySelector('#btn-resume')
const btnReset = document.querySelector('#btn-reset')
  • Initialize application state. Times will be stored as seconds.
const appState = {
  totalTime: 0,
  lapNumber: 1,
  prevLapTime: 0
}
  • Add some helper functions to format seconds into HH:MM:SS
function formatTime (time, separator = ' : ') {
  const hours = Math.floor(time / 3600)
  const minutes = Math.floor(time / 60)
  const seconds = Math.floor(time % 60)
  const paddedTimeElements = [hours, minutes, seconds].map(x => String(x).padStart(2, 0))
  return paddedTimeElements.join(separator)
}
function updateTimerDisplay () {
  totalTimeDisplay.innerText = formatTime(appState.totalTime)
  lapTimeDisplay.innerText = formatTime(appState.totalTime - appState.prevLapTime)
}
  • This counter function will be called at an interval of 1 second.
let interval

function countSecond () {
  appState.totalTime += 1
  updateTimerDisplay()
}
  • Event listeners for the buttons to start/stop/reset the counter. We will setInterval and clearInterval to start/stop our timer. Rest of the code is for conditionally displaying the buttons based on whether the timer is running or not.
function start () {
  initialButtonsDiv.style.display = 'none'
  runningButtonsDiv.style.display = 'flex'
  interval = setInterval(countSecond, 1000)
}

function stop () {
  runningButtonsDiv.style.display = 'none'
  stoppedButtonsDiv.style.display = 'flex'
  clearInterval(interval)
}

function lap () {
  const lapDiv = document.createElement('div')
  lapDiv.classList.add('lap-div')

  const lapNumberSpan = document.createElement('span')
  lapNumberSpan.innerText = appState.lapNumber
  lapDiv.appendChild(lapNumberSpan)

  const lapTimeSpan = document.createElement('span')
  lapTimeSpan.innerText = formatTime(appState.totalTime - appState.prevLapTime, ':')
  lapDiv.appendChild(lapTimeSpan)

  const totalTimeSpan = document.createElement('span')
  totalTimeSpan.innerText = formatTime(appState.totalTime, ':')
  lapDiv.appendChild(totalTimeSpan)

  lapListDiv.appendChild(lapDiv)

  appState.prevLapTime = appState.totalTime
  appState.lapNumber += 1
}

function resume () {
  stoppedButtonsDiv.style.display = 'none'
  runningButtonsDiv.style.display = 'flex'
  interval = setInterval(countSecond, 1000)
}

function reset () {
  stoppedButtonsDiv.style.display = 'none'
  initialButtonsDiv.style.display = 'flex'
  appState.totalTime = 0
  appState.prevLapTime = 0
  updateTimerDisplay()
  lapListDiv.innerHTML = ''
}
  • Attach event listeners to buttons.
btnStart.addEventListener('click', start)
btnStop.addEventListener('click', stop)
btnLap.addEventListener('click', lap)
btnResume.addEventListener('click', resume)
btnReset.addEventListener('click', reset)

Don’t forget to include the CSS file from the repository. Our extension is ready. Let’s run this extension in a browser.

Loading temporary extensions

During development, an extension can be loaded in a browser temporarily. This allows us to test our extensions.

In Firefox:#

  • Type about:debugging in the address bar.
  • Click on This Firefox > Load Temporary Add-on
  • Navigate to your project and select the manifest file.

Loading temporary extensions in Firefox

In Chrome:#

  • Type chrome://extensions/ in the address bar.
  • Switch to developer mode using the button on top-right corner.
  • Click on Load Unpacked
  • Select the folder containing the manifest file.

You will find the webextension’s icon on your browser’s toolbar.

Complete webextension

Also, if you use Firefox and forgot to include icons, be prepared for this. Browser action without an icon It’s a button, but not a “button” button. Firefox gives us perfectly clickable empty space.

Limitations of Popups

Our extension doesn’t do anything special that a normal web page can’t. And, there are several limitations of our popup-based webextension.

  • The popup window closes when a user clicks outside of the popup window. The HTML page loaded inside the popup is destroyed. A fresh page is loaded every time we click on the browser_action button.
  • Javascript running inside a popup’s webpage can access Webextension APIs, but scripts loaded inside a popup’s HTML are not suited for long running tasks.
  • Popups can used to provide an interface and are best used in a combination with other webextension features like Storage and background scripts.

In the next post, we will modify the extension to get past the limitations.
Meanwhile, you may go through the documentation on MDN: