From c0f628b2364ee8fd74a72e31146695025c97f9a9 Mon Sep 17 00:00:00 2001 From: mtyszczak <mateusz.tyszczak@gmail.com> Date: Mon, 17 Mar 2025 14:49:49 +0100 Subject: [PATCH] Add copy event to classic button component --- .../wallets/metamask/MetamaskConnect.vue | 9 ++--- src/components/ui/button/Button.vue | 32 +++++++++++++++-- src/components/ui/copybutton/Button.vue | 3 +- src/utils/copy.ts | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/utils/copy.ts diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index db9d97e..2f5c780 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -142,9 +142,6 @@ onMounted(() => { void connect(false); }); -const copyContent = (content: string) => { - navigator.clipboard.writeText(String(content)); -}; const generateAccountUpdateTransaction = async(): Promise<string> => { const wax = await getWax(); const tx = await wax.createTransaction(); @@ -251,12 +248,12 @@ const updateAccountName = (value: string | any) => { </div> </div> <div class="flex items-center flex-col"> - <Button :disabled="isLoading" @click="copyContent(getAuthorityUpdateSigningLink())" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" :copy="getAuthorityUpdateSigningLink" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> <Separator label="Or" class="mt-8" /> <div class="flex justify-center mt-4"> - <Button :disabled="isLoading" @click="generateAccountUpdateTransaction()" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" :copy="generateAccountUpdateTransaction" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy entire transaction</span> </Button> </div> @@ -277,7 +274,7 @@ const updateAccountName = (value: string | any) => { </div> </div> <div class="flex items-center flex-col"> - <Button :disabled="isLoading" @click="copyContent(getAccountCreateSigningLink())" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <Button :copy="getAccountCreateSigningLink" :disabled="isLoading" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> </div> diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue index 3ba549b..ba01e17 100644 --- a/src/components/ui/button/Button.vue +++ b/src/components/ui/button/Button.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> -import type { HTMLAttributes } from 'vue' +import { ref, type HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { Primitive, type PrimitiveProps } from 'reka-ui' import { type ButtonVariants, buttonVariants } from '.' -import { mdiLoading } from '@mdi/js' +import { mdiCheck, mdiLoading } from '@mdi/js' +import { copyText } from '@/utils/copy' interface Props extends PrimitiveProps { variant?: ButtonVariants['variant'] @@ -11,12 +12,31 @@ interface Props extends PrimitiveProps { class?: HTMLAttributes['class'] loading?: boolean disabled?: boolean + copy?: string | (() => (string | Promise<string>)) } const props = withDefaults(defineProps<Props>(), { as: 'button', loading: false }) + +const copyLoading = ref(false); + +const copyBtn = () => { + if (!props.copy) return; + const text = typeof props.copy === 'function' ? props.copy() : props.copy; + if(text instanceof Promise) + text.then((text) => { + copyText(text); + }); + else + copyText(text); + + copyLoading.value = true; + setTimeout(() => { + copyLoading.value = false; + }, 1000); +}; </script> <template> @@ -25,13 +45,19 @@ const props = withDefaults(defineProps<Props>(), { :as-child="asChild" :disabled="loading || disabled" :class="[ cn(buttonVariants({ variant, size }), props.class)]" + @click="copyBtn" > + <span v-if="copy && copyLoading" class="absolute mx-auto"> + <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path :style="{ + fill: !variant || variant === 'default' ? 'hsl(var(--background))' : 'hsl(var(--foreground))' + }" :d="mdiCheck"/></svg> + </span> <span v-if="loading" class="animate-spin absolute mx-auto"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path :style="{ fill: !variant || variant === 'default' ? 'hsl(var(--background))' : 'hsl(var(--foreground))' }" :d="mdiLoading"/></svg> </span> - <span :style="{ 'visibility': loading ? 'hidden' : 'visible' }" class="inline-flex items-center justify-center gap-2"> + <span :style="{ 'visibility': loading || copyLoading ? 'hidden' : 'visible' }" class="inline-flex items-center justify-center gap-2"> <slot/> </span> </Primitive> diff --git a/src/components/ui/copybutton/Button.vue b/src/components/ui/copybutton/Button.vue index 6694833..4aa3c93 100644 --- a/src/components/ui/copybutton/Button.vue +++ b/src/components/ui/copybutton/Button.vue @@ -4,6 +4,7 @@ import { cn } from '@/lib/utils' import { Primitive, type PrimitiveProps } from 'reka-ui' import { buttonVariants } from '.' import { mdiCheck, mdiContentCopy } from '@mdi/js' +import { copyText } from '@/utils/copy' interface Props extends PrimitiveProps { class?: HTMLAttributes['class']; @@ -18,7 +19,7 @@ const copyBtn = (event: MouseEvent) => { const target = event.target as HTMLElement; const value = target.getAttribute("data-copy"); if (!value) return; - navigator.clipboard.writeText(String(value)); + copyText(value); const oldAttribute = target.children[0].children[0].getAttribute('d'); target.children[0].children[0].setAttribute('d', mdiCheck); diff --git a/src/utils/copy.ts b/src/utils/copy.ts new file mode 100644 index 0000000..b58173c --- /dev/null +++ b/src/utils/copy.ts @@ -0,0 +1,34 @@ +/** + * Copies given text into clipboard. + * + * This function supports 3 ways of copying and fallbacks if any of them fails: + * 1. Using the modern Clipboard API (navigator.clipboard.writeText) - on most browsers works only in secure context (HTTPS) or localhost + * 2. Using the deprecated document.execCommand("copy") - deprecated, but still supported by most browsers - with Firefox works only on "click" event triggered by the user + * 3. Using the prompt function - the most universal way, but requires user interaction + */ +export const copyText = (text: string) => { + try { + if (navigator.clipboard) { // is secure context + return navigator.clipboard.writeText(text).catch(() => { + prompt("Copy to clipboard: Ctrl+C, Enter", text); + }); + } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { // check if we can use deprecated copy command + const textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.width = "0"; + textarea.style.height = "0"; + textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge. + document.body.appendChild(textarea); + textarea.select(); + try { + if(!document.execCommand("copy")) // Security exception may be thrown by some browsers. + throw new Error("Copy command was unsuccessful"); + } finally { + document.body.removeChild(textarea); + } + } + throw new Error("No clipboard support"); + } catch { + prompt("Copy to clipboard: Ctrl+C, Enter", text); + } +} -- GitLab