Action API demo (#574)

This commit is contained in:
Simeon Vincent
2021-04-15 20:15:10 -07:00
committed by GitHub
parent e716678b67
commit 3f48be6b90
25 changed files with 569 additions and 0 deletions

12
api/action/background.js Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// Show the demo page once the extension is installed
chrome.runtime.onInstalled.addListener((_reason) => {
chrome.tabs.create({
url: 'demo/index.html'
});
});

22
api/action/demo/index.css Normal file
View File

@@ -0,0 +1,22 @@
p {
hyphens: initial;
}
.flex {
display: flex;
gap: .25em;
margin: .5em 0;
align-items: flex-end;
}
.spaced {
margin: .5em 0;
}
.full-width {
width: 100%;
}
button {
white-space: nowrap;
}

168
api/action/demo/index.html Normal file
View File

@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="index.js"></script>
<link rel="stylesheet" href="../third-party/awsm/awsm.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<main>
<section>
<h1>Action API Demo</h1>
<p>Before experimenting with these APIs, we recommend you pin the extension's action button to your
toolbar in order to make it easier to see the changes. </p>
<img src="../images/pin-action.png">
</section>
<section id="toggle-state">
<h2>enable / disable</h2>
<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
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>
<div class="flex">
<figure>
<img src="../images/action-enabled.png">
<figcaption>Action enabled</figcaption>
</figure>
<figure>
<img src="../images/action-disabled.png">
<figcaption>Action disabled</figcaption>
</figure>
</div>
</section>
<section id="popup">
<h2>Popup</h2>
<p>This demo's <a href="manifest.json">manifest.json</a> file sets the value of
<code>action.default_popup</code> to <code>popups/popup.html</code>. We chan 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>
Current popup value
<input type="text" id="current-popup-value" disabled>
</label>
</div>
<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>
<!-- badge -->
<section id="badge-text">
<h2>Badge Text</h2>
<p>The action's badge text is a text overlay with a solid background color. This provides a
passive UI surface to share information with the user. It is most commonly used to show a
notification count or number of actions taken on the current page.</p>
<div class="spaced">
<label>
Enter badge text (live update)<br>
<input type="text" id="badge-text-input">
</label>
</div>
<div class="flex">
<label class="full-width">
Current badge text
<input type="text" id="current-badge-text" disabled>
</label>
<button id="clear-badge-button">clear badge text</button>
</div>
<div class="spaced">
<button id="set-badge-background-color-button">Randomize badge background color</button>
</div>
<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 -->
<section id="setIcon">
<h2>Icon</h2>
<p>The <a
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="reset-icon-button">reset action icon</button>
</section>
<!-- badge - hover text (title) -->
<section id="title">
<h2>Hover Text</h2>
<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
href="https://developer.chrome.com/docs/extensions/reference/action/#method-getTitle"><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">
<label>
Enter a new title (debounced)<br>
<input type="text" id="title-input">
</label>
</div>
<div class="flex">
<label class="full-width">
Current title
<input type="text" id="current-title" disabled>
</label>
<button id="reset-title-button">reset title</button>
</div>
<div class="flex">
<figure>
<img src="../images/title-no-hover.png">
<figcaption>Default appearance</figcaption>
</figure>
<figure>
<img src="../images/title-hover.png">
<figcaption>Title appears on hover</figcaption>
</figure>
</section>
</main>
</body>
</html>

240
api/action/demo/index.js Normal file
View File

@@ -0,0 +1,240 @@
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
/**
* @param {number} timeout
* @param {(event: Event) => void} callback
* @return {(event: Event) => void}
*/
function debounce(timeout, callback) {
let timeoutID = 0;
return (event) => {
clearTimeout(timeoutID);
timeoutID = setTimeout(() => callback(event), timeout);
};
}
// ------------------
// .enable / .disable
// ------------------
// The action API does not expose a way to read the action's current enabled/disabled state, so we
// have to track it ourselves.
// Relevant feature request: https://bugs.chromium.org/p/chromium/issues/detail?id=1189295
let actionEnabled = true;
let showToggleState = document.getElementById('show-toggle-state');
document.getElementById('toggle-state-button').addEventListener('click', (_event) => {
if (actionEnabled) {
chrome.action.disable();
} else {
chrome.action.enable();
}
actionEnabled = !actionEnabled;
});
document.getElementById('popup-options').addEventListener('change', async (event) => {
let popup = event.target.value;
await chrome.action.setPopup({ popup });
// Show the updated popup path
await getCurrentPopup();
});
async function getCurrentPopup() {
let popup = await chrome.action.getPopup({});
document.getElementById('current-popup-value').value = popup;
return popup;
};
async function showCurrentPage() {
let popup = await getCurrentPopup();
let pathname = '';
if (popup) {
pathname = new URL(popup).pathname;
}
let options = document.getElementById('popup-options');
let option = options.querySelector(`option[value="${pathname}"]`);
option.selected = true;
}
// Populate popup inputs on on page load
showCurrentPage();
// ----------
// .onClicked
// ----------
// If a popup is specified, our on click handler won't be called. We declare it here rather than in
// the `onclicked-button` handler to prevent the user from accidentally registering multiple
// onClicked listeners.
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: 'https://html5zombo.com/' });
});
document.getElementById('onclicked-button').addEventListener('click', async () => {
// Our listener will only receive the action's click event after clear out the popup URL
await chrome.action.setPopup({ popup: '' });
await showCurrentPage();
});
document.getElementById('onclicked-reset-button').addEventListener('click', async () => {
await chrome.action.setPopup({ popup: 'popups/popup.html' });
await showCurrentPage();
});
// ----------
// badge text
// ----------
async function showBadgeText() {
let text = await chrome.action.getBadgeText({});
document.getElementById('current-badge-text').value = text;
}
// Populate badge text inputs on on page load
showBadgeText();
document.getElementById('badge-text-input').addEventListener('input', async (event) => {
let text = event.target.value;
await chrome.action.setBadgeText({ text });
showBadgeText();
});
document.getElementById('clear-badge-button').addEventListener('click', async () => {
await chrome.action.setBadgeText({ text: '' });
showBadgeText();
});
// ----------------------
// badge background color
// ----------------------
async function showBadgeColor() {
let color = await chrome.action.getBadgeBackgroundColor({});
document.getElementById('current-badge-bg-color').value = JSON.stringify(color, null, 0);
}
// Populate badge background color inputs on on page load
showBadgeColor();
document.getElementById('set-badge-background-color-button').addEventListener('click', async () => {
// To show off this method, we must first make sure the badge has text
let currentText = await chrome.action.getBadgeText({});
if (!currentText) {
chrome.action.setBadgeText({ text: 'hi :)' });
showBadgeText();
}
// Next, generate a random RGBA color
let color = [0, 0, 0].map(() => Math.floor(Math.random * 255));
// Use the default background color ~10% of the time.
//
// NOTE: Alpha color cannot be set due to crbug.com/1184905. At the time of writing (Chrome 89),
// an alpha value of 0 sets the default color while a value of 1-255 will make the RGB color
// fully opaque.
if (Math.random() < 0.1) {
color.push(0);
} else {
color.push(255);
}
chrome.action.setBadgeBackgroundColor({ color });
showBadgeColor();
});
document.getElementById('reset-badge-background-color-button').addEventListener('click', async () => {
chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] });
showBadgeColor();
});
// -----------
// action icon
// -----------
const EMOJI = [
'confetti',
'suit',
'bow',
'dog',
'skull',
'yoyo',
'cat',
];
let lastIconIndex = 0;
document.getElementById('set-icon-button').addEventListener('click', async () => {
// Clear out the badge text in order to make the icon change easier to see
chrome.action.setBadgeText({ text: '' });
// Randomly pick a new icon
let index = lastIconIndex;
index = Math.floor(Math.random() * (EMOJI.length));
if (index === lastIconIndex) {
// Dupe detected! Increment the index & modulo to make sure we don't go out of bounds
index = (index + 1) % EMOJI.length;
}
let emojiFile = `images/emoji-${EMOJI[index]}.png`;
lastIconIndex = index;
// There are easier ways for a page to extract an image's imageData, but the OffscreenCanvas
// approach used here works in both extension pages and service workers.
let response = await fetch(emojiFile);
let blob = await response.blob();
let imageBitmap = await createImageBitmap(blob);
let osc = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
let ctx = osc.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
let imageData = ctx.getImageData(0, 0, osc.width, osc.height);
chrome.action.setIcon({ imageData });
});
document.getElementById('reset-icon-button').addEventListener('click', () => {
let manifest = chrome.runtime.getManifest();
chrome.action.setIcon({ path: manifest.action.default_icon });
});
// -------------
// get/set title
// -------------
let titleInput = document.getElementById('title-input');
let titleInputDebounce = Number.parseInt(titleInput.dataset.debounce || 100);
titleInput.addEventListener('input', debounce(200, async (event) => {
let title = event.target.value;
chrome.action.setTitle({ title });
showActionTitle();
}));
document.getElementById('reset-title-button').addEventListener('click', async (event) => {
let manifest = chrome.runtime.getManifest();
let title = manifest.action.default_title;
chrome.action.setTitle({ title });
showActionTitle();
});
async function showActionTitle() {
let title = await chrome.action.getTitle({});
// If empty, the title falls back to the name of the extension
if (title === '') {
// … which we can get from the extension's manifest
let manifest = chrome.runtime.getManifest();
title = manifest.name;
}
document.getElementById('current-title').value = title;
}
// Populate action title inputs on on page load
showActionTitle();

BIN
api/action/icons/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
api/action/icons/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
api/action/icons/512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
api/action/icons/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

24
api/action/manifest.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "Action API Demo",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Default Title",
"default_popup": "popups/popup.html",
"default_icon": {
"32": "icons/32.png",
"72": "icons/72.png",
"128": "icons/128.png",
"512": "icons/512.png"
}
},
"icons": {
"32": "icons/32.png",
"72": "icons/72.png",
"128": "icons/128.png",
"512": "icons/512.png"
}
}

32
api/action/popups/a.html Normal file
View File

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

32
api/action/popups/b.html Normal file
View File

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

View File

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

7
api/action/third-party/awsm/awsm.css vendored Normal file

File diff suppressed because one or more lines are too long