Changeset 1397


Ignore:
Timestamp:
02/11/18 03:30:39 (2 months ago)
Author:
gogo
Message:

Improve the undo/redo handling considerably by preserving the caret position.

That is, each snapshot saves where the caret was, and undoing to a snapshot puts the caret back in that position.

So if you are typing along and hit ctrl-z to undo what you wrote, you don't get dumped back at the start (or end depending on browser) of the document but can carry on typing immediately where you left off.

Discussion in #360

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/XinhaCore.js

    r1396 r1397  
    48054805    --this._undoPos; 
    48064806  } 
    4807   // use the fasted method (getInnerHTML); 
     4807   
     4808  var snapshotData = {  
     4809    'txt'        : null, 
     4810    'caretInBody': false 
     4811  }; 
     4812   
     4813   
     4814  // Caret preservation, when you hit undo, ideally put the caret back where it was. 
     4815  // To accomplish this we figure out what NODE (Text Node typically) and offset into  
     4816  // it had the caret save this into the parent element of that node if it has one 
     4817  // as an attribute, and attach a classname to be able to find it again 
     4818  // then in the actual undo function we look for that class name after dropping  
     4819  // the HTML back into place, and reverse the process 
     4820   
     4821  // Note that IE <11 does not support this, FF <3.6 won't either,  
     4822  // nor Safari <5.1, Chrome I dont' know it does currently I don't know how long ago 
     4823  // Opera same, don't know how long but it works currently 
     4824  // If a browser doesn't work with it, it won't cause a problem, it just won't  
     4825  // preserve the caret. 
     4826   
     4827  try 
     4828  { 
     4829    // Insert a marker so we know where we are 
     4830    var sel = this.getSelection(); 
     4831    var caretNode    = sel.focusNode; 
     4832    if(caretNode) 
     4833    { 
     4834      var caretOffset  = sel.focusOffset 
     4835      var rng = this.createRange(sel); 
     4836      var caretParent   = this.getParentElement(sel); 
     4837       
     4838      var caretRestorationData = false; 
     4839       
     4840      switch(caretNode.nodeType) 
     4841      { 
     4842        case 3:  // TEXT 
     4843        case 4:  // CDATA 
     4844        case 8:  // COMMENT 
     4845          // We need to record which child node the focus node is of the parent 
     4846          //  default to the end 
     4847          var whichChild = caretParent.childNodes.length-1; 
     4848          for(var i = 0; i < caretParent.childNodes.length; i++) 
     4849          { 
     4850            if(caretParent.childNodes[i] == caretNode) 
     4851            { 
     4852              whichChild = i; 
     4853            } 
     4854          } 
     4855          caretRestorationData = { 
     4856            caretChild:   whichChild, 
     4857            caretOffset:  caretOffset 
     4858          }; 
     4859          break; 
     4860         
     4861        case 1: 
     4862          // Ehhhm, not sure.  This would be the case if the selection is an image, table whatever 
     4863          // 
     4864          // For example <p><img></p> with img selected you get caretNode = img, caretParent = p,  
     4865          // caretOffset = 0 (0th child of the p) 
     4866          // 
     4867          // I was going to make this handled so it would select the image again, but actually 
     4868          // I think it works just fine without this, it feels natural-enough anyway. 
     4869          break; 
     4870      } 
     4871       
     4872      if(caretRestorationData) 
     4873      { 
     4874        if(caretParent == this._doc.body) 
     4875        { 
     4876          // Body is tricky because it won't be included in the snapshot or restoration 
     4877          // unless fullPage mode is being used, since there is only one body then we 
     4878          // can record it in the snapshot data and we know where to put it in undo 
     4879          snapshotData.caretInBody            = true; 
     4880          snapshotData.caretRestorationData   = JSON.stringify(caretRestorationData); 
     4881        } 
     4882        else 
     4883        { 
     4884          // For other elements encode the caret data in the element itself 
     4885          // so we can be sure we are looking at the right one when we find it 
     4886          Xinha.addClass(caretParent, 'xinha-undo-caret'); 
     4887          caretParent.setAttribute('xinha-undo-caret', JSON.stringify(caretRestorationData)); 
     4888        } 
     4889      } 
     4890       
     4891      // Debug helper 
     4892      //console.log({n: caretNode, p: caretOffset, r: rng, e: caretParent}); 
     4893    } 
     4894  } 
     4895  catch(e) 
     4896  { 
     4897    // Browser doesn't support something.  I'm not going to try and support 
     4898    // very old browsers for this caret preservation feature 
     4899     
     4900    // Old IE doesn't support 
     4901    if(Xinha.is_gecko || Xinha.is_webkit) 
     4902    { 
     4903      Xinha.debugMsg('Caret preservation code for undo snapshot failed. If your browser is modern, developers need to check it out in XinhaCore.js (search for caret preservation).','warn'); 
     4904    } 
     4905  } 
     4906   
     4907  // use the faster method (getInnerHTML); 
    48084908  var take = true; 
    4809   var txt = this.getInnerHTML(); 
     4909  snapshotData.txt = this.getInnerHTML(); 
     4910   
     4911  // Find all carets we might have added (in theory, 0 or 1, but always a possibility of more) 
     4912  var existingCarets = Xinha.getElementsByClassName(this._doc.body, 'xinha-undo-caret'); 
     4913   
     4914  // Remove them all 
     4915  for(var i = 0; i < existingCarets.length; i++) 
     4916  { 
     4917    Xinha.removeClass(existingCarets[i], 'xinha-undo-caret'); 
     4918    existingCarets[i].removeAttribute('xinha-undo-caret'); 
     4919  } 
     4920     
    48104921  if ( this._undoPos > 0 ) 
    48114922  { 
    4812     take = (this._undoQueue[this._undoPos - 1] != txt); 
     4923    take = (this._undoQueue[this._undoPos - 1].txt != snapshotData.txt); 
    48134924  } 
    48144925  if ( take ) 
    48154926  { 
    4816     this._undoQueue[this._undoPos] = txt; 
     4927    this._undoQueue[this._undoPos] = snapshotData; 
    48174928  } 
    48184929  else 
     
    48284939  if ( this._undoPos > 0 ) 
    48294940  { 
    4830     var txt = this._undoQueue[--this._undoPos]; 
    4831     if ( txt ) 
    4832     { 
    4833       this.setHTML(txt); 
    4834        
     4941    var snapshotData = this._undoQueue[--this._undoPos]; 
     4942    if ( snapshotData.txt ) 
     4943    { 
     4944      this.setHTML(snapshotData.txt); 
     4945      this._restoreCaretForUndoRedo(snapshotData); 
    48354946    } 
    48364947    else 
     
    48404951  } 
    48414952}; 
     4953 
    48424954/** Custom implementation of redo functionality 
    48434955 * @private 
     
    48474959  if ( this._undoPos < this._undoQueue.length - 1 ) 
    48484960  { 
    4849     var txt = this._undoQueue[++this._undoPos]; 
    4850     if ( txt ) 
    4851     { 
    4852       this.setHTML(txt); 
     4961    var snapshotData = this._undoQueue[++this._undoPos]; 
     4962    if ( snapshotData.txt ) 
     4963    { 
     4964      this.setHTML(snapshotData.txt); 
     4965      this._restoreCaretForUndoRedo(snapshotData); 
    48534966    } 
    48544967    else 
     
    48584971  } 
    48594972}; 
     4973 
     4974/** Used by undo and redo to restore a saved caret position. 
     4975 *  
     4976 *  Undo and redo must have already set the html when they call this. 
     4977 *  
     4978 * @private 
     4979 * @param mixed snapshotData as recorded by _undoTakeSnapshot 
     4980 */ 
     4981 
     4982Xinha.prototype._restoreCaretForUndoRedo = function(snapshotData) 
     4983{ 
     4984  // Caret restoration 
     4985  try 
     4986  { 
     4987    // If the snapped caret was actually in the body as it's parent 
     4988    //  (ie text with no containing element except body) 
     4989    // push that data back into the body element so we can treat it as 
     4990    // any other element 
     4991    if(snapshotData.caretInBody) 
     4992    { 
     4993      Xinha.addClass(this._doc.body, 'xinha-undo-caret'); 
     4994      this._doc.body.setAttribute('xinha-undo-caret', snapshotData.caretRestorationData); 
     4995    } 
     4996     
     4997    // Find the caret data we might have recorded in the html 
     4998    var caretParents = Xinha.getElementsByClassName(this._doc.body,'xinha-undo-caret'); 
     4999     
     5000    // Body itself may be the one 
     5001    if(Xinha._hasClass(this._doc.body, 'xinha-undo-caret')) 
     5002    { 
     5003      caretParents[caretParents.length] = this._doc.body; 
     5004    } 
     5005             
     5006    // Just in case some bug happened and there was more than one caret saved 
     5007    //  we will do them all to clear them, but there should only really be 0 or 1 
     5008    for(var i = 0; i < caretParents.length; i++) 
     5009    { 
     5010      if(caretParents[i].getAttribute('xinha-undo-caret').length) 
     5011      { 
     5012        var caretRestorationData = JSON.parse(caretParents[i].getAttribute('xinha-undo-caret')); 
     5013         
     5014        if(caretParents[i].childNodes.length > caretRestorationData.caretChild) 
     5015        { 
     5016          var rng = this.createRange(); 
     5017          rng.setStart(caretParents[i].childNodes[caretRestorationData.caretChild], caretRestorationData.caretOffset); 
     5018          rng.collapse(true); // collapse to the start, although end would be ok I think, should be the same 
     5019           
     5020          var sel = this.getSelection(); 
     5021          sel.removeAllRanges(); 
     5022          sel.addRange(rng); 
     5023        } 
     5024      } 
     5025       
     5026      Xinha.removeClass(caretParents[i], 'xinha-undo-caret'); 
     5027      caretParents[i].removeAttribute('xinha-undo-caret'); 
     5028    } 
     5029  } 
     5030  catch(e) 
     5031  { 
     5032    // Browser doesn't support something, I'm not going to try and  
     5033    //  implement this on old browsers. 
     5034    if(Xinha.is_gecko || Xinha.is_webkit) 
     5035    { 
     5036      Xinha.debugMsg('Caret restoration code for undo failed. If your browser is modern, developers should check it out in XinhaCore.js (search for caret restoration).','warn'); 
     5037    } 
     5038  } 
     5039} 
     5040 
    48605041/** Disables (greys out) the buttons of the toolbar 
    48615042 * @param {Array} except this array contains ids of toolbar objects that will not be disabled 
Note: See TracChangeset for help on using the changeset viewer.