254254 </template >
255255 </main >
256256
257+ <Transition name =" copy-toast" >
258+ <div
259+ v-if =" copyToast"
260+ :class =" ['copy-toast', copyToast.tone]"
261+ :role =" copyToast.tone === 'error' ? 'alert' : 'status'"
262+ :aria-label =" `${copyToast.title}: ${copyToast.body}`"
263+ aria-live =" polite"
264+ >
265+ <CheckCircle2 v-if =" copyToast.tone === 'success'" :size =" 18" aria-hidden =" true" />
266+ <AlertTriangle v-else :size =" 18" aria-hidden =" true" />
267+ <span >
268+ <strong >{{ copyToast.title }}</strong >
269+ <small >{{ copyToast.body }}</small >
270+ </span >
271+ </div >
272+ </Transition >
273+
257274 <footer class =" footer" >
258275 <span >scan.mergeos.shop</span >
259276 <span >Last sync {{ lastSyncLabel }}</span >
@@ -269,6 +286,7 @@ import {
269286 ArrowDownLeft ,
270287 ArrowUpRight ,
271288 Braces ,
289+ CheckCircle2 ,
272290 Coins ,
273291 Copy ,
274292 ExternalLink ,
@@ -325,6 +343,8 @@ const queryFilter = ref('');
325343const typeFilter = ref (' all' );
326344const showAllTransactions = ref (false );
327345const route = ref (parseRoute ());
346+ const copyToast = ref (null );
347+ let copyToastTimer = 0 ;
328348
329349const tokenSymbol = computed (() => config .value ? .token_symbol || marketplace .value ? .stats ? .token_symbol || ' MRG' );
330350const paymentMode = computed (() => paymentModeLabel (config .value ? .payment_mode ));
@@ -391,6 +411,7 @@ onMounted(() => {
391411
392412onBeforeUnmount (() => {
393413 window .removeEventListener (' popstate' , syncRoute);
414+ window .clearTimeout (copyToastTimer);
394415});
395416
396417async function loadExplorerData () {
@@ -703,18 +724,46 @@ function safeReturnPath(path = '/') {
703724}
704725
705726async function copyValue (value ) {
727+ const text = String (value || ' ' );
706728 try {
707- await navigator .clipboard .writeText ( String (value || ' ' ));
708- } catch {
709- const input = document . createElement ( ' textarea ' );
710- input . value = String (value || ' ' );
711- document . body . appendChild (input);
712- input . select ( );
713- document . execCommand ( ' copy ' );
714- input . remove ( );
729+ if ( navigator .clipboard ? .writeText ) {
730+ await navigator . clipboard . writeText (text);
731+ } else {
732+ fallbackCopyText (text );
733+ }
734+ showCopyToast ( ' success ' , ' Copied to clipboard ' , copiedValueLabel (text) );
735+ } catch (error) {
736+ showCopyToast ( ' error ' , ' Copy failed ' , ' Please copy it manually. ' );
715737 }
716738}
717739
740+ function fallbackCopyText (text ) {
741+ const input = document .createElement (' textarea' );
742+ input .value = text;
743+ input .setAttribute (' readonly' , ' ' );
744+ input .style .position = ' fixed' ;
745+ input .style .left = ' -9999px' ;
746+ document .body .appendChild (input);
747+ input .select ();
748+ const copied = document .execCommand (' copy' );
749+ input .remove ();
750+ if (! copied) throw new Error (' Copy command failed' );
751+ }
752+
753+ function showCopyToast (tone , title , body ) {
754+ window .clearTimeout (copyToastTimer);
755+ copyToast .value = { tone, title, body };
756+ copyToastTimer = window .setTimeout (() => {
757+ copyToast .value = null ;
758+ }, 2600 );
759+ }
760+
761+ function copiedValueLabel (value ) {
762+ const text = String (value || ' ' ).trim ();
763+ if (! text) return ' Value copied.' ;
764+ return text .length > 34 ? shortHash (text, 12 , 8 ) : text;
765+ }
766+
718767function typeLabel (type ) {
719768 return ledgerTypeMeta (type).label ;
720769}
0 commit comments