javascriptasync-awaitclipboardbookmarklet

How do I ensure that the website has focus so the copy to clipboard can happen?


I have the following (sanitised and formatted) javascript code in a bookmarklet/favlet. The code copies text from some objects that match the querySelector definitions. Then it copies it to the clipboard and tells the user it has been copied.

!function () { 
    // get objects from web page
    let n = document.querySelector("span.flipper"), 
        e = n.querySelectorAll("span.OptionLabel"), 
        o = e.length; 

    // loop through and construct output from text in objects
    for (var t = "", r = 0; r < o; r++)
        t += e[r].textContent + ", "; 

    // wait for text to be written to clipboard
    async function c() { 
        await navigator.clipboard.writeText(t); 
        console.log("chesterton's fence") // partial fix??
    } 

    // tell user what was copied
    confirm('Click "OK" to copy the list to your clipboard.' + o + "\n\n" + t + "\n"), c() 
}();

The code works, sometimes. It seems to work more often since I added the console.log. It works if I paste the code into the console on developer tools (Edge). But, mostly it doesn't work when I do "ctrl+L" [to focus addressbar], "* ext" [to find the bookmarklet], and then select the bookmarklet.

The console error in Edge is:

Uncaught (in promise) DOMException: Document is not focused.

I'd like to understand why, and of course to fix the behaviour. I know there are security restrictions on interacting with clipboards, but it sometimes works (maybe only after opening devtools?).

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard#using_the_clipboard_api doesn't seem to give any answers as to why this wouldn't work?

Maybe I'm using async wrongly? It's my first time using that feature.

Maybe it's related to the objects, which are written dynamically by the page? I have to open a drop-down to make the elements appear, then I run my bookmarklet.

The confirm always shows the text I want, so it's only the write to clipboard that fails.

I found, for example, this question Document not focused error when writing to clipboard using Clipboard API in chrome extension which is related but has no answers; other similar questions are >3y old and don't seem to help, so I'm asking again as security will have moved on. It is possible though, it works sometimes, unless that's an Edge Browser bug.

How do I copy to the clipboard in JavaScript? has some promising workarounds using document.execCommand('copy') but that is deprecated and so I want to avoid using it even if Edge currently still supports it. The page is HTTPS, the script is in the URL of a bookmark.


Solution

  • It works if you add a 0ms setTimeout after the confirm(). This is an unusual trick to force some code to execute at the end of the call stack. I suspect this is because confirm() brings up a native browser control, and that steals the focus. The browser probably then doesn't queue the task to give the focus back to the page until the end of the call stack. So you can beat it at it's own game by queueing the clipboard copy after that as well.

    !function () { 
        // get objects from web page
        let n = document.querySelector("span.flipper"), 
            e = n.querySelectorAll("span.OptionLabel"), 
            o = e.length; 
    
        // loop through and construct output from text in objects
        for (var t = "", r = 0; r < o; r++)
            t += e[r].textContent + ", "; 
    
        // wait for text to be written to clipboard
        async function c() { 
            await navigator.clipboard.writeText(t); 
        } 
    
        // tell user what was copied
        confirm('Click "OK" to copy the list to your clipboard.' + o + "\n\n" + t + "\n")
        setTimeout(c, 0) 
    }();
    

    Probably explains why it worked sometimes as well. It's a race condition. This evades it, albeit in a hacky way. Though it's not as bad as it first seems -- actually, this is the recommended way of forcing something to the next "tick" since setImmediate was deprecated.