| 1 | // Clipboard writes for the desktop shell: the async Clipboard API when the |
| 2 | // webview grants it, the Wails runtime bridge when it does not, and a hidden |
| 3 | // textarea + execCommand as the last resort. |
| 4 | |
| 5 | export async function writeClipboardText(value: string): Promise<boolean> { |
| 6 | try { |
| 7 | if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { |
| 8 | await navigator.clipboard.writeText(value); |
| 9 | return true; |
| 10 | } |
| 11 | } catch { |
| 12 | // Permission denied or unavailable — try the Wails bridge. |
| 13 | } |
| 14 | try { |
| 15 | if (typeof window !== "undefined" && (await window.runtime?.ClipboardSetText?.(value))) { |
| 16 | return true; |
| 17 | } |
| 18 | } catch { |
| 19 | // Bridge missing or failed — fall through to execCommand. |
| 20 | } |
| 21 | return fallbackCopyText(value); |
| 22 | } |
| 23 | |
| 24 | // execCommand("copy") needs a selected editable element, so this selects a |
| 25 | // hidden textarea and must hand the user's selection and focus back afterwards. |
| 26 | export function fallbackCopyText(value: string): boolean { |
| 27 | const activeElement = document.activeElement; |
| 28 | const selection = document.getSelection(); |
| 29 | const ranges: Range[] = []; |
| 30 | if (selection) { |
| 31 | for (let index = 0; index < selection.rangeCount; index += 1) { |
| 32 | ranges.push(selection.getRangeAt(index)); |
| 33 | } |
| 34 | } |
| 35 | const textarea = document.createElement("textarea"); |
| 36 | textarea.value = value; |
| 37 | textarea.setAttribute("readonly", ""); |
| 38 | textarea.style.position = "fixed"; |
| 39 | textarea.style.inset = "0 auto auto 0"; |
| 40 | textarea.style.width = "1px"; |
| 41 | textarea.style.height = "1px"; |
| 42 | textarea.style.opacity = "0"; |
| 43 | document.body.appendChild(textarea); |
| 44 | textarea.select(); |
| 45 | let ok = false; |
| 46 | try { |
| 47 | ok = document.execCommand("copy"); |
| 48 | } catch { |
| 49 | // Some WebViews reject execCommand("copy") with NotAllowedError instead of |
| 50 | // returning false; treat that as a failed copy, never a thrown rejection, so |
| 51 | // callers (and writeClipboardText's Promise<boolean> contract) stay honored. |
| 52 | ok = false; |
| 53 | } finally { |
| 54 | textarea.remove(); |
| 55 | if (selection) { |
| 56 | selection.removeAllRanges(); |
| 57 | for (const range of ranges) selection.addRange(range); |
| 58 | } |
| 59 | if (activeElement instanceof HTMLElement) activeElement.focus(); |
| 60 | } |
| 61 | return ok; |
| 62 | } |
| 63 |