Skip to content

Instantly share code, notes, and snippets.

@Solenoden
Last active February 26, 2024 04:17
Show Gist options
  • Save Solenoden/5e6875ad011e350e903ac48bca3ca614 to your computer and use it in GitHub Desktop.
Save Solenoden/5e6875ad011e350e903ac48bca3ca614 to your computer and use it in GitHub Desktop.
Chromium Extensions Article Draft

Chromium Extensions

Please note: this article is written for Manifest v2 which is deprecated. If you wish to publish an extension to the Chrome store you will need to use Manifest v3. How to migrate from Manifest v2 to v3

What is a Chromium Extension?

Chromium browsers are web browsers that are built on top of the open-source Chromium engine. These browsers include Chrome, Brave and Edge.

A Chromium Extension is a web extension built for and that is capable of running on a Chromium browser. This means you can build an extension that not only works for Chrome; which as of May 2022, has a 64.91 global market share; but also for any browsers that run on it's engine.

How to develop a Chromium Extension

Chromium extensions are essentially just Javascript scripts as well HTML pages for the extension dialog and settings page.

Chromium extensions consist of the following:

  • Manifest.json
  • Content Script (JS)
  • Background Script (JS)
  • Extension Dialog (HTML + JS)
  • Settings Page (HTML + JS)

Manifest

The manifest.json file is a configuration file used by the Chromium Engine to retrieve some information about your extension such as:

  • Name
  • Permissions
  • Registered scripts and pages

Content Scripts

Content scripts are Javascript scripts that get injected into the webpage you are currently viewing. From these scripts you can execute code that alters the styling and/or values of HTML elements on the current page.

Content scripts are rather limited in which Chrome APIs they can access. They are pretty much limited to sending and receiving messages, accessing the url of the current page and working with the storage of the current page.

Code can be executed once the content script is loaded or you can use a publish/subscribe pattern with messages to trigger certain logic when:

  • Context Menu items are clicked
  • Buttons are clicked in the extension dialog page
  • Buttons are clicked/values set on the settings page

Background Scripts

Background scripts are also written in Javascript and unlike Content Scripts, do not run within the context of the current webpage being viewed.

While background scripts can't interact with the current webpage being viewed, they are able to access a plethora of Chrome APIs (which fire their own events), not limited to, but including:

  • Tabs
  • Storage (storage for the extension)
  • Bookmarks
  • Alarms

The type of code that you will write in the Background script is code that will take advantage of the Chrome APIs (creating alarms, reacting to bookmarks being created etc.) and registering Context Menu items if your extension require any.

Communication between Content and Background scripts

Communication between Content and Background scripts is done through a publisher/subscriber pattern. In essence, Content and Background scripts fire events/messages that the other listen for, and then execute the appropriate code.

chrome.runtime.onMessage.addListener(message => ...) can be used to subscribe to incoming messages. chrome.runtime.sendMessage({ <MESSAGE_DATA> }) can be used to send messages.

Context Menu

The Context Menu is the menu that pops up when right clicking on a webpage. You can create multiple Context Menu items that you can map code to.

Context Menu items should be registered in the Background script as they are an extension wide feature.

image

Extension Pages (dialog, settings)

Extension pages include the extension dialog, which you can find by right clicking the extension icon in the top right corner of the browser, as well as the settings page which allow you to configure the settings for your extension.

image

Storing extension data

Extensions can store their own data on the browser, allowing you to persist settings.

To work with extension storage, access the storage Chrome API:

  • For reading data: chrome.storage.sync.get(<STORAGE_KEY>, () => { ... //Callback Function })
  • For writing data chrome.storage.sync.set({ <STORAGE_KEY>: <VALUE> })

Demo

The goal: Create a Chrome Extension which can generate a date of birth within the bounds of a minimum and maximum age. These age fields will need to be captured by the user, via the extension dialog, and persisted to the extension's storage.

Step 1: Setting up

Start by forking or cloning this repository which is a boilerplate for Chromium extensions, bundled with Webpack: Boilerplate Repository. This repository was originally forked from a repository created by @samuelsimoes.

The repository is structured as follows:

  • src
    • assets (any image or font assets go here)
    • pages (contains pages such as the extension dialog and settings page)
    • services (contains any service classes we may create)
    • background-script.js
    • content-script.js
    • manifest.json
  • utils (used by Webpack)
  • package.json
  • webpack.config.js

Run npm run build in the root of the project to compile the extension. You'll see that a build file has been created in the root.

To load the extension onto the browser, navigate to your browser's extensions page (eg; chrome://extensions). Enable developer mode in the top right hand corner and finally click the Load Unpacked button in the top left corner and select the build folder on the repository root.

image

You should see a card for the extension on the extensions page, possibly with a warning informing you that Manifest v2 has been deprecated. If after this article you wish to migrate to Manifest v3, you can read this article

To view the extension dialog, pin the extension in the top right corner of the browser and click on the extension's icon

image

Step 2: Understanding the Manifest.json

The Manifest.json contains core information about our extension which is used by the browser such as the extension name and manifest_version. You can go ahead and change the name field if you like.

The permissions field will be populated later to allow us to access the Chrome storage api.

The background, content_scripts and browser_action (extension dialog) fields are used to register the respective scripts and pages

The content_scripts field can contain a matches field which can be used to apply the content script to pages at a specific url. In fact, the content_scripts field is an array which means you could register multiple content_scripts and inject them into pages at specific urls.

Step 3: Capture Maximum and Minimum Age

3.1: Create the Form View

We'll be doing this on the extension dialog page. Firstly, navigate to the src/pages/extension-dialog-page/extension-dialog-page.html file.

Start by creating the inputs for the age fields, replacing everything except the script element inside the body, assigning specific ids to them so that we can select them easily through Javascript.

        <h4>Date of Birth Settings</h4>

        <div style="margin-bottom: 10px">
            <label for="minimum-age-input">Minimum Age</label>
            <input id="minimum-age-input" type="number">
        </div>

        <div>
            <label for="maximum-age-input">Maximum Age</label>
            <input id="maximum-age-input" type="number">
        </div>

Next, add a Save button beneath the inputs:

<button id="save-button">Save</button>

To view the updated extension dialog: re-run the build script, navigate back to chrome://extensions and click on the reload button on the extension's card. Open the extension's dialog, you should see the two inputs and the save button.

3.2: Initialize the Age Fields with Values

We will be using the storage api to persist the max and min age values. In order to use the storage api we will need to register a permission. Navigate to the manifest.json and under permissions add storage. "permissions": ["storage"]

Next, navigate to the extension-dialog-page.js file and replace the file contents with an enum mapping the id numbers of all important elements on the page.

Putting these ids in an enum instead of hardcoding them in multiple places lowers the chance of mispelling the id and also means that we don't need to remember or go back to the HTML to see what the ids are.

const PAGE_ELEMENT_ID = {
    MINIMUM_AGE_INPUT: 'minimum-age-input',
    MAXIMUM_AGE_INPUT: 'maximum-age-input',
    SAVE_BUTTON: 'save-button'
}

Create two functions, initializePage() and initializeAgeInputs(). Inside of initializePage(), call initializeAgeInputs()

function initializePage() {
    initializeAgeInputs();
}

function intializeAgeInputs() {}

Inside of initializeAgeInputs we will call the get method on the chrome.storage.sync api to get two fields in storage. You can put these keys inside of an enum called STORAGE_KEY at the top of the file or in another file and import it.

export const STORAGE_KEY = {
    MINIMUM_AGE: 'minimumAge',
    MAXIMUM_AGE: 'maximumAge'
}

function initializeAgeInputs() {
    chrome.storage.sync.get(STORAGE_KEY.MINIMUM_AGE, result => {
        document.getElementById(PAGE_ELEMENT_ID.MINIMUM_AGE_INPUT).value = result[STORAGE_KEY.MINIMUM_AGE];
    });

    chrome.storage.sync.get(STORAGE_KEY.MAXIMUM_AGE, result => {
        document.getElementById(PAGE_ELEMENT_ID.MAXIMUM_AGE_INPUT).value = result[STORAGE_KEY.MAXIMUM_AGE];
    })
}

Under the enum declaration(s), call the initializePage() method. This will be the entry point for Javascript on the dialog page.

The age fields will still be blank since there isn't an existing value in storage. To test the age input initialization, add the following code above the initializePage() call.

chrome.storage.sync.set({ [STORAGE_KEY.MINIMUM_AGE]: 18 });
chrome.storage.sync.set({ [STORAGE_KEY.MAXIMUM_AGE]: 65 });

Re-run the build script, navigate back to chrome://extensions and click on the reload button on the extension's card. You should now see the values 18 and 65 in the appropriate inputs.

Remove the two lines to set the min and max age once you are done testing.

3.3: Persist/Save the Age Values

Next we will persist the values from the two age inputs when clicking the save button.

Create the below function which retrieves the values in the inputs and then saves them using the Chrome storage api

function saveAgeRange() {
    const minAge = document.getElementById(PAGE_ELEMENT_ID.MINIMUM_AGE_INPUT).value;
    const maxAge = document.getElementById(PAGE_ELEMENT_ID.MAXIMUM_AGE_INPUT).value;

    chrome.storage.sync.set({ [STORAGE_KEY.MINIMUM_AGE]: minAge });
    chrome.storage.sync.set({ [STORAGE_KEY.MAXIMUM_AGE]: maxAge });
    
    alert('Min/Max age has been set');
}

This function needs to be fired when the submit button is clicked. Add this code at the bottom of the initializePage() function.

document.getElementById(PAGE_ELEMENT_ID.SAVE_BUTTON).addEventListener('click', saveAgeRange);

Build and reload to test.

Step 4: Create Context Menu Item

Next we will be creating a context menu item which will replace an input's value with a random date of birth when selected.

Start by adding the contextMenus permission in the manifest.json file to allow our extension to access the contextMenus Chrome API.

We will be using the moment package for date manipulation and formatting. Run the following command to install it: npm install moment.

We will need to two classes to handle context menu items, ContextMenuService and DateOfBirthContextMenuItem:

  • ContextMenuService will contain logic to create context menu items as well as to facilitate communication between the web page and the context menu.
  • DateOfBirthContextMenuItem will contain the configuration for the context menu item (label text etc.) as well as the logic which is ran when the context menu item is clicked.

Create the classes:

src/context-menu-items/date-of-birth.context-menu-item.js

import { STORAGE_KEY } from '../enums/storage-key.enum';
import moment from "moment";

export class DateOfBirthContextMenuItem {
    constructor() {
        this.id = 'dateOfBirth';
        this.title = 'Generate Date Of Birth';
        this.contexts = ['editable'];
    }

    onClick(info, tab) {
        chrome.storage.sync.get([STORAGE_KEY.MINIMUM_AGE, STORAGE_KEY.MAXIMUM_AGE], result => {
            const minAge = result[STORAGE_KEY.MINIMUM_AGE];
            const maxAge = result[STORAGE_KEY.MAXIMUM_AGE];
            const dateOfBirth = this.generateDateOfBirth(minAge, maxAge);

            this.replaceInputText(dateOfBirth);
        });
    }

    replaceInputText(newValue) {
        const input = document.activeElement;

        input.value = newValue;
        input.dispatchEvent(new Event('input', {
            view: window,
            bubbles: true,
            cancelable: true
        }));
        input.dispatchEvent(new Event('change', {
            view: window,
            bubbles: true,
            cancelable: true
        }));
    }

    generateDateOfBirth(minAge, maxAge) {
        const minDateEpoch = new moment().subtract(maxAge, 'years').toDate().getTime();
        const maxDateEpoch = new moment().subtract(minAge, 'years').toDate().getTime();

        const random = new moment(minDateEpoch + Math.random() * (maxDateEpoch - minDateEpoch));
        return random.format('YYYY/MM/DD');
    }
}

src/services/context-menu.service.js

import { DateOfBirthContextMenuItem } from "../context-menu-items/date-of-birth.context-menu-item";

export class ContextMenuService {
    constructor() {
        this.contextMenuItems = [
            new DateOfBirthContextMenuItem()
        ]
    }

    createMenuItems() {
        this.contextMenuItems.forEach(contextMenuItem => {
            const item = Object.assign({}, contextMenuItem);
            delete item.onClick;
            item.onclick = (info, tab) => this.sendOnClickMessage(item.id, tab.id);

            chrome.contextMenus.create(item);
        });
    }

    handleMessage(message) {
        const applicableMenuItem = this.contextMenuItems.find(x => x.id === message.contextMenuId);
        if (applicableMenuItem) applicableMenuItem.onClick(message);
    }

    sendOnClickMessage(contextMenuId, tabId) {
        chrome.tabs.sendMessage(tabId, { contextMenuId });
    }
}

Next, we will need to register our context-menu item with the browser. This is an action that only needs to happen once when the extension is loaded on the browser so we will register it in the background-script.js.

Add the following to the bottom of src/background-script.js:

import { ContextMenuService } from "./services/context-menu.service";

const contextMenuService = new ContextMenuService();
contextMenuService.createMenuItems();

Since we want the context menu item to affect the HTML on the current web page, we will need code to be executed from the content-script.js. Replace the contents of src/content-script.js with the following:

import { ContextMenuService } from "./services/context-menu.service";

chrome.runtime.onMessage.addListener(function (message) {
    new ContextMenuService().handleMessage(message);
});

The above code listens for messages/events sent by the context menu item and then calls the handleMessage function on the ContextMenuService to execute the appropriate logic.

Sources

Further Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment