// Copy text to the clipboard, robust across iOS Safari and non-secure origins. // // The synchronous `execCommand("copy")` path (a hidden, selected textarea) is the // battle-tested, user-gesture-safe primitive: it works on iOS Safari and every // desktop browser. We run it FIRST, inside the click gesture, before any `await` // can spend the page's transient user activation — awaiting a rejected async // `navigator.clipboard.writeText` first is exactly what makes the fallback fail on // iOS. Only if the sync path fails (some sandboxed / permission-locked contexts // disable execCommand) do we fall back to the async Clipboard API. Returns true on // success so callers can toast. export async function copyText(text: string): Promise { if (legacyCopy(text)) return true; if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch { // both paths failed } } return false; } function legacyCopy(text: string): boolean { const active = document.activeElement as HTMLElement | null; // Preserve any text the user had selected on the page — focusing our textarea // collapses it, so snapshot the range and restore it afterwards. const sel = document.getSelection(); const savedRange = sel && sel.rangeCount ? sel.getRangeAt(0) : null; const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); // suppress the iOS keyboard // Hidden but still selectable: iOS won't copy from `display:none`/`opacity:0`, so // clip it out of view while keeping a live, user-selectable node. ta.style.position = "fixed"; ta.style.top = "0"; ta.style.left = "0"; ta.style.width = "1px"; ta.style.height = "1px"; ta.style.padding = "0"; ta.style.border = "none"; ta.style.clip = "rect(0, 0, 0, 0)"; ta.style.userSelect = "text"; ta.style.setProperty("-webkit-user-select", "text"); document.body.appendChild(ta); try { ta.focus(); ta.select(); ta.setSelectionRange(0, text.length); // iOS needs an explicit range return document.execCommand("copy"); } catch { return false; } finally { document.body.removeChild(ta); active?.focus?.(); // restore focus we borrowed for the copy if (savedRange) { sel!.removeAllRanges(); sel!.addRange(savedRange); } } }