Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing undo

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 ?
like image 660
nraynaud Avatar asked Jun 17 '11 13:06

nraynaud


People also ask

How do you implement an undo feature?

If “UNDO” string is encountered, pop the top element from Undo stack and push it to Redo stack. If “REDO” string is encountered, pop the top element of Redo stack and push it into the Undo stack. If “READ” string is encountered, print all the elements of the Undo stack in reverse order.

How do you implement undo and redo in computer?

In most Microsoft Windows applications, the keyboard shortcut for the undo command is Ctrl+Z or Alt+Backspace, and the shortcut for redo is Ctrl+Y or Ctrl+Shift+Z. In most Apple Macintosh applications, the shortcut for the undo command is Command-Z, and the shortcut for redo is Command-Shift-Z.

What is the process of undo?

To undo an action, press Ctrl + Z. To redo an undone action, press Ctrl + Y.

Is undo button an implementation of stack?

Stacks. So, we know at the core undo is just a stack we push to as the user does something, and pop from when we want to undo.


3 Answers

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.

like image 91
Arthur Clemens Avatar answered Sep 27 '22 19:09

Arthur Clemens


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.

like image 29
Paul Tyng Avatar answered Sep 27 '22 21:09

Paul Tyng


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.

like image 20
Francisco Ryan Tolmasky I Avatar answered Sep 27 '22 21:09

Francisco Ryan Tolmasky I