实现撤销功能

16

我正在创建一个地图编辑的Web应用程序,可以创建和编辑折线、多边形等。我在网上找关于撤销实现的信息时遇到了一些困难,我发现了有些人在抱怨“我们需要撤销”和“这是使用闭包的Command模式”,但我认为在完整的撤销/重做界面之间还有很长的路要走。

所以,这是我的问题(我认为是维基百科的好候选人):

  • 我应该管理堆栈,还是有一种方法将我的命令发送到浏览器的堆栈中?(如果是这样,在这种情况下如何处理文本字段中的文本编辑等原生命令)
  • 当一些命令是浏览器本地命令时,如何处理“命令压缩”(命令分组)
  • 如何检测撤销(ctrl+z)按键?
  • 如果我注册一个keyup事件,如何决定是否阻止默认行为?
  • 如果不行,我能否在某个地方注册一些undo事件处理程序?
  • 用户在Web上不习惯撤销操作,我该如何“培训”他们在我的应用程序中探索/撤销操作?

撤销什么?浏览器中的输入字段可以使用Ctrl-Z进行撤销。您需要撤销什么?提交吗? - mplungjan
我的上下文是地图创建,但我想稍微展开一下这个主题。 - nraynaud
3个回答

10
你需要为对象创建和删除编写函数,并将这些函数传递给撤销管理器。参见我的JavaScript撤销管理器的演示文件:https://github.com/ArthurClemens/Javascript-Undo-Manager
该演示代码显示了画布,但该代码是不可知的。
它不包含按键绑定,但可以帮助你进行第一步操作。
我自己在一个Web应用程序中使用了这个撤销管理器,并在保存按钮旁边添加了撤销和重做按钮。

仅管理瞬态编辑器状态(例如仅绘制了折线的一个点而尚未绘制第二个点),或需要撤消堆栈压缩的连续编辑操作(颜色编辑器,滑块)是远远不够的。 - nraynaud
确实,在这种情况下,一个通用的方法是行不通的。对于连续的操作,您可以选择仅存储最终结果。 - Arthur Clemens
问题在于检测“最终结果”,就像直接绑定到滑块的属性一样(这样用户可以立即看到他的操作反馈),你不知道他何时完成了对滑块的调整。你可以尝试通过时间来推断它(2秒没有调整滑块意味着他已经完成了),但很难做到准确,或者使用控件上的某些事件(例如鼠标按钮释放),在运行时压缩撤消堆栈(这意味着您在堆栈上有一些可比较的东西,而不仅仅是简单的闭包,或者您可以标记事件组)。 - nraynaud

5
这是一个使用Knockout JS实现N级撤销的示例:

(function() {
    
    //current state would probably come from the server, hard coded here for example
    var currentState = JSON.stringify({
        firstName: 'Paul',
        lastName: 'Tyng',
        text: 'Text' 
    })
       , undoStack = [] //this represents all the previous states of the data in JSON format
        , performingUndo = false //flag indicating in the middle of an undo, to skip pushing to undoStack when resetting properties
        , viewModel = ko.mapping.fromJSON(currentState); //enriching of state with observables
        
    
    //this creates a dependent observable subscribed to all observables 
    //in the view (toJS is just a shorthand to traverse all the properties)
    //the dependent observable is then subscribed to for pushing state history
    ko.dependentObservable(function() {
        ko.toJS(viewModel); //subscribe to all properties    
    }, viewModel).subscribe(function() {
        if(!performingUndo) {
        undoStack.push(currentState);
        currentState = ko.mapping.toJSON(viewModel);
    }
    });
        
    //pops state history from undoStack, if its the first entry, just retrieve it
        window.undo = function() {
            performingUndo = true;
            if(undoStack.length > 1)
            {
                currentState = undoStack.pop();
                ko.mapping.fromJSON(currentState, {}, viewModel);
            }
            else {
                currentState = undoStack[0];
                ko.mapping.fromJSON(undoStack[0], {}, viewModel);
            }
            performingUndo = false;
    };
    
    ko.applyBindings(viewModel);
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/1.2.1/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
<div>
    <button data-bind="click: function() { undo(); }">Undo</button>
    <input data-bind="value: firstName" />
    <input data-bind="value: lastName" />
    <textarea data-bind="value: text"></textarea>
</div>

它使用MVVM模型,因此您的页面状态用JavaScript对象表示,并且它会为其维护历史记录。

1
Cappuccino自动撤销支持的工作方式是通过告诉撤销管理器应该可撤销哪些属性来实现的。例如,假设您正在管理学生记录,您可能会执行以下操作:
[theUndoManager observeChangesForKeyPath:@"firstName" ofObject:theStudent];
[theUndoManager observeChangesForKeyPath:@"lastName" ofObject:theStudent];

现在无论学生的名称在UI中如何更改,点击撤销都会自动恢复它。Cappuccino还会自动处理同一运行循环中的更改合并,在撤销堆栈上有项目时标记文档为“脏”(需要保存),等等(换句话说,以上应该是支持撤销所需做的全部内容)。

作为另一个例子,如果您想要使添加和删除学生可撤销,您需要执行以下操作:

[theUndoManager observeChangesForKeyPath:@"students" ofObject:theClass];

由于“students”是TheClass中学生的数组,因此对该数组的添加和删除将被跟踪。


谢谢,这很有趣。但是你如何与浏览器中的本地撤销/重做集成? - nraynaud
Cappuccino只需处理这个问题 - 它会自动将其直接绑定到Mac上的cmd-z,以及Windows上的适当键绑定等等。所有这些都应该“只需工作”。 - Francisco Ryan Tolmasky I
我的目标不是盲目地使用cappuccino,我正在为一个不使用cappuccino的应用程序创建一个ando系统。我想了解你以前是如何做到的。但框架代码对于外行人来说真的很难阅读和理解。 - nraynaud
我不确定我理解了——你是说你正在尝试在一个不使用Cappuccino的应用程序中实现类似于Cappuccino的撤消功能吗? - Francisco Ryan Tolmasky I
2
我并不是要求任何人为我编写代码,恰恰相反,我希望每个开发者都能在一个地方获得自然语言的信息,以便自己编写代码。如果我分别提出每个问题,我会得到一些聪明的JS一行代码作为答案,这正是我不想要的。我认为我的真正问题是这个网站不适合这个问题(没有直接简短的聪明答案),但无论如何,我也不知道有什么更好的网站可以解决这个问题。 - nraynaud
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接