Ui-template copy to clip - using ‘watch’ to retrieve other node data as well

How can a ui-template node reliably copy a simple text string to the clipboard?

The following JS code in the ui-template (Dashboard2) works with a button within the ui-template.

However, it is necessary for the ui-template node to receive the text string via a msg also. In the JS code, a msg is received via watch. This is confirmed by console.log.
But the text string is not copied to the clipboard. The alert function says: “undefined”.

The console outputs:
“document.execCommand('cut'/'copy') wurde abgelehnt, weil es nicht von innerhalb einer kurz dauernden benutzergenerierten Ereignisbehandlung aufgerufen wurde.”.
deepl says:
“document.execCommand(‘cut’/'copy') was rejected because it was not called from within a short-lived user-generated event handler.”

[{"id":"71258c04f9bf025c","type":"inject","z":"b25c3ffa5d02edb9","name":"copyThis2Clip","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"copyThis2Clip","payload":"","payloadType":"date","x":230,"y":1180,"wires":[["2377519e41010f8f"]]},{"id":"2377519e41010f8f","type":"ui-template","z":"b25c3ffa5d02edb9","group":"cf2110ce4840bb3c","page":"","ui":"","name":"copy2clip_test","order":1,"width":0,"height":0,"head":"","format":"<template>\n  \n    <span> copy2Clip_2026-01-05</span>\n    <v-btn color=\"primary\" @click=\"this.cClip('hello ')\">\n     hello cClip\n    </v-btn>\n</template>\n\n<script>\n  export default {\n    data() {\n      return {\n        ctext: \"testpattern\"\n      };\n    },\n\n    watch: {\n      msg(newMsg) {\n        if (!newMsg) return;\n\n          if (newMsg.topic === 'copyThis2Clip') {\n            const test = (`watch:: ${new Date(newMsg.payload)}`)\n            this.ctext = test;\n\n            console.log(test);\n          //  this.cClip(test);\n\n            setTimeout(() => {\n              this.cClip(this.ctest);\n            }, 100);\n            newMsg = null;\n          }\n      }\n    },\n\n    methods: {\n\n      cClip(test) {\n        //copy_text(input) {\n          // Get the text field\n          // copyText = document.getElementById(input).innerText;\n          var dummy = document.createElement(\"textarea\");\n          document.body.appendChild(dummy);\n          dummy.value = test;\n          dummy.select();\n          document.execCommand(\"copy\");\n          document.body.removeChild(dummy);\n          alert(\"Copied  Data to Clipboard\\n\\n\" + test)\n      },\n    }\n  }\n</script>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":440,"y":1180,"wires":[[]]},{"id":"cf2110ce4840bb3c","type":"ui-group","name":"Gruppe 1","page":"eb8696622e5576eb","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"eb8696622e5576eb","type":"ui-page","name":"xPlorer_1","ui":"2f9f3adcdf082281","path":"/page6","icon":"home","layout":"grid","theme":"10d4154b82c88eef","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":4,"className":"","visible":true,"disabled":false},{"id":"2f9f3adcdf082281","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":"5","showDisconnectNotification":true,"allowInstall":true},{"id":"10d4154b82c88eef","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

Any advice?

The previous description does not show the complete situation.
In fact, “copy2clip” works when it is triggered from the ui-template page, e.g., by a button.

However, the use case is that another, second modal page is called up from the first ui-template page. The data which needs to be copied is displayed on the second ui-template, the modal page. The user can select one of these data elements using click and thereby transfer it to the clipboard. This does not work!

Presumably, the access with const dummy = document.createElement(“textarea”); does not refer to the modal window, which is why document.execCommand(‘copy’); fails,
but also does not throw a console.error(“Copy failed !”, err);

Is this a plausible explanation and how can it be done correctly?

The clipboard copy succeeds when the button is clicked, since the button click is a 'user gesture' (initiated by a user from the UI), which is allowed to do this.
However, for security reasons, the browser blocks this operation if initiated by a non-UI event, such as a network event (the inject node sends the msg from the Node-red server to the dashboard input socket).

BTW, I would recommend using the native API instead of creating a temporary 'dummy' element. it will also give you an error message.

cClip(test)  {
   navigator.clipboard.writeText(test)
   .then(() => {
      console.log(test+" copied to clipboard!");
   })
   .catch(err => {
      console.error("Failed to copy to clipboard", err);
   });
}

There is no API, no trick, no hidden flag, no synthetic event, no focus hack that can turn a programmatic action into a trusted user gesture.

This is a deliberate security boundary. If it were possible, any website could:

  • copy spam into your clipboard
  • overwrite passwords
  • trigger downloads
  • open popups
  • interact with the OS

…without you clicking anything.

So browsers enforce a strict rule:

Only real, physical user actions (click, tap, keypress) count as trusted gestures.

Everything else — timers, watchers, socket events, promises, async callbacks — is untrusted.

1 Like

AFAIK This should work. Let me check

The "modern" way fails:

errorCaptured TypeError: can't access property "writeText", navigator.clipboard is undefined

navigator has:
userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0"

but no 'clipboard'

Interesting. So I guess you are using FireFox. It has stricter security, which seems to support navigator.clipboard only under HTTPS

Also with Brave, but that's also Firefox:
errorCaptured TypeError: Cannot read properties of undefined (reading 'writeText')

'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'

The browser allows programmatic copy to the clipboard only when it is invoked from within a user gesture.
As you mentioned, you can send an event from the inject node into the template, then open a modal window (v-dialog) from which you can then hit some button (I played with it and it works). Another way would be set visibility on/off to a text-area field & copy button on the main page itself.

In any case, it seems the issue you are facing is that unlike Edge & Chrome, programmatic copy to the clipboard in Firefox & Brave (in both navigator.clipboard and 'dummy' element) requires to be on HTTPS.

BTW, instead of using the watch mechanism (which has issues), I would just set a msg listener in the mounted() area and have full control of incoming messages.

mounted() {
    const vThis = this; // save current 'this', for use inside callbacks & function properties
     // Set Socket listener
    this.$socket.on('msg-input:' + this.id, function(msg)
    {
        vThis.doSomethingWith(msg);
    });
}

This is the way it is going for everything. However, http://localhost should also be allowed. Not sure if FF does though. If Google have their way, all browser API's will require you to be in an HTTPS environment.

No, Brave uses Chromium, Same as Vivaldi, Edge, Chrome and many others.

I would also say that if much of your UI requires ui_templates, you would be much better off not using D2 but rather UIBUILDER. With or without a framework (probably without).

Just to show the copy2clip situation with a button to do the 'copy'
on a ui-template and modal second ui-template

On 1.ui-template --> copy to clip is OK

<template>
    <v-btn color="primary" @click="copy2clip('test on 1.ui-template')">
      copy2clip
    </v-btn>
</template>

<script>
  methods: {
    cClip(ctext) {
        const dummy = document.createElement("textarea");
        dummy.value =   `${ctext} \n ${new Date()}`;
        document.body.appendChild(dummy);
        dummy.select();
        try {
          document.execCommand('copy'); 
        } catch (err) {
          console.error("Copy failed !", err);
        }
   console.log(`copy2clip    ${dummy.value}`)      
        document.body.removeChild(dummy);
    },
</script>

The 1.ui-template has a msg call to a 2.ui-template which opens the modal window.
The second ui-template holds the same and code just to demonstrate the copy2clip.
The text to be transferred is different.

<template>
    <v-btn color="primary" @click="copy2clip('TEST on 2.ui-template ???')">
        copy2clip
    </v-btn>
</template>

Results
1.ui-template console.log

copy2clip    test on 1.ui-template 
 Tue Jan 06 2026 23:23:09 GMT+0100 (Mitteleuropäische Normalzeit)

cntrl V to paste clip:

test on 1.ui-template 
 Tue Jan 06 2026 23:23:09 GMT+0100 (Mitteleuropäische Normalzeit)

2.ui-template console.log

copy2clip    TEST on 2.ui-template ??? 
 Tue Jan 06 2026 23:23:45 GMT+0100 (Mitteleuropäische Normalzeit)

cntrl V to paste clip: (result is the clip content from 1.ui-template)

test on 1.ui-template 
 Tue Jan 06 2026 23:23:09 GMT+0100 (Mitteleuropäische Normalzeit)

Point I don't understand:
With the 1.ui-template the copy to clip is OK.
The same code on a modal ui fails!
Any help very much welcomed ... or is the uibuilder the only way to go?

Here's a fully working example.
flows.json (7.0 KB)

If on Firefox, you can replace the `navigator.clipboard' to the 'dummy element' thing

Even though the example works, it essentially corresponds to what works successfully in my previous post as 1.ui-template.
So, the problem is not with the “first” ui-template, but when a second ui-template is opened as a modal by the first template and a copy to clip is made on this modal window.
Then nothing happens, the browser (Firefox in my case) remains quiet, does not throw an error, does not copy anything to the clipboard. The exception is when I use navigator.clipboard. But that is a different problem.

Maybe I'm missing something here. Is the 2nd modal also a ui-template? (in my example it is a v-dialog, defined in and launched from within the 1st ui-template). Can you share the flow?

In principle, sharing the flow would not be a problem, but it is not suitable for posting in the forum. The internal JS/HTML structure is quite complex.
My understanding so far is that the modal version prevents copy2clip here. Why? That is beyond my knowledge.
As an alternative, I will now use a normal ui-template instead of the modal ui-template. Based on what we know so far, that should be no problem.