Allowing contenteditable to undo after dom modification

jquery undo/redo
javascript undo event
how to save contenteditable javascript
contenteditable mdn
contenteditable alternative
undo in browser
contenteditable events
execcommand inserthtml

Is there a way to modify a contenteditable's elements using javascript so that undo still works?

Hallo appears to be able to do this fine, try clicking the bold button after selecting some text, I grepped through it's source and have no idea how they do it, the only mention is in halloreundo which is some gui toolbar.

I've looked at undo.js but that simply saves the html in an array, which would really limit the size of the undo stack, so I'm after a native solution, if possible.


You can ensure undo-ability of your edit operations by doing them via document.execCommand() instead of direct DOM manipulations.

Check this mini demo that shows the bold command (undoable ofcourse): http://jsfiddle.net/qL6Lpy0c/

Detect, Undo And Redo DOM Changes With , Detect, Undo And Redo DOM Changes With Mutation Observers isn't called every single time a change is made to the DOM but only after all of In our app.​html , imagine we have a contentEditable area, allowing a user to  Allowing contenteditable to undo after dom modification javascript , html , contenteditable You can ensure undo-ability of your edit operations by doing them via document.execCommand() instead of direct DOM manipulations.


This code will save every change on contenteditable in array. You can manually save current state by calling save_history() or attach this function to any event (in example - on keydown). I coded checking of equality of states, so if you will bind save_history on click event - it will not save 10 state if you will click 10 times without any changes in editor. This code will work in every browser which able to run jQuery:

    //array to store canvas objects history
    canvas_history=[];
    s_history=true;
    cur_history_index=0; 
    DEBUG=true;

//store every modification of canvas in history array
function save_history(force){
    //if we already used undo button and made modification - delete all forward history
    if(cur_history_index<canvas_history.length-1){
        canvas_history=canvas_history.slice(0,cur_history_index+1);
        cur_history_index++;
        jQuery('#text_redo').addClass("disabled");
    }
    var cur_canvas=JSON.stringify(jQuery(editor).html());
    //if current state identical to previous don't save identical states
    if(cur_canvas!=canvas_history[cur_history_index] || force==1){
        canvas_history.push(cur_canvas);
        cur_history_index=canvas_history.length-1;
    }
    
    DEBUG && console.log('saved '+canvas_history.length+" "+cur_history_index);
    
    jQuery('#text_undo').removeClass("disabled");        
}


function history_undo(){
    if(cur_history_index>0)
    {
        s_history=false;
        canv_data=JSON.parse(canvas_history[cur_history_index-1]);
        jQuery(editor).html(canv_data);
        cur_history_index--;
        DEBUG && console.log('undo '+canvas_history.length+" "+cur_history_index);        
        jQuery('#text_redo').removeClass("disabled");    
    }
    else{
        jQuery('#text_undo').addClass("disabled");         
    }
}

function history_redo(){
    if(canvas_history[cur_history_index+1])
    {
        s_history=false;
        canv_data=JSON.parse(canvas_history[cur_history_index+1]);       
        jQuery(editor).html(canv_data);
        cur_history_index++;
        DEBUG && console.log('redo '+canvas_history.length+" "+cur_history_index); 
        jQuery('#text_undo').removeClass("disabled"); 
    }
    else{
        jQuery('#text_redo').addClass("disabled");         
    } 
}
jQuery('body').keydown(function(e){
    save_history();
});
jQuery('#text_undo').click(function(e){
    history_undo();
});
jQuery('#text_redo').click(function(e){
    history_redo();
});  
#text_undo.disabled,#text_redo.disabled{
  color: #ccc;
  }
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
<button id="text_undo" class="disabled">Undo</button><button id="text_redo" class="disabled">Redo</button>
<div id="editor" contenteditable="true">Some editable HTML <b>here</b></div>
</body>
  </html>

Removal of browser built-in Undo stack functionality from , It was pointed out that the browser's undo stack would be enti. In addition, the following JS editor projects were contacted and all of them responded TypeIt.​org is a barebones JavaScript editor that uses contenteditable on The old undo manager API did support automatically rolling DOM changes, but  When using contentEditable, calling execCommand() will affect the currently active editable element. Differences in markup generation Use of contenteditable across different browsers has been painful for a long time because of the differences in generated markup between browsers.


As others have stated, the short answer is to use document.execCommand to preserve the browser's undo/redo. If you need to un-doably edit the text programmatically in any way (like to support multi-line tab indents or other shortcuts that manipulate the text), you should use document.execCommand('insertHTML') or 'insertText' when you set the new text state. insertText will create new div children as you edit, which may be troublesome, while 'insertHTML' will not (but 'insertHTML' has some IE support issues you may have to address, well-detailed elsewhere).

This part threw me for a huge loop and is why I'm writing a new answer, because I didn't find it mentioned anywhere:

You may also need to catch the paste event and turn it into an execCommand('insertHTML'), or else any programmatic selection changes you might do after that paste (like resetting the cursor, etc) run the risk of throwing you an error saying the node isn't long enough to make your new selection, though it visibly is. The DOM somehow doesn't recognize the new length of yourDiv.firstNode after you paste, but it will update it after you use execCommand('insertHTML'). There may be other solutions to that, but this was an easy one:

$("#myDiv").on( 'paste', function(e) {
    e.preventDefault();
    var text = e.originalEvent.clipboardData.getData("text/plain");
    document.execCommand("insertHTML", false, text);
});

replace undo/redo with events that only change DOM? · Issue #21 , Since A&B are going to be modified by the undo command, they should be be if the browser dropped its global undo stack for contenteditable elements, So JS is actually just trying to insert a dummy undo entry by letting  4 Allowing contenteditable to undo after dom modification Sep 27 '18 4 How to suppress console error/warning/info messages when executing selenium python scripts using chrome canary Jul 15 '19 2 Vue js - making a style to be inherited by children components May 22 '19


Using JavaScript & contenteditable, <div id="container" class="container"> <p> Edit the following content undo ); // if the content has been changed, enable the save button editElement. Using the getById function, I can find elements in the DOM to manipulate  - Imaging I drag&drop text from editing host A to B, and hit `undo` when the focus is on C Since A&B are going to be modified by the `undo` command, they should be expecting 'beforeinput' (1 for A, 1 for B) and should be able to cancel the modification.


UndoManager and DOM Transaction, For example, many editors make modifications to DOM after an user agent contenteditable content attribute does not define a new undo scope and all in the undo transaction history is needed to allow scripts to determine  MutationObservers are unable to detect changes to CSS styles (like hover state). Timestamps for mutations are not included in the change records. DOM Mutation Events allowed us to hook into DOM node insertion, keeping the call stack intact. This was problematic and came with performance costs,


Making content editable, When an HTML element has contenteditable set to true , the a defaultParagraphSeparator command to allow you to change it. Additionally, Firefox supports the non-standard argument, br , for defaultParagraphSeparator since title="Undo" onclick="formatDoc('undo');" src="data:image/gif;base64  The subject of removing the undo manager functionality from contenteditable by default came up at the Editing Taskforce F2F meeting on 2016-09-22 at TPAC in Lisboa, Portugal. It was pointed out that the browser's undo stack would be entirely useless once the JS editor interrupt the default behavior even in just a few limited cases.