javascriptclipboarddom-eventsexeccommandgoo.gl

How do I make a button that gets an external resource AND copies it to the clipboard?


So I have a button that's supposed to make a fancy share URL, shorten it using goo.gl, and then copy that to the clipboard. The great news is, I have successfully done all of that, but just not all at once.

The problem stems from the spec:

Copy commands triggered from document.execCommand() will only affect the contents of the real clipboard if the event is dispatched from an event that is trusted and triggered by the user, or if the implementation is configured to allow this. How implementations can be configured to allow write access to the clipboard is outside the scope of this specification.

execCommand Unofficial W3C Spec

So it seems that this might not work...

You see in order to shorten the URL, I need to make an AJAX call. I only make this when the user clicks the share button because I have a limit of 1,000,000 shortens per day (and if I generated a new share URL every time the page changed, that can easily be 1,000 new URLs for one user, so I'd be limited to a maximum of 1,000 end users: not the best option). But that means that I have to listen for AJAX events from a thread other than the one which originated the event, effectively losing this blessed state required by execCommand('copy').

Is there any way to have one singular button that both generates the goo.gl URL and then copies said short URL to the clipboard?

For reference, here's what I wrote (Kotlin/JS) and here's the JavaScript output.
Here is a SSCCE that illustrates how it seems like it should work, but does not (based on 陈杨华's answer).


Solution

  • There are 2 solutions that work, but both are flawed. One works only if your request takes less than one seconde, and one is deprecated, so it should not be used in a production environment.

    First one is to use setTimeout, which is one of the few async function that doesn't lose the execCommand "privilege". But it doesn't lose is granted that it's equal or less than 1000ms. So if your request is less than that, you're good to go, but if not, then you have an error. If you combine it with some sort of timeout handling, it can work, but if the requests often take more than 1s then it may not be good enough. Like this for example:

    var toCopy;
    
    const buttonClick = () => {
    
      setTimeout(function() {
        if (toCopy) {
          // strangely, this execCommand will work
          console.log(document.execCommand('copy'));
        }
      }, 1000);
    
      var client = new XMLHttpRequest();
      client.onload = function(data) {
    
        toCopy = this.responseText;
    
      };
    
      // by setting your timeout on your request 
      // with the same duration as the setTiemout
      // you make sure it either works or throws an error
      client.ontimeout = function(data) {
    
        console.log('timeout');
    
      };
      client.timeout = 1000;
      client.open("GET", "https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain");
      client.send();
    
    
    }
    
    $(() => {
      $("button").click(buttonClick)
    })
    
    document.addEventListener('copy', function(e) {
    
      e.preventDefault();
      e.clipboardData.setData('text/plain', toCopy);
    
    });
    

    https://jsfiddle.net/8md4Ltu2/4/

    There's another way to make it work, but it is deprecated, so not to be used. But, just to be thorough, I'll put it here. You can set the asynchronous flag of your XMLHttpRequest to false. The request will be synchronous and the handling of execCommand is then very straightforward. But this synchronous flag is deprecated, the guideline is to throw an error if you try to use it, so it's not to be used. See: https://xhr.spec.whatwg.org/#synchronous-flag

    var toCopy;
    
    const buttonClick = () => {
    
      var client = new XMLHttpRequest();
      client.onload = function(data) {
        toCopy = this.responseText;
        console.log(document.execCommand('copy'));
      };
    
    
      client.open("GET", "https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain", false);
      client.send();
    
    
    }
    
    $(() => {
      $("button").click(buttonClick)
    })
    
    document.addEventListener('copy', function(e) {
    
      e.preventDefault();
      e.clipboardData.setData('text/plain', toCopy);
    
    });
    

    https://jsfiddle.net/zezskm2x/2/