How to make the onPaste event work with a draggable HTML element?

54 views Asked by At

It seems like the onPaste event does not fire on draggable element for whatever reason... (Checked on Firefox and Chrome)

Is there some kind of way (hacky or not) to make it work?

I've been trying with:

  • Adding contenteditable="true" but that breaks the UX, as I don't want the user to actually edit the content
  • Adding transparent/invisible input or contenteditable element, but that either break the paste event or the draggable
  • Making the draggable only true on onMouseDown and false on onMouseUp, but the paste event still fails
  • Wrap the draggable element into an element with paste, wrap the element with paste into the draggable element, still fails...

Here is a minimal example I made to reproduce the problem: playground.

Vue.createApp({
  setup() {
    const pasteEvents = Vue.ref([]);

    async function onPaste(event) {
      console.log(event);
      pasteEvents.value.push("Paste from Element " + event.target.dataset["element"])
    }

    return {
      pasteEvents,
      onPaste
    }
  }
}).mount("#app");
.wrapper {
  display: flex;
  flex-direction: column;
  gap: 16px;
  .paste-area {
    padding: 16px;
    background: #00000011;
    &:focus {
      border: 2px dashed red;
    }
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.8/vue.global.min.js"></script>

<div id="app">
  <div class="wrapper">
    Click on an element and press Ctrl+V
    <div :draggable="true">
      <div class="paste-area" data-element="A" tabindex="0" @paste="onPaste">
        Element A (draggable)
      </div>
    </div>
    <div class="paste-area" tabindex="0" data-element="B" @paste="onPaste">
      Element B (no draggable)
    </div>
    {{ pasteEvents }}
  </div>
</div>

(It's a vue playground, but it doesn't change anything)

The interesting part from the playground:

<div
    draggable="true"
    tabindex="0"
    @paste="onPaste"
>
    Element A (draggable, does not work)
</div>
<div
    tabindex="0"
    @paste="onPaste"
>
    Element B (no draggable, works)
</div>

(For those unfamiliar with Vue, the @paste is basically an addEventListener("paste", () => ...))

Pressing Ctrl+V on the first element does not trigger the paste function, but works fine on the second element.

EDIT: seems like it's the same with every ClipboardEvent (copy/cut/paste)

1

There are 1 answers

1
Kaiido On BEST ANSWER

I couldn't find any reasons in the specs why this would not fire in this case, the focus element is well set and keyboard events fire where they're supposed to, but then again, I find W3C's Clipboard specs to be loosely specced, just like many of their UI specs...

One workaround is to actually set the contenteditable, and then prevent any edits from the user by listening to the beforeinput event.
If you want to get rid of the caret, while you can't, you can at least "hide" it by setting the caret-color to transparent.

// Sorry I don't know Vue enough to make it there
document.addEventListener("beforeinput", (evt) => {
  // You may need to adjust the selector
  if (evt.target.closest("[draggable='true'] [contenteditable]")) {
    evt.preventDefault();
  }
});

Vue.createApp({
  setup() {
    const pasteEvents = Vue.ref([]);

    async function onPaste(event) {
      console.log(event);
      pasteEvents.value.push("Paste from Element " + event.target.dataset["element"])
    }

    return {
      pasteEvents,
      onPaste
    }
  }
}).mount("#app");
.wrapper {
  display: flex;
  flex-direction: column;
  gap: 16px;
  .paste-area {
    padding: 16px;
    background: #00000011;
    /* added a few more specific selectors to handle focus in contenteditable */
    &:focus,&:focus-visible,&:focus-within {
      border: 2px dashed red;
      outline: 0;
    }
  }
}
/* You may need to adjust the selector */
[draggable="true"] [contenteditable] {
  caret-color: transparent;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.8/vue.global.min.js"></script>

<div id="app">
  <div class="wrapper">
    Click on an element and press Ctrl+V
    <div :draggable="true">
      <div :contenteditable class="paste-area" data-element="A" tabindex="0" @paste="onPaste">
        Element A (draggable)
      </div>
    </div>
    <div class="paste-area" tabindex="0" data-element="B" @paste="onPaste">
      Element B (no draggable)
    </div>
    {{ pasteEvents }}
  </div>
</div>

The only visible difference now is that the cursor is actually still here and thus if you do ShiftArrow, the text will be selected from the cursor's position. If this is really a problem, you can probably workaround that by listening to the selectionchange event.