From 4ee59442cef08918aaf5fdc3d99aac0157ace7f4 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:33:31 +0200 Subject: [PATCH] feat(ui): add shared copy button component --- resources/js/app.js | 2 ++ resources/js/copy-button.js | 35 +++++++++++++++++++ .../views/components/copy-button.blade.php | 28 ++++++--------- 3 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 resources/js/copy-button.js diff --git a/resources/js/app.js b/resources/js/app.js index bb41b7f04..900ef8af7 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,3 +1,4 @@ +import { initializeCopyButtonComponent } from './copy-button.js'; import { initializeTerminalComponent } from './terminal.js'; // Livewire 3.5.19+ re-applies `x-cloak` to morphed elements during wire:navigate @@ -12,6 +13,7 @@ document.addEventListener('livewire:navigated', () => { // Keeping this registration independent from the current route also makes it // available before Alpine processes terminal markup after wire:navigate. document.addEventListener('alpine:init', initializeTerminalComponent); +document.addEventListener('alpine:init', initializeCopyButtonComponent); /** * Smooth-scroll a settings section into view, then flash its border for 500ms diff --git a/resources/js/copy-button.js b/resources/js/copy-button.js new file mode 100644 index 000000000..0ce8d5d67 --- /dev/null +++ b/resources/js/copy-button.js @@ -0,0 +1,35 @@ +// Alpine data provider for the component (x-data="copyButton"). +export function initializeCopyButtonComponent() { + window.Alpine.data('copyButton', () => ({ + copied: false, + async copy(value) { + if (value === null || value === undefined) { + window.toast('Value is not available.', { type: 'warning' }); + return; + } + try { + if (navigator.clipboard?.writeText && window.isSecureContext) { + await navigator.clipboard.writeText(value); + } else { + // Deprecated, but the only copy path on plain http (non-secure contexts). + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + const ok = document.execCommand('copy'); + document.body.removeChild(textarea); + if (!ok) { + throw new Error('Copy command was rejected.'); + } + } + this.copied = true; + setTimeout(() => (this.copied = false), 1200); + } catch (e) { + window.toast('Could not copy to clipboard.', { type: 'warning' }); + } + }, + })); +} diff --git a/resources/views/components/copy-button.blade.php b/resources/views/components/copy-button.blade.php index dfdceef20..a266a272d 100644 --- a/resources/views/components/copy-button.blade.php +++ b/resources/views/components/copy-button.blade.php @@ -1,22 +1,16 @@ @props([ - 'value', + 'value' => null, + 'resolve' => null, 'label' => 'Copy to clipboard', ]) -