Making a Chrome Extension that Reads & Writes to the Clipboard
Say you want to make a web app that syncs the user’s clipboard amongst two or more machines with minimal user interaction. Even with document.execCommand()
this is difficult or impossible to do in an asynchronous manner because of browser security restrictions.
document.execCommand('copy')
- which writes the window’s current text selection to the user’s clipboard - only works inside a user click event. You can programmatically select arbitrary text in an invisible input element, but you still need the user’s implicit permission to copy that selection to their clipboard. If you always want to involve the user in the copy process, you’re good to go. If not, you’ll need to resort to a browser extension.
document.execCommand('paste')
- which reads the user’s clipboard and puts it into the focused input element on the page - won’t work at all in Chrome unless it’s run from an extension.
So Let’s Make a Chrome Extension
For the sake of brevity:
- I’m only tackling this problem in Chrome right now. I’m sure it’s possible in other browsers using their own proprietary APIs or extensions.
- I’m going to assume you’ve at least read the Chrome extension getting started tutorial and know the basic extension concepts like manifests, messaging, and background pages.
- I’m going to show the bare minimum amount of code necessary to asynchronously read & write the system clipboard. A more fully-featured, real world app would almost certainly require more permissions and more complicated code.
- This will only handle text clipboard content, not images or other binary objects.
There are three main parts to this extension:
- The manifest inside the extension -
manifest.json
- The background page inside the extension -
background.html
,background.js
- The web app loaded from a remote web host -
index.html
,main.js
Here’s a Github Gist of the complete files.
Web App Page
Our example web app shows the current content of your clipboard when the page loads and updates every time the page becomes visible, for instance after switching to a different app or tab to copy some text and switching back to this test page.
The following shows where the clipboard content should appear in index.html
:
<h4>Current Clipboard Text:</h4>
<p id="output"></p>
The following shows where we register for events to read the clipboard changes in main.js
:
document.addEventListener('DOMContentLoaded', function() {
readClipboard();
// Re-read the clipboard every time the page becomes visible.
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
readClipboard();
}
});
});
The readClipboard
function below sends an asynchronous message to the extension. The extension reads the system clipboard and executes the callback we provide, giving us the clipboard content. The extensionId
is assigned by Chrome the first time you load an extension on the chrome://extensions/
page. Yours will be different.
var extensionId = 'YOUR EXTENSION ID HERE';
function readClipboard() {
chrome.runtime.sendMessage(
extensionId,
{ method: 'getClipboard' },
function(response) {
document.getElementById('clipboard-content').textContent = response;
}
);
}
index.html
also contains a button you can click to write some arbitrary text - the content of the #para
paragraph in this case - to the system clipboard.
<button id="copyButton">Set System Clipboard To The Following:</button>
<p id="para">Ut scelerisque posuere sem, non aliquam ipsum sodales et. Fusce luctus, mauris ut volutpat varius, leo dui posuere ligula, vitae rhoncus leo ipsum eu neque.</p>
main.js
contains a click handler for the copy button that sends a message to the extension to request it to write some text to the system clipboard. chrome.runtime.sendMessage()
is inherently asynchronous - which proves we’re no longer limited to running inside a user event.[1]
document.getElementById('copyButton').addEventListener('click', function() {
var text = document.getElementById('para').textContent;
chrome.runtime.sendMessage(
extensionId,
{ method: 'setClipboard', value: text },
function(response) {
console.log('extension setClipboard response', response);
}
);
});
After clicking the copy button, you can then paste somewhere else to verify it worked.
Manifest
The manifest lists permissions the extension will require from the user, what domains the extension can make requests to, and what domains’ pages are allowed to send messages to the extension.
There are three sections to note:
The background
section declares the name of the background page inside the extension bundle. Chrome creates an invisible, sandboxed DOM for this page automatically.
"background": {
"persistent": false,
"page": "background.html"
},
The permissions
section lists the permissions the extension requires from the user. This extension requires permission to read and write to the user’s system clipboard. The clipboardRead
permission enables the extension’s background script to use document.execCommand('paste')
; clipboardWrite
enables document.execCommand('copy')
.
"permissions": [
"clipboardRead",
"clipboardWrite"
],
The externally_connectable
section declares the domains of webpages that are allowed to send messages directly to the extension using the Chrome messaging API - chrome.runtime.sendMessage
, etc. For this example, I’ve hard-coded it to a localhost domain in order to show the simplest possible solution. In a real web app that’s not running on your local machine, you’d need to specify specific domains, and you’d certainly want to use only an https://
scheme because of the sensitive nature of clipboard data.[2]
"externally_connectable": {
"matches": [
"http://localhost/*"
]
}
Background Page
The background page consists of a simple background.html
document and a background.js
script.
The HTML page consists of a textarea we’ll use to make document.execCommand()
work, and a script tag to load background.js
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="background.js"></script>
</head>
<body>
<textarea id="ta"></textarea>
</body>
</html>
In background.js
we register a handler for Chrome runtime messages sent from the external webpage. The Chrome message API accepts simple request objects. Our request objects consist of a method
property that specifies which operation to perform and an optional value
property if the method accepts a parameter. sendResponse
is the callback function we provided in main.js
:
chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) {
switch (request.method) {
case 'getClipboard':
sendResponse(getClipboard());
break;
case 'setClipboard':
sendResponse(setClipboard(request.value));
break;
default:
console.error('Unknown method "%s"', request.method);
break;
}
});
The getClipboard()
function invokes document.execCommand('paste')
, but in order for it to work, a textarea
or input[type=text]
element must be selected so there’s somewhere for the pasted text to go, even though this background document is invisible. We then immediately read the text back out of the textarea and return it to the webpage:
function getClipboard() {
var result = null;
var textarea = document.getElementById('ta');
textarea.value = '';
textarea.select();
if (document.execCommand('paste')) {
result = textarea.value;
} else {
console.error('failed to get clipboard content');
}
textarea.value = '';
return result;
}
The setClipboard()
function follows a similar pattern. It sets the textarea value to the given text provided by the webpage, then selects the textarea content, and runs document.execCommand('copy')
to copy the textarea content to the system clipboard.
function setClipboard(value) {
var result = false;
var textarea = document.getElementById('ta');
textarea.value = value;
textarea.select();
if (document.execCommand('copy')) {
result = true;
} else {
console.error('failed to get clipboard content');
}
textarea.value = '';
return result;
}
Conclusion
This is an oversimplified example just to show what’s possible with the smallest amount of code. But these same basic techniques can be extended to do a lot more if desired.
References
- Sam Sudar’s Gist
- Message Passing - Google Chrome
- Cut and Copy Commands | Web Updates - Google Developers
-
But if you’re skeptical, wrap the
chrome.runtime.sendMessage()
call in asetTimeout()
. ↩ -
Note that
externally_connectable
does not allow wildcard URLs likehttps://*/*
the way some other parts of the manifest do. If you’re unable to specify the hostnames of the webpages up front, you may need to take a more indirect approach and communicate via a content script. ↩