How to simulate clipboard paste event programmatically?

Hi. I am trying to fix my Imgur plugin to make it compatible with the new CodeMirror which has arrived with 0.13+.

Some historical background: Before this breaking Obsidian update I used to backup the reference to default event handler and replace it with my function. In turn inside of my function I called the default handler whenever I needed it (i.e. when user replied on modal that he wants a regular local paste or when there is no internet connection to perform remote upload).

As you can see, my plugin had a tight coupling to the internals of Obsidian (_handlers private field of CodeMirror object). No wonder it broke eventually, but there was no other way back in the time when I’ve built the very first version of my plugin.

Looks like now there is a straightforward API for adding custom behavior on paste: on(name: 'editor-paste', callback).
The problem is that I do not have a reference to default paste handler to call it when needed. You may say that I can control the execution of default behavior with preventDefault(), but I can’t do it, because my handler function is async. async returns promise faster than the function will await anything (i.e. result from user’s modal or file upload) and it leads to invocation of the default handler immediately. So, if I want to prevent default handler for execution I need to call .preventDefault() early, before any await. On the other hand, if I call .preventDefault() early, the original paste event will never be handled with default handler. I can’t un-preventDefault the original event.

Let’s summarize:

  • My custom handler must be async, because it awaits for Promises of user reply from modal or file upload. It can’t block
  • I can’t use preventDefault() conditionally because of the async handler returning own promise immediately. I need to preventDefault always in the very beginning
  • I do not have a reference to original handler to call it explicitly (like I had it before by peeking into private _handlers field of CodeMirror)

Having all the above conditions in mind, I see the only way to fall back to original handler (even though I do not have a reference to it). The idea is to fire an original event’s copy with a custom property set to true (i.e. shoulBeProcessedByDefaultHandler). Below is the code to demonstrate my idea:

shouldPasteLocally(): Promise<Boolean> {
  ...
}

async onload(): Promise<void> {
  this.registerEvent(
    this.app.workspace.on("editor-paste", async (e: ClipboardEvent) => {
      console.log("Event has arrived");
      if (e.shoulBeProcessedByDefaultHandler) {
        console.log("Passing this event to default handler");
        return;
      }
      console.log("Event not processed yet, using custom plugin's logic");
      e.preventDefault();
      if (await ifUserWantToPasteLocally()) {
        const { files } = e.clipboardData;
        const dt = new DataTransfer();
        for (let i = 0; i < files.length; i++) {
          console.log("Adding file to new event");
          dt.items.add(files.item(i));
        }
        const newEvent = new ClipboardEvent("paste", {
          clipboardData: dt,
        });
        newEvent.shoulBeProcessedByDefaultHandler = true;
        e.target.dispatchEvent(newEvent);
        return
      } 
      // ... logic for remote upload
    })
  );
}

Unfortunately this code does not work as I expect, because nothing seems to happen on dispatchEvent. I say “nothing seems to happen”, because I used dev tools to setup a breakpoint on this type of event (ClipboardEvent), but it does not stop Obsidian execution on attempt to fire an event’s copy. The breakpoint works for original event triggered from the keyboards, but nothing happens on dispatchEvent. What do I miss?

I still do not know if the problem described above is a security restriction of browser or not, but to achieve my goal I’ve used references to default event handlers:

  • markdownView.currentMode.clipboardManager.handlePaste
  • markdownView.currentMode.clipboardManager.handleDrop

Those references are not exposed to public API, Licat has helped me to find them.