Undo Redo



You can undo an operation in Windows by pressing 'Ctrl-Z' or selecting 'Undo' from the Quick Access Toolbar. You can undo an undo by pressing 'Ctrl-Y'or 'Ctrl-Shift-Z,' or by selecting 'Redo' from the QAT. You can undo, redo, or repeat many actions Microsoft Lists, SharePoint, and Teams. You can undo changes, even after you have saved, and then save again, as long as you are within the undo limits (By default Microsoft saves the last 100 undoable actions).

Packages > @fluidframework/undo-redo

This package provides an implementation of an in-memory undo redo stack, as well as handlers for the SharedMap, and SharedSegmentSequence distributed datastructures.

Undo Redo C++

Undo Redo Stack Manager

Undo Redo

The undo redo stack manager is where undo and redo commands are issued, and it holds the stack of all undoable and redoable operations. The undo redo stack manager is a stack of stacks.

The outer stack contains operations, and the inner stack contains all the IRevertible objects that make up that operation. This allows the consumer of the undo redo stack manager to determine the granularity of what is undone or redone.

For instance, you could defined a text operation at the word level, so as a user types you could close the current operation whenever the user types a space. By doing this when the user issues an undo mid-word the characters typed since the last space would be undone, if they issue another undo the previous word would them be undone.

As mentioned above operations are a stack of IRevertible objects. As suggested by the name, these objects have the ability to revert some change which usually means two things. They must be able to track what was changed, and store enough metadata to revert that change.

In order to create IRevertible object there are provided undo redo handlers for commonly used data structures.

Shared Map Undo Redo Handler

The SharedMapUndoRedoHandler generates IRevertible objects, SharedMapRevertible for all local changes made to a SharedMap and pushes them to the current operation on the undo redo stack. These objects are created via the valueChanged event of the SharedMap. This handler will never close the current operation on the stack. This is a fairly simple handler, and a good example to look at for understanding how IRevertible objects should work.

Shared Segment Sequence Undo Redo Handler

The SharedSegmentSequenceUndoRedoHandler generates IRevertible objects, SharedSegmentSequenceRevertible for any SharedSegmentSequence based distributed datastructures like SharedString, SharedObjectSequence, and SharedNumberSequence.

This handler pushes an SharedSegmentSequenceRevertible for every local Insert, Remove, and Annotate operations made to the sequence. The objects are created via the sequenceDelta event of the sequence. Like the SharedMapUndoRedoHandler this handler will never close the current operation on the stack.

This handler is more complex than the SharedMapUndoRedoHandler. The handler itself batches the SharedSegmentSequence changes into the smallest number of IRevertible objects it can to minimize the memory and performance overhead on the SharedSegmentSequence of tracking changes for revert.

Shared Segment Sequence Revertible

The SharedSegmentSequenceRevertible does the heavy lifting of tracking and reverting changes on the underlying SharedSegmentSequence. This is accomplished via TrackingGroup objects. A TrackingGroup creates a bi-direction link between itself and the segment. This link is maintained across segment movement, splits, merges, and removal. When a sequence delta event is fired the segments contained in that event are added to a TrackingGroup. The TrackingGroup is then tracked along with additional metadata, like the delta type and the annotate property changes. From the TrackingGroup’s segments we can find the ranges in the current document that were affected by the original change even in the presence of other changes. The segments also contain the content which can be used. With the ranges, content, and metadata we can revert the original change on the sequence.

As called out above, there is some memory and performance overhead associated with undo redo. This overhead is from the TrackingGroup. This overhead manifests in a few ways:

  • Removed segments in a TrackingGroup will not be garbage collected from the backing tree structure. - Segments can only be merged if they have all the same TrackingGroups.

This object minimizes the number of TrackingGroups created, so this overhead is very low. This undo redo infrastructure is entirely in-memory so it does not affect other users or sessions. If custom IRevertible objects use TrackingGroups this overhead should be kept in mind to avoid possible performance issues.

Redo

Classes

List of classes contained in this package or namespace
ClassDescription
SharedMapRevertibleTracks a change on a shared map allows reverting it
SharedMapUndoRedoHandlerA shared map undo redo handler that will add all local map changes to the provided undo redo stack manager
SharedSegmentSequenceRevertibleTracks a change on a shared segment sequence and allows reverting it
SharedSegmentSequenceUndoRedoHandlerA shared segment sequence undo redo handler that will add all local sequences changes to the provided undo redo stack manager
UndoRedoStackManagerManages the Undo and Redo stacks, and operations withing those stacks. Allows adding items to the current operation on the stack, closing the current operation, and issuing and undo or a redo.

Interfaces

List of interfaces contained in this package or namespace
InterfaceDescription
IRevertible

In this part, we're going to further improve our drawing canvas by adding an undo/redo functionality. To accomplish this, we have to implement a state manager system which keeps track of the various states of the canvas and the objects on it. It's not as bad as it sounds, I promise. Let's go!

The Sample Project

As I'm sure you're aware by now, there's a sample project that goes along with this series of posts. Check it out!

The State Manager

In order to accomplish this undo/redo functionality, we need a class that will keep track of the various states of the drawing canvas. Said class will need to keep a representation of the canvas in JSON, so that said representation can be easily restored.

Lucky for us, FabricJS already provides a way to get the JSON for the canvas: the method toDatalessJSON(). By using this method, we can get the complete state of the canvas at any given time.

The state manager itself will need to keep a stack of states, so that we can pop off the top state to undo. It will also need to keep a separate stack of popped states, so that we can redo.

Let's see the annotated code for our StateManager class:

Now our question is, how do we use this class?

Modifications to DrawingEditor

Undo redo

We must make a few modifications to the root DrawingEditor class.

Undo Redo Command

First, we need some shortcut methods to allow the DrawingEditor to undo, redo, and save the current state:

Undo Redo Hotkey

The method saveState() is used as a shortcut method to allow other methods to a) save the current state and b) render all objects on the canvas again. saveState() needs to be used in quite a few places, most notably whenever a canvas object is modified, created, or deleted.

Our DrawingEditor will now save the state of the canvas whenever objects are changed, created, or deleted. But we still need toolbar items for undo/redo. Guess what that means? We need some new display components!

Components for Undo/Redo

First up is the UndoComponent:

Nothing too complex here, I think. We also need the RedoComponent:

Undo Redo Shortcut

Just like the other components, we need to modify the DrawingEditor and the Razor Page markup and script:

GIF Time!

How Do You Undo

Now it's time to see what we've got! Here's a GIF of the undo/redo functionality in action:

Undo Redo Keyboard Shortcuts

That works pretty well, I'd say! Now we have functioning undo/redo calls in our FabricJS canvas!

Summary

In order to implement undo/redo, we needed to do the following:

  1. Create a StateManager class which could store stacks of canvas state for both undo and redo.
  2. Create undo and redo components that the user could click on.

Don't forget to check out the sample project over on GitHub!

In the next part of this series, we'll implement our own cut/copy/paste functionality, as well as hotkeys! Check out the penultimate part of Drawing with FabricJS and TypeScript!

Undo Redo Log

Happy Drawing!