Skip to content

Commit b5c62b1

Browse files
Simeon VincentIanStanion-googlejpmedley
authored
Add offscreen clipboard cookbook example (GoogleChrome#797)
* Add offscreen clipboard cookbook example * Clarify solution comment * Apply suggestions from code review * Update offscreen.js @dotproto I took some time to consider, and I think keeping the messages and proper verifications in the workflow is a much better approach than simplifying to a purely instructive showing of the offscreen API. It not only demonstrates extension writing best-practice for completely new learners, but also gives the opportunity to demonstrate the potential for having multiple offscreen documents packaged into a project that could potentially be opened. * Update cookbook/offscreen-clipboard-write/offscreen.html * Apply suggestions from code review * Add additional explanitory text * Apply suggestions from code review Co-authored-by: Joe Medley <[email protected]> * Add a README * Missing semi * Rewrap at 80 char This brings descriptive comments in line with the copyright header required by Google. * Rewrap at 80 characters & clarify comments * Remove unreachable return statement Co-authored-by: IanStanion-google <[email protected]> Co-authored-by: Joe Medley <[email protected]>
1 parent c7bd96f commit b5c62b1

File tree

5 files changed

+162
-0
lines changed

5 files changed

+162
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
This recipe shows how to write a string to the system clipboard using the [Offscreen API][1].
2+
3+
## Context
4+
5+
As of January 2023, the web platform has to ways to interact with the clipboard: `document.execCommand()` and `navigator.clipboard` (see [MDN's docs][0]). Unfortunately, neither of these APIs are exposed to JavaScript workers. This means that in order for an extension to read from or write values to the system clipboard, the extension _must_ do so in a web page. Enter the Offscreen API. This API was introduced to give extension developers an unobtrusive way to use DOM APIs in the background.
6+
7+
In the future, the Chrome team is planning to add clipboard support directly to extension service workers. As such, this recipe is written to make it as easy as possible to replace `addToClipboard()`'s offscreen document-based implementation with one that directly uses the appropriate clipboard API.
8+
9+
## Running this extension
10+
11+
1. Clone this repository.
12+
2. Load this directory in Chrome as an [unpacked extension][2].
13+
3. Open the Extension menu and click the extension named "Offscreen API - Clipboard".
14+
15+
You will now have "Hello, World!" on your system clipboard.
16+
17+
[0]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard
18+
[1]: https://developer.chrome.com/docs/extensions/reference/offscreen/
19+
[2]: https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// The value that will be written to the clipboard.
16+
const textToCopy = `Hello world!`;
17+
18+
// When the browser action is clicked, `addToClipboard()` will use an offscreen
19+
// document to write the value of `textToCopy` to the system clipboard.
20+
chrome.action.onClicked.addListener(async () => {
21+
await addToClipboard(textToCopy);
22+
});
23+
24+
// Solution 1 - As of Jan 2023, service workers cannot directly interact with
25+
// the system clipboard using either `navigator.clipboard` or
26+
// `document.execCommand()`. To work around this, we'll create an offscreen
27+
// document and pass it the data we want to write to the clipboard.
28+
async function addToClipboard(value) {
29+
// This pattern ensures that the offscreen document exists before we try to
30+
// send it a message. If we didn't await the `hasDocument()` and
31+
// `createDocument()` calls, the `sendMessage()` call could send a message
32+
// before the offscreen document can register it's `runtime.onMessage`
33+
// listener.
34+
if (await chrome.offscreen.hasDocument()) {
35+
console.debug('Offscreen doc already exists.');
36+
} else {
37+
console.debug('Creating a new offscreen document.');
38+
39+
await chrome.offscreen.createDocument({
40+
url: 'offscreen.html',
41+
reasons: [chrome.offscreen.Reason.CLIPBOARD],
42+
justification: 'Write text to the clipboard.',
43+
});
44+
}
45+
46+
// Now that are sure we have an offscreen document, we can safely dispatch the
47+
// message.
48+
chrome.runtime.sendMessage({
49+
type: 'copy-data-to-clipboard',
50+
target: 'offscreen-doc',
51+
data: value,
52+
});
53+
}
54+
55+
// Solution 2 – Once extension service workers can use the Clipboard API,
56+
// replace the offscreen document based implementation with something like this.
57+
async function addToClipboardV2(value) {
58+
navigator.clipboard.writeText(value);
59+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "Offscreen API - Clipboard",
3+
"version": "1.0",
4+
"manifest_version": 3,
5+
"background": {
6+
"service_worker": "background.js"
7+
},
8+
"action": {},
9+
"permissions": [
10+
"offscreen",
11+
"clipboardWrite"
12+
]
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!DOCTYPE html>
2+
<textarea id="text"></textarea>
3+
<script src="offscreen.js""></script>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Once the message has been posted from the service worker, checks are made to
16+
// confirm the message type and target before proceeding. This is so that the
17+
// module can easily be adapted into existing workflows where secondary uses for
18+
// the document (or alternate offscreen documents) might be implemented.
19+
20+
// Registering this listener when the script is first executed ensures that the
21+
// offscreen document will be able to receive messages when the promise returned
22+
// by `offscreen.createDocument()` resolves.
23+
chrome.runtime.onMessage.addListener(handleMessages);
24+
25+
// This function performs basic filtering and error checking on messages before
26+
// dispatching the
27+
// message to a more specific message handler.
28+
async function handleMessages(message) {
29+
// Return early if this message isn't meant for the offscreen document.
30+
if (message.target !== 'offscreen-doc') {
31+
return;
32+
}
33+
34+
// Dispatch the message to an appropriate handler.
35+
switch (message.type) {
36+
case 'copy-data-to-clipboard':
37+
handleClipboardWrite(message.data);
38+
break;
39+
default:
40+
console.warn(`Unexpected message type received: '${message.type}'.`);
41+
}
42+
}
43+
44+
45+
// We use a <textarea> element for two main reasons:
46+
// 1. preserve the formatting of multiline text,
47+
// 2. select the node's content using this element's `.select()` method.
48+
let textEl = document.querySelector('#text');
49+
50+
// Use the offscreen document's `document` interface to write a new value to the
51+
// system clipboard.
52+
//
53+
// At the time this demo was created (Jan 2023) the `navigator.clipboard` API
54+
// requires that the window is focused, but offscreen documents cannot be
55+
// focused. As such, we have to fall back to `document.execCommand()`.
56+
async function handleClipboardWrite(data) {
57+
// Error if we received the wrong kind of data.
58+
if (typeof data !== 'string') {
59+
throw new TypeError(`Value provided must be a 'string', got '${typeof data}'.`);
60+
}
61+
62+
// `document.execCommand('copy')` works against the user's selection in a web
63+
// page. As such, we must insert the string we want to copy to the web page
64+
// and to select that content in the page before calling `execCommand()`.
65+
textEl.value = data;
66+
textEl.select();
67+
document.execCommand('copy');
68+
}

0 commit comments

Comments
 (0)