Files
AgentHub/client/src/lib/clipboard.ts
T
nntrivi2001 17a5c7470d feat(ui): shared toast, confirm, dialog, copy and IME-safe Enter primitives
One implementation each, for every page to use instead of its own copy:

- lib/toast.ts + components/Toaster.tsx: toast.success|error|info from
  anywhere, rendered in polite/assertive live regions; an error stays until
  dismissed, a success fades, a repeat collapses.
- lib/confirm.ts + components/ConfirmHost.tsx: await confirmDialog({...})
  and confirmDiscard() through the themed ConfirmModal, replacing
  window.confirm and the actions that asked nothing.
- hooks/useDialog.ts: the modal focus contract (focus in, Tab trapped,
  Escape, focus restored), extracted from ConfirmModal; dialogs stack so
  only the top one answers keys. ConfirmModal now uses it.
- hooks/useArmed.ts: two-click confirmation that disarms after 3 s.
- hooks/useUnsavedGuard.ts: beforeunload while a draft is dirty.
- lib/clipboard.ts copyText: Clipboard API with an execCommand fallback for
  non-secure origins, outcome toasted.
- lib/keys.ts isSubmitEnter: ignores the Enter that confirms an IME
  candidate (Vietnamese Telex, CJK).
2026-10-05 13:17:09 +07:00

51 lines
1.5 KiB
TypeScript

/**
* @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://<lan-ip>:4820`. Reports the outcome as
* a toast, so a copy is never silent.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import i18n from "../i18n";
import { toast } from "./toast";
function fallbackCopy(text: string): boolean {
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();
}
}
/**
* 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<boolean> {
let ok = false;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
}
} catch {
/* fall through to the textarea path */
}
if (!ok) ok = fallbackCopy(text);
if (!ok) toast.error(i18n.t("common:copyFailed"));
else if (!quiet) toast.success(i18n.t("common:copied"));
return ok;
}