From 0fc17b5f9b0a092b44193e23aeead77a9772a9c8 Mon Sep 17 00:00:00 2001 From: overtrue Date: Fri, 27 Feb 2026 17:17:23 +0800 Subject: [PATCH] fix: add clipboard fallback for copy actions --- components/copy-input.tsx | 4 +-- components/object/path-links.tsx | 3 +- components/object/versions.tsx | 3 +- lib/clipboard.ts | 50 ++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 lib/clipboard.ts diff --git a/components/copy-input.tsx b/components/copy-input.tsx index 716e1902..05e34418 100644 --- a/components/copy-input.tsx +++ b/components/copy-input.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next" import { RiFileCopyLine } from "@remixicon/react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { copyToClipboard } from "@/lib/clipboard" import { useMessage } from "@/lib/feedback/message" import { cn } from "@/lib/utils" @@ -29,8 +30,7 @@ export function CopyInput({ const handleCopy = async () => { try { - if (!value) throw new Error("No value to copy") - await navigator.clipboard.writeText(value) + await copyToClipboard(value) message.success(t("Copy Success")) } catch { message.error(t("Copy Failed")) diff --git a/components/object/path-links.tsx b/components/object/path-links.tsx index 25a7210c..dab658d6 100644 --- a/components/object/path-links.tsx +++ b/components/object/path-links.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" +import { copyToClipboard } from "@/lib/clipboard" import { RiFileCopyLine, RiCheckLine } from "@remixicon/react" interface Segment { @@ -21,7 +22,7 @@ function useClipboard(value: string, copiedDuring = 3000) { const copy = React.useCallback(async () => { try { - await navigator.clipboard.writeText(value) + await copyToClipboard(value) setCopied(true) setTimeout(() => setCopied(false), copiedDuring) } catch { diff --git a/components/object/versions.tsx b/components/object/versions.tsx index 1dd04cbb..29acbe45 100644 --- a/components/object/versions.tsx +++ b/components/object/versions.tsx @@ -9,6 +9,7 @@ import { DataTable } from "@/components/data-table/data-table" import { useDataTable } from "@/hooks/use-data-table" import { useObject } from "@/hooks/use-object" import { useMessage } from "@/lib/feedback/message" +import { copyToClipboard } from "@/lib/clipboard" import { exportFile } from "@/lib/export-file" import { getContentType } from "@/lib/mime-types" import { formatBytes } from "@/lib/functions" @@ -72,7 +73,7 @@ export function ObjectVersions({ async (versionId: string) => { if (!versionId) return try { - await navigator.clipboard.writeText(versionId) + await copyToClipboard(versionId) message.success(t("Copy Success")) } catch { message.error(t("Copy Failed")) diff --git a/lib/clipboard.ts b/lib/clipboard.ts new file mode 100644 index 00000000..12ec343a --- /dev/null +++ b/lib/clipboard.ts @@ -0,0 +1,50 @@ +export async function copyToClipboard(value: string): Promise { + if (!value) throw new Error("No value to copy") + + if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value) + return + } catch (error) { + // Fallback to legacy copy only when Clipboard API is denied or unsupported in current context + const fallback = legacyCopyToClipboard(value) + if (fallback) return + if (error instanceof Error) throw error + throw new Error("Failed to copy text") + } + } + + const fallback = legacyCopyToClipboard(value) + if (fallback) return + + throw new Error("Failed to copy text") +} + +function legacyCopyToClipboard(value: string): boolean { + if (typeof document === "undefined" || !document?.execCommand) return false + + const textarea = document.createElement("textarea") + textarea.value = value + textarea.setAttribute("readonly", "") + textarea.style.position = "fixed" + textarea.style.top = "0" + textarea.style.left = "0" + textarea.style.opacity = "0" + textarea.style.pointerEvents = "none" + textarea.style.zIndex = "-1" + + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + + let copied = false + try { + copied = document.execCommand("copy") + } catch { + copied = false + } finally { + document.body.removeChild(textarea) + } + + return copied +}