diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000..a8e3912ef3
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+_archive
+third-party
+node_modules
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..c89f8d860a
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,27 @@
+/* eslint-env node */
+module.exports = {
+ extends: ['prettier', 'eslint:recommended'],
+ plugins: ['prettier'],
+ rules: {
+ 'prettier/prettier': ['error'],
+ 'no-unused-vars': [
+ 'warn',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_'
+ }
+ ]
+ },
+ env: {
+ browser: true,
+ webextensions: true,
+ es2021: true,
+ jquery: true,
+ worker: true
+ },
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module'
+ }
+};
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 6777e87e6f..ab7875c22a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -4,7 +4,6 @@ about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
-
---
⚠️ If you have general Chrome Extensions questions, consider posting to the [Chromium Extensions Group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-extensions) or [Stack Overflow](https://stackoverflow.com/questions/tagged/google-chrome-extension).
@@ -14,6 +13,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior, or file the issue is found in:
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
diff --git a/.gitignore b/.gitignore
index 61b4cee870..d85f45326e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
*~
*.DS_store
-
+node_modules
# Temporary directory for debugging extension samples
_debug
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000000..a16d8b1d55
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx lint-staged
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..a8e3912ef3
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,3 @@
+_archive
+third-party
+node_modules
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000000..6b7043e964
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,9 @@
+{
+ "printWidth": 80,
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "bracketSpacing": true,
+ "arrowParens": "always"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7a653458f3..385c575bfc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,17 +1,48 @@
-# Contributing to this Repository
+# How to Contribute
-Thank you for your interest in contributing!
+We'd love to accept your patches and contributions to this project.
-Send us your patches early and often and in whatever shape or form.
+## Before you begin
-## Legal
+### Sign our Contributor License Agreement
-Unfortunately there are some legal hurdles. Sorry about that.
+Contributions to this project must be accompanied by a
+[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
+You (or your employer) retain the copyright to your contribution; this simply
+gives us permission to use and redistribute your contributions as part of the
+project.
-This repository is a Google open source project, and so we require contributors to sign Google's open source Contributor License Agreement.
-It's easy to do, just click here to sign as an [individual](https://developers.google.com/open-source/cla/individual) or [corporation](https://developers.google.com/open-source/cla/corporate).
-Individuals can sign electronically in seconds (see the bottom of the page); corporations will need to email a PDF, or mail.
+If you or your current employer have already signed the Google CLA (even if it
+was for a different project), you probably don't need to do it again.
-We cannot accept PRs or patches larger than fixing typos and the like without a signed CLA.
+Visit to see your current agreements or to
+sign a new one.
-If your Github account doesn't show the name you used to sign, please mention your name in your PR.
+### Review our Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
+
+## Contribution process
+
+### Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+### Setting up your Environment
+
+If you want to contribute to this repository, you need to first [create your own fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
+After forking chrome-extensions-samples to your own Github account, run the following steps to get started:
+
+```sh
+# clone your fork to your local machine
+git clone https://github.com/your-fork/chrome-extensions-samples.git
+
+cd chrome-extensions-samples
+
+# install dependencies
+npm install
+```
diff --git a/README.md b/README.md
index 5e641c0480..44a9626d13 100644
--- a/README.md
+++ b/README.md
@@ -5,16 +5,16 @@ Note that Chrome Apps are deprecated. Learn more [on the Chromium blog](https://
For more information on extensions, see [Chrome Developers](https://developer.chrome.com).
-**Note: Samples for Manifest V3 are still being prepared. In the mean time, consider referring to [_archive/mv2/](_archive/mv2/).**
+**Note: Samples for Manifest V3 are still being prepared. In the mean time, consider referring to [\_archive/mv2/](_archive/mv2/).**
## Samples
The directory structure is as follows:
-* [api-samples/](api-samples/) - extensions focused on a single API package
-* [functional-samples/](functional-samples/) - full featured extensions spanning multiple API packages
-* [_archive/apps/](_archive/apps/) - deprecated Chrome Apps platform (not listed below)
-* [_archive/mv2/](_archive/mv2/) - resources for manifest version 2
+- [api-samples/](api-samples/) - extensions focused on a single API package
+- [functional-samples/](functional-samples/) - full featured extensions spanning multiple API packages
+- [\_archive/apps/](_archive/apps/) - deprecated Chrome Apps platform (not listed below)
+- [\_archive/mv2/](_archive/mv2/) - resources for manifest version 2
To experiment with these samples, please clone this repo and use 'Load Unpacked Extension'.
Read more on [Development Basics](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
@@ -119,3 +119,11 @@ Read more on [Development Basics](https://developer.chrome.com/docs/extensions/m
+
+## Contributing
+
+Please see [the CONTRIBUTING file](/CONTRIBUTING.md) for information on contributing to the `chrome-extensions-samples` project.
+
+## License
+
+`chrome-extensions-samples` are authored by Google and are licensed under the [Apache License, Version 2.0](/LICENSE).
diff --git a/api-samples/action/demo/index.css b/api-samples/action/demo/index.css
index 1617b14b38..212bfec5ea 100644
--- a/api-samples/action/demo/index.css
+++ b/api-samples/action/demo/index.css
@@ -4,13 +4,13 @@ p {
.flex {
display: flex;
- gap: .25em;
- margin: .5em 0;
+ gap: 0.25em;
+ margin: 0.5em 0;
align-items: flex-end;
}
.spaced {
- margin: .5em 0;
+ margin: 0.5em 0;
}
.full-width {
diff --git a/api-samples/action/demo/index.html b/api-samples/action/demo/index.html
index 7e4c06e17b..534c2f7d70 100644
--- a/api-samples/action/demo/index.html
+++ b/api-samples/action/demo/index.html
@@ -1,167 +1,213 @@
-
-
-
-
- Document
-
-
-
-
-
-
-
-
Action API Demo
-
Before experimenting with these APIs, we recommend you pin the extension's action button to your
- toolbar in order to make it easier to see the changes.
-
-
-
-
-
enable / disable
-
-
Clicking the below toggle enabled state button will enable or disable the extensions'
- action button in Chrome's toolbar and extensions menu.
-
-
When disabled, clicking the action will not open a popup or trigger action.onClicked
- events.
-
-
-
-
-
-
- Action enabled
-
-
-
- Action disabled
-
-
-
-
-
-
Popup
-
-
This demo's manifest.json file sets the value of
- action.default_popup to popups/popup.html. We can change that behavior at runtime using action.setPopup.
-
-
-
-
-
-
-
-
Register a handler to change how the action button behaves. Once changed, clicking the
- action will open your new favorite website.
-
-
-
-
-
-
-
-
Badge Text
-
-
The action's badge text is a text overlay with a solid background color. This provides a
- passive UI surface to share information with the user. It is most commonly used to show a
- notification count or number of actions taken on the current page.
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
Action API Demo
+
+ Before experimenting with these APIs, we recommend you pin the
+ extension's action button to your toolbar in order to make it easier
+ to see the changes.
+
+
+
+
+
+
enable / disable
+
+
+ Clicking the below toggle enabled state button will enable or
+ disable the extensions' action button in Chrome's toolbar and
+ extensions menu.
+
+
+
+ When disabled, clicking the action will not open a popup or trigger
+ action.onClicked
+ events.
+
+
+
+
+
+
+
+ Action enabled
+
+
+
+ Action disabled
+
+
+
+
+
+
Popup
+
+
+ This demo's manifest.json file sets the
+ value of action.default_popup to
+ popups/popup.html. We can change that behavior at runtime
+ using
+ action.setPopup.
+
-
-
-
-
-
-
-
-
-
-
-
Icon
-
-
The action.setIcon
- method allows you to change the action button's icon by either providing the path of an image
- or the raw ImageData.
-
-
-
-
-
-
-
-
-
Hover Text
-
-
The action's title is visible when mousing over the extension's action button.
-
-
- Default appearance
-
-
-
- Title appears on hover
-
-
-
-
+
+
+
+
+
+ Register a handler to change how the action button behaves. Once
+ changed, clicking the action will open your new favorite website.
+
+
+
+
+
+
+
+
+
Badge Text
+
+
+ The action's badge text is a text overlay with a solid background
+ color. This provides a passive UI surface to share information with
+ the user. It is most commonly used to show a notification count or
+ number of actions taken on the current page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Icon
+
+
+ The
+ action.setIcon
+ method allows you to change the action button's icon by either
+ providing the path of an image or the raw
+ ImageData.
+
+
+
+
+
+
+
+
+
+
Hover Text
+
+
+ The action's title is visible when mousing over the extension's action
+ button.
+
+
+
+ This value can be read and changed at runtime using the
+ action.getTitle
+ and
+ action.setTitle
+ methods, respectively.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default appearance
+
+
+
+ Title appears on hover
+
+
+
+
+
diff --git a/api-samples/action/demo/index.js b/api-samples/action/demo/index.js
index 5257af12cf..a8c401d991 100644
--- a/api-samples/action/demo/index.js
+++ b/api-samples/action/demo/index.js
@@ -25,39 +25,43 @@ function debounce(timeout, callback) {
// have to track it ourselves.
// Relevant feature request: https://bugs.chromium.org/p/chromium/issues/detail?id=1189295
let actionEnabled = true;
-let showToggleState = document.getElementById('show-toggle-state');
-document.getElementById('toggle-state-button').addEventListener('click', (_event) => {
- if (actionEnabled) {
- chrome.action.disable();
- } else {
- chrome.action.enable();
- }
- actionEnabled = !actionEnabled;
-});
-
-document.getElementById('popup-options').addEventListener('change', async (event) => {
- let popup = event.target.value;
- await chrome.action.setPopup({ popup });
-
- // Show the updated popup path
- await getCurrentPopup();
-});
+const showToggleState = document.getElementById('show-toggle-state');
+document
+ .getElementById('toggle-state-button')
+ .addEventListener('click', (_event) => {
+ if (actionEnabled) {
+ chrome.action.disable();
+ } else {
+ chrome.action.enable();
+ }
+ actionEnabled = !actionEnabled;
+ });
+
+document
+ .getElementById('popup-options')
+ .addEventListener('change', async (event) => {
+ const popup = event.target.value;
+ await chrome.action.setPopup({ popup });
+
+ // Show the updated popup path
+ await getCurrentPopup();
+ });
async function getCurrentPopup() {
- let popup = await chrome.action.getPopup({});
+ const popup = await chrome.action.getPopup({});
document.getElementById('current-popup-value').value = popup;
return popup;
-};
+}
async function showCurrentPage() {
- let popup = await getCurrentPopup();
+ const popup = await getCurrentPopup();
let pathname = '';
if (popup) {
pathname = new URL(popup).pathname;
}
- let options = document.getElementById('popup-options');
- let option = options.querySelector(`option[value="${pathname}"]`);
+ const options = document.getElementById('popup-options');
+ const option = options.querySelector(`option[value="${pathname}"]`);
option.selected = true;
}
@@ -75,129 +79,139 @@ chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: 'https://html5zombo.com/' });
});
-document.getElementById('onclicked-button').addEventListener('click', async () => {
- // Our listener will only receive the action's click event after clear out the popup URL
- await chrome.action.setPopup({ popup: '' });
- await showCurrentPage();
-});
-
-document.getElementById('onclicked-reset-button').addEventListener('click', async () => {
- await chrome.action.setPopup({ popup: 'popups/popup.html' });
- await showCurrentPage();
-});
+document
+ .getElementById('onclicked-button')
+ .addEventListener('click', async () => {
+ // Our listener will only receive the action's click event after clear out the popup URL
+ await chrome.action.setPopup({ popup: '' });
+ await showCurrentPage();
+ });
+
+document
+ .getElementById('onclicked-reset-button')
+ .addEventListener('click', async () => {
+ await chrome.action.setPopup({ popup: 'popups/popup.html' });
+ await showCurrentPage();
+ });
// ----------
// badge text
// ----------
async function showBadgeText() {
- let text = await chrome.action.getBadgeText({});
+ const text = await chrome.action.getBadgeText({});
document.getElementById('current-badge-text').value = text;
}
// Populate badge text inputs on on page load
showBadgeText();
-document.getElementById('badge-text-input').addEventListener('input', async (event) => {
- let text = event.target.value;
- await chrome.action.setBadgeText({ text });
+document
+ .getElementById('badge-text-input')
+ .addEventListener('input', async (event) => {
+ const text = event.target.value;
+ await chrome.action.setBadgeText({ text });
- showBadgeText();
-});
+ showBadgeText();
+ });
-document.getElementById('clear-badge-button').addEventListener('click', async () => {
- await chrome.action.setBadgeText({ text: '' });
+document
+ .getElementById('clear-badge-button')
+ .addEventListener('click', async () => {
+ await chrome.action.setBadgeText({ text: '' });
- showBadgeText();
-});
+ showBadgeText();
+ });
// ----------------------
// badge background color
// ----------------------
async function showBadgeColor() {
- let color = await chrome.action.getBadgeBackgroundColor({});
- document.getElementById('current-badge-bg-color').value = JSON.stringify(color, null, 0);
+ const color = await chrome.action.getBadgeBackgroundColor({});
+ document.getElementById('current-badge-bg-color').value = JSON.stringify(
+ color,
+ null,
+ 0
+ );
}
// Populate badge background color inputs on on page load
showBadgeColor();
-document.getElementById('set-badge-background-color-button').addEventListener('click', async () => {
- // To show off this method, we must first make sure the badge has text
- let currentText = await chrome.action.getBadgeText({});
- if (!currentText) {
- chrome.action.setBadgeText({ text: 'hi :)' });
- showBadgeText();
- }
-
- // Next, generate a random RGBA color
- let color = [0, 0, 0].map(() => Math.floor(Math.random() * 255));
-
- // Use the default background color ~10% of the time.
- //
- // NOTE: Alpha color cannot be set due to crbug.com/1184905. At the time of writing (Chrome 89),
- // an alpha value of 0 sets the default color while a value of 1-255 will make the RGB color
- // fully opaque.
- if (Math.random() < 0.1) {
- color.push(0);
- } else {
- color.push(255);
- }
-
- chrome.action.setBadgeBackgroundColor({ color });
- showBadgeColor();
-});
-
-document.getElementById('reset-badge-background-color-button').addEventListener('click', async () => {
- chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] });
- showBadgeColor();
-});
+document
+ .getElementById('set-badge-background-color-button')
+ .addEventListener('click', async () => {
+ // To show off this method, we must first make sure the badge has text
+ let currentText = await chrome.action.getBadgeText({});
+ if (!currentText) {
+ chrome.action.setBadgeText({ text: 'hi :)' });
+ showBadgeText();
+ }
+
+ // Next, generate a random RGBA color
+ const color = [0, 0, 0].map(() => Math.floor(Math.random() * 255));
+
+ // Use the default background color ~10% of the time.
+ //
+ // NOTE: Alpha color cannot be set due to crbug.com/1184905. At the time of writing (Chrome 89),
+ // an alpha value of 0 sets the default color while a value of 1-255 will make the RGB color
+ // fully opaque.
+ if (Math.random() < 0.1) {
+ color.push(0);
+ } else {
+ color.push(255);
+ }
+
+ chrome.action.setBadgeBackgroundColor({ color });
+ showBadgeColor();
+ });
+
+document
+ .getElementById('reset-badge-background-color-button')
+ .addEventListener('click', async () => {
+ chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] });
+ showBadgeColor();
+ });
// -----------
// action icon
// -----------
-const EMOJI = [
- 'confetti',
- 'suit',
- 'bow',
- 'dog',
- 'skull',
- 'yoyo',
- 'cat',
-];
+const EMOJI = ['confetti', 'suit', 'bow', 'dog', 'skull', 'yoyo', 'cat'];
let lastIconIndex = 0;
-document.getElementById('set-icon-button').addEventListener('click', async () => {
- // Clear out the badge text in order to make the icon change easier to see
- chrome.action.setBadgeText({ text: '' });
-
- // Randomly pick a new icon
- let index = lastIconIndex;
- index = Math.floor(Math.random() * (EMOJI.length));
- if (index === lastIconIndex) {
- // Dupe detected! Increment the index & modulo to make sure we don't go out of bounds
- index = (index + 1) % EMOJI.length;
- }
- let emojiFile = `images/emoji-${EMOJI[index]}.png`;
- lastIconIndex = index;
-
- // There are easier ways for a page to extract an image's imageData, but the approach used here
- // works in both extension pages and service workers.
- let response = await fetch(chrome.runtime.getURL(emojiFile));
- let blob = await response.blob();
- let imageBitmap = await createImageBitmap(blob);
- let osc = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
- let ctx = osc.getContext('2d');
- ctx.drawImage(imageBitmap, 0, 0);
- let imageData = ctx.getImageData(0, 0, osc.width, osc.height);
-
- chrome.action.setIcon({ imageData });
-});
+document
+ .getElementById('set-icon-button')
+ .addEventListener('click', async () => {
+ // Clear out the badge text in order to make the icon change easier to see
+ chrome.action.setBadgeText({ text: '' });
+
+ // Randomly pick a new icon
+ let index = lastIconIndex;
+ index = Math.floor(Math.random() * EMOJI.length);
+ if (index === lastIconIndex) {
+ // Dupe detected! Increment the index & modulo to make sure we don't go out of bounds
+ index = (index + 1) % EMOJI.length;
+ }
+ const emojiFile = `images/emoji-${EMOJI[index]}.png`;
+ lastIconIndex = index;
+
+ // There are easier ways for a page to extract an image's imageData, but the approach used here
+ // works in both extension pages and service workers.
+ const response = await fetch(chrome.runtime.getURL(emojiFile));
+ const blob = await response.blob();
+ const imageBitmap = await createImageBitmap(blob);
+ const osc = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
+ let ctx = osc.getContext('2d');
+ ctx.drawImage(imageBitmap, 0, 0);
+ const imageData = ctx.getImageData(0, 0, osc.width, osc.height);
+
+ chrome.action.setIcon({ imageData });
+ });
document.getElementById('reset-icon-button').addEventListener('click', () => {
- let manifest = chrome.runtime.getManifest();
+ const manifest = chrome.runtime.getManifest();
chrome.action.setIcon({ path: manifest.action.default_icon });
});
@@ -205,23 +219,28 @@ document.getElementById('reset-icon-button').addEventListener('click', () => {
// get/set title
// -------------
-let titleInput = document.getElementById('title-input');
-let titleInputDebounce = Number.parseInt(titleInput.dataset.debounce || 100);
-titleInput.addEventListener('input', debounce(200, async (event) => {
- let title = event.target.value;
- chrome.action.setTitle({ title });
+const titleInput = document.getElementById('title-input');
+const titleInputDebounce = Number.parseInt(titleInput.dataset.debounce || 100);
+titleInput.addEventListener(
+ 'input',
+ debounce(200, async (event) => {
+ const title = event.target.value;
+ chrome.action.setTitle({ title });
- showActionTitle();
-}));
+ showActionTitle();
+ })
+);
-document.getElementById('reset-title-button').addEventListener('click', async (event) => {
- let manifest = chrome.runtime.getManifest();
- let title = manifest.action.default_title;
+document
+ .getElementById('reset-title-button')
+ .addEventListener('click', async (event) => {
+ const manifest = chrome.runtime.getManifest();
+ let title = manifest.action.default_title;
- chrome.action.setTitle({ title });
+ chrome.action.setTitle({ title });
- showActionTitle();
-});
+ showActionTitle();
+ });
async function showActionTitle() {
let title = await chrome.action.getTitle({});
@@ -229,7 +248,7 @@ async function showActionTitle() {
// If empty, the title falls back to the name of the extension
if (title === '') {
// … which we can get from the extension's manifest
- let manifest = chrome.runtime.getManifest();
+ const manifest = chrome.runtime.getManifest();
title = manifest.name;
}
diff --git a/api-samples/action/popups/a.html b/api-samples/action/popups/a.html
index cbd03b756e..805ea507ed 100644
--- a/api-samples/action/popups/a.html
+++ b/api-samples/action/popups/a.html
@@ -1,32 +1,32 @@
-
-
-
-
- Document
-
-
-
-