0

I am having a Textarea blur problem that fails to reposition a floating label after clicking out out of the Textarea after a paste.

I PASTE some text into a Textarea and exit the Textarea in any way (Ctrl-Enter or click outside the area), which then causes the Textarea to Blur, the LABEL (e.g. Summary Annotation) does not move out of the Textarea as the label should. Instead, it stays put and overlaps the pasted text.

Only if I make a manual keystroke BEFORE I leave the Textarea will the label return to it's original position ABOVE the Textarea when it is blurred.

enter image description here enter image description here

Is there a direct way of solving this? Under other circumstances (non VUE 3), I would try to manually trigger the Textarea, but I don't think I can do that with a VUE DOM.

Here is the paste routine:

const textAreaPaste = (e: ClipboardEvent) => {
let clipboardData, pastedData;
let textResult = "";
const target = e.target as HTMLTextAreaElement;

if (e) {
    e.preventDefault();
    nextTick(() => {
        // Get pasted data via clipboard API
        clipboardData = e.clipboardData;
        pastedData = clipboardData ? clipboardData.getData("Text") : "";

        //Remove carriage returns
        const t1 = pastedData.replace(/\r(?!\n)|\n(?!\r)/g, "\n");

        // Insert the text in the correct editing position, if necessary
        if (e.target && (target.selectionStart || String(target.selectionStart) === "0")) {
            var startPos = target.selectionStart;
            var endPos = target.selectionEnd;
            textResult = target.value.substring(0, startPos) + t1 + target.value.substring(endPos, target.value.length);
        } else {
            textResult += t1;
        }
        // Truncate to max length
        (e.target as HTMLInputElement).value =
            textResult.length > maxAnnoLetter ? textResult.substr(0, maxAnnoLetter) : textResult;
        const attribs = (e.target as HTMLInputElement).attributes;
        const id = (attribs as HTMLAttributes).id;
        // Timeout on update necessary
        if (id) {
            setTimeout(() => {
                calcRemainingChars(id.nodeValue, textResult);
            }, 250);
        }
    });
}

};

Norm Strassner
  • 225
  • 3
  • 11
  • I can't reproduce the behavior. To paste text into the textarea, I have to focus it, which immediately pulls the label upwards, and it does not go down again unless the content is empty. Even if I autofocus the textarea, the label is pulled up. Can you add more information how to reproduce the issue? – Moritz Ringler Apr 19 '23 at 20:14
  • I added in my paste function, if that helps. – Norm Strassner Apr 20 '23 at 13:30

2 Answers2

0

Well, I found the answer. I did NOT need to call preventDefault on the event. So often it is one overlooked line.

Without calling preventDefault, the Textarea works as advertised.

Norm Strassner
  • 225
  • 3
  • 11
  • Having now played around with your code a bit (didn't know how much working with paste sucks...), there a few things I don't understand: In the `nextTick` callback, you are doing the paste manually using `e.target.value`, but at that point, the `paste` event should have finished and `e.target.value` already contain the pasted content. And when you update ´e.target.value`, the HTML textarea is changed, but not the variable bound to the `v-textarea`. Also, is undo and redo working after paste? – Moritz Ringler Apr 25 '23 at 22:19
0

May I suggest, instead of working with the godawful paste event, you can put your functionality into an @input listener. The operations you do in your handler (replacing \r\n and cutting length) can be done there as well, without direct DOM manipulations.

Set the handler by using the :value prop and the @input listener instead of v-model:

<v-textarea
  :value="textValue"
  @input="$event => textValue = adjustAfterPaste($event)"
></v-textarea>

You can check if the input comes from a paste by checking if the event's inputType is "insertFromPaste":

const adjustAfterPaste = (e) => {
  const value = e.target.value
  return (e.inputType !== 'insertFromPaste') ? value : value
    .replace(/\r(?!\n)|\n(?!\r)/g, "\n")
    .substring(0, maxlength);
}

Upsides seem glaringly obvious (not having to work with an unchangeable event, no direct DOM manipulations, not having to perform the paste manually, clear structure, brevity). But there are two downsides to this approach:

  • The replacement will be run on the whole string, not just on the pasted content, unless you figure out the pasted content by comparing the old and the new value
  • As with all other current approaches to change pasted content, it breaks undo/redo

Here is a snippet, I am using toUpperCase() to make the change more visible. Again, note that the change is applied to the whole string:

const { createApp, ref } = Vue;
const { createVuetify } = Vuetify
const vuetify = createVuetify()

const adjustAfterPaste = (e, maxlength) => {
  const value = e.target.value
  return (true || e.inputType !== 'insertFromPaste') ? value : value
    .toUpperCase()
    .substring(0, maxlength);
}

const App = {
  setup(){
    return {
      textValue: ref(''),
      maxlength: ref(50),
      adjustAfterPaste,
    }
  }
}
createApp(App).use(vuetify).mount('#app')
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.css" />
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<div id="app">
  <v-app>
    <v-main>
      <v-textarea
        label="textarea"
        :value="textValue"
        @input="textValue = adjustAfterPaste($event)"
        :maxlength="maxlength"
      ></v-textarea>
       Content: {{ textValue }}
    </v-main>
  </v-app>

</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@3/dist/vuetify.min.js"></script>

Note that automatically cutting text after paste leads to users not recognizing it and sending cropped data. It is usually preferable to let them paste and mark the field as invalid.

Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34