Skip to the code
cf.

Undo/redo history

Write a class UndoHistory that holds one current value and remembers how it got there. new UndoHistory(initial, options) starts with initial as the current value and nothing to undo or redo. Values can be anything, including undefined, null, and 0.

value is a getter for the current value.

push(value) makes value the current one. The old current value becomes the latest step you can undo, and everything you could redo is dropped, as in an editor when you type after undoing.

undo() steps back one value and redo() steps forward one. Both return the current value after the step. With nothing to undo or redo, they change nothing and return the current value.

canUndo and canRedo are getters that say whether undo() and redo() would do anything.

options.limit caps how many steps you can undo. There's no limit by default. When a push goes over the limit, the oldest step is forgotten.

const text = new UndoHistory("");
text.push("H");
text.push("Hi");
text.undo();     // "H"
text.undo();     // ""
text.redo();     // "H"
text.push("Ho"); // the redo step "Hi" is gone
text.canRedo;    // false
 
const steps = new UndoHistory(0, { limit: 2 });
steps.push(1);
steps.push(2);
steps.push(3);   // a third step goes over the limit: the step back to 0 is forgotten
steps.undo();    // 2
steps.undo();    // 1
steps.canUndo;   // false

What the tests check

  1. starts at the initial value with nothing to undo or redo
    const history = new UndoHistory("a");
    expect(history.value).toBe("a");
    expect(history.canUndo).toBe(false);
    expect(history.canRedo).toBe(false);
    
  2. push makes the new value current
    const history = new UndoHistory("a");
    history.push("b");
    expect(history.value).toBe("b");
    expect(history.canUndo).toBe(true);
    
  3. undo steps back one value at a time and returns it
    const history = new UndoHistory("a");
    history.push("b");
    history.push("c");
    expect(history.undo()).toBe("b");
    expect(history.undo()).toBe("a");
    expect(history.value).toBe("a");
    expect(history.canUndo).toBe(false);
    expect(history.canRedo).toBe(true);
    
  4. redo steps forward again
    const history = new UndoHistory("a");
    history.push("b");
    history.push("c");
    history.undo();
    history.undo();
    expect(history.redo()).toBe("b");
    expect(history.redo()).toBe("c");
    expect(history.canRedo).toBe(false);
    
  5. push after an undo drops the redo steps
    const history = new UndoHistory("a");
    history.push("b");
    history.push("c");
    history.undo();
    history.push("d");
    expect(history.canRedo).toBe(false);
    expect(history.redo()).toBe("d");
    expect(history.undo()).toBe("b");
    
  6. undo and redo with nothing to do return the current value
    const history = new UndoHistory(1);
    expect(history.undo()).toBe(1);
    expect(history.redo()).toBe(1);
    expect(history.value).toBe(1);
    expect(history.canUndo).toBe(false);
    expect(history.canRedo).toBe(false);
    
  7. stores undefined, null, and 0 like any other value
    const history = new UndoHistory(undefined);
    history.push(0);
    history.push(null);
    expect(history.undo()).toBe(0);
    expect(history.undo()).toBeUndefined();
    expect(history.canUndo).toBe(false);
    expect(history.redo()).toBe(0);
    expect(history.redo()).toBeNull();
    
  8. keeps at most limit steps to undo
    const history = new UndoHistory(0, { limit: 2 });
    history.push(1);
    history.push(2);
    history.push(3);
    expect(history.undo()).toBe(2);
    expect(history.undo()).toBe(1);
    expect(history.canUndo).toBe(false);
    expect(history.undo()).toBe(1);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).