Replacement for deprecated execCommand("copy")

Maybe this is a general question, not specific to Prosemirror: how can I fire a copy event programmatically? “CTRL+c” works, but I want to fire it with a button in the GUI. In particular I want to make a NodeSelection and put the node on the clipboard.

See navigator.clipboard.writeText, but note the restrictions.

Thank you, Marijn.

I already saw it, but it seems to work only with simple text.

navigator.clipboard.write is more general, but also much more complex.

I want to copy an entire Prosemirror Node.

I saw the restrictions and the different permissions in different browsers.

Since I need to copy and paste inside the same prosemirror document, perhaps the way around this is implementing a parallel, internal clipboard.

I found this solution:

  • call a tr.setMeta("copy-as-json", true) to copy the current selection, or tr.setMeta("copy-as-json", node) for an arbitrary node

  • a Plugin intercepts that setMeta and

    • copies the stringified JSON of a Node or a Slice in the state of the Plugin,

    • executes navigator.clipboard.writeText(stringified_node_or_slice_json)

  • the handlePaste of the same Plugin checks whether the last copied item on the clipboard is the same as the state memorized in the Plugin

  • when the two strings are equal, the Node/Slice is reconstructed with fromJSON and the current selection is replaced with it

Here’s the code:

const META_COPY_AS_JSON = 'copy-as-json'

type JsonClipboardItemType = 'node' | 'slice'

class JsonClipboardItem {
  constructor(
    readonly type: JsonClipboardItemType,
    readonly content: string,
  ) {
  }
}

export const JsonPastePlugin = new Plugin({
  key: new PluginKey('JsonPaste'),
  props: {
    handlePaste(view, event, slice) {
      const item = this.getState(view.state)
      const copiedText = event.clipboardData?.getData('text/plain')
      if (item && item.content && item.content === copiedText) {
        const { dispatch, state } = view
        if (dispatch) {
          const json = JSON.parse(item.content)
          let tr: Transaction
          if (item.type === 'node') {
            tr = state.tr.replaceSelectionWith(Node.fromJSON(state.schema, json))
          } else {
            tr = state.tr.replaceSelection(Slice.fromJSON(state.schema, json))
          }
          dispatch(tr)
          return true
        }
      }
      return false
    },
  },
  state: {
    init(): JsonClipboardItem | undefined {
      return undefined
    },
    apply(tr, value, oldState, newState): JsonClipboardItem | undefined {
      const metaCopyAsJson = tr.getMeta(META_COPY_AS_JSON)
      let type: JsonClipboardItemType | undefined = undefined
      let json: string | undefined = undefined
      if (metaCopyAsJson === true) {
        const { selection } = newState
        if (selection instanceof NodeSelection) {
          type = 'node'
          json = JSON.stringify(selection.node)
        } else {
          type = 'slice'
          json = JSON.stringify(selection.content)
        }
      } else if (metaCopyAsJson instanceof Node) {
        type = 'node'
        json = JSON.stringify(metaCopyAsJson)
      }
      if (type && json) {
        navigator.clipboard.writeText(json)
        return new JsonClipboardItem(type, json)
      }
      return value
    },
  }
})