如何在JavaScript中处理撤销/重做事件?

问题描述:

我试图检测何时一个表单输入的值更改使用JavaScript & JQuery。不幸的是,我发现JQuery的$(elem).change()不足,因为它只在elem失去焦点时触发更改事件。我必须立即知道表格输入值有变化时。为此,我已经缩小了与输入的值可能发生变化相关联KEYUP撤消重做事件。但是,JavaScript和JQuery似乎都没有办法处理撤销或重做。如何在JavaScript中处理撤销/重做事件?

var onChange = function() 
{ 
    alert('Checking for changes...'); 
}; 

$(this).off('keyup').on('keyup', onChange); 
$(this).off('paste').on('paste', onChange); 
$(this).off('cut').on('cut', onChange); 

$(this).off('undo').on('undo', onChange); // undo ? 
$(this).off('redo').on('redo', onChange); // redo ? 

我已经在Javascript/JQuery中使用了撤销/重做事件,但没有找到有用的东西。有人可以帮助如何处理撤消/重做事件吗?

+0

如果没有别的,你可以检查ctrl-z击键,这是大多数系统上的撤销序列。尽管如此,如果用户在浏览器的菜单中执行了“编辑 - >撤消”,那么也不知道是否会触发。 – 2013-03-20 05:46:56

+0

为什么你不能在这里使用切换功能...... !! – EnterJQ 2013-03-20 05:47:23

+0

@MarcB好点,捕捉击键肯定会奏效。但是没有办法检测'edit - > undo' – Ian 2013-03-20 05:48:43

JavaScript中没有撤消或重做事件。如果你想要这样的功能,你必须自己写在JavaScript中或找到一个提供这种功能的库。

如果您试图捕捉所有可能的方式,即可以更改输入控件,以便您可以立即看到此类更改,请查看以下示例代码:http://jsfiddle.net/jfriend00/6qyS6/,该代码为输入控件实现了更改回调。这段代码并不是直接为下拉菜单设计的,但由于它是一种输入控件的形式,因此您可以调整此代码为下拉菜单创建自己的更改事件。

那么,*在他们的无限智慧是禁止我只发布一个jsFiddle的引用,所以我必须粘贴在这里的所有代码(出于某种原因,jsFiddles单独出来,而不是其他网站引用)。我不是代表这是一个确切的解决方案,但作为一个模板,你可以使用如何检测用户更改输入控制:

(function($) { 

    var isIE = false; 
    // conditional compilation which tells us if this is IE 
    /*@cc_on 
    isIE = true; 
    @*/ 

    // Events to monitor if 'input' event is not supported 
    // The boolean value is whether we have to 
    // re-check after the event with a setTimeout() 
    var events = [ 
     "keyup", false, 
     "blur", false, 
     "focus", false, 
     "drop", true, 
     "change", false, 
     "input", false, 
     "textInput", false, 
     "paste", true, 
     "cut", true, 
     "copy", true, 
     "contextmenu", true 
    ]; 
    // Test if the input event is supported 
    // It's too buggy in IE so we never rely on it in IE 
    if (!isIE) { 
     var el = document.createElement("input"); 
     var gotInput = ("oninput" in el); 
     if (!gotInput) { 
      el.setAttribute("oninput", 'return;'); 
      gotInput = typeof el["oninput"] == 'function'; 
     } 
     el = null; 
     // if 'input' event is supported, then use a smaller 
     // set of events 
     if (gotInput) { 
      events = [ 
       "input", false, 
       "textInput", false 
      ]; 
     } 
    } 

    $.fn.userChange = function(fn, data) { 
     function checkNotify(e, delay) { 
      // debugging code 
      if ($("#logAll").prop("checked")) { 
       log('checkNotify - ' + e.type); 
      } 

      var self = this; 
      var this$ = $(this); 

      if (this.value !== this$.data("priorValue")) { 
       this$.data("priorValue", this.value); 
       fn.call(this, e, data); 
      } else if (delay) { 
       // The actual data change happens after some events 
       // so we queue a check for after. 
       // We need a copy of e for setTimeout() because the real e 
       // may be overwritten before the setTimeout() fires 
       var eCopy = $.extend({}, e); 
       setTimeout(function() {checkNotify.call(self, eCopy, false)}, 1); 
      } 
     } 

     // hook up event handlers for each item in this jQuery object 
     // and remember initial value 
     this.each(function() { 
      var this$ = $(this).data("priorValue", this.value); 
      for (var i = 0; i < events.length; i+=2) { 
       (function(i) { 
        this$.on(events[i], function(e) { 
         checkNotify.call(this, e, events[i+1]); 
        }); 
       })(i); 
      } 
     }); 
    } 
})(jQuery);  

function log(x) { 
    jQuery("#log").append("<div>" + x + "</div>"); 
} 

// hook up our test engine  
$("#clear").click(function() { 
    $("#log").html(""); 
}); 


$("#container input").userChange(function(e) { 
    log("change - " + e.type + " (" + this.value + ")"); 
}); 
+2

这永远不会在Javascript中实现,至少始终如一(我可以看到浏览器中有怪癖),除非实际的撤销/重做事件被合并。同时,捕获ctrl + z键事件确实是唯一的可能性,但非常不完整。 (不是downvoter) – Ian 2013-03-20 05:54:53

+0

感谢您的出色反应!我会花时间阅读代码,但我想问一下,这样做是否可以处理由撤销或重做导致的更改? – 2013-03-20 06:01:29

+0

@czarpino - 我还没有测试过如何使用浏览器的撤消功能,所以我不知道 - 你需要尝试一下。如果按下撤销键时焦点位于字段中,则应该处理该操作。但是,如果焦点在其他地方,并且用户使用菜单撤消,我不知道。它似乎在jsFiddle中适用于我的Chrome,但我不知道您希望涵盖的具体测试用例。 – jfriend00 2013-03-20 06:14:58

热键由John Resig的(jQuery的作者)可以帮助

https://github.com/jeresig/jquery.hotkeys

自述文件

如果你想使用一个以上的调节剂(如Alt + Ctrl + z)你应该按照字母顺序来定义它们,例如alt + ctrl + shift

+1

这是最接近/唯一的可能性。仍然不会捕获'编辑 - >撤消' – Ian 2013-03-20 05:51:58

您可以监视使用MutationObserver的所有更改。这不会为每个keydown和keyup事件提供事件,但它可以合并多个更改并作为单个事件发给您。约MutationObserver https://developer.mozilla.org/en/docs/Web/API/MutationObserver

<input type="text"/> 
<script> 
var newInput = ""; 
var oldInput = [$('input').val()]; 
$('input').on('input',function(){ 
    newInput = $(this).val(); 
    redo = false; 
    $(oldInput).each(function(i){if(newInput==oldInput[i]){redo = true; return false}); 
    if(redo){ 
     console.log('do code for an undo or redo'); 
    } 
oldInput.push(newInput); 
console.log([oldInput,newInput]); 
}); 
</script> 

的基本概念

var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 
    var observer = new MutationObserver(function(mutations) { 
    mutations.forEach(function(mutation) { 
     // mutation.target will give you element which has been modified. 
     // mutation.addedNodes and mutation.removedNodes will give you operations that were performed on the node 
     // happy coding :) 
     } 
    }); 
    }); 
    observer.observe(elementsToMonitor, { 
    attributes: true, 
    childList: true, 
    characterData: true 
    }); 

更多信息是存储先前输入的值,并检查新的输入值等于前几次之一。这并不完美(例如退格触发它),效率稍低(见下一段),但你应该能够得到你想要的结果。你可以查看代码来撤消它,看看它实际上保留了什么(我认为它只是保持大多数输入在彼此的时间范围内保持丢失状态),而不是保留所有以前的输入。

曾经有一段时间,我在一个项目中需要类似的东西。对于我来说,标记的解决方案似乎并不优雅,无法得到结果。我使用了几个在这里回答的事情的组合来做到这一点。

function UndoListener(options){ 
    if(!options.el instanceof HTMLElement) return; 
    this.el = options.el; 
    this.callback = options.callback || function(){}; 
    this.expectedChange = false; 
    this.init(); 
} 

UndoListener.prototype = { 
    constructor: UndoListener, 
    addListeners: function(){ 
     this.el.addEventListener('keydown', (e) => this.expectedChange = this.eventChecker(e)); 
     this.el.addEventListener('cut', (e) => this.expectedChange = true); 
     this.el.addEventListener('paste', (e) => this.expectedChange = true); 
    }, 
    addObserver: function(){ 
     this.observer = new MutationObserver((mt) => { 
      if(!this.expectedChange){ 
       this.expectedChange = true; 
       this.observer.disconnect(); 
       this.callback.call(this.el, { 
        original: [...mt].shift().oldValue, 
        current: this.el.innerText 
       }); 
       this.addObserver(); 
      } 
      this.expectedChange = false; 
     }); 

     this.observer.observe(this.el, { 
      characterData: true, 
      subtree: true, 
      characterDataOldValue: true 
     }); 
    }, 
    eventChecker: function(event) { 
     return !(~['z','y'].indexOf(event.key) && (event.ctrlKey || event.metaKey)); 
    }, 
    init: function(){ 
     this.addListeners(); 
     this.addObserver(); 
    } 
} 

这使用MutationObserver“catch”撤消事件。它这样做是因为MutationObserver在事件触发后触发。我们检查事件是否是预期事件,如keydown或cut,并允许在没有回调触发的情况下进行更改。如果事件是意外的,我们假设发生了撤消。这不能区分撤销和重做;回调也会触发。用法:

var catcher = new UndoListener({ 
    el: document.querySelector('.container'), 
    callback: function(val){ 
     console.log('callback fired', val); 
    } 
}); 

我有这个working in action on codepen