This document is a preliminary draft of a specification for the Undo API. It is based on and supercedes the Undo Manager and DOM transaction specification.

This document defines APIs for registering undoable and redoable actions with the user agent, and how those actions are invoked by the user agent.

This is work in progress.

Introduction

Many rich text editors on the Web add editing operations that are not natively supported by execCommand and other Web APIs. For example, many editors make modifications to DOM after an user agent executed user editing actions to work-around user agent bugs and to customize for their use.

However, doing so breaks user agent's native undo and redo because the user agent cannot undo DOM modifications made by scripts. This forces the editors to re-implement undo and redo entirely from scratch, and many editors, indeed, store innerHTML as string and recreate the entire editable region whenever a user tires to undo and redo. This is very inefficient and has limited the depth of their undo history.

Also, any Web app that tries to mix contenteditable region or text fields with canvas or other non-text editable regions will have to reimplement undo and redo of contenteditable regions as well because the user agent typically has one undo history per document, and there is no easy way to add new undo entry to the user agent's native undo history.

This specification tries to address above issues by providing ways to define undo scopes, add items to user agent's native undo history, and create a sequence of DOM changes that can be automatically undone or redone by user agents.

Much of this introduction was copy & pasted from Undo Manager and DOM transaction.

Definition

An undo history is an ordered list of undo items, which represents an reversible user interaction which can be undone or redone such as editing of text in an editing host. Each undo history has an undo position, which indicates a location between two undo items in the undo history, and currently executing undo or redo flag, which is a boolean flag initially set to false, indicating whether there is an on-going undo or redo operation. The user agent uses the undo action of the undo item immediately succeeding the undo position to undo the last reversible user interaction, and uses the redo action of the undo item immediately preceeding the undo position to redo the first reversible user interaction.

An alternative design is to undo stack and redo stack as two different list, and move undo item whenever it's undone / redone.

Each undo item has a undo label, which is a human readable description of the reversible user interaction, undo action, and redo action. Each undo item also has a merged flag, which is initally set to false, and indicates whether the undo item should be grouped together with the succeeding undo item when triggering undo or triggering redo.

An undo scope defines a set of DOM nodes associated with a unique undo history. A document whose browsing context is a top-level browsing context must be in an unique undo scope, which is not associated with any other document whose browsing context is a top-level browsing context. A document whose browsing context is a nested browsing context may have an unique undo scope, which is not associated with the document of its top-level browsing context in accordance with the platform convention.

The user agent may decide to have a separate undo history per frame. For this proposal allows WebKit's behavior, which is to have a single undo history for all browsing contexts under a single top-level browsing context.

UndoItem interface

UndoItem interface represents an undo item. Each UndoItem has optional undo callback and optional redo callback.

        [
          Exposed=Window
        ] interface UndoItem {
          constructor(UndoItemInit initDict);
          readonly attribute DOMString label;
          readonly attribute boolean merged;
        };

        callback UndoItemCallback = void (); // FIXME: Use VoidFunction but WebIDL doesn't export it.

        dictionary UndoItemInit {
          required DOMString label;
          boolean merged = false;
          UndoItemCallback undo = null;
          UndoItemCallback redo = null;
        };
      

The UndoItem(initDict) constructor, when invoked, must run these steps:

  1. Let item be a new {{UndoItem}} object.

  2. Set the undo label of undo item defined by item to {{UndoItemInit/label}}.

  3. Set the merged flag of undo item defined by item to {{UndoItemInit/merged}}.

  4. Set the undo callback of item to {{UndoItemInit/undo}} if it's not null.

  5. Set the redo callback of item to {{UndoItemInit/redo}} if it's not null.

  6. Return item.

The label attribute's getter must return the undo label of the undo item represented by context object.

The merged attribute's getter must return the merged flag of the undo item represented by context object.

When an UndoItem's undo callback is not null, the undo action of the represented undo item must invoke the undo callback.

Similarly, when an UndoItem's redo callback is not null, the redo action of the represented undo item must invoke the redo callback.

UndoManager interface

UndoManager interface provides a mechanism to query and manipulate undo history. Each UndoManager has an associated node.

        [
          Exposed=Window
        ] interface UndoManager {
          void undo();
          void redo();
          void clearUndo();
          void clearRedo();
          void addItem(UndoItem item);
          void removeItem(unsigned long index);
          UndoItem item(unsigned long index);
          readonly attribute unsigned long length;
          readonly attribute unsigned long position;
        };
      

The undo() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  3. Trigger undo on history.

The redo() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  3. Trigger redo on history.

The clearUndo() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  3. Clear undo items on history.

The clearRedo() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  3. Clear redo items on history.

The addItem() method, when invoked, must run these steps:

  1. If the undo item represented by item is already in any undo history, throw {{InvalidModificationError}}.

  2. Let history be the undo history represented by context object.

  3. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  4. If the merged flag of the undo item represented by item is set, and history does not have any undo item after its undo position, throw {{InvalidStateError}}.

  5. If the browsing context of the node document of context object's node is null, throw {{InvalidStateError}}.

  6. Clear redo items on hisotry represented by undoManager.

  7. Insert undo item item to history.

We currently allow an UndoItem which was once inserted into an undo manager but later removed to another undo manager. We should consider adding a used flag, which is initally false and set to true whenever UndoItem is inserted to an UndoManager and exiting early here.

The removeItem() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If currently executing undo or redo flag of history is true, throw {{InvalidStateError}}.

  3. If item is greater than the number of undo items in history, throw {{IndexSizeError}}.

  4. While item is greater than 0, and the undo item at item has its merged flag set,

    1. Decrement item by 1.

  5. Let done be a boolean flag, initially set to false.

  6. While done is false:

    1. If undo position of history is greater than item, decrement undo position by 1.

    2. If undo item at item in history has merged flag unset, set done to true.

    3. Remove undo item at item from history.

removeItem() would currently remove any undo item currently merged with the one being requeste to be removed.

The item() method, when invoked, must run these steps:

  1. Let history be the undo history represented by context object.

  2. If item is greater than the number of undo items in history, return null and abort these steps.

  3. Otherwise, return the undo item at item.

The length attribute's getter must return the number of undo items in the undo history represented by context object.

The position attribute's getter must return the undo position of the undo history represented by context object.

Extensions to the Element interface

        partial interface Element {
          readonly attribute UndoManager undoManager;
          [CEReactions] attribute boolean undoScope;
        };
      

Each element is optionally associated with an UndoManager. It is initially null.

The undoManager attribute's getter must return the UndoManager associated with the context object if there is one. Otherwise it must return null.

The undoScope attribute must reflect the "undoscope" content attribute.

"undoscope" is a boolean attribute. When it is set on an element, the element defines an unique undo scope.

When element's "undoscope" content attribute changes, the user agent must run the following steps in attribute change steps:

  1. If oldValue is null and value is not null, try to associate a new undo manager with element.

  2. If oldValue is not null and value is null, disassociate the undo manager of element.

The user agent must disassociate the undo manager of element in removing steps for an element.

The practical implication of always running this algorithm whenever a node is removed is that the undo manager is cleared & removed whenever a node is disconnected.

Processing Model

The undo scope of a node node is defined by the result of running these steps:

  1. If node is not connected, abort these steps and return null.

  2. Let doc be the node's node document.

  3. If the browsing context of doc is null, abort these steps and return null.

  4. Let ancestorNodes be node's shadow-including inclusive ancestor in reverse tree order.

  5. For each ancestor in ancestorNodes:

    1. If ancestor is not an element, then continue.

    2. Let manager be the UndoManager associated with ancestor.

    3. If manager is not null, abort these steps and return the undo scope of ancestor.

  6. Return the undo scope of doc.

Manipuating Undo History

To clear undo items on an undo history history, run these steps:

  1. Remove every undo item after the undo position of history.

To clear redo items on an undo history history, run these steps:

  1. Let position be the undo position of history.

  2. Remove every undo item before position in history.

  3. Set the undo position of history to 0.

To insert undo item item to an undo history history, run these steps:

  1. Clear redo items of history.

  2. Insert item to the begnning of history.

The user agent may clear undo items or clear redo items of the undo history associated with the undo scope of a top-level browsing context or the undo scope of the currently focused area of a top-level browsing context whenever deemed necessary to user agent's user interface or to match the platform convention if the undo scope is not null.

Undo items created by user agents

When the user manipulates the states of input, textarea, select, and details elements, or elements that are editable, the user agent should insert undo item a new undo item representing the manipulation to the undo history associated with the undo scope of the manipulated elements if the undo scope is not null. The undo action of such an undo item is to undo the manipulation, and the undo action is to reapply the same manipulation.

Triggering Undo and Redo

To trigger undo on an undo history history, run these steps:

  1. Set currently executing undo or redo flag of history to true.

  2. Let done be a boolean flag, initially set to false.

  3. While done is false:

    1. If history does not contain any undo item after the undo position, set done to true.

    2. Otherwise,

      1. Let item be the first item after the undo position in history.

      2. If the merged flag of item is not set, set done to true.

      3. Execute the undo action of item.

      4. Increment the undo position of history by 1.

  4. Set currently executing undo or redo flag of history to false.

To trigger redo on an undo history history, run these steps:

  1. Set currently executing undo or redo flag of history to true.

  2. Let done be a boolean flag, initially set to false.

  3. While done is false:

    1. If history does not contain any undo item before the undo position, set done to true.

    2. Otherwise,

      1. Let item be the last item before the undo position in history.

      2. If the merged flag of item is not set, set done to true.

      3. Otherwise,

        1. Execute the redo action of item.

        2. Decrement the undo position of history by 1.

      Otherwise clause above is important. Unlike triggering undo, triggering redo has to stop when the previous undo item has the merged flag set.

  4. Set currently executing undo or redo flag of history to false.

When the user indicates to the user agent that the last reversible user interaction should be undone, the user agent must trigger undo on the undo history associated with the undo scope of the currently focused area of a top-level browsing context if currently executing undo or redo flag of the undo history is false.

When the user indicates to the user agent that the last previously undone reversible user interaction should be redone, the user agent must trigger redo on the undo history associated with the undo scope of the currently focused area of a top-level browsing context if currently executing undo or redo flag of the undo history is false.

Manipulating undo manager

To try to associate a new undo manager with element, run these steps:

  1. If element is not connected, abort these steps.

  2. If element is edtiable, abort these steps.

  3. If the browsing context of element's node document is null, abort these steps.

  4. Let manager be a new UndoManager representing a new undo history.

  5. Associate the element with manager.

To disassociate the undo manager of element, run these steps:

  1. Let manager be the element's associated UndoManager.

  2. If manager is null, abort these steps.

  3. Otherwise, clear redo items and clear undo items on the undo history represented by undoManager.

  4. Set the element's associated UndoManager to null.

Acknowledgements

Special thanks to