Sometimes you need to intercept a paste event so you can filter or modify clipboard text before it enters a textarea. The moment you call preventDefault(), though, the browser’s built-in paste behavior disappears, and two small but important details go with it:

  1. After pasting, the caret should land immediately after the inserted text, not jump to the very end of the field.
  2. If some text is selected, the pasted content should replace that selection.

Those behaviors feel so natural that you only notice them once they break. If you manually handle paste without restoring them, the result feels wrong right away.

The code first

    <textarea id="text" style="width: 996px; height: 423px;"></textarea>
    <script>
        // 监听输入框粘贴事件
        document.getElementById('text').addEventListener('paste', function (e) {
            e.preventDefault();
            let clipboardData = e.clipboardData.getData('text');
            // 这里写你对剪贴板的私货
            let tc = document.querySelector("#text");
            tc.focus();
            const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
            if(tc.selectionStart != tc.selectionEnd){
                tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
            }else{
                tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
            }

            // 重新设置光标位置
            tc.selectionEnd =tc.selectionStart = start
        });
    </script>

What native paste behavior actually does

Take this text:

染念真的很生气

If the caret is placed right after 真的 and you paste 不要, a custom paste handler often ends up producing this:

染念真的不要很生气|

The text insertion is correct, but the caret position is not. In normal browser behavior, the caret should appear immediately after the pasted text:

染念真的不要|很生气

The second case is selection replacement. Suppose the textarea contains:

染念真的不要很生气

Now select 真的 and paste 求你. A bad manual implementation may produce:

染念真的求你不要很生气|

But what users expect is the default replacement behavior:

染念求你|不要很生气

So if you intercept paste, you are not just inserting text. You also need to rebuild the browser’s cursor logic.

The key: selectionStart and selectionEnd

To restore replacement behavior, you need the current selection range.

  • tc.selectionStart gives the start position of the selection or caret.
  • tc.selectionEnd gives the end position of the selection or caret.

If those two values are equal, there is no selected range. The user has simply placed the caret at a single insertion point.

For example:

233|333 ^--^ 1--4 tc.selectionEnd=4,tc.selectionStart = 4

In that case, inserting pasted text means splitting the original value into two parts:

  • everything before the caret: tc.value.substring(0,tc.selectionStart)
  • everything after the caret: tc.value.substring(tc.selectionStart)

Then put the clipboard text in between.

That is exactly what this branch does:

tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);

If selectionStart and selectionEnd are different, then the user has selected a range. In that case, the selected text should be replaced, so the trailing part must start from tc.selectionEnd instead.

Example:

|233333| ^------^ 1------7 tc.selectionEnd=7,tc.selectionStart = 1

That is why the code uses a conditional check. The only real difference between insertion and replacement is which index is used for the second substring.

Why focus() appears here

Before reading the caret position, the code calls:

tc.focus();

The point is to ensure the textarea is focused so the selection information can be read correctly. Once the element is focused, you can reliably access the caret or selection range and work from there.

Calculating the new caret position

The first problem was that after manual paste, the caret often ends up in the wrong place. To fix that, you need to calculate where the caret should move after insertion.

This line does that:

const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;

The logic is simple: take the content before the insertion point, append the pasted text, and measure the length. That length is exactly where the caret should land—right after the pasted content.

One detail matters a lot: this position must be calculated before tc.value is reassigned.

That is because once value changes, the original indices no longer describe the old state of the textarea. If you wait until after the reassignment to compute something like:

(tc.value.substr(0,tc.selectionStart)+clipboardData).length

then you are already working with modified content, and the cursor repositioning loses its meaning.

Restoring the caret correctly

After updating tc.value, the final step is:

tc.selectionEnd =tc.selectionStart = start

Both properties are set to the same value so that the selection collapses into a single caret position. If they are not equal, you are not placing the cursor—you are leaving a range selected.

That one line restores the feel of native paste behavior: text is inserted or replaced correctly, and the caret lands exactly where users expect.