Reputation: 133
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
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