Add API sample for chrome.scripting API. (#830)
* Add API sample for chrome.scripting API. * Run formatter. * Address feedback. * Update licence headers. * Invert tab colours.
This commit is contained in:
Родитель
dc2174377a
Коммит
cf2df4f073
|
@ -0,0 +1,24 @@
|
|||
# chrome.scripting API
|
||||
|
||||
This sample demonstrates how to use the [chrome.scripting](https://developer.chrome.com/docs/extensions/reference/scripting/) API to inject JavaScript into web pages.
|
||||
|
||||
## Overview
|
||||
|
||||
Once installed, clicking this extension's action icon will open an extension page.
|
||||
|
||||
<img src="screenshot.png" height=300 alt="Screenshot showing the chrome.scripting API demo running in Chrome.">
|
||||
|
||||
## Features
|
||||
|
||||
This sample allows you to experiment with the following injection mechanisms:
|
||||
|
||||
- [Dynamic Declarations](https://developer.chrome.com/docs/extensions/mv3/content_scripts/#dynamic-declarative), where a content script is registered at runtime.
|
||||
- [Programmatic Injection](https://developer.chrome.com/docs/extensions/mv3/content_scripts/#programmatic), where a script is programatically executed in a tab which is already open.
|
||||
|
||||
Learn more at https://developer.chrome.com/docs/extensions/mv3/content_scripts/.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Programmatic injection is handled in the service worker. A tab is opened to a specific URL (https://example.com/#inject-programmatic). When the page finishes loading, a script is then run using `chrome.scripting.executeScript`.
|
||||
|
||||
When registering a dynamic content script, a tab is automatically opened if using the default matches URL. Otherwise, no tab is opened and the correct URL needs to be manually navigated to.
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
alert('Hello World!');
|
|
@ -0,0 +1,76 @@
|
|||
/* Copyright 2023 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. */
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
border: 1px solid #dadce0;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
background: #f6f9fe;
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-top: 1px solid #dadce0;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabs label:first-child {
|
||||
border-right: 1px solid grey;
|
||||
}
|
||||
|
||||
.tabs label.selected {
|
||||
background: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tabs label {
|
||||
display: block;
|
||||
background: #f6f9fe;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 7px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabs input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0 20px;
|
||||
background: #f6f6f6;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.buttons button {
|
||||
margin-right: 15px;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<!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>Scripting Demo</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script defer src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<form>
|
||||
<h1>Scripting Demo</h1>
|
||||
<div class="tabs">
|
||||
<label>
|
||||
Dynamic
|
||||
<input type="radio" name="type" value="dynamic" checked></input>
|
||||
</label>
|
||||
<label class="selected">
|
||||
Programmatic
|
||||
<input type="radio" name="type" value="programmatic"></input>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>Persist across sessions</span>
|
||||
<select name="persist" value="no">
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Run at</span>
|
||||
<select name="run-at" value="document_idle">
|
||||
<option value="document_start">document_start</option>
|
||||
<option value="document_end">document_end</option>
|
||||
<option value="document_idle">document_idle</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>World</span>
|
||||
<select name="world" value="ISOLATED">
|
||||
<option value="ISOLATED">ISOLATED</option>
|
||||
<option value="MAIN">MAIN</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>All Frames</span>
|
||||
<select name="all-frames" value="no">
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Matches</span>
|
||||
<input type="text" name="matches" value='https://example.com/*'>
|
||||
</label>
|
||||
<p class="hint">Note: Make sure you request the required <code>host_permissions</code> in the manifest.</p>
|
||||
<div class="buttons programmatic-buttons">
|
||||
<button type="button" id="inject-programmatic">Execute Script</button>
|
||||
</div>
|
||||
<div class="buttons dynamic-buttons">
|
||||
<button type="button" id="register-dynamic" disabled>Register Script</button>
|
||||
<button type="button" id="unregister-dynamic" disabled>Unregister Script</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const DYNAMIC_SCRIPT_ID = 'dynamic-script';
|
||||
|
||||
async function isDynamicContentScriptRegistered() {
|
||||
const scripts = await chrome.scripting.getRegisteredContentScripts();
|
||||
return scripts.some((s) => s.id === DYNAMIC_SCRIPT_ID);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector('#inject-programmatic')
|
||||
.addEventListener('click', async () => {
|
||||
// Unregister the dynamic content script to avoid multiple injections.
|
||||
const dynamicContentScriptRegistered =
|
||||
await isDynamicContentScriptRegistered();
|
||||
|
||||
if (dynamicContentScriptRegistered) {
|
||||
await chrome.scripting.unregisterContentScripts({
|
||||
ids: [DYNAMIC_SCRIPT_ID]
|
||||
});
|
||||
}
|
||||
|
||||
// Now, execute the script. We handle this in the service worker so we can
|
||||
// wait for the tab to open and **then** inject our script.
|
||||
const world = document.querySelector("[name='world']").value;
|
||||
chrome.runtime.sendMessage({
|
||||
name: 'inject-programmatic',
|
||||
options: { world }
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('#register-dynamic')
|
||||
.addEventListener('click', async () => {
|
||||
const persistAcrossSessions =
|
||||
document.querySelector("[name='persist']").value === 'yes';
|
||||
const matches = document.querySelector("[name='matches']").value;
|
||||
const runAt = document.querySelector("[name='run-at']").value;
|
||||
const allFrames =
|
||||
document.querySelector("[name='persist']").value === 'yes';
|
||||
const world = document.querySelector("[name='world']").value;
|
||||
|
||||
await chrome.scripting.registerContentScripts([
|
||||
{
|
||||
id: DYNAMIC_SCRIPT_ID,
|
||||
js: ['content-script.js'],
|
||||
persistAcrossSessions,
|
||||
matches: [matches],
|
||||
runAt,
|
||||
allFrames,
|
||||
world
|
||||
}
|
||||
]);
|
||||
|
||||
// Only open the page by default if the `matches` field hasn't been changed.
|
||||
if (matches === 'https://example.com/*') {
|
||||
await chrome.tabs.create({ url: 'https://example.com' });
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('#unregister-dynamic')
|
||||
.addEventListener('click', async () => {
|
||||
await chrome.scripting.unregisterContentScripts({
|
||||
ids: [DYNAMIC_SCRIPT_ID]
|
||||
});
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const PROGRAMMATIC_TAB_SELECTOR = "[name='type'][value='programmatic']";
|
||||
const DYNAMIC_TAB_SELECTOR = "[name='type'][value='dynamic']";
|
||||
|
||||
function updateUI() {
|
||||
const type = document.querySelector(PROGRAMMATIC_TAB_SELECTOR).checked
|
||||
? 'programmatic'
|
||||
: 'dynamic';
|
||||
|
||||
// Update selected tab.
|
||||
document.querySelector(PROGRAMMATIC_TAB_SELECTOR).parentElement.className =
|
||||
type === 'programmatic' ? 'selected' : '';
|
||||
document.querySelector(DYNAMIC_TAB_SELECTOR).parentElement.className =
|
||||
type === 'dynamic' ? 'selected' : '';
|
||||
|
||||
// Only show some fields for dynamic scripts.
|
||||
document.querySelector("[name='run-at']").parentElement.style.display =
|
||||
type === 'dynamic' ? '' : 'none';
|
||||
document.querySelector("[name='persist']").parentElement.style.display =
|
||||
type === 'dynamic' ? '' : 'none';
|
||||
document.querySelector("[name='all-frames']").parentElement.style.display =
|
||||
type === 'dynamic' ? '' : 'none';
|
||||
document.querySelector("[name='matches']").parentElement.style.display =
|
||||
type === 'dynamic' ? '' : 'none';
|
||||
document.querySelector('.hint').style.display =
|
||||
type === 'dynamic' ? '' : 'none';
|
||||
|
||||
// Update visible buttons.
|
||||
document.querySelector('.programmatic-buttons').style.display =
|
||||
type === 'programmatic' ? 'flex' : 'none';
|
||||
document.querySelector('.dynamic-buttons').style.display =
|
||||
type === 'dynamic' ? 'flex' : 'none';
|
||||
|
||||
// Decide if the register or unregister button is visible for dynamic scripts.
|
||||
isDynamicContentScriptRegistered().then((dynamicContentScriptRegistered) => {
|
||||
document
|
||||
.querySelector('#register-dynamic')
|
||||
.toggleAttribute('disabled', dynamicContentScriptRegistered);
|
||||
document
|
||||
.querySelector('#unregister-dynamic')
|
||||
.toggleAttribute('disabled', !dynamicContentScriptRegistered);
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
|
||||
document
|
||||
.querySelector(PROGRAMMATIC_TAB_SELECTOR)
|
||||
.addEventListener('change', (e) => {
|
||||
e.target.checked && updateUI();
|
||||
});
|
||||
|
||||
document.querySelector(DYNAMIC_TAB_SELECTOR).addEventListener('change', (e) => {
|
||||
e.target.checked && updateUI();
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "Scripting API Demo",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "sw.js"
|
||||
},
|
||||
"permissions": [
|
||||
"scripting",
|
||||
"webNavigation",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://example.com/*"
|
||||
],
|
||||
"action": {}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 63 KiB |
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
chrome.action.onClicked.addListener(openDemoTab);
|
||||
|
||||
function openDemoTab() {
|
||||
chrome.tabs.create({ url: 'index.html' });
|
||||
}
|
||||
|
||||
chrome.webNavigation.onDOMContentLoaded.addListener(async ({ tabId, url }) => {
|
||||
if (url !== 'https://example.com/#inject-programmatic') return;
|
||||
const { options } = await chrome.storage.local.get('options');
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content-script.js'],
|
||||
...options
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(async ({ name, options }) => {
|
||||
if (name === 'inject-programmatic') {
|
||||
await chrome.storage.local.set({ options });
|
||||
await chrome.tabs.create({
|
||||
url: 'https://example.com/#inject-programmatic'
|
||||
});
|
||||
}
|
||||
});
|
Загрузка…
Ссылка в новой задаче