11

I have overridden the paste event. I noticed that because the event's default behavior is prevented, it is not currently possible to undo the "paste" with Ctrl+Z.

$(this).on('paste', function (evt) {
  // Get the pasted data via the Clipboard API.
  // evt.originalEvent must be used because this is jQuery, not pure JS.
  // https://stackoverflow.com/a/29831598
  var clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
  var pastedData = clipboardData.getData('text/plain');

  // Trim the data and set the value.
  $(this).val($.trim(pastedData));

  // Prevent the data from actually being pasted.
  evt.preventDefault();
});

Is there a way to override the undo functionality or do the above differently such that Ctrl+Z will work?

Related questions

bernie
  • 9,820
  • 5
  • 62
  • 92
mbomb007
  • 3,788
  • 3
  • 39
  • 68
  • All you are doing is trimming the text of extra whitespace? Maybe let the paste occur naturally and just trim when submitting your form, or on another event? – chazsolo Apr 25 '19 at 16:29
  • @chazsolo The textfield has a character limit so the data would be truncated on a natural paste if the whitespace put it past the limit. So if the whitespace was leading, that is not nice for the user. – mbomb007 Apr 25 '19 at 16:48
  • @chazsolo Good workaround idea. However, we should also consider the case where the input is modified further (more than trimed) or inserted at the current cursor position like here https://stackoverflow.com/questions/16813587/paste-event-modify-content-and-return-it-at-the-same-place In that case, handling form submission is not enough. – bernie May 16 '19 at 11:19

3 Answers3

7

Use

document.execCommand("insertText", false, $.trim(pastedData));

instead of

 $(this).val($.trim(pastedData));

It will preserve the undo history.

$('#inputElement').on('paste', function (evt) {
  var clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
  var pastedData = clipboardData.getData('text/plain');
  this.select(); // To replace the entire text
  document.execCommand("insertText", false, $.trim(pastedData));
  evt.preventDefault();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<textarea id="inputElement"></textarea>
Munim Munna
  • 17,178
  • 6
  • 29
  • 58
  • For anyone wondering what that second parameter stands for... [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand): "A Boolean indicating whether the default user interface should be shown. This is not implemented in Mozilla." Ok... now check the [spec](https://w3c.github.io/editing/execCommand.html#execcommand())? **TODO: Define behavior for show UI.** So it seems useless for now. – bernie May 16 '19 at 18:20
  • 1
    It works in Chrome, but I can't get it to work in Firefox 66 or IE 11. – bernie May 16 '19 at 18:39
  • @bernie Just pretend IE 11 doesn't exist. Edge is its replacement. – mbomb007 May 17 '19 at 14:20
  • 1
    @mbomb007 That's debatable, but what about Firefox? – bernie May 20 '19 at 08:40
  • @bernie Edge quite literally started at version 12, because it follows IE 11. If something doesn't work in FF though, that's not great. – mbomb007 May 20 '19 at 21:35
  • This doesn't work quite right. It replaces all of the text with the paste. To fix, you need to use `this.focus()` instead of `this.select()`. [See on JSFiddle](https://jsfiddle.net/mf8v97en/). And still doesn't work on Firefox. – mbomb007 May 21 '19 at 14:23
  • 1
    I found the [Firefox bug report](https://bugzilla.mozilla.org/show_bug.cgi?id=1220696) for this issue. They seem to be working on it. – mbomb007 May 21 '19 at 14:52
  • @mbomb007 `this.select()` has been used in reference to your [this](https://stackoverflow.com/posts/comments/98369767?noredirect=1) comment. – Munim Munna May 21 '19 at 16:13
  • @MunimMunna Type a character, then paste something after it. The paste removes the characters you already typed. That's wrong. – mbomb007 May 21 '19 at 16:43
  • @mbomb007 based on your comment that seemed the desired behavior. A comment in my code states that already. – Munim Munna May 22 '19 at 02:03
  • @mbomb007 okk, then remove the line `this.select()` to get the desired behavior (as instructed by the answer). – Munim Munna May 22 '19 at 14:02
3

A possible solution is to implement an undo stack manually. The algorithm would be something like:

  • The undo stack starts empty.
  • Add a listener for input events that pushes a new entry on an "undo stack" when the input is different from the input of the last input stack element. This listener should at a minimum be debounced to avoid single-letter undo stack elements.
  • The paste event listener also pushes an entry on the undo stack when invoked.
  • Add a keydown listener that intercepts CTRL-Z and pops the last entry from the undo stack.

It sure seems like a lot of work for something that is already built-in to the browsers so I'm hoping there's a better solution.

bernie
  • 9,820
  • 5
  • 62
  • 92
  • 1
    You'd also have to track cursor location. You could use a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to track changes, perhaps, but still a lot of work. – mbomb007 May 16 '19 at 13:18
1

I found a way to make it work. Starting with this answer, I changed it to use .focus() instead of .select(), which fixes the pasting. Then, to make pasting work in Firefox, I had to keep the fallback that doesn't preserve undo history. This will have to do until Firefox fixes the bug (See bug report).

function insertAtCaretTrim(element, text) {
    element[0].focus();
    // Attempt to preserve edit history for undo.
    var inserted = document.execCommand("insertText", false, $.trim(text));
  
    // Fallback if execCommand is not supported.
    if (!inserted) {
        var caretPos = element[0].selectionStart;
        var value = element.val();

        // Get text before and after current selection.
        var prefix = value.substring(0, caretPos);
        var suffix = value.substring(element[0].selectionEnd, value.length);

        // Overwrite selected text with pasted text and trim. Limit to maxlength.
        element.val((prefix + $.trim(text) + suffix).substring(0, element.attr('maxlength')));

        // Set the cursor position to the end of the paste.
        caretPos += text.length;
        element.focus();
        element[0].setSelectionRange(caretPos, caretPos);
    }
}

var $inputs = $("input");

$inputs.each(function () {
    $(this).on('paste', function (evt) {
    var clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
    var pastedData = clipboardData.getData('text/plain');

    // Trim the data and set the value.
    insertAtCaretTrim($(this), pastedData);
    
    evt.preventDefault();
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input type="text" maxvalue="10" />

Code is also in a JSFIddle: https://jsfiddle.net/mf8v97en/5/

mbomb007
  • 3,788
  • 3
  • 39
  • 68