1. #1
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Post HtmlEditor Undo/Redo Plugin

    HtmlEditor Undo/Redo Plugin


    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.

    Code:
    // 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.
    Attached Files
    Last edited by dangreenfield; 17 Sep 2008 at 2:22 PM. Reason: Added live demo link

  2. #2
    Ext User
    Join Date
    Jul 2007
    Location
    Florida
    Posts
    9,996
    Vote Rating
    6
    mjlecomte will become famous soon enough mjlecomte will become famous soon enough

      0  

    Default


    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/

  3. #3
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Default


    Quote Originally Posted by mjlecomte View Post
    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.

  4. #4
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Arrow Live Demo

    Live Demo


    A live demo of the htmleditor undo/redo function can be seen here.

  5. #5
    Sencha User queej's Avatar
    Join Date
    Aug 2008
    Location
    Ithaca, NY
    Posts
    144
    Vote Rating
    0
    queej is on a distinguished road

      0  

    Question Great Plugin, But Safari Problems

    Great Plugin, But Safari Problems


    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.)
    Tripping the light fantastic.

  6. #6
    Sencha User
    Join Date
    Mar 2008
    Posts
    566
    Vote Rating
    0
    moegal is on a distinguished road

      0  

    Default


    very nice plugin. Have you seen a replacement for the default hyperlink button? That would be a huge improvement.

    Thanks, Marty

  7. #7
    Ext User
    Join Date
    Feb 2008
    Posts
    13
    Vote Rating
    0
    BigBadOwl is on a distinguished road

      0  

    Default Errors

    Errors


    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


    undoredo_error1.jpg

    undoredo_error2.jpg

    undoredo_error3.jpg

    undoredo_errors.zip

  8. #8
    Ext JS Premium Member
    Join Date
    Jan 2008
    Location
    España
    Posts
    215
    Vote Rating
    0
    vizcano is on a distinguished road

      0  

    Default


    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

    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