Hone logo
Hone
Problems

Implementing Undo/Redo Functionality for a Text Editor State

This challenge focuses on building a robust undo/redo system in JavaScript, a common and essential feature in many applications, especially text editors and form builders. The goal is to allow users to revert to previous states and then reapply those changes, providing a smooth and forgiving user experience.

Problem Description

You are tasked with creating a JavaScript class that manages the state of a simulated text editor. This class should allow for operations that modify the editor's content, and crucially, support undoing these operations and redoing them.

Key Requirements:

  1. State Management: The system must store a history of the editor's states.
  2. Operation Execution: A method should exist to perform an operation that modifies the editor's state. Each operation should be recorded.
  3. Undo Functionality: A method to undo the last performed operation, reverting the state to what it was before that operation.
  4. Redo Functionality: A method to redo an operation that was previously undone, restoring the state to what it was after that operation.
  5. Disabling Undo/Redo: When a new operation is performed after one or more undos, the redo history should be cleared.
  6. History Boundaries: The undo and redo functionalities should gracefully handle being called when there are no more operations to undo or redo.

Expected Behavior:

  • When an operation is performed, the current state is saved.
  • When undo() is called, the editor reverts to the previous state. If undo() is called again, it reverts to the state before that.
  • When redo() is called after undo(), the editor moves forward in the history, reapplying the undone operation.
  • If a new operation is performed after undo() has been called, any subsequent redo() calls should do nothing, as the redo history is effectively invalidated.

Edge Cases to Consider:

  • Calling undo() when no operations have been performed or when all operations have been undone.
  • Calling redo() when no operations have been undone or when all undone operations have been redone.
  • Performing multiple operations in succession, then undoing and redoing.

Examples

Example 1: Basic Operations, Undo, and Redo

// Assume 'editor' is an instance of your StateManager class
// Initial state: ""

// Perform operation 1: Add "Hello"
editor.performOperation("Hello");
// Current state: "Hello"
// Undo stack: ["Hello"]
// Redo stack: []

// Perform operation 2: Add " World"
editor.performOperation("Hello World");
// Current state: "Hello World"
// Undo stack: ["Hello", "Hello World"]
// Redo stack: []

// Undo the last operation
editor.undo();
// Current state: "Hello"
// Undo stack: ["Hello"]
// Redo stack: ["Hello World"]

// Undo the previous operation
editor.undo();
// Current state: ""
// Undo stack: []
// Redo stack: ["Hello World", "Hello"]

// Redo the last undone operation
editor.redo();
// Current state: "Hello"
// Undo stack: ["Hello"]
// Redo stack: ["Hello World"]

// Redo the next undone operation
editor.redo();
// Current state: "Hello World"
// Undo stack: ["Hello", "Hello World"]
// Redo stack: []

Example 2: Performing a New Operation After Undoing

// Assume 'editor' is an instance of your StateManager class
// Initial state: ""

// Perform operation 1: Add "Apple"
editor.performOperation("Apple");
// Current state: "Apple"
// Undo stack: ["Apple"]
// Redo stack: []

// Perform operation 2: Add " Banana"
editor.performOperation("Apple Banana");
// Current state: "Apple Banana"
// Undo stack: ["Apple", "Apple Banana"]
// Redo stack: []

// Undo operation 2
editor.undo();
// Current state: "Apple"
// Undo stack: ["Apple"]
// Redo stack: ["Apple Banana"]

// Perform a NEW operation 3: Add " Cherry"
editor.performOperation("Apple Cherry"); // This invalidates the redo history
// Current state: "Apple Cherry"
// Undo stack: ["Apple", "Apple Cherry"]
// Redo stack: []  <- Redo stack is cleared

// Attempt to redo (should do nothing)
editor.redo();
// Current state: "Apple Cherry"
// Undo stack: ["Apple", "Apple Cherry"]
// Redo stack: []

// Undo operation 3
editor.undo();
// Current state: "Apple"
// Undo stack: ["Apple"]
// Redo stack: ["Apple Cherry"]

Example 3: Edge Cases (No Undo/Redo Possible)

// Assume 'editor' is an instance of your StateManager class
// Initial state: ""

// No operations performed
editor.undo(); // Should do nothing, return the current state ""
editor.redo(); // Should do nothing, return the current state ""

// Perform one operation
editor.performOperation("First");
// Current state: "First"
// Undo stack: ["First"]
// Redo stack: []

editor.undo();
// Current state: ""
// Undo stack: []
// Redo stack: ["First"]

editor.undo(); // Should do nothing, return the current state ""
// Current state: ""
// Undo stack: []
// Redo stack: ["First"]

editor.redo();
// Current state: "First"
// Undo stack: ["First"]
// Redo stack: []

editor.redo(); // Should do nothing, return the current state "First"
// Current state: "First"
// Undo stack: ["First"]
// Redo stack: []

Constraints

  • The state can be any JavaScript value (string, number, object, etc.), but for simplicity, you can assume it will primarily be strings representing text.
  • The performOperation method will receive the new state of the editor. It does not need to implement the logic for how to change the state, only how to record it and manage the history.
  • The maximum number of operations to store in the undo history can be large, but consider that very large histories might impact performance. For this challenge, assume a reasonable limit is acceptable, or implement without an explicit limit if it simplifies the design.
  • The implementation should be efficient for common use cases of undo/redo.

Notes

  • You will need two data structures to manage the undo and redo histories. Stacks are a natural fit for this problem.
  • Consider what happens to the redo stack when a new operation is performed after an undo.
  • Think about how to represent the "current state" within your manager.
  • The performOperation method should receive the resulting state after an operation, not the operation itself. For example, if the current text is "Hello" and the user types " World", performOperation would be called with "Hello World".
Loading editor...
javascript