How to use the Synchronous Clipboard API without native code?

I use the Synchronous Clipboard API to implement copy/paste in my Elm note-taking app. As the clipboardData only is available during the event, I rely on Native code to do side effects:

Html.div
    [ Html.Events.onWithOptions "copy"
        preventDefault
        (Decode.at [ "clipboardData" ] Decode.value
            |> Decode.map
                (\clipboard ->
                    let
                        _ =
                            Clipboard.setData "text/plain" selectionToText model clipboard

                        _ =
                            Clipboard.setData "text/x-rexpad-content" selectionContent model clipboard
                    in
                        NoAction
                )
        )
    , ...
    ]
    [ ... ]

Where Clipboard.setData has the type setData : String -> (a -> String) -> a -> Decode.Value -> Decode.Value with this JavaScript implementation:

function setData(mimeType, transformer, model, clipboardData) {
  var data = transformer(model);
  if (data !== "") {
    clipboardData.setData(mimeType, data);
  }
  return clipboardData;
}

How could I do this without Native code on 0.18? Will 0.19 bring something that makes it possible?

2 Likes

What prevents you from using a port for this? You have the clipboardData Value at hand, why not just put that through an outgoing port and do the sideeffects there?

The clipboardData value can not be used outside of the clipboard event. This is for privacy reasons I believe, preventing web pages to spy on the clipboard without the user knowing about it. During clipboard events, it is clear that the user intends to give access. Most things in Elm are asynchronous, but decoding happens synchronously.

2 Likes

Ah, I didn’t know about that privacy feature. Hmm, I only heard that user interaction handlers will be synchronous in 0.19, with an opt-in for async, so maybe the message will get forwarded to JavaScript via a port completely synchronous? I’m not sure, though. Interesting topic, I’m curious what the answer to this is.

3 Likes

We do this through delegated listeners, so we listen for click events on the body (in vanilla JS) that happen to have a data-copy-clipboard attribute that points to the stuff we want to copy, something like

<button data-copy-to-clipboard="#some-input"></button>

Then in the handler we run something like

  let elem = target && document.getElementById(target);
  if (elem) {
    elem.select();
    document.execCommand("copy");
  }
2 Likes

Yes, this is a good click-to-copy-to-clipboard solution! I guess I could do something similar. I’m not sure about the performance though as I would have to prepare the to-be-copied text for several mime times in real time while drag-selecting the text.

In 0.19 the scheduling of ports will change so that you can run functions that need to be in the same call stack as a user event handler. See: Window.confirm without native code?

Yes, I’m looking forward to 0.19 and synchronous event processing. And if that doesn’t do it, perhaps expanding on libs for the web APIs will.

Forgive me! I missed your first question, how to do it in 0.18. I use a custom element:


5 Likes

Thanks! I should look more into custom elements. In my case, I want to react on the clipboard events (onpaste and like) directly on the contenteditable (with content rendered in Elm).

1 Like

Right on! The one really important thing is that if you have a custom element with children that are rendered by Elm, you just can’t modify those children in a way that makes them different from what the virtual DOM thinks is true. As long as that rule is followed you can attach event listeners to whatever elements you want and do whatever you need to do.

2 Likes

Hi @luke I want to ask do you need any special webpack to make this technique (copyedit elm + js) to work? I’m using create-elm-app and I couldn’t get the js file to load. Thanks.

update
I’ve done more research on this and add webcomponents polyfill and fixed this.
Leave it here for any other people who came across this later.

2 Likes