2

I want clicking my Chrome extension’s browser action button to copy some text, but the methods used on websites don’t work.

(I realize there are some similar questions, however they answer copying text in injected scripts or browser action popup scripts, not browser action scripts.)

Boilerplate

manifest.json:

{
    "name": "Example",
    "version": "0.0.0",
    "manifest_version": 2,
    "permissions": ["clipboardWrite"],
    "browser_action": {"default_title": "Copy some text"},
    "background": {
        "scripts": ["events.js"],
        "persistent": false
    }
}

events.js:

chrome.browserAction.onClicked.addListener(_ => {
    copy(new Date().toISOString().slice(0, 19))
})

// Define copy() here.

Approach 1

Don’t define copy and hope it’s defined globally as in the console.

It isn’t.

Approach 2

function copy(text) {
    navigator.clipboard.writeText(text)
}

This fails with the error message “DOMException: Document is not focused”.

Approach 3

function copy(text) {
    focus()
    navigator.clipboard.writeText(text)
}

This behaves the same as approach 2.

Approach 4

function copy(text) {
    const ta = document.createElement('textarea')
    ta.value = text
    ta.select()
    document.execCommand('copy')
    ta.remove()
}

This fails without an error message.

Approach 5

Inject a copying script into the active tab. I didn’t code this one because it would break if there are no accessible active tabs, tabs messed with their globals, JS is paused on the active tab, etc, and would also need excessive permissions.

Approach 6

function copy(text) {
    open('copy.html?' + encodeURIComponent(text), '', 'width=1,height=1')
}

(Setting width and height forces opening a window, not a tab, in order to preserve the user’s tab selection and reduce visual impact.)

copy.html:

<!doctype html>
<meta charset="utf-8">
<title>Copying…</title>
<div></div>
<script src="copy.js"></script>

copy.js:

(async _ => {
    await navigator.clipboard.writeText(decodeURIComponent(location.search.slice(1)))
    close()
})()

This works but isn’t great because it’s visually glitchy, slow, and circuitous.

twhb
  • 4,294
  • 2
  • 20
  • 23

1 Answers1

6

The textarea element should be added to the live DOM first e.g. to document.body and focused because execCommand operates on document.activeElement. You can hide the textarea so it doesn't flicker.

function copy(text) {
  const ta = document.createElement('textarea');
  ta.style.cssText = 'opacity:0; position:fixed; width:1px; height:1px; top:0; left:0;';
  ta.value = text;
  document.body.appendChild(ta);
  ta.focus();
  ta.select();
  document.execCommand('copy');
  ta.remove();
}

Might wanna set a few more CSS props to none or 0 like border/padding/margin just in case.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136