clipboard.ts58 lines · main
1import { noop } from 'lodash'
2import { toast } from 'sonner'
3
4type ClipboardText = string | Promise<string>
5
6/**
7 * Copy text content (string or Promise<string>) into Clipboard. Safari doesn't support write text into clipboard async,
8 * so if you need to load text content async before coping, please use Promise<string> for the 1st arg.
9 *
10 * IF YOU NEED TO CHANGE THIS FUNCTION, PLEASE TEST IT IN SAFARI with a promised string. Expiring URL to a file in a
11 * private bucket will do.
12 *
13 * Copied code from https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
14 */
15export const copyToClipboard = async (str: ClipboardText, callback = noop) => {
16 const focused = window.document.hasFocus()
17 if (!focused) {
18 toast.error('Unable to copy to clipboard')
19 return
20 }
21
22 try {
23 if (typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write) {
24 // NOTE: Safari locks down the clipboard API to only work when triggered
25 // by a direct user interaction. You can't use it async in a promise.
26 // But! You can wrap the promise in a ClipboardItem, and give that to
27 // the clipboard API.
28 // Found this on https://developer.apple.com/forums/thread/691873
29 const text = new ClipboardItem({
30 'text/plain': Promise.resolve(str).then((text) => new Blob([text], { type: 'text/plain' })),
31 })
32
33 let resolve = () => {}
34 let reject = () => {}
35 const promise = new Promise<void>((res, rej) => {
36 resolve = res
37 reject = rej
38 })
39 // Safari also seems to require that the promise resolve soon after the
40 // clipboard write call, adding a setTimeout with 0 delay seems to work.
41 // Returning the promise to ensure the caller can await the clipboard
42 // copy operation intuitively.
43 setTimeout(() => {
44 navigator.clipboard.write([text]).then(callback).then(resolve).catch(reject)
45 }, 0)
46 return promise
47 }
48
49 // NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
50 // but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
51 // Good news is that other than Safari, Firefox does not care about
52 // Clipboard API being used async in a Promise.
53 await Promise.resolve(str).then((text) => navigator.clipboard?.writeText(text))
54 callback()
55 } catch {
56 toast.error('Unable to copy to clipboard')
57 }
58}