17a5c7470d
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).
51 lines
1.5 KiB
TypeScript
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;
|
|
}
|