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/
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>
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);
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With