返回 DeepSeek-Reasonix
CopyButton.tsx
根目录 / desktop / frontend / src / components / CopyButton.tsx
1 import { useEffect, useRef, useState } from "react";
2 import { Check, Copy } from "lucide-react";
3 import { useT } from "../lib/i18n";
4
5 function fallbackCopyText(value: string): boolean {
6 const activeElement = document.activeElement;
7 const selection = document.getSelection();
8 const ranges: Range[] = [];
9 if (selection) {
10 for (let index = 0; index < selection.rangeCount; index += 1) {
11 ranges.push(selection.getRangeAt(index));
12 }
13 }
14 const textarea = document.createElement("textarea");
15 textarea.value = value;
16 textarea.setAttribute("readonly", "");
17 textarea.style.position = "fixed";
18 textarea.style.inset = "0 auto auto 0";
19 textarea.style.width = "1px";
20 textarea.style.height = "1px";
21 textarea.style.opacity = "0";
22 document.body.appendChild(textarea);
23 textarea.select();
24 let ok = false;
25 try {
26 ok = document.execCommand("copy");
27 } finally {
28 textarea.remove();
29 if (selection) {
30 selection.removeAllRanges();
31 for (const range of ranges) selection.addRange(range);
32 }
33 if (activeElement instanceof HTMLElement) activeElement.focus();
34 }
35 return ok;
36 }
37
38 async function writeClipboardText(value: string): Promise<void> {
39 try {
40 await navigator.clipboard.writeText(value);
41 return;
42 } catch {
43 /* try the desktop runtime below */
44 }
45 try {
46 if (typeof window !== "undefined" && (await window.runtime?.ClipboardSetText?.(value))) return;
47 } catch {
48 /* runtime unavailable in browser dev */
49 }
50 if (fallbackCopyText(value)) return;
51 throw new Error("clipboard unavailable");
52 }
53
54 // CopyButton copies text to the clipboard on click and briefly flips to a check.
55 // Clipboard writes are best-effort across browser dev, Wails, and webviews, so
56 // the visible acknowledgement stays tied to the user action.
57 export function CopyButton({
58 text,
59 getText,
60 className,
61 label,
62 showInlineLabel = true,
63 }: {
64 text?: string;
65 getText?: () => string | Promise<string>;
66 className?: string;
67 label?: string;
68 showInlineLabel?: boolean;
69 }) {
70 const t = useT();
71 const [copied, setCopied] = useState(false);
72 const timerRef = useRef<number | null>(null);
73 const actionLabel = label ?? t("msg.copy");
74 const stateLabel = copied ? t("msg.copied") : actionLabel;
75
76 useEffect(() => {
77 return () => {
78 if (timerRef.current != null) window.clearTimeout(timerRef.current);
79 };
80 }, []);
81
82 const copy = async () => {
83 try {
84 const value = getText ? await getText() : text ?? "";
85 void writeClipboardText(value).catch(() => {});
86 setCopied(true);
87 if (timerRef.current != null) window.clearTimeout(timerRef.current);
88 timerRef.current = window.setTimeout(() => {
89 setCopied(false);
90 timerRef.current = null;
91 }, 1200);
92 } catch {
93 /* clipboard unavailable */
94 }
95 };
96 return (
97 <button
98 className={[
99 "copybtn",
100 copied ? "copybtn--copied" : "",
101 className ?? "",
102 ].filter(Boolean).join(" ")}
103 onClick={copy}
104 aria-label={stateLabel}
105 title={actionLabel}
106 type="button"
107 >
108 {copied ? <Check size={13} /> : <Copy size={13} />}
109 {showInlineLabel && (
110 <span className="copybtn__label-inline">{stateLabel}</span>
111 )}
112 </button>
113 );
114 }
115
115 lines Plain Text