PDA

View Full Version : HtmlEditor Undo/Redo Plugin



dangreenfield
2 Dec 2007, 4:16 PM
Sadly, IE undo/redo functionality (using ctrl-z and ctrl-y) crashes badly when the innerHTML is used by the syncValue function in the HtmlEditor component. After text is modified, IE just spits the dummy and refuses to reverse the changes anymore. Firefox doesn't suffer from this problem, and its ability to undo and redo changes works well.

So, in creating my Undo/Redo plugin, I have focused solely on providing replacement functionality for IE only. For Firefox, and other browsers, I've simply tied the existing functionality to the buttons that my plugin adds to the toolbar.

One failing in my plugin, is that I'm unable to save the state of the highlighted text prior to changes being made. This is because the code of the original Ext.form.HtmlEditor component doesn't have an overridable function that is called before any changes take place. I thought of adding it to my extended Ext.ux.HTMLEditor component, but realised that I was getting too close to the point of completely rewriting the original component altogether, which would be a little too rude of me to do.

This isn't too bad a problem as you might expect though, as it only affects text that is deleted (ie, it simply reappears and disappears, with undo and redo respectively, without highlighting), but with toolbar functions you, at least, get the highlighted text after the event, as I save the selection bookmark with each change. So, in other words, it half works!

As with any soft-coded undo/redo function, the entire content is saved with each change. If you're worried about memory (which you really shouldn't considering that it's only text), you can also pass a size parameter to the plugin, which will limit the number of revisions to keep a record of, otherwise, it will keep the full history of changes.

Note: Changes are recorded whenever any change is made, however, if you type fairly quickly, several letters may have changed with each record, due to the time it takes for the script to run. This is standard.

One extra feature that this version has over its other browser versions, is that it keeps the history throughout the entire editing session, even when you enter source edit mode, so you can practically reverse all changes and then rewatch them being added as source code changes. I have, however, had to wrap all changes while in source edit mode into one single change to avoid half-edited source code changes being applied while in normal edit mode (as they look horrific).

Note: This plugin is designed to be used with my extended version of the HtmlEditor (see http://extjs.com/forum/showthread.php?t=19480). It's just too hard to write a plugin for the original HtmlEditor.


// Ext.ux.HTMLEditorUndoRedo
// a plugin that adds Undo/Redo functionality to Ext.ux.HtmlEditor
// note: this is mostly for IE only as Gecko functionality is adequate,
// however, code is included to add toolbar buttons for non-IE browsers
Ext.ux.HTMLEditorUndoRedo = function(size) {

// PRIVATE

// pointer to Ext.ux.HTMLEditor
var editor;

// IE only: variables
// size parameter limits the rollback history
var volume = size || -1;
var history = new Array();
var index = 0;
var placeholder = 0;
var count = 0;
var ignore = false;

// IE only: updates the toolbar buttons
var updateToolbar = function() {
editor.tb.items.map.undo.setDisabled(index < 2);
editor.tb.items.map.redo.setDisabled(index == count);
}

// IE only: updates the editor body
var reset = function() {
editor.getEditorBody().innerHTML = history[index].content;
resetBookmark();
}

// IE only: updates the element (when in source edit mode)
var resetElement = function() {
editor.el.dom.value = history[index].content;
resetBookmark();
}

// IE only: reposition the cursor
var resetBookmark = function() {
var range = editor.doc.selection.createRange();
range.moveToBookmark(history[index].bookmark);
range.select();
}

// PUBLIC

return {

// Ext.ux.HTMLEditorUndoRedo.init
// called upon instantiation
init: function(htmlEditor) {
editor = htmlEditor;

// add the undo and redo buttons to the toolbar.
// insert before the sourceedit button
editor.tb.insertToolsBefore('sourceedit', ['-',
{
itemId: 'undo',
cls: 'x-btn-icon x-edit-undo',
queryEnabled: ! Ext.isIE,
handler: Ext.isIE ? this.undo : editor.relayBtnCmd,
scope: Ext.isIE ? this : editor,
clickEvent: 'mousedown',
tooltip: {
title: 'Undo (Ctrl+Z)',
text: 'Undo the last edit.',
cls: 'x-html-editor-tip'
}
}, {
itemId: 'redo',
cls: 'x-btn-icon x-edit-redo',
queryEnabled: ! Ext.isIE,
handler: Ext.isIE ? this.redo : editor.relayBtnCmd,
scope: Ext.isIE ? this : editor,
clickEvent: 'mousedown',
tooltip: {
title: 'Redo (Ctrl+Y)',
text: 'Redo the last edit.',
cls: 'x-html-editor-tip'
}
}, '-']);

// IE only: set up event listeners
if (Ext.isIE) {

// set element listeners on render
editor.on('render', function() {

// monitor for ctrl-z (undo) and ctrl-y (redo) keys
var keyCommands = [{
key: 'z',
ctrlKey: true,
fn: this.undo,
scope: this
}, {
key: 'y',
ctrlKey: true,
fn: this.redo,
scope: this
}];
new Ext.KeyMap(editor.getEditorBody(), keyCommands);

// record changed data when in source edit mode
editor.el.on('keyup', this.record, this);
}, this);

// record changed data when saved back to element
editor.on('sync', function() {
if (ignore) {
ignore = false;
}
else {
this.record();
}
}, this);

// perform maintenance when edit mode has changed
editor.on('editmodechange', function() {

// set a placeholder when source edit mode is selected
if (editor.sourceEditMode) {
placeholder = index;
}

// else record all changes made in source edit mode as a
// single historic entry.
// note: undo/redo functions continue to work while in
// source edit mode (even when undoing changes made before
// the mode was changed), but those made while in source
// edit mode are no longer available once source edit mode
// is exited as they can appear undesirable or meaningless
// when in normal edit mode, so they are rolled together
// to form a single historic change
else {

// if changes were made while in source edit mode then
if (index > placeholder) {

// if starting point was lost to history then
if (placeholder < 0) {

// record all source edit mode changes as first
// historic record
placeholder == 0;
history[placeholder] = history[index];
}

// else check to see if data has actually changed
// while in source edit mode then
else if (history[placeholder].content != history[index].content) {

// record all source edit mode changes as single
// historic record, to follow last record change
// made in normal edit mode
placeholder++;
history[placeholder] = history[index];
}

// reset index and count to placeholder
index = placeholder;
count = index;
}

// if no changes were made then reset count as it
// may have grown if changes were made and reversed
else {
count = placeholder;
}

// update the undo/redo buttons on the toolbar
updateToolbar();
}
} , this);
}
},

// IE only: record changes to data
record: function() {

// get the current html content from the element
var content = editor.el.dom.value;

// if no historic records exist yet or content has
// changed since the last record then
if (index == 0 || history[index].content != content) {

// if size of rollbacks has been reached then drop
// the oldest record from the array
if (count == volume) {
history.shift();
placeholder--;
}

// else increment the index
else {
index++;
}

// record the changed content and cursor position
history[index] = {
content: content,
bookmark: editor.doc.selection.createRange().getBookmark()
};
count = index;
}

// update the undo/redo buttons on the toolbar
updateToolbar();
},

// IE only: perform the undo
undo: function() {

// ensure that there is data to undo
if (index > 1) {

// decrement the index
index--;

// if in source edit mode then update the element directly
if (editor.sourceEditMode) {
resetElement();
}

// else update the editor body
else {
reset();

// ignore next record request as syncValue is called
// by Ext.form.HtmlEditor.updateToolBar and we don't
// want our undo reversed again
ignore = true;

// update the editor toolbar and return focus
editor.updateToolbar();
editor.deferFocus();
}

// update the undo/redo buttons on the toolbar
updateToolbar();
}
},

// IE only: perform the redo
redo: function() {

// ensure that there is data to redo
if (index < count) {

// increment the index
index++;

// if in source edit mode then update the element directly
if (editor.sourceEditMode) {
resetElement();
}

// else update the editor body
else {
reset();

// ignore next record request as syncValue is called
// by Ext.form.HtmlEditor.updateToolBar and we don't
// want our redo reversed again
ignore = true;

// update the editor toolbar and return focus
editor.updateToolbar();
editor.deferFocus();
}

// update the undo/redo buttons on the toolbar
updateToolbar();
}
}
}
}


I have attached a demo showing the functionality. It includes my Ext.ux.HTMLEditor component.

For those who wish to see a live demo, please click here (http://coder.4realhost.com/undoredo/).

mjlecomte
3 Dec 2007, 5:52 PM
Thanks for this.

I think you already stated that this is specific for an html editor, but I'll ask anyway, could this be used for other components, for example an editor grid?

I saw an undo feature here also:
http://tof2k.com/ext/formbuilder/

dangreenfield
5 Dec 2007, 1:25 AM
could this be used for other components, for example an editor grid?

No, this was built specifically for Ext.ux.HTMLEditor. I've commented the code, though, so it should be quite easy for you to adapt it to other purposes.

dangreenfield
14 Mar 2008, 12:49 PM
A live demo of the htmleditor undo/redo function can be seen here (http://coder.4realhost.com/undoredo/).

queej
8 Jan 2009, 7:49 AM
This is a really nice plugin for the editor, that I hope gets incorporated into the core eventually. In Safari 3, however, the undo and redo buttons do not work consistently. When I highlight something and bold it, for example, I can't always undo as I can in other browsers. Does anyone else have this problem and/or a suggestion for a fix? (I am using this extension in conjunction with htmleditorimage, also.)

moegal
21 Jan 2009, 12:02 PM
very nice plugin. Have you seen a replacement for the default hyperlink button? That would be a huge improvement.

Thanks, Marty

BigBadOwl
10 Feb 2009, 9:24 AM
Hi there

I get these three errors. The first error occurs when you try paste something in. The second one occurs when you click on an image and the third occurs when you try leave or refresh the page.

All of these are from the demo site (but occur on my site too). The browser with the problem is IE 7 running on Vista.

Any ideas


11859

11860

11861

11862

vizcano
16 Dec 2009, 4:37 AM
Hi

I'm addapting your Undo/Redo extension to Ext.ux.form.HtmlEditor.Plugin but i cant deal with ctrl+y and ctrl+z key events, dont know why i get an undefined when trying "this.cmp.getEditorBody()"

This is my code



onRender: function(){
var cmp = this.cmp;
var btn_undo = this.cmp.getToolbar().addButton({
itemId: 'undo',
iconCls: 'x-edit-undo',
handler: this.undo,
scope: this,
tooltip: {
title: 'Deshacer'
},
overflowText: 'Deshacer'
});
var btn_redo = this.cmp.getToolbar().addButton({
itemId: 'redo',
iconCls: 'x-edit-redo',
handler: this.redo,
scope: this,
tooltip: {
title: 'Rehacer'
},
overflowText: 'Rehacer'
});
if (Ext.isIE) {
// monitor for ctrl-z (undo) and ctrl-y (redo) keys
var keyCommands = [{
key: 'z',
ctrlKey: true,
fn: this.undo,
scope: this
}, {
key: 'y',
ctrlKey: true,
fn: this.redo,
scope: this
}];
new Ext.KeyMap(this.cmp.getEditorBody(), keyCommands);

// record changed data when in source edit mode
this.cmp.el.on('keyup', this.record, this);
}

}


Another thing i would like to ask u is that if you have made any changes to this extension, to update the code i'm using :)