Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allowing contenteditable to undo after dom modification

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.

like image 923
simonzack Avatar asked Jan 29 '15 14:01

simonzack


3 Answers

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/

like image 105
techfoobar Avatar answered Nov 12 '22 21:11

techfoobar


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>
like image 10
Alexander Istomin Avatar answered Nov 12 '22 20:11

Alexander Istomin


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);
});
like image 6
gss Avatar answered Nov 12 '22 20:11

gss