Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to undo changes made from script on contenteditable div

Let's have contenteditable div. Browser itself manage undo on it. But when additional content changes (or touching selection ranges) are made from script (in addition to user action) then it stops behave as user expected.

In other words when user hit Ctrl+Z then div content is not reverted to previous state.

See following simplified artificial example:

https://codepen.io/farin/pen/WNEMVEB

const editor = document.getElementById("editor")
editor.addEventListener("keydown", ev => {  
  if (ev.key === 'a') {
    const sel = window.getSelection()
    const range = window.getSelection().getRangeAt(0)
    const node = range.startContainer;
    const value = node.nodeValue       
    node.nodeValue = value + 'aa'        
    range.setStart(node, value.length + 2)
    range.setEnd(node, value.length + 2)
    ev.preventDefault()
  }
})

All written 'a' letters are doubled.

Undo is ok as long as there is no 'a' typed. When user typed 'a' (appended to text as double 'aa') and hits Ctrl+Z, then he expects both 'a' will be removed and cursor moves back to original position.

Instead only one 'a' is reverted on undo and second one added by script remain.

If event is also prevented by preventDefault() (which is not needed in this example, but in my real world example i can hardly avoid it) then all is worse. Because undo reverts previous user action.

I could images that whole undo/redo stuff will be managed by script, but it means implementation of whole undo/redo logic. That's too complicated, possible fragile and with possible many glitches.

Instead I would like tell browser something like that there is atomic change which should be reverted by one user undo. Is this possible?

like image 830
farincz Avatar asked Jan 25 '26 02:01

farincz


1 Answers

You can store the "revisions" in an array, then push the innerHTML of the div to it whenever you programmatically change the innerHTML of it.

Then, you can set the innerHTML of the div to the last item in the revisions array whenever the user uses the Ctrl + Z shortcut.

const previousRevisions = []

function saveState() {
  previousRevisions.push(editor.innerHTML)
}

function undoEdit() {
  if (previousRevisions.length > 0) {
    editor.innerHTML = previousRevisions.pop();
  }
}

const editor = document.getElementById("editor")

editor.addEventListener("keydown", ev => {
  if (ev.key === 'a') {
    saveState()
    const sel = window.getSelection()
    const range = window.getSelection().getRangeAt(0)
    const node = range.startContainer;
    const value = node.nodeValue
    node.nodeValue = value + 'a'
    range.setStart(node, value.length + 1)
    range.setEnd(node, value.length + 1)
  } else if (ev.ctrlKey && ev.key == 'z') {
    undoEdit()
  }
})
#editor{width:600px;min-height:250px;border:1px solid black;font-size:24px;margin:0 auto;padding:10px;font-family:monospace;word-break:break-all}
<div id="editor" contenteditable="true">type here&nbsp;</div>

The benefit of this solution is that it will not conflict with the browser's native Ctrl + Z shortcut behavior.

like image 115
Spectric Avatar answered Jan 26 '26 18:01

Spectric