J.P.
J.P.

Reputation: 133

How to use Clipboard API to write image to clipboard in Safari

The following code (adapted from here) successfully writes an image file to the clipboard upon a button click in Chrome:

document.getElementById('copy-button').addEventListener('click', async () => {
  try {
    const data = await fetch('image.png')
    const blob = await data.blob()
    await navigator.clipboard.write(
      [new ClipboardItem({[blob.type]: blob})]
    )
    console.log('success')
  } catch (err) {
    console.log(`${err.name}:  ${err.message}`)
  }
})

(Similar code also works with chaining the promises with .then() or copying the contents of a <canvas> using .toBlob() with a callback function)

However, this fails in Safari, throwing a NotAllowedError. I suspect this is something to do with the asynchronous making of the blob causing Safari think that the call to write() is 'outside the scope of a user gesture (such as "click" or "touch" event handlers)' as described here, since control is released from the event handler during the await portions.

For example, the following code pre-loads the blob into a global variable when the script first runs, and the call to write() does not need to wait for any other async code to finish executing:

let imageBlob
(async function () {
  const data = await fetch('image.png')
  const blob = await data.blob()
  imageBlob = blob
  console.log('Image loaded into memory')
})()

document.getElementById('image-button-preload').addEventListener('click', () => {
  const clipItem = new ClipboardItem({[imageBlob.type]: imageBlob})
  navigator.clipboard.write([clipItem]).then( () => {
    console.log('success')
  }, (reason) => {
    console.log(reason)
  })
})

But this is clearly not ideal, especially if the image data is something dynamically created (e.g. in a canvas).

So, the question: How can I generate an image blob and write this to the clipboard upon a user action which Safari/webkit will accept? (Or, is this a bug in Safari/webkit's implementation of the API)

Upvotes: 2

Views: 2242

Answers (1)

Leah Zorychta
Leah Zorychta

Reputation: 13429

The solution (for safari) is to assign a Promise to the value of the hashmap you pass into ClipboardItem like this:

document.getElementById('copy-button').addEventListener('click', async () => {
  try {
    const makeImagePromise = async () => {
      const data = await fetch('image.png')
      return await data.blob()
    }
    await navigator.clipboard.write(
      [new ClipboardItem({[blob.type]: makeImagePromise() })]
    )
    console.log('success')
  } catch (err) {
    console.log(`${err.name}:  ${err.message}`)
  }
})

That way you're calling clipboard.write without awaiting, and Safari will await the promise for you that generates the image.

Note: Other browsers may not support passing a promise to ClipboardItem, so you'll likely want to check if the UserAgent contains Mac or iOS in it before doing this.

Upvotes: 4

Related Questions