Add prettier and eslint (#831)

* Ignores archived samples
* Uses eslint/recommended rules
* Runs prettier and eslint (including --fix) pre-commit via husky
* Adds new npm scripts: 'lint', 'lint:fix' and 'prettier'
* Does not lint inline js code
* Fix all prettier and eslint errors
* Add custom prettier rules
* Apply custom prettier rules
* Update readme to explain how to setup the repo
* addressed comments
This commit is contained in:
Sebastian Benz
2023-02-22 13:25:39 +01:00
committed by GitHub
parent 299f2134cb
commit dc2174377a
83 changed files with 4798 additions and 1074 deletions

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
_archive
third-party
node_modules

27
.eslintrc.js Normal file
View File

@@ -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'
}
};

View File

@@ -4,7 +4,6 @@ about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' 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). ⚠️ 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** **To Reproduce**
Steps to reproduce the behavior, or file the issue is found in: Steps to reproduce the behavior, or file the issue is found in:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
*~ *~
*.DS_store *.DS_store
node_modules
# Temporary directory for debugging extension samples # Temporary directory for debugging extension samples
_debug _debug

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
_archive
third-party
node_modules

9
.prettierrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always"
}

View File

@@ -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. If you or your current employer have already signed the Google CLA (even if it
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). was for a different project), you probably don't need to do it again.
Individuals can sign electronically in seconds (see the bottom of the page); corporations will need to email a PDF, or mail.
We cannot accept PRs or patches larger than fixing typos and the like without a signed CLA. Visit <https://cla.developers.google.com/> 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
```

View File

@@ -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). 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 ## Samples
The directory structure is as follows: The directory structure is as follows:
* [api-samples/](api-samples/) - extensions focused on a single API package - [api-samples/](api-samples/) - extensions focused on a single API package
* [functional-samples/](functional-samples/) - full featured extensions spanning multiple API packages - [functional-samples/](functional-samples/) - full featured extensions spanning multiple API packages
* [_archive/apps/](_archive/apps/) - deprecated Chrome Apps platform (not listed below) - [\_archive/apps/](_archive/apps/) - deprecated Chrome Apps platform (not listed below)
* [_archive/mv2/](_archive/mv2/) - resources for manifest version 2 - [\_archive/mv2/](_archive/mv2/) - resources for manifest version 2
To experiment with these samples, please clone this repo and use 'Load Unpacked Extension'. 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). 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
</tr> </tr>
</tbody> </tbody>
</table> </table>
## 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).

View File

@@ -4,13 +4,13 @@ p {
.flex { .flex {
display: flex; display: flex;
gap: .25em; gap: 0.25em;
margin: .5em 0; margin: 0.5em 0;
align-items: flex-end; align-items: flex-end;
} }
.spaced { .spaced {
margin: .5em 0; margin: 0.5em 0;
} }
.full-width { .full-width {

View File

@@ -1,167 +1,213 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<script defer src="index.js"></script> <script defer src="index.js"></script>
<link rel="stylesheet" href="../third-party/awsm/awsm.css"> <link rel="stylesheet" href="../third-party/awsm/awsm.css" />
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css" />
</head> </head>
<body> <body>
<main> <main>
<section> <section>
<h1>Action API Demo</h1> <h1>Action API Demo</h1>
<p>Before experimenting with these APIs, we recommend you pin the extension's action button to your <p>
toolbar in order to make it easier to see the changes. </p> Before experimenting with these APIs, we recommend you pin the
<img src="../images/pin-action.png"> extension's action button to your toolbar in order to make it easier
</section> to see the changes.
</p>
<img src="../images/pin-action.png" />
</section>
<section id="toggle-state"> <section id="toggle-state">
<h2>enable / disable</h2> <h2>enable / disable</h2>
<p>Clicking the below <em>toggle enabled state</em> button will enable or disable the extensions' <p>
action button in Chrome's toolbar and extensions menu.</p> Clicking the below <em>toggle enabled state</em> button will enable or
disable the extensions' action button in Chrome's toolbar and
extensions menu.
</p>
<p>When disabled, clicking the action will not open a popup or trigger <a <p>
href="https://developer.chrome.com/docs/extensions/reference/action/#event-onClicked"><code>action.onClicked</code></a> When disabled, clicking the action will not open a popup or trigger
events.</p> <a
href="https://developer.chrome.com/docs/extensions/reference/action/#event-onClicked"
><code>action.onClicked</code></a
>
events.
</p>
<button id="toggle-state-button">toggle enabled state</button> <button id="toggle-state-button">toggle enabled state</button>
<div class="flex"> <div class="flex">
<figure> <figure>
<img src="../images/action-enabled.png"> <img src="../images/action-enabled.png" />
<figcaption>Action enabled</figcaption> <figcaption>Action enabled</figcaption>
</figure> </figure>
<figure> <figure>
<img src="../images/action-disabled.png"> <img src="../images/action-disabled.png" />
<figcaption>Action disabled</figcaption> <figcaption>Action disabled</figcaption>
</figure> </figure>
</div> </div>
</section> </section>
<section id="popup"> <section id="popup">
<h2>Popup</h2> <h2>Popup</h2>
<p>This demo's <a href="manifest.json">manifest.json</a> file sets the value of <p>
<code>action.default_popup</code> to <code>popups/popup.html</code>. We can change that behavior at runtime using <a This demo's <a href="manifest.json">manifest.json</a> file sets the
href="https://developer.chrome.com/docs/extensions/reference/action/#method-setPopup"><code>action.setPopup</code></a>.</p> value of <code>action.default_popup</code> to
<code>popups/popup.html</code>. We can change that behavior at runtime
using
<a
href="https://developer.chrome.com/docs/extensions/reference/action/#method-setPopup"
><code>action.setPopup</code></a
>.
</p>
<label>
Change popup page<br>
<select id="popup-options">
<option value="/popups/popup.html">Hello world (default)</option>
<option value="/popups/a.html">A</option>
<option value="/popups/b.html">B</option>
<option value="">onClicked handler</option>
</select>
</label>
<div class="spaced">
<label> <label>
Current popup value Change popup page<br />
<input type="text" id="current-popup-value" disabled> <select id="popup-options">
<option value="/popups/popup.html">Hello world (default)</option>
<option value="/popups/a.html">A</option>
<option value="/popups/b.html">B</option>
<option value="">onClicked handler</option>
</select>
</label> </label>
</div>
<p>Register a handler to change how the action button behaves. Once changed, clicking the <div class="spaced">
action will open your new favorite website.</p> <label>
<button id="onclicked-button">Change action click behavior</button> Current popup value
<button id="onclicked-reset-button">reset</button> <input type="text" id="current-popup-value" disabled />
</section> </label>
</div>
<!-- badge --> <p>
Register a handler to change how the action button behaves. Once
changed, clicking the action will open your new favorite website.
</p>
<button id="onclicked-button">Change action click behavior</button>
<button id="onclicked-reset-button">reset</button>
</section>
<section id="badge-text"> <!-- badge -->
<h2>Badge Text</h2>
<p>The action's badge text is a text overlay with a solid background color. This provides a <section id="badge-text">
passive UI surface to share information with the user. It is most commonly used to show a <h2>Badge Text</h2>
notification count or number of actions taken on the current page.</p>
<div class="spaced"> <p>
<label> The action's badge text is a text overlay with a solid background
Enter badge text (live update)<br> color. This provides a passive UI surface to share information with
<input type="text" id="badge-text-input"> the user. It is most commonly used to show a notification count or
</label> number of actions taken on the current page.
</div> </p>
<div class="flex"> <div class="spaced">
<label class="full-width"> <label>
Current badge text Enter badge text (live update)<br />
<input type="text" id="current-badge-text" disabled> <input type="text" id="badge-text-input" />
</label> </label>
<button id="clear-badge-button">clear badge text</button> </div>
</div>
<div class="spaced"> <div class="flex">
<button id="set-badge-background-color-button">Randomize badge background color</button> <label class="full-width">
</div> Current badge text
<input type="text" id="current-badge-text" disabled />
</label>
<button id="clear-badge-button">clear badge text</button>
</div>
<div class="flex"> <div class="spaced">
<label class="full-width"> <button id="set-badge-background-color-button">
Current badge color Randomize badge background color
<input type="text" id="current-badge-bg-color" disabled> </button>
</label> </div>
<button id="reset-badge-background-color-button">reset badge color</button>
</div>
</section> <div class="flex">
<label class="full-width">
Current badge color
<input type="text" id="current-badge-bg-color" disabled />
</label>
<button id="reset-badge-background-color-button">
reset badge color
</button>
</div>
</section>
<!-- badge - icon --> <!-- badge - icon -->
<section id="setIcon"> <section id="setIcon">
<h2>Icon</h2> <h2>Icon</h2>
<p>The <a <p>
href="https://developer.chrome.com/docs/extensions/reference/action/#method-setIcon"><code>action.setIcon</code></a> The
method allows you to change the action button's icon by either providing the path of an image <a
or the raw <a href="https://developer.mozilla.org/en-US/docs/Web/API/ImageData">ImageData</a>.</p> href="https://developer.chrome.com/docs/extensions/reference/action/#method-setIcon"
><code>action.setIcon</code></a
>
method allows you to change the action button's icon by either
providing the path of an image or the raw
<a href="https://developer.mozilla.org/en-US/docs/Web/API/ImageData"
>ImageData</a
>.
</p>
<button id="set-icon-button">set a new action icon</button> <button id="set-icon-button">set a new action icon</button>
<button id="reset-icon-button">reset action icon</button> <button id="reset-icon-button">reset action icon</button>
</section> </section>
<!-- badge - hover text (title) --> <!-- badge - hover text (title) -->
<section id="title"> <section id="title">
<h2>Hover Text</h2> <h2>Hover Text</h2>
<p>The action's title is visible when mousing over the extension's action button.</p> <p>
The action's title is visible when mousing over the extension's action
button.
</p>
<p>This value can be read and changed at runtime using the <a <p>
href="https://developer.chrome.com/docs/extensions/reference/action/#method-getTitle"><code>action.getTitle</code></a> This value can be read and changed at runtime using the
and <a <a
href="https://developer.chrome.com/docs/extensions/reference/action/#method-setTitle"><code>action.setTitle</code></a> href="https://developer.chrome.com/docs/extensions/reference/action/#method-getTitle"
methods, respectively.</p> ><code>action.getTitle</code></a
>
and
<a
href="https://developer.chrome.com/docs/extensions/reference/action/#method-setTitle"
><code>action.setTitle</code></a
>
methods, respectively.
</p>
<div class="spaced"> <div class="spaced">
<label> <label>
Enter a new title (debounced)<br> Enter a new title (debounced)<br />
<input type="text" id="title-input"> <input type="text" id="title-input" />
</label> </label>
</div> </div>
<div class="flex"> <div class="flex">
<label class="full-width"> <label class="full-width">
Current title Current title
<input type="text" id="current-title" disabled> <input type="text" id="current-title" disabled />
</label> </label>
<button id="reset-title-button">reset title</button> <button id="reset-title-button">reset title</button>
</div> </div>
<div class="flex"> <div class="flex">
<figure> <figure>
<img src="../images/title-no-hover.png"> <img src="../images/title-no-hover.png" />
<figcaption>Default appearance</figcaption> <figcaption>Default appearance</figcaption>
</figure> </figure>
<figure> <figure>
<img src="../images/title-hover.png"> <img src="../images/title-hover.png" />
<figcaption>Title appears on hover</figcaption> <figcaption>Title appears on hover</figcaption>
</figure> </figure>
</section> </div>
</main> </section>
</body> </main>
</body>
</html> </html>

View File

@@ -25,39 +25,43 @@ function debounce(timeout, callback) {
// have to track it ourselves. // have to track it ourselves.
// Relevant feature request: https://bugs.chromium.org/p/chromium/issues/detail?id=1189295 // Relevant feature request: https://bugs.chromium.org/p/chromium/issues/detail?id=1189295
let actionEnabled = true; let actionEnabled = true;
let showToggleState = document.getElementById('show-toggle-state'); const showToggleState = document.getElementById('show-toggle-state');
document.getElementById('toggle-state-button').addEventListener('click', (_event) => { document
if (actionEnabled) { .getElementById('toggle-state-button')
chrome.action.disable(); .addEventListener('click', (_event) => {
} else { if (actionEnabled) {
chrome.action.enable(); chrome.action.disable();
} } else {
actionEnabled = !actionEnabled; chrome.action.enable();
}); }
actionEnabled = !actionEnabled;
});
document.getElementById('popup-options').addEventListener('change', async (event) => { document
let popup = event.target.value; .getElementById('popup-options')
await chrome.action.setPopup({ popup }); .addEventListener('change', async (event) => {
const popup = event.target.value;
await chrome.action.setPopup({ popup });
// Show the updated popup path // Show the updated popup path
await getCurrentPopup(); await getCurrentPopup();
}); });
async function getCurrentPopup() { async function getCurrentPopup() {
let popup = await chrome.action.getPopup({}); const popup = await chrome.action.getPopup({});
document.getElementById('current-popup-value').value = popup; document.getElementById('current-popup-value').value = popup;
return popup; return popup;
}; }
async function showCurrentPage() { async function showCurrentPage() {
let popup = await getCurrentPopup(); const popup = await getCurrentPopup();
let pathname = ''; let pathname = '';
if (popup) { if (popup) {
pathname = new URL(popup).pathname; pathname = new URL(popup).pathname;
} }
let options = document.getElementById('popup-options'); const options = document.getElementById('popup-options');
let option = options.querySelector(`option[value="${pathname}"]`); const option = options.querySelector(`option[value="${pathname}"]`);
option.selected = true; option.selected = true;
} }
@@ -75,129 +79,139 @@ chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: 'https://html5zombo.com/' }); chrome.tabs.create({ url: 'https://html5zombo.com/' });
}); });
document.getElementById('onclicked-button').addEventListener('click', async () => { document
// Our listener will only receive the action's click event after clear out the popup URL .getElementById('onclicked-button')
await chrome.action.setPopup({ popup: '' }); .addEventListener('click', async () => {
await showCurrentPage(); // 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 () => { document
await chrome.action.setPopup({ popup: 'popups/popup.html' }); .getElementById('onclicked-reset-button')
await showCurrentPage(); .addEventListener('click', async () => {
}); await chrome.action.setPopup({ popup: 'popups/popup.html' });
await showCurrentPage();
});
// ---------- // ----------
// badge text // badge text
// ---------- // ----------
async function showBadgeText() { async function showBadgeText() {
let text = await chrome.action.getBadgeText({}); const text = await chrome.action.getBadgeText({});
document.getElementById('current-badge-text').value = text; document.getElementById('current-badge-text').value = text;
} }
// Populate badge text inputs on on page load // Populate badge text inputs on on page load
showBadgeText(); showBadgeText();
document.getElementById('badge-text-input').addEventListener('input', async (event) => { document
let text = event.target.value; .getElementById('badge-text-input')
await chrome.action.setBadgeText({ text }); .addEventListener('input', async (event) => {
const text = event.target.value;
await chrome.action.setBadgeText({ text });
showBadgeText(); showBadgeText();
}); });
document.getElementById('clear-badge-button').addEventListener('click', async () => { document
await chrome.action.setBadgeText({ text: '' }); .getElementById('clear-badge-button')
.addEventListener('click', async () => {
await chrome.action.setBadgeText({ text: '' });
showBadgeText(); showBadgeText();
}); });
// ---------------------- // ----------------------
// badge background color // badge background color
// ---------------------- // ----------------------
async function showBadgeColor() { async function showBadgeColor() {
let color = await chrome.action.getBadgeBackgroundColor({}); const color = await chrome.action.getBadgeBackgroundColor({});
document.getElementById('current-badge-bg-color').value = JSON.stringify(color, null, 0); document.getElementById('current-badge-bg-color').value = JSON.stringify(
color,
null,
0
);
} }
// Populate badge background color inputs on on page load // Populate badge background color inputs on on page load
showBadgeColor(); showBadgeColor();
document.getElementById('set-badge-background-color-button').addEventListener('click', async () => { document
// To show off this method, we must first make sure the badge has text .getElementById('set-badge-background-color-button')
let currentText = await chrome.action.getBadgeText({}); .addEventListener('click', async () => {
if (!currentText) { // To show off this method, we must first make sure the badge has text
chrome.action.setBadgeText({ text: 'hi :)' }); let currentText = await chrome.action.getBadgeText({});
showBadgeText(); if (!currentText) {
} chrome.action.setBadgeText({ text: 'hi :)' });
showBadgeText();
}
// Next, generate a random RGBA color // Next, generate a random RGBA color
let color = [0, 0, 0].map(() => Math.floor(Math.random() * 255)); const color = [0, 0, 0].map(() => Math.floor(Math.random() * 255));
// Use the default background color ~10% of the time. // 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), // 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 // an alpha value of 0 sets the default color while a value of 1-255 will make the RGB color
// fully opaque. // fully opaque.
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
color.push(0); color.push(0);
} else { } else {
color.push(255); color.push(255);
} }
chrome.action.setBadgeBackgroundColor({ color }); chrome.action.setBadgeBackgroundColor({ color });
showBadgeColor(); showBadgeColor();
}); });
document.getElementById('reset-badge-background-color-button').addEventListener('click', async () => { document
chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] }); .getElementById('reset-badge-background-color-button')
showBadgeColor(); .addEventListener('click', async () => {
}); chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] });
showBadgeColor();
});
// ----------- // -----------
// action icon // action icon
// ----------- // -----------
const EMOJI = [ const EMOJI = ['confetti', 'suit', 'bow', 'dog', 'skull', 'yoyo', 'cat'];
'confetti',
'suit',
'bow',
'dog',
'skull',
'yoyo',
'cat',
];
let lastIconIndex = 0; let lastIconIndex = 0;
document.getElementById('set-icon-button').addEventListener('click', async () => { document
// Clear out the badge text in order to make the icon change easier to see .getElementById('set-icon-button')
chrome.action.setBadgeText({ text: '' }); .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 // Randomly pick a new icon
let index = lastIconIndex; let index = lastIconIndex;
index = Math.floor(Math.random() * (EMOJI.length)); index = Math.floor(Math.random() * EMOJI.length);
if (index === lastIconIndex) { if (index === lastIconIndex) {
// Dupe detected! Increment the index & modulo to make sure we don't go out of bounds // Dupe detected! Increment the index & modulo to make sure we don't go out of bounds
index = (index + 1) % EMOJI.length; index = (index + 1) % EMOJI.length;
} }
let emojiFile = `images/emoji-${EMOJI[index]}.png`; const emojiFile = `images/emoji-${EMOJI[index]}.png`;
lastIconIndex = index; lastIconIndex = index;
// There are easier ways for a page to extract an image's imageData, but the approach used here // 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. // works in both extension pages and service workers.
let response = await fetch(chrome.runtime.getURL(emojiFile)); const response = await fetch(chrome.runtime.getURL(emojiFile));
let blob = await response.blob(); const blob = await response.blob();
let imageBitmap = await createImageBitmap(blob); const imageBitmap = await createImageBitmap(blob);
let osc = new OffscreenCanvas(imageBitmap.width, imageBitmap.height); const osc = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
let ctx = osc.getContext('2d'); let ctx = osc.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0); ctx.drawImage(imageBitmap, 0, 0);
let imageData = ctx.getImageData(0, 0, osc.width, osc.height); const imageData = ctx.getImageData(0, 0, osc.width, osc.height);
chrome.action.setIcon({ imageData }); chrome.action.setIcon({ imageData });
}); });
document.getElementById('reset-icon-button').addEventListener('click', () => { 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 }); chrome.action.setIcon({ path: manifest.action.default_icon });
}); });
@@ -205,23 +219,28 @@ document.getElementById('reset-icon-button').addEventListener('click', () => {
// get/set title // get/set title
// ------------- // -------------
let titleInput = document.getElementById('title-input'); const titleInput = document.getElementById('title-input');
let titleInputDebounce = Number.parseInt(titleInput.dataset.debounce || 100); const titleInputDebounce = Number.parseInt(titleInput.dataset.debounce || 100);
titleInput.addEventListener('input', debounce(200, async (event) => { titleInput.addEventListener(
let title = event.target.value; 'input',
chrome.action.setTitle({ title }); debounce(200, async (event) => {
const title = event.target.value;
chrome.action.setTitle({ title });
showActionTitle(); showActionTitle();
})); })
);
document.getElementById('reset-title-button').addEventListener('click', async (event) => { document
let manifest = chrome.runtime.getManifest(); .getElementById('reset-title-button')
let title = manifest.action.default_title; .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() { async function showActionTitle() {
let title = await chrome.action.getTitle({}); 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 empty, the title falls back to the name of the extension
if (title === '') { if (title === '') {
// … which we can get from the extension's manifest // … which we can get from the extension's manifest
let manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
title = manifest.name; title = manifest.name;
} }

View File

@@ -1,32 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<style> <style>
.center { .center {
min-height: 100px; min-height: 100px;
min-width: 200px; min-width: 200px;
display: grid; display: grid;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1ch; gap: 1ch;
background-color: salmon; background-color: salmon;
} }
.text { .text {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: white; color: white;
} }
</style> </style>
</head> </head>
<body> <body>
<h2>Action API Demo</h2> <h2>Action API Demo</h2>
<div class="center"> <div class="center">
<span class="text">A</span> <span class="text">A</span>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,32 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<style> <style>
.center { .center {
min-height: 100px; min-height: 100px;
min-width: 200px; min-width: 200px;
display: grid; display: grid;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1ch; gap: 1ch;
background-color: royalblue; background-color: royalblue;
} }
.text { .text {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: white; color: white;
} }
</style> </style>
</head> </head>
<body> <body>
<h2>Action API Demo</h2> <h2>Action API Demo</h2>
<div class="center"> <div class="center">
<span class="text">B</span> <span class="text">B</span>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,32 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<style> <style>
.center { .center {
min-height: 100px; min-height: 100px;
min-width: 200px; min-width: 200px;
display: grid; display: grid;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1ch; gap: 1ch;
background-color: lightseagreen; background-color: lightseagreen;
} }
.text { .text {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: white; color: white;
} }
</style> </style>
</head> </head>
<body> <body>
<h2>Action API Demo</h2> <h2>Action API Demo</h2>
<div class="center"> <div class="center">
<span class="text">Hello, world!</span> <span class="text">Hello, world!</span>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -6,14 +6,16 @@
// Initialize the demo on install // Initialize the demo on install
chrome.runtime.onInstalled.addListener((reason) => { chrome.runtime.onInstalled.addListener((reason) => {
if (reason !== chrome.runtime.OnInstalledReason.INSTALL) { return } if (reason !== chrome.runtime.OnInstalledReason.INSTALL) {
return;
}
openDemoTab(); openDemoTab();
// Create an alarm so we have something to look at in the demo // Create an alarm so we have something to look at in the demo
chrome.alarms.create('demo-default-alarm', { chrome.alarms.create('demo-default-alarm', {
delayInMinutes: 1, delayInMinutes: 1,
periodInMinutes: 1, periodInMinutes: 1
}); });
}); });

View File

@@ -43,7 +43,7 @@ body {
.alarm-display, .alarm-display,
.alarm-log { .alarm-log {
min-height: 2em; min-height: 2em;
padding: .5em; padding: 0.5em;
background-color: hsl(0, 0%, 95%); background-color: hsl(0, 0%, 95%);
border: 1px solid hsl(0, 0%, 80%); border: 1px solid hsl(0, 0%, 80%);
border-radius: 4px; border-radius: 4px;
@@ -55,7 +55,7 @@ body {
} }
.alarm-log > * { .alarm-log > * {
padding: .5em; padding: 0.5em;
} }
.alarm-log > *:not(:first-child) { .alarm-log > *:not(:first-child) {
@@ -67,7 +67,7 @@ body {
} }
.alarm-row { .alarm-row {
padding: .5em; padding: 0.5em;
position: relative; position: relative;
border-top: 1px solid transparent; border-top: 1px solid transparent;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
@@ -75,7 +75,7 @@ body {
.alarm-row:hover, .alarm-row:hover,
.alarm-log > *:hover { .alarm-log > *:hover {
background: hsl(190, 20%, 90%) background: hsl(190, 20%, 90%);
} }
.alarm-row:not(:first-child) { .alarm-row:not(:first-child) {
@@ -92,6 +92,6 @@ body {
.alarm-row__cancel-button { .alarm-row__cancel-button {
position: absolute; position: absolute;
top: .5em; top: 0.5em;
right: .5em; right: 0.5em;
} }

View File

@@ -1,66 +1,75 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css" />
<script defer src="index.js"></script> <script defer src="index.js"></script>
</head> </head>
<body> <body>
<section>
<h2>Create Alarm</h2>
<form class="create-alarm">
<div class="create-alarm__label">Name</div>
<div class="create-alarm__value">
<input type="text" name="alarm-name" value="my-alarm">
</div>
<div class="create-alarm__label">Initial delay *</div>
<div class="create-alarm__value">
<input type="number" step="0.1" name="time-value" min="0" value=1>
<select name="time-format">
<option id="format-minutes" value="min" selected>minutes</option>
<option id="format-ms" value="ms">milliseconds</option>
</select>
</div>
<div class="create-alarm__label">Repetition period *</div>
<div class="create-alarm__value">
<input type="number" step="0.1" min="0" name="period" value="0"> minutes
<br><i>Non-zero values create a repeating alarm that repeats every period.</i>
</div>
<div class="create-alarm__label">
*
</div>
<div class="create-alarm__value">
<i>Can be set to &lt; 1 min in an unpacked extension, but not in a distributed CRX file.</i>
</div>
<button type="submit" class="create-alarm__submit">Submit</button>
</form>
</section>
<section class="col-2">
<section class="">
<h2>Current Alarms
<div class="display-buttons">
<button id="clear-display">Cancel all alarms</button>
<button id="refresh-display" title="Clear display and re-recreate alarm UI">Refresh</button>
</div>
</h2>
<pre class="alarm-display"></pre>
</section>
<section> <section>
<h2>Alarm log</h2> <h2>Create Alarm</h2>
<pre class="alarm-log"></pre> <form class="create-alarm">
<div class="create-alarm__label">Name</div>
<div class="create-alarm__value">
<input type="text" name="alarm-name" value="my-alarm" />
</div>
<div class="create-alarm__label">Initial delay *</div>
<div class="create-alarm__value">
<input type="number" step="0.1" name="time-value" min="0" value="1" />
<select name="time-format">
<option id="format-minutes" value="min" selected>minutes</option>
<option id="format-ms" value="ms">milliseconds</option>
</select>
</div>
<div class="create-alarm__label">Repetition period *</div>
<div class="create-alarm__value">
<input type="number" step="0.1" min="0" name="period" value="0" />
minutes <br /><i
>Non-zero values create a repeating alarm that repeats every
period.</i
>
</div>
<div class="create-alarm__label">*</div>
<div class="create-alarm__value">
<i
>Can be set to &lt; 1 min in an unpacked extension, but not in a
distributed CRX file.</i
>
</div>
<button type="submit" class="create-alarm__submit">Submit</button>
</form>
</section> </section>
</section>
</body> <section class="col-2">
<section class="">
<h2>
Current Alarms
<div class="display-buttons">
<button id="clear-display">Cancel all alarms</button>
<button
id="refresh-display"
title="Clear display and re-recreate alarm UI"
>
Refresh
</button>
</div>
</h2>
<pre class="alarm-display"></pre>
</section>
<section>
<h2>Alarm log</h2>
<pre class="alarm-log"></pre>
</section>
</section>
</body>
</html> </html>

View File

@@ -13,26 +13,26 @@ const pad = (val, len = 2) => val.toString().padStart(len, '0');
// DOM event bindings // DOM event bindings
//// Alarm display buttons // Alarm display buttons
clearButton.addEventListener('click', () => manager.cancelAllAlarms()); clearButton.addEventListener('click', () => manager.cancelAllAlarms());
refreshButton.addEventListener('click', () => manager.refreshDisplay()); refreshButton.addEventListener('click', () => manager.refreshDisplay());
//// New alarm form // New alarm form
form.addEventListener('submit', (event) => { form.addEventListener('submit', (event) => {
event.preventDefault(); event.preventDefault();
let formData = new FormData(form); const formData = new FormData(form);
let data = Object.fromEntries(formData); const data = Object.fromEntries(formData);
// Extract form values // Extract form values
let name = data['alarm-name']; const name = data['alarm-name'];
let delay = Number.parseFloat(data['time-value']); const delay = Number.parseFloat(data['time-value']);
let delayFormat = data['time-format']; const delayFormat = data['time-format'];
let period = Number.parseFloat(data['period']); const period = Number.parseFloat(data['period']);
// Prepare alarm info for creation call // Prepare alarm info for creation call
let alarmInfo = {}; const alarmInfo = {};
if (delayFormat === 'ms') { if (delayFormat === 'ms') {
// Specified in milliseconds, use `when` property // Specified in milliseconds, use `when` property
@@ -62,15 +62,15 @@ class AlarmManager {
} }
logMessage(message) { logMessage(message) {
let date = new Date(); const date = new Date();
let pad = (val, len = 2) => val.toString().padStart(len, '0'); const pad = (val, len = 2) => val.toString().padStart(len, '0');
let h = pad(date.getHours()); const h = pad(date.getHours());
let m = pad(date.getMinutes()); const m = pad(date.getMinutes());
let s = pad(date.getSeconds()); const s = pad(date.getSeconds());
let ms = pad(date.getMilliseconds(), 3); const ms = pad(date.getMilliseconds(), 3);
let time = `${h}:${m}:${s}.${ms}`; const time = `${h}:${m}:${s}.${ms}`;
let logLine = document.createElement('div'); const logLine = document.createElement('div');
logLine.textContent = `[${time}] ${message}`; logLine.textContent = `[${time}] ${message}`;
// Log events in reverse chronological order // Log events in reverse chronological order
@@ -78,20 +78,20 @@ class AlarmManager {
} }
handleAlarm = async (alarm) => { handleAlarm = async (alarm) => {
let json = JSON.stringify(alarm); const json = JSON.stringify(alarm);
this.logMessage(`Alarm "${alarm.name}" fired\n${json}}`); this.logMessage(`Alarm "${alarm.name}" fired\n${json}}`);
await this.refreshDisplay(); await this.refreshDisplay();
} };
handleCancelAlarm = async (event) => { handleCancelAlarm = async (event) => {
if (!event.target.classList.contains('alarm-row__cancel-button')) { if (!event.target.classList.contains('alarm-row__cancel-button')) {
return; return;
} }
let name = event.target.parentElement.dataset.name; const name = event.target.parentElement.dataset.name;
await this.cancelAlarm(name); await this.cancelAlarm(name);
await this.refreshDisplay(); await this.refreshDisplay();
} };
async cancelAlarm(name) { async cancelAlarm(name) {
// TODO: Remove custom promise wrapper once the Alarms API supports promises // TODO: Remove custom promise wrapper once the Alarms API supports promises
@@ -111,18 +111,18 @@ class AlarmManager {
// Thin wrapper around alarms.create to log creation event // Thin wrapper around alarms.create to log creation event
createAlarm(name, alarmInfo) { createAlarm(name, alarmInfo) {
chrome.alarms.create(name, alarmInfo); chrome.alarms.create(name, alarmInfo);
let json = JSON.stringify(alarmInfo, null, 2).replace(/\s+/g, ' '); const json = JSON.stringify(alarmInfo, null, 2).replace(/\s+/g, ' ');
this.logMessage(`Created "${name}"\n${json}`); this.logMessage(`Created "${name}"\n${json}`);
this.refreshDisplay(); this.refreshDisplay();
} }
renderAlarm(alarm, isLast) { renderAlarm(alarm, isLast) {
let alarmEl = document.createElement('div'); const alarmEl = document.createElement('div');
alarmEl.classList.add('alarm-row'); alarmEl.classList.add('alarm-row');
alarmEl.dataset.name = alarm.name; alarmEl.dataset.name = alarm.name;
alarmEl.textContent = JSON.stringify(alarm, 0, 2) + (isLast ? '' : ','); alarmEl.textContent = JSON.stringify(alarm, 0, 2) + (isLast ? '' : ',');
let cancelButton = document.createElement('button'); const cancelButton = document.createElement('button');
cancelButton.classList.add('alarm-row__cancel-button'); cancelButton.classList.add('alarm-row__cancel-button');
cancelButton.textContent = 'cancel'; cancelButton.textContent = 'cancel';
alarmEl.appendChild(cancelButton); alarmEl.appendChild(cancelButton);
@@ -142,15 +142,15 @@ class AlarmManager {
resolve(wasCleared); resolve(wasCleared);
}); });
}) });
} }
async populateDisplay() { async populateDisplay() {
// TODO: Remove custom promise wrapper once the Alarms API supports promises // TODO: Remove custom promise wrapper once the Alarms API supports promises
return new Promise((resolve) => { return new Promise((resolve) => {
chrome.alarms.getAll((alarms) => { chrome.alarms.getAll((alarms) => {
for (let [index, alarm] of alarms.entries()) { for (const [index, alarm] of alarms.entries()) {
let isLast = index === alarms.length - 1; const isLast = index === alarms.length - 1;
this.renderAlarm(alarm, isLast); this.renderAlarm(alarm, isLast);
} }
resolve(); resolve();
@@ -163,16 +163,15 @@ class AlarmManager {
#refreshing = false; #refreshing = false;
async refreshDisplay() { async refreshDisplay() {
if (this.#refreshing) { return } // refresh in progress, bail if (this.#refreshing) {
return;
} // refresh in progress, bail
this.#refreshing = true; // acquire lock this.#refreshing = true; // acquire lock
try { try {
await Promise.all([ await Promise.all([this.clearDisplay(), this.populateDisplay()]);
this.clearDisplay(),
this.populateDisplay(),
]);
} finally { } finally {
this.#refreshing = false; // release lock this.#refreshing = false; // release lock
} }
} }
@@ -181,5 +180,5 @@ class AlarmManager {
} }
} }
let manager = new AlarmManager(display, log); const manager = new AlarmManager(display, log);
manager.refreshDisplay(); manager.refreshDisplay();

View File

@@ -5,8 +5,6 @@
"background": { "background": {
"service_worker": "bg-wrapper.js" "service_worker": "bg-wrapper.js"
}, },
"permissions": [ "permissions": ["alarms"],
"alarms"
],
"action": {} "action": {}
} }

View File

@@ -3,56 +3,54 @@
// found in the LICENSE file. // found in the LICENSE file.
// When you specify "type": "module" in the manifest background, // When you specify "type": "module" in the manifest background,
// you can include the service worker as an ES Module, // you can include the service worker as an ES Module,
import { tldLocales } from './locales.js' import { tldLocales } from './locales.js';
// Add a listener to create the initial context menu items, // Add a listener to create the initial context menu items,
// context menu items only need to be created at runtime.onInstalled // context menu items only need to be created at runtime.onInstalled
chrome.runtime.onInstalled.addListener(async () => { chrome.runtime.onInstalled.addListener(async () => {
for (let [tld, locale] of Object.entries(tldLocales)) { for (const [tld, locale] of Object.entries(tldLocales)) {
chrome.contextMenus.create({ chrome.contextMenus.create({
id: tld, id: tld,
title: locale, title: locale,
type: 'normal', type: 'normal',
contexts: ['selection'], contexts: ['selection']
}); });
} }
}); });
// Open a new search tab when the user clicks a context menu // Open a new search tab when the user clicks a context menu
chrome.contextMenus.onClicked.addListener((item, tab) => { chrome.contextMenus.onClicked.addListener((item, tab) => {
const tld = item.menuItemId const tld = item.menuItemId;
let url = new URL(`https://google.${tld}/search`) const url = new URL(`https://google.${tld}/search`);
url.searchParams.set('q', item.selectionText) url.searchParams.set('q', item.selectionText);
chrome.tabs.create({ url: url.href, index: tab.index + 1 }); chrome.tabs.create({ url: url.href, index: tab.index + 1 });
}); });
// Add or removes the locale from context menu // Add or removes the locale from context menu
// when the user checks or unchecks the locale in the popup // when the user checks or unchecks the locale in the popup
chrome.storage.onChanged.addListener(({ enabledTlds }) => { chrome.storage.onChanged.addListener(({ enabledTlds }) => {
if (typeof enabledTlds === 'undefined') return if (typeof enabledTlds === 'undefined') return;
let allTlds = Object.keys(tldLocales) const allTlds = Object.keys(tldLocales);
let currentTlds = new Set(enabledTlds.newValue); const currentTlds = new Set(enabledTlds.newValue);
let oldTlds = new Set(enabledTlds.oldValue ?? allTlds); const oldTlds = new Set(enabledTlds.oldValue ?? allTlds);
let changes = allTlds.map((tld) => ({ const changes = allTlds.map((tld) => ({
tld, tld,
added: currentTlds.has(tld) && !oldTlds.has(tld), added: currentTlds.has(tld) && !oldTlds.has(tld),
removed: !currentTlds.has(tld) && oldTlds.has(tld) removed: !currentTlds.has(tld) && oldTlds.has(tld)
})) }));
for (let { tld, added, removed } of changes) { for (const { tld, added, removed } of changes) {
if (added) { if (added) {
chrome.contextMenus.create({ chrome.contextMenus.create({
id: tld, id: tld,
title: tldLocales[tld], title: tldLocales[tld],
type: 'normal', type: 'normal',
contexts: ['selection'], contexts: ['selection']
}); });
} } else if (removed) {
else if (removed) {
chrome.contextMenus.remove(tld); chrome.contextMenus.remove(tld);
} }
} }
}); });

View File

@@ -6,14 +6,14 @@
export const tldLocales = { export const tldLocales = {
'com.au': 'Australia', 'com.au': 'Australia',
'com.br': 'Brazil', 'com.br': 'Brazil',
'ca': 'Canada', ca: 'Canada',
'cn': 'China', cn: 'China',
'fr': 'France', fr: 'France',
'it': 'Italy', it: 'Italy',
'co.in': 'India', 'co.in': 'India',
'co.jp': 'Japan', 'co.jp': 'Japan',
'com.ms': 'Mexico', 'com.ms': 'Mexico',
'ru': 'Russia', ru: 'Russia',
'co.za': 'South Africa', 'co.za': 'South Africa',
'co.uk': 'United Kingdom' 'co.uk': 'United Kingdom'
}; };

View File

@@ -12,8 +12,8 @@
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"icons": { "icons": {
"16": "globalGoogle16.png", "16": "globalGoogle16.png",
"48": "globalGoogle48.png", "48": "globalGoogle48.png",
"128": "globalGoogle128.png" "128": "globalGoogle128.png"
} }
} }

View File

@@ -1,26 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title>Global Context Search</title>
<style>
body {
min-width: 300px;
font-size: 15px;
}
<head> input {
<title>Global Context Search</title> margin: 5px;
<style> outline: none;
body { }
min-width: 300px; </style>
font-size: 15px; </head>
}
input { <body>
margin: 5px; <h2>Global Google Search</h2>
outline: none; <h3>Countries</h3>
} <form id="form"></form>
</style> <script src="popup.js" type="module"></script>
</head> </body>
</html>
<body>
<h2>Global Google Search</h2>
<h3>Countries</h3>
<form id="form"></form>
<script src="popup.js" type="module"></script>
</body>
</html>

View File

@@ -3,48 +3,47 @@
// found in the LICENSE file. // found in the LICENSE file.
// TLD: top level domain; the "com" in "google.com" // TLD: top level domain; the "com" in "google.com"
import { tldLocales } from './locales.js' import { tldLocales } from './locales.js';
createForm().catch(console.error); createForm().catch(console.error);
async function createForm() { async function createForm() {
let { enabledTlds = Object.keys(tldLocales) } = await chrome.storage.sync.get('enabledTlds'); const { enabledTlds = Object.keys(tldLocales) } =
let checked = new Set(enabledTlds) await chrome.storage.sync.get('enabledTlds');
const checked = new Set(enabledTlds);
let form = document.getElementById('form'); const form = document.getElementById('form');
for (let [tld, locale] of Object.entries(tldLocales)) { for (const [tld, locale] of Object.entries(tldLocales)) {
let checkbox = document.createElement('input'); const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.type = 'checkbox';
checkbox.checked = checked.has(tld); checkbox.checked = checked.has(tld);
checkbox.name = tld; checkbox.name = tld;
checkbox.addEventListener('click', (event) => { checkbox.addEventListener('click', (event) => {
handleCheckboxClick(event).catch(console.error) handleCheckboxClick(event).catch(console.error);
}) });
let span = document.createElement('span'); const span = document.createElement('span');
span.textContent = locale; span.textContent = locale;
let div = document.createElement('div'); const div = document.createElement('div');
div.appendChild(checkbox); div.appendChild(checkbox);
div.appendChild(span); div.appendChild(span);
form.appendChild(div); form.appendChild(div);
} }
} }
async function handleCheckboxClick(event) { async function handleCheckboxClick(event) {
let checkbox = event.target const checkbox = event.target;
let tld = checkbox.name const tld = checkbox.name;
let enabled = checkbox.checked const enabled = checkbox.checked;
let { enabledTlds = Object.keys(tldLocales) } = await chrome.storage.sync.get('enabledTlds'); const { enabledTlds = Object.keys(tldLocales) } =
let tldSet = new Set(enabledTlds) await chrome.storage.sync.get('enabledTlds');
const tldSet = new Set(enabledTlds);
if (enabled) tldSet.add(tld)
else tldSet.delete(tld) if (enabled) tldSet.add(tld);
else tldSet.delete(tld);
await chrome.storage.sync.set({ enabledTlds: [...tldSet] })
await chrome.storage.sync.set({ enabledTlds: [...tldSet] });
} }

View File

@@ -7,4 +7,4 @@
"action": { "action": {
"default_popup": "popup.html" "default_popup": "popup.html"
} }
} }

View File

@@ -1,15 +1,15 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="popup.js" type="module"></script> <script src="popup.js" type="module"></script>
</head> </head>
<body> <body>
<form id="control-row"> <form id="control-row">
<label for="input">Domain:</label> <label for="input">Domain:</label>
<input type="text" id="input"> <input type="text" id="input" />
<br> <br />
<button id="go">Clear Cookies</button> <button id="go">Clear Cookies</button>
</form> </form>
<span id="message" hidden></span> <span id="message" hidden></span>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,7 @@
const form = document.getElementById("control-row"); const form = document.getElementById('control-row');
const go = document.getElementById("go"); const go = document.getElementById('go');
const input = document.getElementById("input"); const input = document.getElementById('input');
const message = document.getElementById("message"); const message = document.getElementById('message');
// The async IIFE is necessary because Chrome <89 does not support top level await. // The async IIFE is necessary because Chrome <89 does not support top level await.
(async function initPopupWindow() { (async function initPopupWindow() {
@@ -11,13 +11,15 @@ const message = document.getElementById("message");
try { try {
let url = new URL(tab.url); let url = new URL(tab.url);
input.value = url.hostname; input.value = url.hostname;
} catch {} } catch {
// ignore
}
} }
input.focus(); input.focus();
})(); })();
form.addEventListener("submit", handleFormSubmit); form.addEventListener('submit', handleFormSubmit);
async function handleFormSubmit(event) { async function handleFormSubmit(event) {
event.preventDefault(); event.preventDefault();
@@ -26,7 +28,7 @@ async function handleFormSubmit(event) {
let url = stringToUrl(input.value); let url = stringToUrl(input.value);
if (!url) { if (!url) {
setMessage("Invalid URL"); setMessage('Invalid URL');
return; return;
} }
@@ -38,11 +40,15 @@ function stringToUrl(input) {
// Start with treating the provided value as a URL // Start with treating the provided value as a URL
try { try {
return new URL(input); return new URL(input);
} catch {} } catch {
// ignore
}
// If that fails, try assuming the provided input is an HTTP host // If that fails, try assuming the provided input is an HTTP host
try { try {
return new URL("http://" + input); return new URL('http://' + input);
} catch {} } catch {
// ignore
}
// If that fails ¯\_(ツ)_/¯ // If that fails ¯\_(ツ)_/¯
return null; return null;
} }
@@ -53,7 +59,7 @@ async function deleteDomainCookies(domain) {
const cookies = await chrome.cookies.getAll({ domain }); const cookies = await chrome.cookies.getAll({ domain });
if (cookies.length === 0) { if (cookies.length === 0) {
return "No cookies found"; return 'No cookies found';
} }
let pending = cookies.map(deleteCookie); let pending = cookies.map(deleteCookie);
@@ -76,7 +82,7 @@ function deleteCookie(cookie) {
// To remove cookies set with a Secure attribute, we must provide the correct protocol in the // To remove cookies set with a Secure attribute, we must provide the correct protocol in the
// details object's `url` property. // details object's `url` property.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Secure // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Secure
const protocol = cookie.secure ? "https:" : "http:"; const protocol = cookie.secure ? 'https:' : 'http:';
// Note that the final URL may not be valid. The domain value for a standard cookie is prefixed // Note that the final URL may not be valid. The domain value for a standard cookie is prefixed
// with a period (invalid) while cookies that are set to `cookie.hostOnly == true` do not have // with a period (invalid) while cookies that are set to `cookie.hostOnly == true` do not have
@@ -87,7 +93,7 @@ function deleteCookie(cookie) {
return chrome.cookies.remove({ return chrome.cookies.remove({
url: cookieUrl, url: cookieUrl,
name: cookie.name, name: cookie.name,
storeId: cookie.storeId, storeId: cookie.storeId
}); });
} }
@@ -98,5 +104,5 @@ function setMessage(str) {
function clearMessage() { function clearMessage() {
message.hidden = true; message.hidden = true;
message.textContent = ""; message.textContent = '';
} }

View File

@@ -4,13 +4,18 @@
chrome.commands.onCommand.addListener(async (command) => { chrome.commands.onCommand.addListener(async (command) => {
const tabs = await chrome.tabs.query({ currentWindow: true }); const tabs = await chrome.tabs.query({ currentWindow: true });
// Sort tabs according to their index in the window. // Sort tabs according to their index in the window.
tabs.sort((a, b) => { return a.index < b.index; }); tabs.sort((a, b) => {
let activeIndex = tabs.findIndex((tab) => { return tab.active; }); return a.index < b.index;
let lastTab = tabs.length - 1; });
const activeIndex = tabs.findIndex((tab) => {
return tab.active;
});
const lastTab = tabs.length - 1;
let newIndex = -1; let newIndex = -1;
if (command === 'flip-tabs-forward') if (command === 'flip-tabs-forward') {
newIndex = activeIndex === 0 ? lastTab : activeIndex - 1; newIndex = activeIndex === 0 ? lastTab : activeIndex - 1;
else // 'flip-tabs-backwards' }
newIndex = activeIndex === lastTab ? 0 : activeIndex + 1; // 'flip-tabs-backwards'
else newIndex = activeIndex === lastTab ? 0 : activeIndex + 1;
chrome.tabs.update(tabs[newIndex].id, { active: true, highlighted: true }); chrome.tabs.update(tabs[newIndex].id, { active: true, highlighted: true });
}); });

View File

@@ -32,4 +32,4 @@
"48": "images/tabFlipper48.png", "48": "images/tabFlipper48.png",
"128": "images/tabFlipper128.png" "128": "images/tabFlipper128.png"
} }
} }

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<body> <body>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
function faviconURL(u) { function faviconURL(u) {
const url = new URL(chrome.runtime.getURL("/_favicon/")); const url = new URL(chrome.runtime.getURL('/_favicon/'));
url.searchParams.set("pageUrl", u); // this encodes the URL as well url.searchParams.set('pageUrl', u); // this encodes the URL as well
url.searchParams.set("size", "32"); url.searchParams.set('size', '32');
return url.toString(); return url.toString();
} }
const img = document.createElement('img'); const img = document.createElement('img');
// chrome-extension://EXTENSION_ID/_favicon/?pageUrl=https%3A%2F%2Fwww.google.com&size=32 // chrome-extension://EXTENSION_ID/_favicon/?pageUrl=https%3A%2F%2Fwww.google.com&size=32
img.src = faviconURL("https://www.google.com") img.src = faviconURL('https://www.google.com');
document.body.appendChild(img); document.body.appendChild(img);

View File

@@ -5,6 +5,6 @@
// This event is fired with the user accepts the input in the omnibox. // This event is fired with the user accepts the input in the omnibox.
chrome.omnibox.onInputEntered.addListener((text) => { chrome.omnibox.onInputEntered.addListener((text) => {
// Encode user input for special characters , / ? : @ & = + $ # // Encode user input for special characters , / ? : @ & = + $ #
var newURL = 'https://www.google.com/search?q=' + encodeURIComponent(text); const newURL = 'https://www.google.com/search?q=' + encodeURIComponent(text);
chrome.tabs.create({ url: newURL }); chrome.tabs.create({ url: newURL });
}); });

View File

@@ -6,12 +6,12 @@
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
"omnibox": { "keyword" : "nt" }, "omnibox": { "keyword": "nt" },
"action": { "action": {
"default_icon": { "default_icon": {
"16": "newtab_search16.png", "16": "newtab_search16.png",
"32": "newtab_search32.png" "32": "newtab_search32.png"
} }
}, },
"icons": { "icons": {
"16": "newtab_search16.png", "16": "newtab_search16.png",

View File

@@ -3,11 +3,11 @@
* source code is governed by a BSD-style license that can be found in the * source code is governed by a BSD-style license that can be found in the
* LICENSE file. * LICENSE file.
--> -->
<!DOCTYPE HTML> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Printers</title> <title>Printers</title>
<link href="printers.css" rel="stylesheet" type="text/css"> <link href="printers.css" rel="stylesheet" type="text/css" />
<script src="printers.js"></script> <script src="printers.js"></script>
</head> </head>

View File

@@ -6,42 +6,46 @@ function onPrintButtonClicked(printerId, dpi) {
var ticket = { var ticket = {
version: '1.0', version: '1.0',
print: { print: {
color: {type: 'STANDARD_MONOCHROME'}, color: { type: 'STANDARD_MONOCHROME' },
duplex: {type: 'NO_DUPLEX'}, duplex: { type: 'NO_DUPLEX' },
page_orientation: {type: 'LANDSCAPE'}, page_orientation: { type: 'LANDSCAPE' },
copies: {copies: 1}, copies: { copies: 1 },
dpi: {horizontal_dpi: dpi.horizontal_dpi, vertical_dpi: dpi.vertical_dpi}, dpi: {
horizontal_dpi: dpi.horizontal_dpi,
vertical_dpi: dpi.vertical_dpi
},
media_size: { media_size: {
width_microns: 210000, width_microns: 210000,
height_microns: 297000, height_microns: 297000,
vendor_id: 'iso_a4_210x297mm' vendor_id: 'iso_a4_210x297mm'
}, },
collate: {collate: false} collate: { collate: false }
} }
}; };
fetch('test.pdf') fetch('test.pdf')
.then(response => response.arrayBuffer()) .then((response) => response.arrayBuffer())
.then(arrayBuffer => { .then((arrayBuffer) => {
const request = { const request = {
job: { job: {
printerId: printerId, printerId: printerId,
title: 'test job', title: 'test job',
ticket: ticket, ticket: ticket,
contentType: 'application/pdf', contentType: 'application/pdf',
document: new Blob( document: new Blob([new Uint8Array(arrayBuffer)], {
[new Uint8Array(arrayBuffer)], {type: 'application/pdf'}) type: 'application/pdf'
} })
}; }
chrome.printing.submitJob(request, (response) => { };
if (response !== undefined) { chrome.printing.submitJob(request, (response) => {
console.log(response.status); if (response !== undefined) {
} console.log(response.status);
if (chrome.runtime.lastError !== undefined) { }
console.log(chrome.runtime.lastError.message); if (chrome.runtime.lastError !== undefined) {
} console.log(chrome.runtime.lastError.message);
}); }
}); });
});
} }
function createPrintButton(onClicked) { function createPrintButton(onClicked) {
@@ -52,12 +56,12 @@ function createPrintButton(onClicked) {
} }
function createPrintersTable() { function createPrintersTable() {
chrome.printing.getPrinters(function(printers) { chrome.printing.getPrinters(function (printers) {
const tbody = document.createElement('tbody'); const tbody = document.createElement('tbody');
for (let i = 0; i < printers.length; ++i) { for (let i = 0; i < printers.length; ++i) {
const printer = printers[i]; const printer = printers[i];
chrome.printing.getPrinterInfo(printer.id, function(response) { chrome.printing.getPrinterInfo(printer.id, function (response) {
const columnValues = [ const columnValues = [
printer.id, printer.id,
printer.name, printer.name,
@@ -67,7 +71,7 @@ function createPrintersTable() {
printer.isDefault, printer.isDefault,
printer.recentlyUsedRank, printer.recentlyUsedRank,
JSON.stringify(response.capabilities), JSON.stringify(response.capabilities),
response.status, response.status
]; ];
let tr = document.createElement('tr'); let tr = document.createElement('tr');
@@ -79,10 +83,14 @@ function createPrintersTable() {
} }
const printTd = document.createElement('td'); const printTd = document.createElement('td');
printTd.appendChild(createPrintButton(function() { printTd.appendChild(
onPrintButtonClicked( createPrintButton(function () {
printer.id, response.capabilities.printer.dpi.option[0]); onPrintButtonClicked(
})); printer.id,
response.capabilities.printer.dpi.option[0]
);
})
);
tr.appendChild(printTd); tr.appendChild(printTd);
tbody.appendChild(tr); tbody.appendChild(tr);
@@ -94,6 +102,6 @@ function createPrintersTable() {
}); });
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
createPrintersTable(); createPrintersTable();
}); });

View File

@@ -9,6 +9,6 @@ chrome.action.onClicked.addListener((tab) => {
}); });
function showReadme(info, tab) { function showReadme(info, tab) {
let url = chrome.runtime.getURL("readme.html"); const url = chrome.runtime.getURL('readme.html');
chrome.tabs.create({ url }); chrome.tabs.create({ url });
} }

View File

@@ -1,14 +1,14 @@
let imageIds = ["test2", "test4"]; const imageIds = ['test2', 'test4'];
let loadButton = document.createElement('button'); const loadButton = document.createElement('button');
loadButton.innerText = 'Load images'; loadButton.innerText = 'Load images';
loadButton.addEventListener('click', handleLoadRequest); loadButton.addEventListener('click', handleLoadRequest);
document.querySelector('body').append(loadButton); document.querySelector('body').append(loadButton);
function handleLoadRequest() { function handleLoadRequest() {
for (let id of imageIds) { for (const id of imageIds) {
let element = document.getElementById(id); const element = document.getElementById(id);
element.src = chrome.runtime.getURL(`${id}.png`); element.src = chrome.runtime.getURL(`${id}.png`);
} }
} }

View File

@@ -18,13 +18,14 @@
], ],
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": [ "test1.png", "test2.png" ], "resources": ["test1.png", "test2.png"],
"matches": [ "https://web-accessible-resources-1.glitch.me/*" ] "matches": ["https://web-accessible-resources-1.glitch.me/*"]
}, { },
"resources": [ "test3.png", "test4.png" ], {
"matches": [ "https://web-accessible-resources-2.glitch.me/*" ], "resources": ["test3.png", "test4.png"],
"matches": ["https://web-accessible-resources-2.glitch.me/*"],
"use_dynamic_url": true "use_dynamic_url": true
} }
], ],
"key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQCnCTnUK8jgYTxnQLdtE6QzkZgn3rZv0U1naCx4csdSDqYEBXgW2pR2m/uUIAU1HzAUfkDckqTezyIG1bPw8l5X8FyWfgMQANFgTPXGRNXTmDSqHcqvS7zvuEr0xF12oGLBKa7cdEsaQzdfDWsm5BlwFIPfPXUokaHEGvxPBjrXHQmx+Z4xAyhzNh+v5bFr63lsL0ysS8z4KVKc1G1lcUZnp7Oz9n0pZP9QW0Oei2KCumDqGpqVd249232a0E9TUeQ+lqAxiN4ybzBgUT5al7Yh1nIhGHxPyRnihtHmx+hxupCuhzXeaoKjWiADp+FEK/aPAzvP5ynLDQHelez/eGdF" "key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQCnCTnUK8jgYTxnQLdtE6QzkZgn3rZv0U1naCx4csdSDqYEBXgW2pR2m/uUIAU1HzAUfkDckqTezyIG1bPw8l5X8FyWfgMQANFgTPXGRNXTmDSqHcqvS7zvuEr0xF12oGLBKa7cdEsaQzdfDWsm5BlwFIPfPXUokaHEGvxPBjrXHQmx+Z4xAyhzNh+v5bFr63lsL0ysS8z4KVKc1G1lcUZnp7Oz9n0pZP9QW0Oei2KCumDqGpqVd249232a0E9TUeQ+lqAxiN4ybzBgUT5al7Yh1nIhGHxPyRnihtHmx+hxupCuhzXeaoKjWiADp+FEK/aPAzvP5ynLDQHelez/eGdF"
} }

View File

@@ -3,46 +3,127 @@
License: none (public domain) License: none (public domain)
*/ */
html, body, div, span, applet, object, iframe, html,
h1, h2, h3, h4, h5, h6, p, blockquote, pre, body,
a, abbr, acronym, address, big, cite, code, div,
del, dfn, em, img, ins, kbd, q, s, samp, span,
small, strike, strong, sub, sup, tt, var, applet,
b, u, i, center, object,
dl, dt, dd, ol, ul, li, iframe,
fieldset, form, label, legend, h1,
table, caption, tbody, tfoot, thead, tr, th, td, h2,
article, aside, canvas, details, embed, h3,
figure, figcaption, footer, header, hgroup, h4,
menu, nav, output, ruby, section, summary, h5,
time, mark, audio, video { h6,
margin: 0; p,
padding: 0; blockquote,
border: 0; pre,
font-size: 100%; a,
font: inherit; abbr,
vertical-align: baseline; acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
} }
/* HTML5 display-role reset for older browsers */ /* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, article,
footer, header, hgroup, menu, nav, section { aside,
display: block; details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
} }
body { body {
line-height: 1; line-height: 1;
} }
ol, ul { ol,
list-style: none; ul {
list-style: none;
} }
blockquote, q { blockquote,
quotes: none; q {
quotes: none;
} }
blockquote:before, blockquote:after, blockquote:before,
q:before, q:after { blockquote:after,
content: ''; q:before,
content: none; q:after {
content: "";
content: none;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
} }

View File

@@ -1,91 +1,123 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Accessible Resources - Readme</title> <title>Web Accessible Resources - Readme</title>
<style> <style>
body { body {
font-family: Helvetica, Arial, sans-serif; font-family: Helvetica, Arial, sans-serif;
font-size: 14px; font-size: 14px;
} }
table { table {
padding: 0; padding: 0;
border-collapse: collapse; border-collapse: collapse;
}
th {
background: hsl(0, 0%, 90%);
padding: 0.25em 0.5em;
text-align: left;
}
td {
padding: 0.25em 0.5em;
border-top: 1px solid hsl(0, 0%, 50%);
}
</style>
</head>
<body>
<h1>Web Accessible Resources Demo</h1>
<p>This demo shows off the core features of web accessible resources.</p>
<p>
In this demo we have 4 images (test1.png, etc.) that we want to expose on
2 different websites. Each website should only be able to load two
specific images, but both websites will attempt to access all 4 images. To
do this, we define a set of
<a
href="https://developer.chrome.com/docs/extensions/mv3/manifest/web_accessible_resources/"
><code>"web_accessable_resources"</code></a
>
in our <a href="manifest.json">manifest.json</a>. This object specifies
what assets should be accessible to which external resources.
</p>
} <p>
th { The first image on each site is statically referenced by the site using a
background: hsl(0,0%,90%); URL in the following format:
padding: .25em .5em; <code>chrome-extension://&lt;extension-id>/&lt;image-path></code>. The
text-align: left; second image on each site will only be injected into the page when you
} click the "Load images" button for that page. This injection is performed
td { by using
padding: .25em .5em; <a
border-top: 1px solid hsl(0,0%,50%); href="https://developer.chrome.com/docs/extensions/reference/runtime/#method-getURL"
} >chrome.runtime.getURL()</a
</style> >
</head> to build the image's URL at runtime.
<body> </p>
<h1>Web Accessible Resources Demo</h1>
<p>This demo shows off the core features of web accessible resources.</p>
<p>In this demo we have 4 images (test1.png, etc.) that we want to expose on 2 different websites.
Each website should only be able to load two specific images, but both websites will attempt to
access all 4 images. To do this, we define a set of <a
href="https://developer.chrome.com/docs/extensions/mv3/manifest/web_accessible_resources/"><code>"web_accessable_resources"</code></a>
in our <a href="manifest.json">manifest.json</a>. This object specifies what assets should be
accessible to which external resources.</p>
<p>The first image on each site is statically referenced by the site using a URL in the following <table>
format: <code>chrome-extension://&lt;extension-id>/&lt;image-path></code>. The second image on <thead>
each site will only be injected into the page when you click the "Load images" button for that <tr>
page. This injection is performed by using <a <th>File</th>
href="https://developer.chrome.com/docs/extensions/reference/runtime/#method-getURL">chrome.runtime.getURL()</a> <th>Target domain</th>
to build the image's URL at runtime.</p> <th>Injection method</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code><a href="test1.png">test1.png</a></code>
</td>
<td>web-accessible-resources-1.glitch.me</td>
<td>Statically referenced</td>
</tr>
<tr>
<td>
<code><a href="test2.png">test2.png</a></code>
</td>
<td>web-accessible-resources-1.glitch.me</td>
<td>Dynamically injected</td>
</tr>
<tr>
<td>
<code><a href="test3.png">test3.png</a></code>
</td>
<td>web-accessible-resources-2.glitch.me</td>
<td>Statically referenced</td>
</tr>
<tr>
<td>
<code><a href="test4.png">test4.png</a></code>
</td>
<td>web-accessible-resources-2.glitch.me</td>
<td>Dynamically injected</td>
</tr>
</tbody>
</table>
<table> <figure>
<thead> <figcaption>
<tr> https://web-accessible-resources-1.glitch.me/ can access images
<th>File</th> <a href="test1.png"><code>test1.png</code></a> and
<th>Target domain</th> <a href="test2.png"><code>test2.png</code></a>
<th>Injection method</th> </figcaption>
</tr> <iframe
</thead> src="https://web-accessible-resources-1.glitch.me/"
<tbody> width="100%"
<tr> height="200"
<td><code><a href="test1.png">test1.png</a></code></td> ></iframe>
<td>web-accessible-resources-1.glitch.me</td> </figure>
<td>Statically referenced</td> <figure>
</tr> <figcaption>
<tr> https://web-accessible-resources-2.glitch.me/ can access images
<td><code><a href="test2.png">test2.png</a></code></td> <a href="test3.png"><code>test3.png</code></a> and
<td>web-accessible-resources-1.glitch.me</td> <a href="test4.png"><code>test4.png</code></a>
<td>Dynamically injected</td> </figcaption>
</tr> <iframe
<tr> src="https://web-accessible-resources-2.glitch.me/"
<td><code><a href="test3.png">test3.png</a></code></td> width="100%"
<td>web-accessible-resources-2.glitch.me</td> height="200"
<td>Statically referenced</td> ></iframe>
</tr> </figure>
<tr> </body>
<td><code><a href="test4.png">test4.png</a></code></td>
<td>web-accessible-resources-2.glitch.me</td>
<td>Dynamically injected</td>
</tr>
</tbody>
</table>
<figure>
<figcaption>https://web-accessible-resources-1.glitch.me/ can access images
<a href="test1.png"><code>test1.png</code></a> and <a
href="test2.png"><code>test2.png</code></a></figcaption>
<iframe src="https://web-accessible-resources-1.glitch.me/" width=100% height=200></iframe>
</figure>
<figure>
<figcaption>https://web-accessible-resources-2.glitch.me/ can access images
<a href="test3.png"><code>test3.png</code></a> and <a
href="test4.png"><code>test4.png</code></a></figcaption>
<iframe src="https://web-accessible-resources-2.glitch.me/" width=100% height=200></iframe>
</figure>
</body>
</html> </html>

View File

@@ -29,7 +29,7 @@ async function addToClipboard(value) {
await chrome.offscreen.createDocument({ await chrome.offscreen.createDocument({
url: 'offscreen.html', url: 'offscreen.html',
reasons: [chrome.offscreen.Reason.CLIPBOARD], reasons: [chrome.offscreen.Reason.CLIPBOARD],
justification: 'Write text to the clipboard.', justification: 'Write text to the clipboard.'
}); });
// Now that we have an offscreen document, we can dispatch the // Now that we have an offscreen document, we can dispatch the
@@ -37,11 +37,11 @@ async function addToClipboard(value) {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
type: 'copy-data-to-clipboard', type: 'copy-data-to-clipboard',
target: 'offscreen-doc', target: 'offscreen-doc',
data: value, data: value
}); });
} }
// Solution 2  Once extension service workers can use the Clipboard API, // Solution 2 Once extension service workers can use the Clipboard API,
// replace the offscreen document based implementation with something like this. // replace the offscreen document based implementation with something like this.
async function addToClipboardV2(value) { async function addToClipboardV2(value) {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);

View File

@@ -6,8 +6,5 @@
"service_worker": "background.js" "service_worker": "background.js"
}, },
"action": {}, "action": {},
"permissions": [ "permissions": ["offscreen", "clipboardWrite"]
"offscreen",
"clipboardWrite"
]
} }

View File

@@ -1,3 +1,3 @@
<!DOCTYPE html> <!DOCTYPE html>
<textarea id="text"></textarea> <textarea id="text"></textarea>
<script src="offscreen.js""></script> <script src="offscreen.js"></script>

View File

@@ -41,11 +41,10 @@ async function handleMessages(message) {
} }
} }
// We use a <textarea> element for two main reasons: // We use a <textarea> element for two main reasons:
// 1. preserve the formatting of multiline text, // 1. preserve the formatting of multiline text,
// 2. select the node's content using this element's `.select()` method. // 2. select the node's content using this element's `.select()` method.
let textEl = document.querySelector('#text'); const textEl = document.querySelector('#text');
// Use the offscreen document's `document` interface to write a new value to the // Use the offscreen document's `document` interface to write a new value to the
// system clipboard. // system clipboard.
@@ -56,7 +55,9 @@ let textEl = document.querySelector('#text');
async function handleClipboardWrite(data) { async function handleClipboardWrite(data) {
// Error if we received the wrong kind of data. // Error if we received the wrong kind of data.
if (typeof data !== 'string') { if (typeof data !== 'string') {
throw new TypeError(`Value provided must be a 'string', got '${typeof data}'.`); throw new TypeError(
`Value provided must be a 'string', got '${typeof data}'.`
);
} }
// `document.execCommand('copy')` works against the user's selection in a web // `document.execCommand('copy')` works against the user's selection in a web
@@ -66,6 +67,6 @@ async function handleClipboardWrite(data) {
textEl.select(); textEl.select();
document.execCommand('copy'); document.execCommand('copy');
//Job's done! Close the offscreen document. // Job's done! Close the offscreen document.
window.close(); window.close();
} }

View File

@@ -5,10 +5,7 @@
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
"permissions": [ "permissions": ["scripting", "activeTab"],
"scripting",
"activeTab"
],
"action": { "action": {
"default_popup": "popup.html" "default_popup": "popup.html"
} }

View File

@@ -13,7 +13,7 @@ body {
min-height: 10em; min-height: 10em;
} }
main { main {
padding: 1em .5em; padding: 1em 0.5em;
display: grid; display: grid;
place-items: center; place-items: center;
} }

View File

@@ -1,21 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css" />
<script src="popup.js" defer></script> <script src="popup.js" defer></script>
</head> </head>
<body> <body>
<main> <main>
<div> <div>
<button id="inject-file">Inject file</button> <button id="inject-file">Inject file</button>
</div> </div>
<div> <div>
<button id="inject-function">Inject function</button> <button id="inject-function">Inject function</button>
</div> </div>
</main> </main>
</body> </body>
</html> </html>

View File

@@ -1,17 +1,17 @@
let injectFile = document.getElementById('inject-file'); const injectFile = document.getElementById('inject-file');
let injectFunction = document.getElementById('inject-function'); const injectFunction = document.getElementById('inject-function');
async function getCurrentTab() { async function getCurrentTab() {
let queryOptions = { active: true, currentWindow: true }; const queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions); const [tab] = await chrome.tabs.query(queryOptions);
return tab; return tab;
} }
injectFile.addEventListener('click', async () => { injectFile.addEventListener('click', async () => {
let tab = await getCurrentTab(); const tab = await getCurrentTab();
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: {tabId: tab.id}, target: { tabId: tab.id },
files: ['content-script.js'] files: ['content-script.js']
}); });
}); });
@@ -21,11 +21,11 @@ function showAlert(givenName) {
} }
injectFunction.addEventListener('click', async () => { injectFunction.addEventListener('click', async () => {
let tab = await getCurrentTab(); const tab = await getCurrentTab();
let name = 'World'; const name = 'World';
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: {tabId: tab.id}, target: { tabId: tab.id },
func: showAlert, func: showAlert,
args: [name] args: [name]
}); });

View File

@@ -3,9 +3,7 @@
"description": "A browser action with a popup dump of all bookmarks, including search, add, edit and delete.", "description": "A browser action with a popup dump of all bookmarks, including search, add, edit and delete.",
"version": "1.1", "version": "1.1",
"manifest_version": 3, "manifest_version": 3,
"permissions": [ "permissions": ["bookmarks"],
"bookmarks"
],
"action": { "action": {
"default_title": "My Bookmarks", "default_title": "My Bookmarks",
"default_icon": "icon.png", "default_icon": "icon.png",

View File

@@ -1,3 +1,3 @@
#editdialog input { #editdialog input {
width: 100% width: 100%;
} }

View File

@@ -1,19 +1,19 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="third-party/jquery-ui.css"> <link rel="stylesheet" href="third-party/jquery-ui.css" />
<link rel="stylesheet" href="third-party/jquery-ui.structure.css"> <link rel="stylesheet" href="third-party/jquery-ui.structure.css" />
<link rel="stylesheet" href="third-party/jquery-ui.theme.css"> <link rel="stylesheet" href="third-party/jquery-ui.theme.css" />
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css" />
<script src="third-party/jquery-1.12.4.js"></script> <script src="third-party/jquery-1.12.4.js"></script>
<script src="third-party/jquery-ui-1.12.1.js"></script> <script src="third-party/jquery-ui-1.12.1.js"></script>
</head> </head>
<body style="width: 400px"> <body style="width: 400px">
<div>Search Bookmarks: <input id="search"></div> <div>Search Bookmarks: <input id="search" /></div>
<div id="bookmarks"></div> <div id="bookmarks"></div>
<div id="editdialog"></div> <div id="editdialog"></div>
<div id="deletedialog"></div> <div id="deletedialog"></div>
<div id="adddialog"></div> <div id="adddialog"></div>
<div id="test-frame"></div> <div id="test-frame"></div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

View File

@@ -12,14 +12,16 @@ $('#search').change(function () {
// Traverse the bookmark tree, and print the folder and nodes. // Traverse the bookmark tree, and print the folder and nodes.
function dumpBookmarks(query) { function dumpBookmarks(query) {
var bookmarkTreeNodes = chrome.bookmarks.getTree(function (bookmarkTreeNodes) { const bookmarkTreeNodes = chrome.bookmarks.getTree(function (
bookmarkTreeNodes
) {
$('#bookmarks').append(dumpTreeNodes(bookmarkTreeNodes, query)); $('#bookmarks').append(dumpTreeNodes(bookmarkTreeNodes, query));
}); });
} }
function dumpTreeNodes(bookmarkNodes, query) { function dumpTreeNodes(bookmarkNodes, query) {
var list = $('<ul>'); const list = $('<ul>');
for (var i = 0; i < bookmarkNodes.length; i++) { for (let i = 0; i < bookmarkNodes.length; i++) {
list.append(dumpNode(bookmarkNodes[i], query)); list.append(dumpNode(bookmarkNodes[i], query));
} }
@@ -29,12 +31,15 @@ function dumpTreeNodes(bookmarkNodes, query) {
function dumpNode(bookmarkNode, query) { function dumpNode(bookmarkNode, query) {
if (bookmarkNode.title) { if (bookmarkNode.title) {
if (query && !bookmarkNode.children) { if (query && !bookmarkNode.children) {
if (String(bookmarkNode.title.toLowerCase()).indexOf(query.toLowerCase()) == -1) { if (
String(bookmarkNode.title.toLowerCase()).indexOf(query.toLowerCase()) ==
-1
) {
return $('<span></span>'); return $('<span></span>');
} }
} }
var anchor = $('<a>'); const anchor = $('<a>');
anchor.attr('href', bookmarkNode.url); anchor.attr('href', bookmarkNode.url);
anchor.text(bookmarkNode.title); anchor.text(bookmarkNode.title);
@@ -47,114 +52,136 @@ function dumpNode(bookmarkNode, query) {
}); });
var span = $('<span>'); var span = $('<span>');
var options = bookmarkNode.children ? const options = bookmarkNode.children
$('<span>[<a href="#" id="addlink">Add</a>]</span>') : ? $('<span>[<a href="#" id="addlink">Add</a>]</span>')
$('<span>[<a id="editlink" href="#">Edit</a> <a id="deletelink" ' + : $(
'href="#">Delete</a>]</span>'); '<span>[<a id="editlink" href="#">Edit</a> <a id="deletelink" ' +
var edit = bookmarkNode.children ? $('<table><tr><td>Name</td><td>' + 'href="#">Delete</a>]</span>'
'<input id="title"></td></tr><tr><td>URL</td><td><input id="url">' + );
'</td></tr></table>') : $('<input>'); const edit = bookmarkNode.children
? $(
'<table><tr><td>Name</td><td>' +
'<input id="title"></td></tr><tr><td>URL</td><td><input id="url">' +
'</td></tr></table>'
)
: $('<input>');
// Show add and edit links when hover over. // Show add and edit links when hover over.
span.hover(function () { span
span.append(options); .hover(
$('#deletelink').click(function (event) { function () {
console.log(event) span.append(options);
$('#deletedialog').empty().dialog({ $('#deletelink').click(function (event) {
autoOpen: false, console.log(event);
closeOnEscape: true, $('#deletedialog')
title: 'Confirm Deletion', .empty()
modal: true, .dialog({
show: 'slide', autoOpen: false,
position: { closeOnEscape: true,
my: "left", title: 'Confirm Deletion',
at: "center", modal: true,
of: event.target.parentElement.parentElement show: 'slide',
}, position: {
buttons: { my: 'left',
'Yes, Delete It!': function () { at: 'center',
chrome.bookmarks.remove(String(bookmarkNode.id)); of: event.target.parentElement.parentElement
span.parent().remove(); },
$(this).dialog('destroy'); buttons: {
}, 'Yes, Delete It!': function () {
Cancel: function () { chrome.bookmarks.remove(String(bookmarkNode.id));
$(this).dialog('destroy'); span.parent().remove();
} $(this).dialog('destroy');
} },
}).dialog('open'); Cancel: function () {
}); $(this).dialog('destroy');
$('#addlink').click(function (event) { }
edit.show(); }
$('#adddialog').empty().append(edit).dialog({ })
autoOpen: false, .dialog('open');
closeOnEscape: true, });
title: 'Add New Bookmark', $('#addlink').click(function (event) {
modal: true, edit.show();
show: 'slide', $('#adddialog')
position: { .empty()
my: "left", .append(edit)
at: "center", .dialog({
of: event.target.parentElement.parentElement autoOpen: false,
}, closeOnEscape: true,
buttons: { title: 'Add New Bookmark',
'Add': function () { modal: true,
edit.hide(); show: 'slide',
chrome.bookmarks.create({ position: {
parentId: bookmarkNode.id, my: 'left',
title: $('#title').val(), url: $('#url').val() at: 'center',
}); of: event.target.parentElement.parentElement
$('#bookmarks').empty(); },
$(this).dialog('destroy'); buttons: {
window.dumpBookmarks(); Add: function () {
}, edit.hide();
'Cancel': function () { chrome.bookmarks.create({
edit.hide(); parentId: bookmarkNode.id,
$(this).dialog('destroy'); title: $('#title').val(),
} url: $('#url').val()
} });
}).dialog('open'); $('#bookmarks').empty();
}); $(this).dialog('destroy');
$('#editlink').click(function (event) { window.dumpBookmarks();
edit.show(); },
edit.val(anchor.text()); Cancel: function () {
$('#editdialog').empty().append(edit).dialog({ edit.hide();
autoOpen: false, $(this).dialog('destroy');
closeOnEscape: true, }
title: 'Edit Title', }
modal: true, })
show: 'fade', .dialog('open');
position: { });
my: "left", $('#editlink').click(function (event) {
at: "center", edit.show();
of: event.target.parentElement.parentElement edit.val(anchor.text());
}, $('#editdialog')
buttons: { .empty()
'Save': function () { .append(edit)
edit.hide(); .dialog({
chrome.bookmarks.update(String(bookmarkNode.id), { autoOpen: false,
title: edit.val() closeOnEscape: true,
}); title: 'Edit Title',
anchor.text(edit.val()); modal: true,
options.show(); show: 'fade',
$(this).dialog('destroy'); position: {
}, my: 'left',
'Cancel': function () { at: 'center',
edit.hide(); of: event.target.parentElement.parentElement
$(this).dialog('destroy'); },
} buttons: {
} Save: function () {
}).dialog('open'); edit.hide();
}); chrome.bookmarks.update(String(bookmarkNode.id), {
options.fadeIn(); title: edit.val()
}, });
anchor.text(edit.val());
options.show();
$(this).dialog('destroy');
},
Cancel: function () {
edit.hide();
$(this).dialog('destroy');
}
}
})
.dialog('open');
});
options.fadeIn();
},
// unhover // unhover
function () { function () {
options.remove(); options.remove();
}).append(anchor); }
)
.append(anchor);
} }
var li = $(bookmarkNode.title ? '<li>' : '<div>').append(span); const li = $(bookmarkNode.title ? '<li>' : '<div>').append(span);
if (bookmarkNode.children && bookmarkNode.children.length > 0) { if (bookmarkNode.children && bookmarkNode.children.length > 0) {
li.append(dumpTreeNodes(bookmarkNode.children, query)); li.append(dumpTreeNodes(bookmarkNode.children, query));
} }

View File

@@ -4,7 +4,7 @@ This example fetches the favicon from www.google.com and inserts it at the top l
Note: This extension does not work on `chrome://extensions`. Note: This extension does not work on `chrome://extensions`.
See [Fetching favicons](https://developer.chrome.com/docs/extensions/mv3/favicon) to learn more. See [Fetching favicons](https://developer.chrome.com/docs/extensions/mv3/favicon) to learn more.
## Testing the extension ## Testing the extension

View File

@@ -1,12 +1,12 @@
function faviconURL(u) { function faviconURL(u) {
const url = new URL(chrome.runtime.getURL("/_favicon/")); const url = new URL(chrome.runtime.getURL('/_favicon/'));
url.searchParams.set("pageUrl", u); // this encodes the URL as well url.searchParams.set('pageUrl', u); // this encodes the URL as well
url.searchParams.set("size", "32"); url.searchParams.set('size', '32');
return url.toString(); return url.toString();
} }
const imageOverlay = document.createElement('img'); const imageOverlay = document.createElement('img');
imageOverlay.src = faviconURL("https://www.google.com"); imageOverlay.src = faviconURL('https://www.google.com');
imageOverlay.alt = "Google's favicon"; imageOverlay.alt = "Google's favicon";
imageOverlay.classList.add('favicon-overlay'); imageOverlay.classList.add('favicon-overlay');
document.body.appendChild(imageOverlay); document.body.appendChild(imageOverlay);

View File

@@ -1,7 +1,7 @@
.favicon-overlay { .favicon-overlay {
all: initial !important; all: initial !important;
position: fixed !important; position: fixed !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
z-index: 9999 !important; z-index: 9999 !important;
} }

View File

@@ -2,8 +2,8 @@
"name": "Chromium Milestones", "name": "Chromium Milestones",
"version": "1.0", "version": "1.0",
"manifest_version": 3, "manifest_version": 3,
"action": {"default_popup": "popup.html"}, "action": { "default_popup": "popup.html" },
"description": "Shows the Chromium release milestone a given code review was merged into.", "description": "Shows the Chromium release milestone a given code review was merged into.",
"host_permissions": [ "https://crrie.com/" ], "host_permissions": ["https://crrie.com/"],
"permissions": [ "activeTab" ] "permissions": ["activeTab"]
} }

View File

@@ -12,19 +12,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
chrome.tabs.query({active : true}).then(tabs => getMilestone(tabs)); chrome.tabs.query({ active: true }).then((tabs) => getMilestone(tabs));
function getMilestone(tabs) { function getMilestone(tabs) {
const div = document.createElement("div"); const div = document.createElement('div');
document.body.appendChild(div); document.body.appendChild(div);
const url = tabs[0].url; const url = tabs[0].url;
const origin = 'https://chromium-review.googlesource.com'; const origin = 'https://chromium-review.googlesource.com';
const search = `^${origin}/c/chromium/src/\\+/(\\d+)`; const search = `^${origin}/c/chromium/src/\\+/(\\d+)`;
const match = url.match(search); const match = url.match(search);
if (match != undefined && match.length == 2) { if (match != undefined && match.length == 2) {
getMilestoneForRevId(match[1]).then( getMilestoneForRevId(match[1]).then((milestone) =>
(milestone) => milestone != '' ? (div.innerText = `m${milestone}`) milestone != '' ? (div.innerText = `m${milestone}`) : window.close()
: window.close()); );
} else { } else {
window.close(); window.close();
} }

View File

@@ -19,4 +19,3 @@ Then, click on "Allow Extension to Access to top sites". You will see the follow
If you accept, it will display a list of your top sites. If you accept, it will display a list of your top sites.
<img src="https://wd.imgix.net/image/BhuKGJaIeLNPW9ehns59NfwqKxF2/ibZ6PqWHsU2v0Y1h0ig2.png" alt="New tab displaying top sites" width="400"/> <img src="https://wd.imgix.net/image/BhuKGJaIeLNPW9ehns59NfwqKxF2/ibZ6PqWHsU2v0Y1h0ig2.png" alt="New tab displaying top sites" width="400"/>

View File

@@ -3,9 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Demonstrates optional permissions in extensions", "description": "Demonstrates optional permissions in extensions",
"permissions": ["storage"], "permissions": ["storage"],
"optional_permissions": [ "optional_permissions": ["topSites"],
"topSites"
],
"icons": { "icons": {
"16": "images/icon16.png", "16": "images/icon16.png",
"32": "images/icon32.png", "32": "images/icon32.png",

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>New Tab - Optional Permissions</title> <title>New Tab - Optional Permissions</title>
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css" />
</head> </head>
<body> <body>
<div id="todo_div" class="center colorFun"> <div id="todo_div" class="center colorFun">
@@ -12,7 +12,7 @@
<div id="display_top"></div> <div id="display_top"></div>
<form class="center"> <form class="center">
<input id="todo_value" placeholder="My focus today is..." /> <input id="todo_value" placeholder="My focus today is..." />
<input type="submit" value="Submit"> <input type="submit" value="Submit" />
</form> </form>
<footer></footer> <footer></footer>
<script src="newtab.js"></script> <script src="newtab.js"></script>

View File

@@ -23,35 +23,35 @@ const todo = document.getElementById('display_todo');
const form = document.querySelector('form'); const form = document.querySelector('form');
const footer = document.querySelector('footer'); const footer = document.querySelector('footer');
const createTop = () => { chrome.topSites.get((topSites) => { const createTop = () => {
topSites.forEach((site) => { chrome.topSites.get((topSites) => {
let div = document.createElement('div'); topSites.forEach((site) => {
div.className = 'colorFun'; const div = document.createElement('div');
let tooltip = document.createElement('span'); div.className = 'colorFun';
tooltip.innerText = site.title; const tooltip = document.createElement('span');
tooltip.className = 'tooltip'; tooltip.innerText = site.title;
let url = document.createElement('a'); tooltip.className = 'tooltip';
url.href = site.url; const url = document.createElement('a');
let hostname = (new URL(site.url)).hostname; url.href = site.url;
let image = document.createElement('img'); const hostname = new URL(site.url).hostname;
image.title = site.title; const image = document.createElement('img');
image.src = 'https://logo.clearbit.com/' + hostname; image.title = site.title;
url.appendChild(image); image.src = 'https://logo.clearbit.com/' + hostname;
div.appendChild(url); url.appendChild(image);
div.appendChild(tooltip); div.appendChild(url);
sites_div.appendChild(div); div.appendChild(tooltip);
}) sites_div.appendChild(div);
})}; });
});
};
chrome.permissions.contains({ permissions: ['topSites'] }).then((result) => {
chrome.permissions.contains({permissions: ['topSites']}).then((result)=>{
if (result) { if (result) {
// The extension has the permissions. // The extension has the permissions.
createTop(); createTop();
} else { } else {
// The extension doesn't have the permissions. // The extension doesn't have the permissions.
let button = document.createElement('button'); const button = document.createElement('button');
button.innerText = 'Allow Extension to Access Top Sites'; button.innerText = 'Allow Extension to Access Top Sites';
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
chrome.permissions.request(newPerms).then((granted) => { chrome.permissions.request(newPerms).then((granted) => {
@@ -66,21 +66,21 @@ chrome.permissions.contains({permissions: ['topSites']}).then((result)=>{
}); });
footer.appendChild(button); footer.appendChild(button);
} }
}) });
form.addEventListener('submit', () => { form.addEventListener('submit', () => {
let todo_value = document.getElementById('todo_value'); const todo_value = document.getElementById('todo_value');
chrome.storage.sync.set({todo: todo_value.value}); chrome.storage.sync.set({ todo: todo_value.value });
}); });
function setToDo() { function setToDo() {
chrome.storage.sync.get(['todo']).then((value)=>{ chrome.storage.sync.get(['todo']).then((value) => {
if (!value.todo) { if (!value.todo) {
todo.innerText = ''; todo.innerText = '';
} else { } else {
todo.innerText = value.todo; todo.innerText = value.todo;
} }
}) });
}; }
setToDo(); setToDo();

View File

@@ -37,11 +37,21 @@ h1 {
} }
@keyframes color-extravaganza { @keyframes color-extravaganza {
0% {background-color: #4285F4;} 0% {
10% {background-color: #4285F4;} background-color: #4285f4;
25% {background-color: #EA4335;} }
50% {background-color: #FBBC04;} 10% {
100% {background-color: #34A853;} background-color: #4285f4;
}
25% {
background-color: #ea4335;
}
50% {
background-color: #fbbc04;
}
100% {
background-color: #34a853;
}
} }
.colorFun { .colorFun {
@@ -69,8 +79,8 @@ h1 {
transition: opacity 0.3s; transition: opacity 0.3s;
} }
#todo_div,
#todo_div, .colorFun:hover .tooltip { .colorFun:hover .tooltip {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
animation-name: color-extravaganza; animation-name: color-extravaganza;

View File

@@ -3,7 +3,7 @@ function reddenPage() {
} }
chrome.action.onClicked.addListener((tab) => { chrome.action.onClicked.addListener((tab) => {
if(!tab.url.includes("chrome://")) { if (!tab.url.includes('chrome://')) {
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: { tabId: tab.id }, target: { tabId: tab.id },
function: reddenPage function: reddenPage

View File

@@ -4,10 +4,7 @@
"manifest_version": 3, "manifest_version": 3,
"version": "0.1", "version": "0.1",
"description": "Turns the page red when you click the icon", "description": "Turns the page red when you click the icon",
"permissions": [ "permissions": ["activeTab", "scripting"],
"activeTab",
"scripting"
],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
} }

View File

@@ -9,10 +9,8 @@ chrome.alarms.onAlarm.addListener(() => {
type: 'basic', type: 'basic',
iconUrl: 'stay_hydrated.png', iconUrl: 'stay_hydrated.png',
title: 'Time to Hydrate', title: 'Time to Hydrate',
message: 'Everyday I\'m Guzzlin\'!', message: "Everyday I'm Guzzlin'!",
buttons: [ buttons: [{ title: 'Keep it Flowing.' }],
{ title: 'Keep it Flowing.' }
],
priority: 0 priority: 0
}); });
}); });

View File

@@ -3,11 +3,7 @@
"description": "Demonstrates usage and features of the event page by reminding user to drink water", "description": "Demonstrates usage and features of the event page by reminding user to drink water",
"version": "1.0", "version": "1.0",
"manifest_version": 3, "manifest_version": 3,
"permissions": [ "permissions": ["alarms", "notifications", "storage"],
"alarms",
"notifications",
"storage"
],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
@@ -21,4 +17,4 @@
"48": "drink_water48.png", "48": "drink_water48.png",
"128": "drink_water128.png" "128": "drink_water128.png"
} }
} }

View File

@@ -21,7 +21,7 @@ found in the LICENSE file. -->
} }
button:hover { button:hover {
outline: #80DEEA dotted thick; outline: #80deea dotted thick;
} }
</style> </style>
<!-- <!--
@@ -29,13 +29,13 @@ found in the LICENSE file. -->
--> -->
</head> </head>
<body> <body>
<img src='./stay_hydrated.png' id='hydrateImage'> <img src="./stay_hydrated.png" id="hydrateImage" />
<!-- An Alarm delay of less than the minimum 1 minute will fire <!-- An Alarm delay of less than the minimum 1 minute will fire
in approximately 1 minute increments if released --> in approximately 1 minute increments if released -->
<button id="sampleMinute" value="1">Sample minute</button> <button id="sampleMinute" value="1">Sample minute</button>
<button id="min15" value="15">15 Minutes</button> <button id="min15" value="15">15 Minutes</button>
<button id="min30" value="30">30 Minutes</button> <button id="min30" value="30">30 Minutes</button>
<button id="cancelAlarm">Cancel Alarm</button> <button id="cancelAlarm">Cancel Alarm</button>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,20 +4,20 @@
'use strict'; 'use strict';
function setAlarm(event) { function setAlarm(event) {
let minutes = parseFloat(event.target.value); const minutes = parseFloat(event.target.value);
chrome.action.setBadgeText({text: 'ON'}); chrome.action.setBadgeText({ text: 'ON' });
chrome.alarms.create({delayInMinutes: minutes}); chrome.alarms.create({ delayInMinutes: minutes });
chrome.storage.sync.set({minutes: minutes}); chrome.storage.sync.set({ minutes: minutes });
window.close(); window.close();
} }
function clearAlarm() { function clearAlarm() {
chrome.action.setBadgeText({text: ''}); chrome.action.setBadgeText({ text: '' });
chrome.alarms.clearAll(); chrome.alarms.clearAll();
window.close(); window.close();
} }
//An Alarm delay of less than the minimum 1 minute will fire // An Alarm delay of less than the minimum 1 minute will fire
// in approximately 1 minute increments if released // in approximately 1 minute increments if released
document.getElementById('sampleMinute').addEventListener('click', setAlarm); document.getElementById('sampleMinute').addEventListener('click', setAlarm);
document.getElementById('min15').addEventListener('click', setAlarm); document.getElementById('min15').addEventListener('click', setAlarm);

View File

@@ -14,12 +14,12 @@
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
chrome.action.setBadgeText({ chrome.action.setBadgeText({
text: "OFF", text: 'OFF'
}); });
}); });
const extensions = 'https://developer.chrome.com/docs/extensions' const extensions = 'https://developer.chrome.com/docs/extensions';
const webstore = 'https://developer.chrome.com/docs/webstore' const webstore = 'https://developer.chrome.com/docs/webstore';
// When the user clicks on the extension action // When the user clicks on the extension action
chrome.action.onClicked.addListener(async (tab) => { chrome.action.onClicked.addListener(async (tab) => {
@@ -27,25 +27,25 @@ chrome.action.onClicked.addListener(async (tab) => {
// We retrieve the action badge to check if the extension is 'ON' or 'OFF' // We retrieve the action badge to check if the extension is 'ON' or 'OFF'
const prevState = await chrome.action.getBadgeText({ tabId: tab.id }); const prevState = await chrome.action.getBadgeText({ tabId: tab.id });
// Next state will always be the opposite // Next state will always be the opposite
const nextState = prevState === 'ON' ? 'OFF' : 'ON' const nextState = prevState === 'ON' ? 'OFF' : 'ON';
// Set the action badge to the next state // Set the action badge to the next state
await chrome.action.setBadgeText({ await chrome.action.setBadgeText({
tabId: tab.id, tabId: tab.id,
text: nextState, text: nextState
}); });
if (nextState === "ON") { if (nextState === 'ON') {
// Insert the CSS file when the user turns the extension on // Insert the CSS file when the user turns the extension on
await chrome.scripting.insertCSS({ await chrome.scripting.insertCSS({
files: ["focus-mode.css"], files: ['focus-mode.css'],
target: { tabId: tab.id }, target: { tabId: tab.id }
}); });
} else if (nextState === "OFF") { } else if (nextState === 'OFF') {
// Remove the CSS file when the user turns the extension off // Remove the CSS file when the user turns the extension off
await chrome.scripting.removeCSS({ await chrome.scripting.removeCSS({
files: ["focus-mode.css"], files: ['focus-mode.css'],
target: { tabId: tab.id }, target: { tabId: tab.id }
}); });
} }
} }

View File

@@ -1,4 +1,4 @@
let color = '#3aa757'; const color = '#3aa757';
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({ color }); chrome.storage.sync.set({ color });

View File

@@ -8,6 +8,5 @@ button {
} }
button.current { button.current {
box-shadow: 0 0 0 2px white, box-shadow: 0 0 0 2px white, 0 0 0 4px black;
0 0 0 4px black; }
}

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="button.css"> <link rel="stylesheet" href="button.css" />
</head> </head>
<body> <body>
<div id="buttonDiv"> <div id="buttonDiv"></div>
</div>
<div> <div>
<p>Choose a different background color!</p> <p>Choose a different background color!</p>
</div> </div>

View File

@@ -1,12 +1,12 @@
let page = document.getElementById("buttonDiv"); const page = document.getElementById('buttonDiv');
let selectedClassName = "current"; const selectedClassName = 'current';
const presetButtonColors = ["#3aa757", "#e8453c", "#f9bb2d", "#4688f1"]; const presetButtonColors = ['#3aa757', '#e8453c', '#f9bb2d', '#4688f1'];
// Reacts to a button click by marking the selected button and saving // Reacts to a button click by marking the selected button and saving
// the selection // the selection
function handleButtonClick(event) { function handleButtonClick(event) {
// Remove styling from the previously selected color // Remove styling from the previously selected color
let current = event.target.parentElement.querySelector( const current = event.target.parentElement.querySelector(
`.${selectedClassName}` `.${selectedClassName}`
); );
if (current && current !== event.target) { if (current && current !== event.target) {
@@ -14,20 +14,20 @@ function handleButtonClick(event) {
} }
// Mark the button as selected // Mark the button as selected
let color = event.target.dataset.color; const color = event.target.dataset.color;
event.target.classList.add(selectedClassName); event.target.classList.add(selectedClassName);
chrome.storage.sync.set({ color }); chrome.storage.sync.set({ color });
} }
// Add a button to the page for each supplied color // Add a button to the page for each supplied color
function constructOptions(buttonColors) { function constructOptions(buttonColors) {
chrome.storage.sync.get("color", (data) => { chrome.storage.sync.get('color', (data) => {
let currentColor = data.color; const currentColor = data.color;
// For each color we were provided… // For each color we were provided…
for (let buttonColor of buttonColors) { for (const buttonColor of buttonColors) {
// …create a button with that color… // …create a button with that color…
let button = document.createElement("button"); const button = document.createElement('button');
button.dataset.color = buttonColor; button.dataset.color = buttonColor;
button.style.backgroundColor = buttonColor; button.style.backgroundColor = buttonColor;
@@ -37,7 +37,7 @@ function constructOptions(buttonColors) {
} }
// …and register a listener for when that button is clicked // …and register a listener for when that button is clicked
button.addEventListener("click", handleButtonClick); button.addEventListener('click', handleButtonClick);
page.appendChild(button); page.appendChild(button);
} }
}); });

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="button.css"> <link rel="stylesheet" href="button.css" />
</head> </head>
<body> <body>
<button id="changeColor"></button> <button id="changeColor"></button>

View File

@@ -1,24 +1,24 @@
// Initialize button with users' preferred color // Initialize button with users' preferred color
let changeColor = document.getElementById("changeColor"); const changeColor = document.getElementById('changeColor');
chrome.storage.sync.get("color", ({ color }) => { chrome.storage.sync.get('color', ({ color }) => {
changeColor.style.backgroundColor = color; changeColor.style.backgroundColor = color;
}); });
// When the button is clicked, inject setPageBackgroundColor into current page // When the button is clicked, inject setPageBackgroundColor into current page
changeColor.addEventListener("click", async () => { changeColor.addEventListener('click', async () => {
let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: { tabId: tab.id }, target: { tabId: tab.id },
func: setPageBackgroundColor, func: setPageBackgroundColor
}); });
}); });
// The body of this function will be executed as a content script inside the // The body of this function will be executed as a content script inside the
// current page // current page
function setPageBackgroundColor() { function setPageBackgroundColor() {
chrome.storage.sync.get("color", ({ color }) => { chrome.storage.sync.get('color', ({ color }) => {
document.body.style.backgroundColor = color; document.body.style.backgroundColor = color;
}); });
} }

View File

@@ -1 +1 @@
console.log("This is a popup!"); console.log('This is a popup!');

View File

@@ -12,9 +12,7 @@
}, },
"content_scripts": [ "content_scripts": [
{ {
"js": [ "js": ["scripts/content.js"],
"scripts/content.js"
],
"matches": [ "matches": [
"https://developer.chrome.com/docs/extensions/*", "https://developer.chrome.com/docs/extensions/*",
"https://developer.chrome.com/docs/webstore/*" "https://developer.chrome.com/docs/webstore/*"

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
const article = document.querySelector("article"); const article = document.querySelector('article');
// `document.querySelector` may return null if the selector doesn't match anything. // `document.querySelector` may return null if the selector doesn't match anything.
if (article) { if (article) {
@@ -24,7 +24,7 @@ if (article) {
* regular expression character class "\w" to match against "word characters" because it only * regular expression character class "\w" to match against "word characters" because it only
* matches against the Latin alphabet. Instead, we match against any sequence of characters that * matches against the Latin alphabet. Instead, we match against any sequence of characters that
* *are not* a whitespace characters. See the below link for more information. * *are not* a whitespace characters. See the below link for more information.
* *
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/ */
const wordMatchRegExp = /[^\s]+/g; const wordMatchRegExp = /[^\s]+/g;
@@ -32,16 +32,16 @@ if (article) {
// matchAll returns an iterator, convert to array to get word count // matchAll returns an iterator, convert to array to get word count
const wordCount = [...words].length; const wordCount = [...words].length;
const readingTime = Math.round(wordCount / 200); const readingTime = Math.round(wordCount / 200);
const badge = document.createElement("p"); const badge = document.createElement('p');
// Use the same styling as the publish information in an article's header // Use the same styling as the publish information in an article's header
badge.classList.add("color-secondary-text", "type--caption"); badge.classList.add('color-secondary-text', 'type--caption');
badge.textContent = `⏱️ ${readingTime} min read`; badge.textContent = `⏱️ ${readingTime} min read`;
// Support for API reference docs // Support for API reference docs
const heading = article.querySelector("h1"); const heading = article.querySelector('h1');
// Support for article docs with date // Support for article docs with date
const date = article.querySelector("time")?.parentNode; const date = article.querySelector('time')?.parentNode;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
(date ?? heading).insertAdjacentElement("afterend", badge); (date ?? heading).insertAdjacentElement('afterend', badge);
} }

View File

@@ -11,10 +11,6 @@
"action": { "action": {
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"host_permissions": [ "host_permissions": ["https://developer.chrome.com/*"],
"https://developer.chrome.com/*" "permissions": ["tabGroups"]
],
"permissions": [
"tabGroups"
]
} }

View File

@@ -14,26 +14,26 @@
const tabs = await chrome.tabs.query({ const tabs = await chrome.tabs.query({
url: [ url: [
"https://developer.chrome.com/docs/webstore/*", 'https://developer.chrome.com/docs/webstore/*',
"https://developer.chrome.com/docs/extensions/*", 'https://developer.chrome.com/docs/extensions/*'
], ]
}); });
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator
const collator = new Intl.Collator(); const collator = new Intl.Collator();
tabs.sort((a, b) => collator.compare(a.title, b.title)); tabs.sort((a, b) => collator.compare(a.title, b.title));
const template = document.getElementById("li_template"); const template = document.getElementById('li_template');
const elements = new Set(); const elements = new Set();
for (const tab of tabs) { for (const tab of tabs) {
const element = template.content.firstElementChild.cloneNode(true); const element = template.content.firstElementChild.cloneNode(true);
const title = tab.title.split("-")[0].trim(); const title = tab.title.split('-')[0].trim();
const pathname = new URL(tab.url).pathname.slice("/docs".length); const pathname = new URL(tab.url).pathname.slice('/docs'.length);
element.querySelector(".title").textContent = title; element.querySelector('.title').textContent = title;
element.querySelector(".pathname").textContent = pathname; element.querySelector('.pathname').textContent = pathname;
element.querySelector("a").addEventListener("click", async () => { element.querySelector('a').addEventListener('click', async () => {
// need to focus window as well as the active tab // need to focus window as well as the active tab
await chrome.tabs.update(tab.id, { active: true }); await chrome.tabs.update(tab.id, { active: true });
await chrome.windows.update(tab.windowId, { focused: true }); await chrome.windows.update(tab.windowId, { focused: true });
@@ -41,11 +41,11 @@ for (const tab of tabs) {
elements.add(element); elements.add(element);
} }
document.querySelector("ul").append(...elements); document.querySelector('ul').append(...elements);
const button = document.querySelector("button"); const button = document.querySelector('button');
button.addEventListener("click", async () => { button.addEventListener('click', async () => {
const tabIds = tabs.map(({ id }) => id); const tabIds = tabs.map(({ id }) => id);
const group = await chrome.tabs.group({ tabIds }); const group = await chrome.tabs.group({ tabIds });
await chrome.tabGroups.update(group, { title: "DOCS" }); await chrome.tabGroups.update(group, { title: 'DOCS' });
}); });

3389
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "chrome-extensions-samples",
"version": "1.0.0",
"private": true,
"description": "Official samples for Chrome Extensions and the Chrome Apps platform.",
"scripts": {
"prettier": "npx prettier **/*.{md,html} -w",
"lint": "eslint **/*.js",
"lint:fix": "npm run lint -- --fix",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "git+https://github.com/GoogleChrome/chrome-extensions-samples.git"
},
"keywords": [],
"author": "The Chrome Team",
"license": "Apache 2.0",
"bugs": {
"url": "https://github.com/GoogleChrome/chrome-extensions-samples/issues"
},
"homepage": "https://github.com/GoogleChrome/chrome-extensions-samples#readme",
"devDependencies": {
"eslint": "^8.34.0",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-prettier": "4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.1.2",
"prettier": "2.8.4"
},
"lint-staged":{
"**/*.js":[
"npx eslint --fix"
],
"**/*.{md,html}":[
"npx prettier --write"
]
}
}