rniwa@0: rniwa@0: rniwa@0: rniwa@0: rniwa@0: UndoManager and DOM Transaction rniwa@0: rniwa@0: rniwa@0: rniwa@0: rniwa@0:
rniwa@0:

UndoManager and DOM Transaction

rniwa@0: rniwa@19:

Editor's draft — 20 August 2012

rniwa@0: rniwa@0:
rniwa@0:
Editor:
rniwa@0:
Ryosuke Niwa <rniwa@webkit.org> rniwa@0:
rniwa@0: rniwa@0:
Acknowledgements
rniwa@13:
Anne van Kesteren, Annie Sullivan, Alex Russell, Alex Vincent, Aryeh Gregor, Caio Marcelo de Oliveira Filho, Ehsan Akhgari, Eric Uhrhane, rniwa@14: Frederico Caldeira Knabben, Ian Hickson, Johan "Spocke" Sörlin, Jonas Sicking, Ojan Vafai, Olli Pettay, Rakesh Chaitanya KN, Sukolsak Sakshuwong
rniwa@0: rniwa@0:
Latest version:
rniwa@3:
rniwa@3: http://dvcs.w3.org/hg/undomanager/raw-file/tip/undomanager.html
rniwa@0: rniwa@0:
Previous versions:
rniwa@19:
rniwa@19: http://dvcs.w3.org/hg/undomanager/raw-file/77b8999a67d6/undomanager.html (20 August 2012)
rniwa@16:
rniwa@16: http://dvcs.w3.org/hg/undomanager/raw-file/3fbf142909a7/undomanager.html (8 June 2012)
rniwa@11:
rniwa@11: http://dvcs.w3.org/hg/undomanager/raw-file/17a725399127/undomanager.html (29 May 2012)
rniwa@2:
rniwa@2: http://rniwa.com/editing/undomanager-2011-03-27.html
rniwa@8:
rniwa@8: http://rniwa.com/editing/undomanager-2011-12-05.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-12-01.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-11-29.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-10-27.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-10-20.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-10-09.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-09-11.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-08-30.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-08-09.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-08-08.html
rniwa@0:
rniwa@0: http://rniwa.com/editing/undomanager-2011-07-26.html
rniwa@0: rniwa@0:
Use cases:
rniwa@0:
rniwa@0: http://wiki.whatwg.org/wiki/UndoManager_Problem_Descriptions
rniwa@0: rniwa@0:
rniwa@0: rniwa@0:
rniwa@0: rniwa@0:

Status

rniwa@0: rniwa@0:

This document is an early proposal of the specification for Undo Manager and rniwa@0: DOM transaction. This specification will replace the UndoManager section rniwa@0: of the main HTML specification.

rniwa@0: rniwa@0:

Table of Contents

rniwa@0: rniwa@0: rniwa@0: rniwa@0:

Introduction

rniwa@0: rniwa@0:

This specification defines the API to manage user agent's undo transaction history rniwa@0: (also known as undo stack) and make objects that can be managed by rniwa@0: the undo transaction history.

rniwa@0: rniwa@0:

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

rniwa@0: rniwa@0:

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

rniwa@0: rniwa@0:

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

rniwa@0: rniwa@0:

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

rniwa@0: rniwa@0:

Undo Scope and Undo Manager

rniwa@0: rniwa@0:

Definitions

rniwa@0: rniwa@0:

The user agent must associate an undo transaction history, rniwa@0: a list of sequences of DOM transactions, rniwa@0: with each UndoManager object.

rniwa@0: rniwa@0:

The undo transaction history has an undo position. rniwa@0: This is the position between two entries in the undo transaction history's list where the next rniwa@0: entry represents what needs to happen when undo is done, rniwa@0: and the previous entry represents what needs to happen when redo is done.

rniwa@0: rniwa@0:

The undo scope is the collection of DOM nodes that are managed by the same UndoManager. rniwa@0: A document node or an element with undoscope attribute that is either an editing host rniwa@0: or not editable defines a new undo scope, rniwa@0: and all descendent nodes of the element, excluding elements with and descendent nodes of elements with undoscope attribute, rniwa@0: will be managed by a new UndoManager. rniwa@0: An undo scope host is a document, or an element with undoscope attribute that is either rniwa@0: an editing host rniwa@0: or not editable.

rniwa@0: rniwa@0:

Scoping Undo Transaction History

rniwa@0:

The undoscope attribute is a rniwa@0: boolean attribute rniwa@0: that controls the default undo scope of an element. It is to separate undo transaction histories of multiple editable regions without scripts. rniwa@0: Using undoscope content attribute, authors can easily set text fields in a widget to have a separate undo transaction histories for example.

rniwa@0: rniwa@0:

When the undoscope content attribute is added to an editing host rniwa@0: or an element that is not editable, the user agent must define new undo scope for the element, rniwa@0: and create a new UndoManager to manage any DOM changes made to all descendent nodes of rniwa@0: the element excluding undo scope hosts and their descendents.

rniwa@0: rniwa@0:

When the undoscope content attribute is removed from an editing host rniwa@0: or an element that is not editable, the user agent must remove all entries in the undo transaction history rniwa@7: of the corresponding undo scope without unapplying or reapplying them and rniwa@7: disconnect the corresponding UndoManager for the scope. rniwa@0: After the removal, the node from which the content attribute is removed and their descendent nodes, excluding undo scope hosts and their descendents, rniwa@0: belong to the undo scope of the closest ancestor with rniwa@0: the undoscope content attribute or of the document.

rniwa@0: rniwa@0:

Undo scope and contenteditable

rniwa@0:

contenteditable content attribute does not define a new undo scope rniwa@0: and all editing hosts share the same UndoManager by default. rniwa@14: And the undoscope content attribute on an editable element is ignored rniwa@14: except on editing hosts.

rniwa@0: rniwa@14:

When the contenteditable content attribute is set to true on an element, rniwa@14: the user agent must disconnect the UndoManagers rniwa@14: of all descendent undo scope hosts of the element that have become rniwa@14: editable due to the change rniwa@14: as if the undoscope content attribute was removed from those nodes.

rniwa@0: rniwa@14:

Conversely, when the contenteditable rniwa@14: content attribute is removed from an element or set to false, the user agent must behave as if rniwa@14: undoscope content attribute is removed and added back to all descendent nodes of the element that have become rniwa@14: non-editable due to the change.

rniwa@0: rniwa@15:
rniwa@18:

In the following example, the first child element of the container becomes editable when the container's rniwa@18: contentEditable IDL is set to true, resulting in the first child element's UndoManager rniwa@18: to be disconnected. rniwa@18: The second child element's UndoManager isn't disconnected rniwa@18: because the second child did not become editable as a result of the assignment.

rniwa@15: rniwa@15:
rniwa@15: <div id="container">
rniwa@15:     <div undoscope>This will be editable</div>
rniwa@17:     <div contenteditable="false" undoscope>This will remain not editable.</div>
rniwa@15: </div>
rniwa@15: <script>
rniwa@15: var container = document.getElementById('container');
rniwa@15: var children = container.getElementsByTagName('*');
rniwa@15: children[0].undoManager.transact({executeAutomatic: function () {}});
rniwa@15: children[1].undoManager.transact({executeAutomatic: function () {}});
rniwa@15: container.contentEditable = true;
rniwa@15: alert(children[0].undoManager); // Alerts null
rniwa@15: alert(children[1].undoManager.length); // Alerts 1
rniwa@15: </script>
rniwa@15: 
rniwa@15:
rniwa@15: rniwa@0:

undoScope IDL attribute

rniwa@0: rniwa@0:
partial interface Element {
rniwa@0:     attribute boolean undoScope;
rniwa@0: };
rniwa@0:
rniwa@0:
element . undoScope
rniwa@0:

Returns true if the element is an undo scope host and false otherwise.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

The undoScope IDL attribute of Element interfaces must reflect rniwa@0: the undoscope content attribute.

rniwa@0:
rniwa@0: rniwa@0:

The UndoManager interface

rniwa@0: rniwa@0:

To manage transaction entries in the undo transaction history, the UndoManager interface can be used:

rniwa@0: rniwa@0:
interface UndoManager {
rniwa@17:     void transact(in DOMTransaction transaction, in boolean merge);
rniwa@0:     void undo();
rniwa@0:     void redo();
rniwa@0:     getter DOMTransaction[] item(in unsigned long index);
rniwa@0:     readonly attribute unsigned long length;
rniwa@0:     readonly attribute unsigned long position;
rniwa@0:     void clearUndo();
rniwa@0:     void clearRedo();
rniwa@0: };
rniwa@0: rniwa@0:
rniwa@0:
document . undoManager
rniwa@0:

Returns the UndoManager object.

rniwa@0: rniwa@0:
element . undoManager
rniwa@0:

Returns the UndoManager object.

rniwa@0: rniwa@0:
undoManager . transact(transaction, rniwa@0: merge)
rniwa@2:

Clears entries above the current undo position, applies transaction, rniwa@2: and adds it to the beginning of the first entry in undo transaction history, rniwa@9: or of a new undo transaction history if merge is set to false.

rniwa@0: rniwa@0:
undoManager . undo()
rniwa@0:

Unapplies all DOM transactions in the entry immediately after rniwa@0: the current position in the reverse order and increments rniwa@0: position by 1 rniwa@0: if position < rniwa@0: length.

rniwa@0: rniwa@0:
undoManager . redo()
rniwa@0:

Reapplies all DOM transactions in the entry immediately before rniwa@0: the current position and decrements position by 1 rniwa@0: if position > 0

rniwa@0: rniwa@0:
undoManager . position
rniwa@0:

Returns the number of the current entry in the undo transaction history. (Entries at and past this point are redo entries.)

rniwa@0: rniwa@0:
undoManager . length
rniwa@0:

Returns the number of entries in the undo transaction history.

rniwa@0: rniwa@0:
data = undoManager . item(index)
rniwa@0:
undoManager[index]
rniwa@0:

Returns the entry with index index in the undo transaction history.

rniwa@0:

Returns null if index is out of range.

rniwa@0: rniwa@0:
undoManager . clearUndo()
rniwa@0:

Removes entries in the undo transaction history before rniwa@0: position and rniwa@0: resets position to 0.

rniwa@0: rniwa@0:
undoManager . clearRedo()
rniwa@0:

Removes entries in the undo transaction history after rniwa@0: position.

rniwa@0: rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

UndoManager objects represent and manage their node's rniwa@0: undo transaction history.

rniwa@0: rniwa@0:

The object's supported property indices are the rniwa@0: numbers in the range zero to length-1, rniwa@0: unless the length is zero, in which rniwa@0: case there are no supported property indices.

rniwa@0: rniwa@0:

The transact(transaction, merge) will

rniwa@0:
    rniwa@19:
  1. If any UndoManager is already in the process of applying, rniwa@7: unapplying, or reapplying a DOM transaction, rniwa@7: or the UndoManager had been disconnected, then throw rniwa@0: INVALID_ACCESS_ERR and stop.
  2. rniwa@0:
  3. Clear all entries between before the current undo position without unapplying rniwa@0: or reapplying the transactions in the entires.
  4. rniwa@0:
  5. Apply the transaction.
  6. rniwa@9:
  7. If merge is set to false, add an empty entry to the beginning of the undo transaction history.
  8. rniwa@4:
  9. Add the applied transaction to the beginning of the first entry in the undo transaction history.
  10. rniwa@0:
  11. Fire a DOM transaction event for the transaction applied in step 3 at the undo scope host rniwa@7: of this UndoManager if the UndoManager had not already been disconnected.
  12. rniwa@0:
rniwa@0: rniwa@0:

The undo() will

rniwa@0:
    rniwa@19:
  1. If any UndoManager is already in the process of applying, rniwa@7: unapplying, or reapplying a DOM transaction, rniwa@7: or the UndoManager had been disconnected, then throw rniwa@7: INVALID_ACCESS_ERR and stop.
  2. rniwa@0:
  3. If positionlength, stop.
  4. rniwa@4:
  5. Otherwise, unapply DOM transactions in the entry immediately after rniwa@4: the undo position in the order and increment position by 1.
  6. rniwa@7:
  7. Fire an undo event for the transaction unapplied in step 3 at the undo scope host rniwa@7: of this UndoManager if the UndoManager had not already been disconnected.
  8. rniwa@0:
rniwa@0: rniwa@0:

The redo() will

rniwa@0:
    rniwa@19:
  1. If any UndoManager is already in the process of applying, rniwa@7: unapplying, or reapplying a DOM transaction, rniwa@7: or the UndoManager had been disconnected, then throw rniwa@7: INVALID_ACCESS_ERR and stop.
  2. rniwa@0:
  3. If position ≤ 0, stop.
  4. rniwa@4:
  5. Otherwise, reapply DOM transactions in the entry immediately before rniwa@4: the undo position in the reverse order and decrement position by 1.
  6. rniwa@7:
  7. Fire a redo event for the transaction unapplied in step 3 at the undo scope host rniwa@7: of this UndoManager if the UndoManager had not already been disconnected.
  8. rniwa@0:
rniwa@0: rniwa@0:

The item(n) rniwa@0: method must return a new array representing the nth entry in the undo transaction history if rniwa@10: 0 ≤ n < length, or null otherwise.

rniwa@0: rniwa@0:

Being able to access an arbitrary element in the undo transaction history is needed to allow scripts to determine rniwa@2: whether new DOM transaction and the last DOM transaction should being to the same entry or not.

rniwa@0: rniwa@0:

The position rniwa@0: attribute must return the index of the undo position in the undo transaction history. rniwa@0: If there are no DOM transactions to undo, then the value must be same as rniwa@0: length attribute. rniwa@0: If there are no DOM transactions to redo, then the value must be zero.

rniwa@0: rniwa@0:

The length rniwa@0: attribute must return the number of entries in the undo transaction history. rniwa@0: This is the length.

rniwa@0: rniwa@0:

The clearUndo() rniwa@7: method must throw INVALID_ACCESS_ERR rniwa@19: if any UndoManager is already in the process of applying, rniwa@7: unapplying, or reapplying a DOM transaction, rniwa@7: or the UndoManager had been disconnected, rniwa@7: otherwise it must remove all entries in the undo transaction history before the undo position, rniwa@4: and move the undo position to the top (set position to zero).

rniwa@0: rniwa@0:

The clearRedo() rniwa@7: method must throw INVALID_ACCESS_ERR rniwa@19: if any UndoManager is already in the process of applying, rniwa@7: unapplying, or reapplying a DOM transaction, rniwa@7: or the UndoManager had been disconnected, rniwa@7: otherwise it must remove all entries in the undo transaction history after the undo position.

rniwa@0: rniwa@0:

The active undo manager is the UndoManager of the focused node in the document. rniwa@0: If no node has focus, then it's assumed to be of the document.

rniwa@7: rniwa@7:

To disconnect an UndoManager means to deprive the ability to add or remove entries in rniwa@7: the undo transaction history of the UndoManager. rniwa@7: Once the UndoManager is disconnected, transact(), rniwa@7: undo(), redo(), clearUndo(), rniwa@7: and clearRedo() will all throw rniwa@7: INVALID_ACCESS_ERR.

rniwa@0:

rniwa@0: rniwa@0:

Each entry in the UndoManager consists of one or more DOM transactions, rniwa@0: all of which are unapplied and reapplied togehter in one rniwa@0: undo or redo. rniwa@0:

rniwa@0: rniwa@0:
rniwa@0:

Because item() returns new array on each call, rniwa@0: modifying the array does not have any effect on the sequence of DOM transactions of the entry, rniwa@0: and two return values of item() are alwys different objects.

rniwa@0: rniwa@0:
rniwa@0: document.undoManager.transact(...);
rniwa@0: document.undoManager.transact(..., true);
rniwa@0: document.undoManager.transact(..., true);
rniwa@0: alert(document.undoManager.item(0).length); // Alerts 3
rniwa@0: document.undoManager.item(0).pop();
rniwa@0: alert(document.undoManager.item(0).length); // Still alerts 3
rniwa@0: alert(document.undoManager.item(0) === document.undoManager.item(0)); // Alerts false
rniwa@0: 
rniwa@0:
rniwa@0: rniwa@0:

A typical use case for having multiple DOM transactions in one entry is for typing rniwa@0: multiple letters, spaces, and new lines that must be undone or redone in one step.

rniwa@0: rniwa@0:
rniwa@0:

In the following example, letters "o" and "k" are inserted by two automatic DOM transactions rniwa@0: that form one entry in the undo transaction history of the UndoManager. rniwa@0: A br element and string "hi" are then inserted by another two automatic DOM transactions rniwa@0: to form entry in the undo transaction history. All transactions have the label "Typing".

rniwa@0: rniwa@0:
rniwa@0: // Assume myEditor is some element that has undoscope attribute, and insert(node) is a function that inserts the specified node at where the caret is.
rniwa@0: myEditor.undoManager.transact({executeAutomatic: function () {
rniwa@0:     insert(document.createTextNode('o')); }, label: 'Typing'});
rniwa@0: myEditor.undoManager.transact({executeAutomatic: function () {
rniwa@0:     insert(document.createTextNode('k')); }, label: 'Typing'}, true);
rniwa@0: myEditor.undoManager.transact({executeAutomatic: function () {
rniwa@0:     insert(document.createElement('br')); }, label: 'Typing'});
rniwa@0: myEditor.undoManager.transact({executeAutomatic: function () {
rniwa@0:     insert(document.createTextNode('hi')); }, label: 'Typing'}), true);
rniwa@0: 
rniwa@0:

When the first undo is executed immediately after this code is ran, the last two transactions are unapplied, rniwa@0: and the br element and string "hi" will be removed from the DOM. The second undo will unapply the first two transactions and remove "o" and "k".

rniwa@0:
rniwa@0: rniwa@0:

Because Mac OS X and other frameworks expect applications to provide an array of undo items, simply dispatching undo and redo events rniwa@0: and having scripts manage undo transaction history would not let the user agent populate the native UI properly.

rniwa@0: rniwa@0:

undoManager IDL attribute

rniwa@0: rniwa@0:
partial interface Element {
rniwa@0:     attribute UndoManager undoManager;
rniwa@0: };
rniwa@0:
rniwa@0:
element . undoManager
rniwa@0:

Returns the UndoManager object associated with the element's undo scope rniwa@0: if the element is an undo scope host, or null otherwise.

rniwa@0:
rniwa@0: rniwa@0:
partial interface Document {
rniwa@0:     attribute UndoManager undoManager;
rniwa@0: };
rniwa@0:
rniwa@0:
document . undoManager
rniwa@0:

Returns the UndoManager object associated with the document.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

The undoManager IDL attribute of rniwa@0: Document and rniwa@0: Element interfaces must return the object implementing rniwa@0: the UndoManager interface for the undo scope if the node is an undo scope host. rniwa@0: If the node is not an undo scope host, it must return null.

rniwa@0:
rniwa@0: rniwa@0:

Undo: moving forward in the undo transaction history

rniwa@0: rniwa@0:

When the user invokes an undo operation, or when the rniwa@0: execCommand() method is called with rniwa@0: the undo command, the user agent must perform an undo operation on the active undo manager by calling rniwa@0: the undo() method.

rniwa@0: rniwa@0:

Redo: moving backward in the undo transaction history

rniwa@0: rniwa@0:

When the user invokes a redo operation, or when the rniwa@0: execCommand() method is called with rniwa@0: the redo command, the user agent must perform an redo operation on the active undo manager by calling rniwa@0: the redo() method.

rniwa@0: rniwa@0:

DOM Transaction and DOM changes

rniwa@0: rniwa@0:

A DOM transaction is an ordered set of DOM changes associated with rniwa@0: a unique undo scope host that can be applied, unapplied, rniwa@0: or reapplied.

rniwa@0: rniwa@0:

To apply a DOM transaction means to make the associated DOM changes rniwa@0: under the associated undo scope host. rniwa@0: And to unapply and to reapply rniwa@0: a DOM transaction means, respectively, to revert and to remake the associated DOM changes under the associated undo scope host. rniwa@0: rniwa@0:

A DOM transaction can be unapplied or reapplied if it appears, respectively, immediately after or immediately before rniwa@0: the undo position in the associated UndoManager's undo transaction history.

rniwa@0: rniwa@0:

Mutations of DOM

rniwa@0:

DOM changes of a node is a sequence s1, s2, ... sn where each si with 1 ≤ i ≤ n is either one of:

rniwa@0: rniwa@0: rniwa@0:

The DOM state of a node is the state of all descendent nodes and their attributes that are affected by DOM changes of the element. rniwa@0: If two DOM states of a node are equal, then the node and all its descendent nodes must be identical.

rniwa@0: rniwa@0:

Reverting DOM changes

rniwa@0: rniwa@0:

To revert DOM changes of the sequence s1, s2, ... sn, rniwa@0: revert each si with 1 ≤ i ≤ n in the reverse order sn, sn-1, ... s1 as specified below:

rniwa@0: rniwa@0:

To revert inserting a node into a parent before a child, rniwa@0: run these steps:

rniwa@0:
    rniwa@0:
  1. If node is not null and its parent is not parent, then terminate these steps.
  2. rniwa@0:
  3. If child is not null and its parent is not parent, then terminate these steps.
  4. rniwa@0:
  5. If child is not null and its previous sibling is not node, then terminate these steps.
  6. rniwa@0:
  7. Pre-remove node from parent.
  8. rniwa@0:
rniwa@0: rniwa@0:

To revert removing a node from a parent, rniwa@0: let child be the next sibling of node before the removal, and run these steps:

rniwa@0:
    rniwa@0:
  1. If node is not null and its parent is not null, then terminate these steps.
  2. rniwa@0:
  3. If child is not null and its parent is not parent, then terminate these steps.
  4. rniwa@0:
  5. Pre-insert child into parent before child.
  6. rniwa@0:
rniwa@0: rniwa@0:

To revert replacing data of a node with an offset, count, and data, rniwa@0: let replacedData be the substringed data with node, offset, and count before the replacement, rniwa@0: and run these steps:

rniwa@0:
    rniwa@0:
  1. If node's length attribute is less than offset, terminate these steps.
  2. rniwa@0:
  3. Replace data of node with offset, the length of data, and replacedData.
  4. rniwa@0:
rniwa@0: rniwa@0:

To revert changing an attribute whose rniwa@0: namespace is namespace and rniwa@0: local name is localName to value, rniwa@0: let oldValue be the content attribute value before the change, rniwa@0: oldPrefix be the namespace prefix before the change, rniwa@0: and change the attribute to oldValue rniwa@0: and set the namespace prefix to oldPrefix.

rniwa@0: rniwa@0:

To revert appending an attribute whose rniwa@0: namespace is namespace and rniwa@0: local name is localName to a node rniwa@0: and setting the namespace prefix to prefix, rniwa@0: run these steps.

rniwa@0:
    rniwa@0:
  1. If a content attribute whose namespace rniwa@0: is namespace and local name rniwa@0: is localName doesn't exist on the node, then terminate these steps.
  2. rniwa@0:
  3. Otherwise, remove the attribute rniwa@0: whose namespace is namespace and rniwa@0: local name is localName.
  4. rniwa@0:
rniwa@0: rniwa@0:

To revert removing an attribute whose rniwa@0: namespace is namespace and rniwa@0: local name is localName from a node, rniwa@0: let oldValue be the content attribute value and rniwa@0: oldPrefix be the namespace prefix rniwa@0: both before the removal, and run these steps.

rniwa@0:
    rniwa@0:
  1. If a content attribute whose namespace rniwa@0: is namespace and local name rniwa@0: is localName exists on the node, then terminate these steps.
  2. rniwa@0:
  3. Otherwise, create and append the attribute whose rniwa@0: namespace is namespace and rniwa@0: local name is localName, rniwa@0: with oldValue as the content attribute value rniwa@0: and set the namespace prefix rniwa@0: to oldPrefix.
  4. rniwa@0:
rniwa@0: rniwa@0:

To revert setting textarea rniwa@0: element's value IDL attribute rniwa@0: and input element's rniwa@0: value IDL attribute rniwa@0: to value, set element's value IDL attribute to rniwa@0: the raw value of the textarea element rniwa@0: and the value of the input element rniwa@0: before the setting respectively.

rniwa@0: rniwa@0:

Reapplying DOM changes

rniwa@0: rniwa@0:

To reapply DOM changes of the sequence s1, s2, ... sn, rniwa@0: reapply each si with 1 ≤ i ≤ n in the same order s1, s2, ... sn as specified below:

rniwa@0: rniwa@0:

To reapply inserting rniwa@0: a node into a parent before a child, run these steps:

rniwa@0:
    rniwa@0:
  1. If node is not null and its parent is not null, then terminate these steps.
  2. rniwa@0:
  3. If child is not null and its parent is not parent, then terminate these steps.
  4. rniwa@0:
  5. Pre-insert rniwa@0: child into parent before child.
  6. rniwa@0:
rniwa@0: rniwa@0:

To reapply removing rniwa@0: a node from a parent, let child be the next sibling of node before the removal, rniwa@0: and run these steps:

rniwa@0:
    rniwa@0:
  1. If node is not null and its parent is not parent, then terminate these steps.
  2. rniwa@0:
  3. If child is not null and its parent is not parent, then terminate these steps.
  4. rniwa@0:
  5. If child is not null and its previous sibling is not node, then terminate these steps.
  6. rniwa@0:
  7. Pre-remove rniwa@0: node from parent.
  8. rniwa@0:
rniwa@0: rniwa@0:

To reapply replacing data rniwa@0: of a node with an offset, count, and data, and run these steps:

rniwa@0:
    rniwa@0:
  1. If node's length rniwa@0: attribute is less than offset, terminate these steps.
  2. rniwa@0:
  3. Replace data of node rniwa@0: with offset, count, and replacedData.
  4. rniwa@0:
rniwa@0: rniwa@0:

To reapply changing an attribute rniwa@0: whose namespace is namespace rniwa@0: and local name is localName rniwa@0: to value, let prefix be the rniwa@0: namespace prefix after the change, rniwa@0: and change the attribute to value and set rniwa@0: the namespace prefix to prefix.

rniwa@0: rniwa@0:

To reapply appending rniwa@0: an attribute whose namespace is namespace rniwa@0: and local name is localName rniwa@0: with value as the content attribute value rniwa@0: to a node and setting the namespace prefix rniwa@0: to prefix, run these steps:

rniwa@0:
    rniwa@0:
  1. If a content attribute whose namespace rniwa@0: is namespace and local name is rniwa@0: localName exists on the node, then terminate these steps.
  2. rniwa@0:
  3. Otherwise, append the attribute rniwa@0: whose namespace is namespace and rniwa@0: local name is localName rniwa@0: with value as the content attribute value rniwa@0: to the node, and set namespace prefix rniwa@0: to prefix.
  4. rniwa@0:
rniwa@0: rniwa@0:

To reapply removing rniwa@0: an attribute whose namespace is namespace rniwa@0: and local name is localName rniwa@0: from a node, run these steps.

rniwa@0:
    rniwa@0:
  1. If a content attribute whose namespace is rniwa@0: namespace and local name is rniwa@0: localName doesn't exist on the node, then terminate these steps.
  2. rniwa@0:
  3. Otherwise, remove the attribute rniwa@0: whose namespace is namespace and rniwa@0: local name is localName.
  4. rniwa@0:
rniwa@0: rniwa@0:

To reapply setting textarea rniwa@0: element's value IDL attribute rniwa@0: or input elment's rniwa@0: value IDL attribute rniwa@0: to value, set element's value IDL attribute to value.

rniwa@0: rniwa@0:

The DOMTransaction interface

rniwa@0: rniwa@0:
[NoInterfaceObject]
rniwa@0: interface DOMTransaction {
rniwa@0:     attribute DOMString label;
rniwa@0:     attribute Function? executeAutomatic;
rniwa@0:     attribute Function? execute;
rniwa@0:     attribute Function? undo;
rniwa@0:     attribute Function? redo;
rniwa@0: };
rniwa@0: rniwa@0:
rniwa@0:

The DOMTransaction interface is to be implemented by content scripts that implement a DOM transaction.

rniwa@0: rniwa@0:

label attribute must return null or a string that rniwa@0: describes the semantics of the transaction such as "Inserting text" or "Deleting selection". rniwa@0: The user agent may expose this string or a part of this string through its native UI such as menu bar or context menu. rniwa@0: When there are multiple transactions in a single entry of rniwa@0: the undo transaction history, the user agent that doesn't support displaying rniwa@0: multiple labels for each entry must use the label of the first transaction in the sequence of the entry.

rniwa@0: rniwa@0:

executeAutomatic(), execute(), rniwa@0: undo(), and redo() are attributes that must be supported, rniwa@0: as IDL attributes, by objects implementing the DOMTransaction interface.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

Any changes made to the value of executeAutomatic, rniwa@0: execute, undo, rniwa@0: or redo attributes will take effect immediately. In the following example, rniwa@0: execute and undo attributes are modified:

rniwa@0: rniwa@0:
rniwa@12: document.undoManager.transact({ execute: function () {
rniwa@12:     this.execute = function () { alert('foo'); }
rniwa@0:     alert('bar');
rniwa@0: }, undo: function () { alert('baz'); } }); // alerts 'bar'
rniwa@0: document.undoManager.item(0)[0].undo = function() { alert('foobar'); }
rniwa@0: docuemnt.undoManager.undo(); // alerts 'foobar'
rniwa@0: 
rniwa@0: rniwa@0:
rniwa@0:

executeAutomatic attribute must return a valid function if the transaction is a automatic DOM transaction, rniwa@0: and undefined if it is a manual DOM transaction immediately before the transaction is applied. rniwa@0: Any changes made to the value of the executeAutomatic attribute while the transaction is being applied or after the transaction had been applied rniwa@0: should not change the type of the DOM transaction.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

All DOM changes made in execute or rniwa@12: executeAutomatic take effect immediately. rniwa@7: Sometimes, this disconnects the undoManager to which it belongs.

rniwa@0: rniwa@0:
rniwa@0: var scope = document.createElement('div');
rniwa@0: scope.undoScope = true;
rniwa@0: document.body.appendChild(scope);
rniwa@12: scope.undoManager.transact({executeAutomatic: function () {
rniwa@0:     scope.appendChild("foo");
rniwa@0:     alert(scope.textContent); // "foo"
rniwa@0:     scope.undoScope = false;
rniwa@0: }});
rniwa@0: scope.undoManager.undo(); // Throws an error because undoManager returns null.
rniwa@0: 
rniwa@0: rniwa@0:

Automatic DOM transactions

rniwa@0: rniwa@0:

An automatic DOM transaction is a DOM transaction where DOM changes rniwa@0: are tracked by the user agent and the logic to unapply or reapply rniwa@0: the transaction is implicitly created by the user agent.

rniwa@0: rniwa@0:
rniwa@0:

When an automatic DOM transaction is applied, rniwa@0: the user agent must call the function returned by the executeAutomatic attribute if the attribute returns a valid function object. rniwa@0: All DOM changes made by the method in the corresponding undo scope of the UndoManager rniwa@0: must be tracked by the user agent.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@5:

All DOM changes made outside of the undo scope take effect immediately rniwa@5: but are ignored for the purpose of undo and redo. rniwa@0: In the following example, undo() will only remove "bar" and "foo" rniwa@0: remains in the body.

rniwa@0: rniwa@0:
rniwa@0: var scope = document.createElement('div');
rniwa@0: scope.undoScope = true;
rniwa@0: document.body.appendChild(scope);
rniwa@0: scope.undoManager.transact({executeAutomatic: function () {
rniwa@0:     document.body.appendChild("foo");
rniwa@0:     scope.appendChild("bar");
rniwa@0: }});
rniwa@0: scope.undoManager.undo();
rniwa@5: alert(document.body.textContent); // Alerts "foo".
rniwa@0: 
rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

When an automatic DOM transaction is unapplied, rniwa@0: the user agent must revert DOM changes made inside the undo scope of the the UndoManager rniwa@0: while applying the transaction, and call the function returned by the undo attribute if the attribute returns a valid function object.

rniwa@0: rniwa@0:

When an automatic DOM transaction is reapplied, rniwa@0: the user agent must reapply DOM changes made inside the undo scope of the the UndoManager rniwa@0: while applying the transaction. rniwa@0: The user agent must then call the function returned by the redo attribute if the attribute returns a valid function object.

rniwa@0: rniwa@0:

The user agent must also restore selection after rniwa@0: unapplying or reapplying an automatic DOM transaction rniwa@0: in accordance to user agent's platform convention.

rniwa@0: rniwa@0:

The user agent must implement user editing actions and rniwa@0: drag and drop rniwa@0: as automatic DOM transactions, and any application defined automatic DOM transactions must be compatible with rniwa@0: user editing actions.

rniwa@0:
rniwa@0: rniwa@0:

In an automatic DOM transaction, execute attribute is ignored.

rniwa@0: rniwa@0:

Automatic transactions and manual DOM changes

rniwa@0:

Authors should not modify nodes that are used by automatic DOM transactions rniwa@0: in reverting or reapplying rniwa@0: DOM changes as it will interfere with the user agent's attempt to unapply rniwa@0: or reapply automatic DOM transactions.

rniwa@0: rniwa@0:
rniwa@0:

In the following example, the user agent terminates steps early while reverting rniwa@0: the insertion of rniwa@0: the text node " world" in the first call to undo() rniwa@0: and doesn't make any DOM changes.

rniwa@0:
rniwa@0: var b = document.createTextNode("b");
rniwa@0: b.appendChild(document.createTextNode("hello"));
rniwa@0: document.body.appendChild(b);
rniwa@0: document.undoManager.transact({ executeAutomatic: function () {
rniwa@0:     document.body.appendChild(document.createTextNode(" world")); }});
rniwa@0: b.appendChild(document.body.lastChild);
rniwa@0: document.undoManager.undo(); // No-op.
rniwa@0: 
rniwa@0: rniwa@0:

On the other hand, if we store the DOM state as done below, then the call to rniwa@0: undo() will successfully remove the the text node from the body.

rniwa@0:
rniwa@0: document.undoManager.redo(); // No-op.
rniwa@0: document.body.appendChild(b.lastChild);
rniwa@0: document.undoManager.undo(); // " world" is removed from document.body
rniwa@0: 
rniwa@0: rniwa@0:

Manual DOM transactions

rniwa@0:

A manual DOM transaction is a DOM transaction where the logic to rniwa@0: apply, unapply, or reapply rniwa@0: the transaction is explicitly defined by an application. rniwa@0: It provides a way to communicate with user agent's undo transaction history, e.g. to populate user agent's undo menu.

rniwa@0: rniwa@0:
rniwa@12:

When a manual DOM transaction is applied, the user agent must call rniwa@0: the function returned by the execute if the attribute returns a valid function object.

rniwa@0: rniwa@12:

When a manual DOM transaction is unapplied, the user agent must call rniwa@0: the function returned by the undo attribute if the attribute returns a valid function object.

rniwa@0: rniwa@12:

When a manual DOM transaction is reapplied, the user agent must call rniwa@0: the function returned by the redo attribute if the attribute returns a valid function object.

rniwa@0:
rniwa@0: rniwa@0:

In a manual DOM transaction, executeAutomatic attribute is ignored.

rniwa@0: rniwa@0:

Manual DOM transactions may be incompatible with automatic DOM transactions, in particular, rniwa@0: with user editing actions rniwa@0: if manual DOM transaction mutates nodes that are dependent on by automatic DOM transactions.

rniwa@0: rniwa@0:
rniwa@0:

Manual DOM transactions will let authors populate items rniwa@0: in the undo transaction history. In particular, this will let rniwa@0: the user agent to fill native UIs such as menu bars to display undoable actions.

rniwa@0: rniwa@0:
rniwa@0: function drawLine(start, end, style) {
rniwa@0:     document.undoManager.transact({ execute: function () {
rniwa@0:             // Draw a line on canvas
rniwa@0:         }, undo: function () {
rniwa@0:             // Undraw a line
rniwa@0:         }, redo: function () { this.execute(); },
rniwa@0:         'Draw a line'
rniwa@0:     });
rniwa@0: }
rniwa@0: 
rniwa@0: rniwa@0:

In this example, drawLine() will add a new entry to the document's rniwa@0: undo transaction history and the user agent can communicate the existence rniwa@0: of this undoable action via UIs such as context menu and menubars.

rniwa@0: rniwa@0: rniwa@0:

Transaction, Undo, and Redo Events

rniwa@0: rniwa@0:

When a new DOM transaction is applied by transact() method to an undo transaction history rniwa@0: of a UndoManager, the user agent must fire a DOM transaction event using rniwa@0: the TransactionEvent interface. rniwa@0: When a DOM transaction is unapplied or reapplied though undo() method or rniwa@0: redo() method, of a UndoManager, the user agent must fire rniwa@0: an undo event and a redo event respectively.

rniwa@0: rniwa@0:

The DOMTransactionEvent interface

rniwa@0: rniwa@0:
[Constructor(DOMString type, optional EventInit eventInitDict)]
rniwa@0: interface DOMTransactionEvent : Event {
rniwa@0:     readonly attribute Object transaction;
rniwa@0: };
rniwa@0: rniwa@0:
rniwa@0:
DOMTransactionEvent . transaction
rniwa@0:

Returns the transaction object that triggered this event.

rniwa@0:
rniwa@0: rniwa@0:
rniwa@0:

The transaction attribute of rniwa@0: the DOMTransactionEvent interface must return the object that implements rniwa@0: the DOMTransactionEvent interface that triggered the event.

rniwa@0: rniwa@0:

When the user agent is required to fire a DOM transaction event for a DOM transaction t at rniwa@0: an undo scope host h, the user agent must run the following steps:

rniwa@0:
    rniwa@0:
  1. Create a DOMTransactionEvent object and initialize it to have the name "DOMTransaction", rniwa@0: to bubble, to not cancelable, and to have the transaction attribute initialized to t.
  2. rniwa@0:
  3. Dispatch the newly created DOMTransactionEvent object at the node h.
  4. rniwa@0:
rniwa@0: rniwa@0:

When the user agent is required to fire an undo event and fire a redo event for a DOM transaction t at rniwa@0: an undo scope host h, the user agent must run the following steps:

rniwa@0:
    rniwa@0:
  1. Create a DOMTransactionEvent object and initialize it to have the name "undo" and rniwa@0: "redo" respectively, to bubble, to not cancelable, and to have the transaction attribute initialized to t.
  2. rniwa@0:
  3. Dispatch the newly created TransactionEvent object at the node h.
  4. rniwa@0:
rniwa@0: rniwa@0:

The target node is always set to a undo scope host or a node that was a undo scope host rniwa@0: immediately before t was applied, unapplied, or reapplied.

rniwa@0:
rniwa@0: rniwa@0: rniwa@0: rniwa@0: