javascripthtmltextareapreventdefault

How to make undo work in an HTML textarea after setting the value?


I have a <textarea> element that I listen for certain key presses from. Like if a user types the Tab key, I prevent the default action of changing focus and add the tab character at the correct position.

The problem is when users press one of the keys I listen for, undo goes a little haywire. How do I get the undo/redo functionality to work? I thought about listening for ctrl/cmd-z and ctrl/cmd-shift-z key presses, recording everything, and processing the undos/redos, but then the edit and context menu choices wouldn't work...

You can see by typing letters with tabs and enters and then trying to undo and redo:

const textarea = document.querySelector('textarea')
textarea.addEventListener('keydown', function (event) {
  if (event.key == "Tab") {
    event.preventDefault()
    const cursor = textarea.selectionStart
    textarea.value = textarea.value.slice(0, cursor) + '\t' + textarea.value.slice(textarea.selectionEnd)
    textarea.selectionStart = textarea.selectionEnd = cursor + 1
  } else if (event.key == "Enter") {
    event.preventDefault()
    const cursor = textarea.selectionStart
    textarea.value = textarea.value.slice(0, cursor) + '\n' + textarea.value.slice(textarea.selectionEnd)
    textarea.selectionStart = textarea.selectionEnd = cursor + 1
  }
})
<textarea cols="50" rows="20"></textarea>


Solution

  • I believe the problem's core is the lack of interaction between JavaScript and the browser's default methods of undoing. Appending text using JavaScript to a textarea does not in any way tell the browser's "undo" to delete the appended text, as the browser's "undo" is only meant for removing text the user inputted, not text JavaScript inputted.

    Take for example your code. Upon pushing Enter, you tell the event listener to preventDefault, which altogether prevents the Enter key from appending user input to the textarea. You then synthesize the input using JavaScript, which the browser's "undo" does not keep track of.

    You can overcome this lack of interaction by using document.execCommand(). You can check it's browser support via the link.

    const textarea = document.querySelector('textarea');
    
    textarea.addEventListener('keydown', function (event) {
      const cursor = textarea.selectionStart;
      if (event.key === "Tab") {
        event.preventDefault();
        // appends a tab and makes the browser's default undo/redo aware and automatically moves cursor
        document.execCommand("insertText", false, '\t');
      } else if (event.key === "Enter") {
        event.preventDefault();
        document.execCommand("insertText", false, '\n');
      }
    });
    <textarea cols="50" rows="8"></textarea>