HTML Input - Undo history lost when setting input value programmatically
Asked Answered
I

2

8

I have an HTML input. When a user types in it, I've set up the 'input' event to handle updating the input to a filtered version of what the user typed (as well as updating selectionStart and selectionEnd for smooth UX). This happens constantly in order to give the proper effect.

What I've noticed, however, is that whenever JS sets the value of an input via input.value = '...';, it appears the undo history for the input disappears. That is, pressing Ctrl-Z with it focused no longer steps back to the previous state.

Is there any way to either provide the input custom undo history, or otherwise prevent it from losing the history whilst still changing its value?


Here is a minimal example of my issue:
After typing in the top input (which rudimentarily adds periods between every character), Ctrl-Z does not undo.

<body>
    <input type="text" id="textbox" placeholder="No undo"/><br/>
    <input type="text" id="textbox2" placeholder="Undo"/>
    <script>
        var tbx = document.getElementById("textbox");
        tbx.addEventListener('input', () => {
            tbx.value = tbx.value + '.'
        });
    </script>
</body>
Illogic answered 9/10, 2021 at 2:34 Comment(1)
Related question: Is it possible to edit a text input with javascript and add to the Undo stack?Agrestic
D
0

You can try storing the input's previous value in a variable, then listen for the Ctrl + Z key combination in a keydown event listener attached to the input. When it is fired, you can set the value of the input to the previous stored value.

btn.addEventListener('click', function() {
  savePrevInput(input.value)
  input.value = "Hello World!";
})

var prevInput;

function savePrevInput(input) {
  prevInput = input;
}

input.addEventListener("keydown", function(e) {
  if (e.ctrlKey && e.keyCode == 90) {
    if (prevInput) {
      input.value = prevInput;
      input.selectionStart = prevInput.length;
    }
  }
})
<input id="input" />

<button id="btn">Change</button>
Dichroism answered 9/10, 2021 at 2:48 Comment(6)
Yes, I considered this. Should I be concerned about any kind of compatibility issues with this? I'm on Windows and couldn't recall if Apple uses Cmd or Ctrl-Z for undo, and how that appears in the event. Also wasn't sure if the default undo shortcut could be remapped..? (or maybe that's excessive concern :-)Illogic
@Illogic it should be OK. The regular Ctrl + Z behavior still works with this implementation.Dichroism
Ok, thanks! I was able to fully replace undo/redo by listening to 'keydown' and checking for Ctrl-Z, Ctrl-Y, and Ctrl-Shift-Z. I actually prevented default in my case.Illogic
Uncaught TypeError: Cannot read properties of undefined (reading 'length')Haematocele
@Spectric, LuisAFK is correct. I didn't actually use your code (I used an undo stack) but pressing ctrl-Z without hitting "Change" breaks it.Illogic
@Illogic sorry, didn't notice that! I've fixed it.Dichroism
R
1

"listen and reimplement ctrl+z" is a bad approach. there are other ways to trigger the built-in undo/redo in browser uis, which you can not intercept or reimplement, such as the context menu

the correct approach is to not change the value property, but use document.execCommand() with "insertText" or "delete" command as appropriate

Renita answered 31/3 at 6:22 Comment(2)
Hey @vvv, welcome to SO! Great point! Yes, reimplementing Ctrl-Z likely won't support context-menu or mobile/tablet undo behaviors. And it does look like triggering "selectAll" then "insertText" with document.execCommand will replace the contents with one undoable action! (It does appear execCommand is deprecated though.. developer.mozilla.org/en-US/docs/Web/API/document/execCommand)Illogic
I'm thinking about it now, though. Specifically in my case, this wouldn't work because the "insertText" command needs to replace the current history state vs. push a new one. i.e., As a user types, my code modifies the input value and replaces the contents. If the user undoes, it reverts the value to the 'unfiltered' version. But that then triggers my code and filters and replaces the contents again, essentially just disabling undo... Your answer works if the input's contents are updated in the 'onchange' handler but not if in the 'oninput' handler (like mine).Illogic
D
0

You can try storing the input's previous value in a variable, then listen for the Ctrl + Z key combination in a keydown event listener attached to the input. When it is fired, you can set the value of the input to the previous stored value.

btn.addEventListener('click', function() {
  savePrevInput(input.value)
  input.value = "Hello World!";
})

var prevInput;

function savePrevInput(input) {
  prevInput = input;
}

input.addEventListener("keydown", function(e) {
  if (e.ctrlKey && e.keyCode == 90) {
    if (prevInput) {
      input.value = prevInput;
      input.selectionStart = prevInput.length;
    }
  }
})
<input id="input" />

<button id="btn">Change</button>
Dichroism answered 9/10, 2021 at 2:48 Comment(6)
Yes, I considered this. Should I be concerned about any kind of compatibility issues with this? I'm on Windows and couldn't recall if Apple uses Cmd or Ctrl-Z for undo, and how that appears in the event. Also wasn't sure if the default undo shortcut could be remapped..? (or maybe that's excessive concern :-)Illogic
@Illogic it should be OK. The regular Ctrl + Z behavior still works with this implementation.Dichroism
Ok, thanks! I was able to fully replace undo/redo by listening to 'keydown' and checking for Ctrl-Z, Ctrl-Y, and Ctrl-Shift-Z. I actually prevented default in my case.Illogic
Uncaught TypeError: Cannot read properties of undefined (reading 'length')Haematocele
@Spectric, LuisAFK is correct. I didn't actually use your code (I used an undo stack) but pressing ctrl-Z without hitting "Change" breaks it.Illogic
@Illogic sorry, didn't notice that! I've fixed it.Dichroism

© 2022 - 2024 — McMap. All rights reserved.