Search code examples
google-chrome-extension

Copy to the clipboard from extension context menu without using `scripting` or `content_scripts`?


Background: I would like to write an extension, where right-clicking on the extension icon and clicking on a context menu item copies something to the clipboard.

But I also want to avoid using the scripting permission or injecting content_scripts in the manifest.json, because some corporate environments don't allow it (for good reason).

But how can I copy to the clipboard from a chrome.contextMenus.onClicked handler if I don't use scripting or content_scripts? Neither document.execCommand nor navigator.clipboard.write are available in background scripts (where both document and navigator.clipboard are undefined) and there is no page to send a message to, since the action.default_popup pages cannot be opened from background scripts and I don't want to use content injection.

Any ideas?


Solution

  • The cookbook.offscreen-clipboard-write example is a working full example of using chrome.offscreen. The essense is that offscreen was created for exactly this kind of purpose.

    Here is a summary, but do see the two above links for more details:

    First you add "offscreen" to the permissions in manifest.json.

    Then in background.js:

    // This could also be chrome.contextMenus.onClicked.addListener()
    chrome.action.onClicked.addListener(async () => {
      await chrome.offscreen.createDocument({
        url: chrome.runtime.getURL('offscreen.html'),
        reasons: [chrome.offscreen.Reason.CLIPBOARD],
        justification: 'Need to copy data to the clipboard',
      });
      chrome.runtime.sendMessage({
        type: 'copy-data-to-clipboard',
        target: 'offscreen-doc',
        data: 'put this string on the clipboard'
      });
    });
    

    offscreen.html has:

    <!DOCTYPE html>
    <textarea id="text"></textarea>
    <script src="offscreen.js"></script>
    

    And offscreen.js:

    chrome.runtime.onMessage.addListener(handleMessages);
    
    async function handleMessages(message) {
      if (message.target !== 'offscreen-doc') {
        return;
      }
    
      switch (message.type) {
        case 'copy-data-to-clipboard':
          handleClipboardWrite(message.data);
          break;
        default:
          console.warn(`Unexpected message type received: '${message.type}'.`);
      }
    }
    
    const textEl = document.querySelector('#text');
    
    async function handleClipboardWrite(data) {
      try {
        if (typeof data !== 'string') {
          throw new TypeError(
            `Value provided must be a 'string', got '${typeof data}'.`
          );
        }
    
        textEl.value = data;
        textEl.select();
        document.execCommand('copy');
      } finally {
        window.close();
      }
    }