Copy/Paste of JSON data, anyone know how to do it?

I am struggling to get copying of JSON data to work all the time. There is a browser restriction that you cannot do

    event.clipboardData.setData('application/json', payload);

Unless you do it during a copy event handler against the document. This appears to be restricted for security reasons, although why it is a security risk versus copying text never seems to be explained.

Here is how I have attempted to do this in an Elm application:

const copy = new Copy(app);

app.ports.jsonToClipboard.subscribe((data) => {
    copy.jsonToClipboard(data);
});

class Copy {
    constructor(app) {
        this.app = app;

        this.copyCallback = this.copyCallback.bind(this);
        document.addEventListener('copy', this.copyCallback);
    }

    // Gets called from the port.
    jsonToClipboard(json) {
        console.log("2. jsonToClipboard.");
        this.jsonPayload = json;
    }

    copyCallback(e) {
        console.log("3. Copy callback.");
        if (this.jsonPayload != null) {
            e.clipboardData.setData('application/json', JSON.stringify(this.jsonPayload));
            console.log("4. JSON to clipboard - non null json payload.");
            this.jsonPayload = null;
            e.preventDefault();
            return;
        }
    }

Then in the Elm code:

-- This gets called during a keyboard subscription handler when Ctrl+C is pressed.
-- Browser.Events.onKeyDown Keyboard.keyDownDecoder 
--     |> Sub.map KeyboardShortcut
    let
        _ =
            Debug.log "1. copyElementsFromSelection" "send json to Copy"
    in
    ( model
    , encodeElements elementsToCopy |> Ports.jsonToClipboard
    )

If I then press Ctrl+C in my application, sometimes I get:

1. copyElementsFromSelection: "json to Copy"
2. jsonToClipboard.
3. Copy callback.
4. JSON to clipboard - non null json payload.

And about as often I get:

1. copyElementsFromSelection: "json to Copy"
3. Copy callback.
2. jsonToClipboard.

So you can see the problem is that the call to Ports.jsonToClipboard and the browsers document level copy event handler race against each other. If the port call completes first it works as I want it to, but if the copy event handler runs first it does not find any JSON to put in the clipboard.

Does anyone know how to make this work with Elm?

Sorry the code above is not complete, its just snippets from a larger application. However, over the next few days I will extract a runnable example so that I can share it better.

A couple of ideas occured to me:

  • If I put the keydown handler on a DOM element, rather than using a subscription, will the port get called more immediately during handling of the Ctrl+C event?

  • I could, when the user selects something but before they do Ctrl+C pass the JSON data through the port, so it will definitely be there when the copy event runs. However, I also want to handle Ctrl+C for text in other “normal” situations in the application, such as if the user selects some text to copy. The test if (this.jsonPayload != null) would then not work to guard against intercepting the copy when the user is just doing it with text.

(The JSON data contains data specific to my application, and is taken from the users selection in a drawing program type UI.)

I don’t know if Browser.Events.onKeyDown runs synchronously or not, but if you put a keydown handler on a DOM element you can force it to be synchronous for sure, by using stopPropagationOn, documented here: VirtualDom - virtual-dom 1.0.3

You might want to avoid Browser.Events.onKeyDown anyway, since you can’t do preventDefault in it, right?

Btw, I’m curious – what does it mean to copy with the content type application/json? What does it enable the user to do?

In this case I do not want to preventDefault, since the copy event must use the browsers default handling. This is important to be able to send JSON to the clipboard I think, otherwise it does not work.
But that is ok, The VirtualDom docs suggest that a synchronous handler will be created when stopPropagation is used, and that disallows preventDefault so all good there.

Worth a try…

application/json was just the first MIME type that popped into my head since I was copying some JSON. I should probably use application/myappname to reflect that the data is specific to my application only. The only reason I am using the clipboard at all is so that application data can be cut and pasted between windows, and also used in combination with the clipboard containing text - otherwise I would just make an internal clipboard in the Elm code and it would all be much easier.

What would it even mean to put the keydown handler on a DOM element? I don’t think that will do anything:

Keyboard events are only generated by <inputs>, <textarea> and anything with the contentEditable attribute or with tabindex="-1".

So I can rule that option out.

So the issue is that when the race condition occurs you don’t know whether the elm event will come or not (otherwise you could wait for it). Does this specific copy event only occur on certain elements?
Then you could probably filter by the events target on the elements class/id/whatever.

This is not a really clean solution though :frowning:
Maybe using a web-component could work? Then the data could already be in JS and the race condition would never occur?

I did wonder about that, so checked. As far as I can make out it works like this:

  • If the browser Selection is None, the copy events target is the DOM <body>.
  • If the browser Selection is a Range, the copy events target is where the selection is.

For copying text and html this makes sense, and default browser behaviour is to copy the selection into the clipboard, when there is one.

My application which is mimicking this behaviour does not actually set up a browser Selection for the items to be copied - I don’t event think it could since they are elements within an SVG, but not necessarily contiguous and I think a browser selection is always defined by a start and end path into the DOM, so covers a contiguous region of it.

However, I think I could do this:

Whenever the user selects some items in the SVG, place them somewhere in a staging area ready to be copied just in case the user does that (what my jsonToClipboard function is doing). Also clear any browser selection at this point in time too.

When the user triggers copy with Ctrl+C, let this fall through to the default browser behaviour and copy event handler on the document. Now check the selection to determine what is being copied - Selection == None means that the staged JSON is to be copied if there is one, Selection == Range means that the user is copying some text or html from the page.

Will try this out soon.

Yes, provides another way to write into the DOM. So I would set up a custom element, and set a property on it containing the data for whatever is selected on the drawing. Document level copy event handler would also exist on the custom element, and it could pick up the data from property.

I think the property would need to be set ahead of the Ctrl+C event being fired, rather than in response to it, since there would be a delay before the view is updated. So similar to the ports way I described above in that regard.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.