【问题标题】:How to handle undo/redo event in javascript?如何在javascript中处理撤消/重做事件?
【发布时间】:2013-03-09 02:45:20
【问题描述】:

我正在尝试使用 Javascript 和 JQuery 检测表单输入的值何时发生变化。不幸的是,我发现 JQuery 的 $(elem).change() 不足,因为它仅在 elem 失去焦点时触发更改事件。我必须立即知道表单输入的值何时发生变化。为此,我已将与输入值可能更改相关的事件范围缩小到 keyuppastecut撤消重做。但是,javascript 和 JQuery 似乎都没有处理撤消或重做的方法。

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 中搜索了撤消/重做事件,但没有发现任何有用的信息。有人可以帮忙处理撤消/重做事件吗?

【问题讨论】:

  • 如果不出意外,您可以检查 ctrl-z 击键,这将是大多数系统上的撤消序列。不过,不知道如果用户在浏览器的菜单中执行edit -> undo 是否会触发。
  • 为什么这里不能使用切换功能......!!
  • @MarcB 好点,捕捉击键肯定会奏效。但是没有办法检测到edit -> undo
  • @undefined 但是对于edit -> undo/redo(或通过键盘)的浏览器/操作系统操作没有真正的事件可以捕获,所以这并不适用
  • 糟糕。我的第一条评论中有一个错字。 没有没有撤消/重做事件...

标签: javascript jquery


【解决方案1】:

您可以使用 MutationObserver 监控所有更改。这不会为每个 keydown 和 keyup 提供事件,但它会合并多个更改并将其作为单个事件提供给您。

  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 
   });

关于 MutationObserver 的更多信息 https://developer.mozilla.org/en/docs/Web/API/MutationObserver

【讨论】:

    【解决方案2】:

    我创建了一个状态撤消/重做快照管理器类,它非常适合跟踪带有子输入的整个 HTML 元素的更改历史记录。如果您想在更精细的级别上拍摄快照,您可以绑定按键事件,而不是我在示例中使用的更改事件。

      <div id="buttons">
         <button type="button" id="undo_btn">Undo</button>
         <button type="button" id="redo_btn">Redo</button>
      </div>
      <br/><br/>
      <div id="content">
         <label>
            Input1:
            <input type="text" value="" />
         </label>
         <br/><br/>
         <label>
            Input2:
            <input type="text" value="" />
         </label>
         <br/><br/>
         <label>
            Input3:
            <input type="text" value="" />
         </label>
         <br/><br/>
         <label>
            Input4:
            <input type="text" value="" />
         </label>
         <br/><br/>
      </div>
    
      <script type="text/javascript">
      var StateUndoRedo = function() {
         var init = function(opts) {
            var self = this;
            self.opts = opts;
            if(typeof(self.opts['undo_disabled']) == 'undefined') {
               self.opts['undo_disabled'] = function() {};
            }
            if(typeof(self.opts['undo_enabled']) == 'undefined') {
               self.opts['undo_enabled'] = function() {};
            }
            if(typeof(self.opts['redo_disabled']) == 'undefined') {
               self.opts['redo_disabled'] = function() {};
            }
            if(typeof(self.opts['redo_enabled']) == 'undefined') {
               self.opts['redo_enabled'] = function() {};
            }
            if(typeof(self.opts['restore']) == 'undefined') {
               self.opts['restore'] = function() {};
            }
            self.opts['undo_disabled']();
            self.opts['redo_disabled']();
         }
    
         var add = function(state) {
            var self = this;
            if(typeof(self.states) == 'undefined') {
               self.states = [];
            }
            if(typeof(self.state_index) == 'undefined') {
               self.state_index = -1;
            }
            self.state_index++;
            self.states[self.state_index] = state;
            self.states.length = self.state_index + 1;
            if(self.state_index > 0) {
               self.opts['undo_enabled']();
            }
            self.opts['redo_disabled']();
         }
    
         var undo = function() {
            var self = this;
            if(self.state_index > 0) {
               self.state_index--;
               if(self.state_index == 0) {
                  self.opts['undo_disabled']();
               } else {
                  self.opts['undo_enabled']();
               }
               self.opts['redo_enabled']();
    
               self.opts['restore'](self.states[self.state_index]);
           }
         }
    
         var redo = function() {
            var self = this;
            if(self.state_index < self.states.length) {
               self.state_index++;
               if(self.state_index == self.states.length - 1) {
                  self.opts['redo_disabled']();
               } else {
                  self.opts['redo_enabled']();
               }
               self.opts['undo_enabled']();
    
               self.opts['restore'](self.states[self.state_index]);
           }
         }
    
         var restore = function() {
            var self = this;
            self.opts['restore'](self.states[self.state_index]);
         }
    
         var clear = function() {
            var self = this;
            self.state_index = 0;
            //self.states = [];
         }
    
         return {
            init: init,
            add: add,
            undo: undo,
            redo: redo,
            restore: restore,
            clear: clear
         };
      };
    
      //initialize object
      var o = new StateUndoRedo();
      o.init({
         'undo_disabled': function() {
            //make the undo button hidden
            document.getElementById("undo_btn").disabled = true;
         },
         'undo_enabled': function() {
            //make the undo button visible
            document.getElementById("undo_btn").disabled = false;
         },
         'redo_disabled': function() {
            //make the redo button hidden
            document.getElementById("redo_btn").disabled = true;
         },
         'redo_enabled': function() {
            //make the redo button visible
            document.getElementById("redo_btn").disabled = false;
         },
         'restore': function(state) {
            //replace the current content with the restored state content
            document.getElementById("content").innerHTML = state;
         }
      });
    
      //initialize first state
      o.add(document.getElementById("content").innerHTML);
      o.restore();
      o.clear();
    
      //bind click events for undo/redo buttons
      document.getElementById("undo_btn").addEventListener("click", function() {
         o.undo();
      });
      document.getElementById("redo_btn").addEventListener("click", function() {
         o.redo();
      });
    
      //bind change events for content element
      document.getElementById('content').addEventListener("change", function(event) {
         // the following is required since vanilla JS innerHTML 
         // does not capture user-changed values of inputs
         // so we set the attributes explicitly (use jQuery to avoid this)
         var elems = document.querySelectorAll("#content input");
         for(var i = 0; i < elems.length; i++) {
            elems[i].setAttribute("value", elems[i].value);
         }
    
         //take a snapshot of the current state of the content element
         o.add(document.getElementById("content").innerHTML);
      });
      </script>
    

    查看这个 JSFiddle:https://jsfiddle.net/up73q4t0/56/

    【讨论】:

      【解决方案3】:

      曾几何时,我正在从事的项目中需要这样的东西。对我来说,获得结果的标记解决方案似乎并不那么优雅。我结合了这里回答的几件事来做到这一点。

      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 来“捕捉”撤消事件。这样做是因为 MutationObserver 在事件触发后触发。我们检查事件是否是预期事件,如 keydown 或 cut 并允许更改发生而不触发回调。如果事件是意外的,我们假设发生了撤消。这不能区分撤消和重做;回调将触发。用法:

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

      我有这个working in action on codepen

      【讨论】:

      • 不幸的是,MutationObserver 不适用于表单元素,例如 textarea。
      【解决方案4】:
      <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>
      

      基本概念是存储以前的输入值并检查新输入值是否等于以前的输入值之一。它并不完美(例如退格触发它)并且效率有点低(见下一段),但你应该能够得到你想要的结果。

      您可以查看撤消代码以查看其实际保留的内容,而不是保留所有以前的输入(我认为它只是让大多数输入在彼此的时间范围内丢失)。

      【讨论】:

        【解决方案5】:

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

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

        好吧,StackOverflow 以其无限的智慧禁止我发布对 jsFiddle 的引用,因此我必须将所有代码粘贴到此处(出于某种原因,jsFiddles 被单独列出,而不是其他 Web 引用)。我并不是将其表示为一个精确的解决方案,而是作为一个模板,您可以使用它来检测用户对输入控件的更改:

        (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 + ")");
        });
        

        【讨论】:

        • 感谢您的出色回复!我会花时间阅读代码,但我想提前问一下这是否可以处理撤消或重做引起的更改?
        • @czarpino - 我没有用浏览器的撤消功能测试它是如何使用的,所以我不知道 - 你需要尝试一下。如果按下撤消键时焦点在该字段中,则应该处理该问题。但是,如果焦点在其他地方并且用户使用菜单中的撤消,我不知道。它似乎在 jsFiddle 的 Chrome 中对我有用,但我不知道您要涵盖的具体测试用例。
        • 谢谢@jfriend00! input 事件似乎是处理撤消和重做事件的“技巧”。
        【解决方案6】:

        John Resig(JQuery 的创建者)的热键可能会有所帮助

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

        来自自述文件

        如果您想使用多个修饰符(例如 alt+ctrl+z),您应该按字母顺序定义它们,例如alt+ctrl+shift

        【讨论】:

        • 这是最接近/唯一的可能性。仍然没有捕获edit -&gt; undo
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-11-25
        • 1970-01-01
        • 2010-12-06
        • 1970-01-01
        • 2014-03-01
        • 2011-05-11
        • 1970-01-01
        相关资源
        最近更新 更多