16

I'm creating a map editing webapp where we can create and edit polylines, polygons etc. I've some trouble finding informations on undo implementation on the web, I find whining about "we need undo" and "here is my Command pattern using closures" but I think between that and a full undo/redo interface there is quite some road.

So, here are my questions (good candidate for wiki I think):

  • Should I manage the stack, or is there a way to send my commands to the browser's stack ? (and how do I handle native commands, like text edits in textifields in this case)
  • how do I handle "command compression" (command grouping) when some commands are browser native
  • How do I detect the undo (ctrl+z) keystroke?
  • If I register a keyup event, how do I decide if I prevent default or not?
  • If not, can I register some undoevent handler somewhere ?
  • Users are not used to undo on the web, how can I "train" them to explore/undo on my application ?
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
nraynaud
  • 4,924
  • 7
  • 39
  • 54

3 Answers3

10

You need to have functions for object creation and deletion. Then pass those functions to the undo manager. See the demo file of my javascript undo manager: https://github.com/ArthurClemens/Javascript-Undo-Manager

The demo code shows canvas, but the code is agnostic.

It doesn't contain key bindings, but may help you with the first steps.

Myself I have used this in a web application with buttons for undo and redo, next to save.

Arthur Clemens
  • 2,848
  • 24
  • 18
  • It's far from enough to manage transient editor states (like when only one point of a polyline was drawn and not yet the second), or continuous edition actions that need undo stack compression (color editor, slider). – nraynaud Aug 12 '11 at 11:28
  • Indeed, one generic approach won't work in that case. For continuous actions you can choose to store only the end result. – Arthur Clemens Aug 15 '11 at 15:12
  • the problem is detecting the "end result" like a property directly bound to a slider (so the user have immediate feedback on his action), you don't know when he's finished with fiddling the slider. You can either try to infer it with time (2s without fiddling the slider means he's finished) but it's tricky to get right, with some event (release of the mouse button) on the control, just compress the undo stack on the fly (it means you have some comparable stuff on the stack, not simple closures, or you can mark event groups. – nraynaud Aug 18 '11 at 16:18
5

Here is a sample of N-Level undo using Knockout JS:

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

It uses an MVVM model so your page state is represented in a javascript object that it maintains a history for.

Donald Duck
  • 8,409
  • 22
  • 75
  • 99
Paul Tyng
  • 7,924
  • 1
  • 33
  • 57
1

The way Cappuccino's automatic undo support works is by telling the undo manager what properties should be undoable. For example, pretend you are managing records of students, you might do something like:

[theUndoManager observeChangesForKeyPath:@"firstName" ofObject:theStudent];
[theUndoManager observeChangesForKeyPath:@"lastName" ofObject:theStudent];

Now regardless of how the students name is changed in the UI, hitting undo will automatically revert it back. Cappuccino also automatically handles coalescing changes in the same run loop, marking the document as "dirty" (needing save) when there are items on the undo stack, etc etc (in other words, the above should be ALL you need to do to support undo).

As another example, if you wanted to make additions and deletions of students undoable, you'd do the following:

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

Since "students" is an array of students in theClass, then additions and deletions from this array will be tracked.

  • thanks that's interesting, but how do you integrate with the native undo/redo in the browser ? – nraynaud Jun 18 '11 at 09:55
  • Cappuccino just handles that -- it automatically ties it directly into cmd-z on mac, and the appropriate key bindings on windows, etc etc. It should all "just work". – Francisco Ryan Tolmasky I Jul 04 '11 at 07:20
  • My aim is not to use cappuccino blindly, I'm creating an ando system for a application that doesn't use cappuccino. I'd like to understand how you did it before. But the framework code is really hard to read and follow for profanes. – nraynaud Jul 04 '11 at 19:13
  • I'm not sure I understand -- are you saying that you are trying to implement undo features similar to cappuccino, but in an app that won't be using cappuccino? – Francisco Ryan Tolmasky I Jul 06 '11 at 06:04
  • exactly, that's why I'd like to open the hood. – nraynaud Jul 07 '11 at 13:32
  • If I understand correctly then you are asking me to explain to you how to write the feature from scratch. If that's the case I think that's a little outside the scope of what can be answered in a StackOverflow response. This is a complex feature with many considerations and you have access to the entirety of the source as LGPL already. So even if I was willing to spend the time going over it in depth with you (I'm not), these text areas with 600 char limits wouldn't be the best place to do it. – Francisco Ryan Tolmasky I Jul 07 '11 at 21:18
  • hum, ok. I though my initial question with 6 bullets (to wich you answered zero) was clear enough from the start. My aim was to expose all those "considerations" and "complexities" in a readable way on the internet for others to know. The source code is basically impossible to make sense of, and even spending 2h on it I couldn't answer any of my questions, that why I asked the author to make an official answer in an answer box, not in a comment. – nraynaud Jul 08 '11 at 11:33
  • Recall that I came to answer this here not because I randomly found this on stackoverflow but because you asked me on twitter after having talked about cappuccino's undo -- so I'm not sure where the animosity is coming from. The problem with your question is that it is the classic "please write this code for me" -- each one of your six points could be its own individual stack overflow question with a lengthy answer. You can choose to be offended by this, or notice that you have had no responses at all and thus appreciate what I'm saying as constructive criticism and restructure your question. – Francisco Ryan Tolmasky I Jul 08 '11 at 15:43
  • 2
    I'm not asking anybody to write my code for me, exactly the converse, I'd like every developer to have the natural language information in one place to write their own code from. If I ask each question separately I'll get smart-ass JS oneliners from karma-hungry hordes as answers, wich is exactly what I don't want. I think my real problem is that it's the wrong site for this question (to open, no direct short smart answer), but I don't know of any better site for this anyways. – nraynaud Jul 09 '11 at 17:35