1import { 2 ClipboardUnavailableException, 3 CopyFailureException, 4 NoPermissionException, 5 PasteFailureException, 6} from './Exceptions'; 7import { 8 base64toBlob, 9 blobToBase64Async, 10 findHtmlInClipboardAsync, 11 findImageInClipboardAsync, 12 getImageSizeFromBlobAsync, 13 htmlToPlainText, 14 isClipboardPermissionDeniedAsync, 15} from './Utils'; 16import { 17 ClipboardImage, 18 GetImageOptions, 19 GetStringOptions, 20 SetStringOptions, 21 StringFormat, 22} from '../Clipboard.types'; 23 24export default { 25 get name(): string { 26 return 'ExpoClipboard'; 27 }, 28 async getStringAsync(options: GetStringOptions): Promise<string> { 29 if (!navigator.clipboard) { 30 throw new ClipboardUnavailableException(); 31 } 32 33 try { 34 switch (options.preferredFormat) { 35 case StringFormat.HTML: { 36 // Try reading HTML first 37 const clipboardItems = await navigator.clipboard.read(); 38 const blob = await findHtmlInClipboardAsync(clipboardItems); 39 if (!blob) { 40 // Fall back to plain text 41 return await navigator.clipboard.readText(); 42 } 43 return await new Response(blob).text(); 44 } 45 default: { 46 let text = await navigator.clipboard.readText(); 47 if (!text || text === '') { 48 // If there's no direct plain text, try reading HTML 49 const clipboardItems = await navigator.clipboard.read(); 50 const blob = await findHtmlInClipboardAsync(clipboardItems); 51 const blobText = await blob?.text(); 52 text = htmlToPlainText(blobText ?? ''); 53 } 54 return text; 55 } 56 } 57 } catch (e) { 58 // it might fail, because user denied permission 59 if (e.name === 'NotAllowedError' || (await isClipboardPermissionDeniedAsync())) { 60 throw new NoPermissionException(); 61 } 62 63 try { 64 // Internet Explorer 65 // @ts-ignore 66 return window.clipboardData.getData('Text'); 67 } catch { 68 return Promise.reject(new Error('Unable to retrieve item from clipboard')); 69 } 70 } 71 }, 72 // TODO: (barthap) The `setString` was deprecated in SDK 45. Remove this function in a few SDK cycles. 73 setString(text: string): boolean { 74 const textField = document.createElement('textarea'); 75 textField.textContent = text; 76 document.body.appendChild(textField); 77 textField.select(); 78 try { 79 document.execCommand('copy'); 80 return true; 81 } catch { 82 return false; 83 } finally { 84 document.body.removeChild(textField); 85 } 86 }, 87 async setStringAsync(text: string, options: SetStringOptions): Promise<boolean> { 88 switch (options.inputFormat) { 89 case StringFormat.HTML: { 90 if (!navigator.clipboard) { 91 throw new ClipboardUnavailableException(); 92 } 93 94 try { 95 const clipboardItemInput = createHtmlClipboardItem(text); 96 await navigator.clipboard.write([clipboardItemInput]); 97 return true; 98 } catch (e) { 99 // it might fail, because user denied permission 100 if (e.name === 'NotAllowedError' || (await isClipboardPermissionDeniedAsync())) { 101 throw new NoPermissionException(); 102 } 103 throw new CopyFailureException(e.message); 104 } 105 } 106 default: { 107 try { 108 if (!navigator.clipboard) { 109 throw new Error(); 110 } 111 await navigator.clipboard.writeText(text); 112 return true; 113 } catch { 114 // we can fall back to legacy behavior in any kind of failure 115 // including navigator.clipboard unavailability 116 return this.setString(text); 117 } 118 } 119 } 120 }, 121 async hasStringAsync(): Promise<boolean> { 122 return await clipboardHasTypesAsync(['text/plain', 'text/html']); 123 }, 124 async getImageAsync(_options: GetImageOptions): Promise<ClipboardImage | null> { 125 if (!navigator.clipboard) { 126 throw new ClipboardUnavailableException(); 127 } 128 129 try { 130 const clipboardItems = await navigator.clipboard.read(); 131 const blob = await findImageInClipboardAsync(clipboardItems); 132 if (!blob) { 133 return null; 134 } 135 136 const [data, size] = await Promise.all([ 137 blobToBase64Async(blob), 138 getImageSizeFromBlobAsync(blob), 139 ]); 140 141 return { data, size }; 142 } catch (e) { 143 // it might fail, because user denied permission 144 if (e.name === 'NotAllowedError' || (await isClipboardPermissionDeniedAsync())) { 145 throw new NoPermissionException(); 146 } 147 throw new PasteFailureException(e.message); 148 } 149 }, 150 async setImageAsync(base64image: string): Promise<void> { 151 if (!navigator.clipboard) { 152 throw new ClipboardUnavailableException(); 153 } 154 155 try { 156 // we set it always to `image/png` because it's the only format supported by the clipboard 157 // but it seems to work even when provided jpeg data 158 const blob = base64toBlob(base64image, 'image/png'); 159 await navigator.clipboard.write([ 160 new ClipboardItem({ 161 [blob.type]: blob, 162 }), 163 ]); 164 } catch (err: any) { 165 throw new CopyFailureException(err.message); 166 } 167 }, 168 async hasImageAsync(): Promise<boolean> { 169 return await clipboardHasTypesAsync(['image/png', 'image/jpeg']); 170 }, 171 addClipboardListener(): void {}, 172 removeClipboardListener(): void {}, 173}; 174 175/** 176 * Resolves to true if clipboard has one of provided {@link types}. 177 * @throws `ClipboardUnavailableException` if AsyncClipboard API is not available 178 * @throws `NoPermissionException` if user denied permission 179 */ 180async function clipboardHasTypesAsync(types: string[]): Promise<boolean> { 181 if (!navigator.clipboard) { 182 throw new ClipboardUnavailableException(); 183 } 184 185 try { 186 const clipboardItems = await navigator.clipboard.read(); 187 return clipboardItems.flatMap((item) => item.types).some((type) => types.includes(type)); 188 } catch (e) { 189 // it might fail, because user denied permission 190 if (e.name === 'NotAllowedError' || (await isClipboardPermissionDeniedAsync())) { 191 throw new NoPermissionException(); 192 } 193 throw e; 194 } 195} 196 197function createHtmlClipboardItem(htmlString: string): ClipboardItem { 198 return new ClipboardItem({ 199 // @ts-ignore `Blob` from `lib.dom.d.ts` and the one from `@types/react-native` differ 200 'text/html': new Blob([htmlString], { type: 'text/html' }), 201 // @ts-ignore `Blob` from `lib.dom.d.ts` and the one from `@types/react-native` differ 202 'text/plain': new Blob([htmlToPlainText(htmlString)], { type: 'text/plain' }), 203 }); 204} 205