jQuery 如何在javascript中处理撤消/重做事件?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15516218/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 15:14:03  来源:igfitidea点击:

How to handle undo/redo event in javascript?

javascriptjquery

提问by Czar Pino

I'm trying to detect whenever a form input's value changes using Javascript & JQuery. Unfortunately, I find JQuery's $(elem).change()insufficient because it only fires the change event when elemloses focus. I have to immediatelyknow when there are changes in the form input's value. To this end, I've narrowed down events associated with a possible change in an input's value to keyup, paste, cut, undo, and redo. However, neither javascript nor JQuery seem to have a way of dealing with undo or redo.

我正在尝试使用 Javascript 和 JQuery 检测表单输入的值何时发生变化。不幸的是,我发现 JQuery 是$(elem).change()不够的,因为它只在elem失去焦点时触发更改事件。我必须立即知道表单输入值何时发生变化。为此,我将与输入值可能更改相关的事件范围缩小到keyuppastecutundoredo。但是,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 ?

I've googled for undo/redo event in Javascript/JQuery but didn't find anything helpful. Can someone help on how to deal with undo/redo events?

我在 Javascript/JQuery 中搜索了撤消/重做事件,但没有找到任何有用的信息。有人可以帮助处理撤消/重做事件吗?

回答by jfriend00

There is no undo or redo event in javascript. If you wanted such functionality, you'd either have to write it yourself in javascript or find a library that offered such functionality.

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

If you're trying to trap all possible ways that an input control can be changed so you can see such a change immediately, then take a look at this sample code: http://jsfiddle.net/jfriend00/6qyS6/which implemented a change callback for an input control. This code wasn't designed directly for a drop-down, but since it's a form of an input control, you can probably adapt this code to create your own change event for a drop-down.

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

Well, StackOverflow in their infinite wisdom is prohibiting me from posting just a reference to a jsFiddle so I have to paste all the code in here (for some reason, jsFiddles are singled out as opposed to other web references). I'm not representing this as an exact solution, but as a template you could use for how to detect user changes to an input control:

好吧,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 + ")");
});

回答by Rohan Doshi

You can monitor all the changes using MutationObserver. This won't give you event for every keydown and keyup, but it kind of consolidate multiple changes and give it out to you as single event.

您可以使用 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 
   });

More info about MutationObserver https://developer.mozilla.org/en/docs/Web/API/MutationObserver

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

回答by wmitchell

Hot Keys by John Resig (Creator of JQuery) may help

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

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

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

From the readme file

从自述文件

If you want to use more than one modifiers (e.g. alt+ctrl+z) you should define them by an alphabetical order e.g. alt+ctrl+shift

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

回答by Siavash

<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>

The basic concept is to store previous input values and check if the new input value equals one of those previous ones. It's not perfect (e.g. a backspace triggers it) and a little inefficient (see next paragraph), but you should be able to get your desired results.

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

Instead of keeping all previous inputs, you could look at the code for undo to see what it actually keeps (I think it just keeps most inputs as lost as they are within a timeframe of each other).

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

回答by Donnie D'Amato

There was a time when I needed something like this in a project I was working on. The marked solution didn't seem that elegant to me to get the result. I used a combination of a couple of things answered here to do it.

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

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

This uses MutationObserverto "catch" undo events. It does this because MutationObserver fires after events fire. We check if the event was an expected event, like keydown or cut and allow the change to occur without the callback firing. If the event was unexpected, we assume an undo has occurred. This cannot differentiate between undo and redo; the callback will fire on either. Usage:

这使用MutationObserver来“捕捉”撤消事件。这样做是因为 MutationObserver 在事件触发后触发。我们检查该事件是否是预期事件,例如 keydown 或 cut,并允许在不触发回调的情况下发生更改。如果事件是意外的,我们假设发生了撤消。这无法区分撤消和重做;回调将触发。用法:

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

I have this working in action on codepen.

在 codepen 上有这个工作

回答by Dane Iracleous

I created a state undo/redo snapshot manager class, which would be great for tracking the change history on an entire HTML element with child inputs. If you want to take snapshots on a more granular level, you can bind on keypress events instead of the change events I used in my example.

我创建了一个状态撤消/重做快照管理器类,它非常适合跟踪具有子输入的整个 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>

See this JSFiddle: https://jsfiddle.net/up73q4t0/56/

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