/** * @file clipboard.ts * @description `copyText(text)` — the one copy path for every "Copy …" action. * Uses the async Clipboard API when it exists and falls back to a hidden * textarea + `execCommand("copy")`, because `navigator.clipboard` is undefined * on a non-secure origin such as `http://:4820`. Reports the outcome as * a toast, so a copy is never silent. `writeClipboard(text)` is the same two * paths with no toast, for a copy the user did not click (a terminal * selection); it hands the focus back to wherever it was. * @author Nguyễn Ngọc Trí Vĩ */ import i18n from "../i18n"; import { toast } from "./toast"; function fallbackCopy(text: string): boolean { // Selecting the textarea moves the focus: give it back (a terminal keeps typing). const was = document.activeElement as HTMLElement | null; const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { return document.execCommand("copy"); } catch { return false; } finally { ta.remove(); was?.focus?.({ preventScroll: true }); } } /** Copy `text` silently: the Clipboard API, else the textarea path. */ export async function writeClipboard(text: string): Promise { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } } catch { /* fall through to the textarea path */ } return fallbackCopy(text); } /** * Copy text to the clipboard and toast the result. * @param quiet Skip the success toast (the caller shows its own feedback). * @returns Whether the copy succeeded. */ export async function copyText(text: string, quiet = false): Promise { const ok = await writeClipboard(text); if (!ok) toast.error(i18n.t("common:copyFailed")); else if (!quiet) toast.success(i18n.t("common:copied")); return ok; }