This document is an early proposal of the specification for Undo Manager and DOM transaction. This specification will replace the UndoManager section of the main HTML specification.
This specification defines the API to manage user agent's undo transaction history (also known as undo stack) and make objects that can be managed by the undo transaction history.
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 stack.
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 transaction history per document, and there is no easy way to add new undo entry to the user agent's native undo transaction history.
This specification tries to address above issues by providing ways to define undo scopes, add items to user agent's native undo transaction history, and create a sequence of DOM changes that can be automatically undone or redone by user agents.
The user agent must associate an undo transaction history,
a list of sequences of DOM transactions,
with each UndoManager
object.
The undo transaction history has an undo position. This is the position between two entries in the undo transaction history's list where the next entry represents what needs to happen when undo is done, and the previous entry represents what needs to happen when redo is done.
The undo scope is the collection of DOM nodes that are managed by the same UndoManager
.
A document node or an element with undoscope
attribute that is either an editing host
or not editable defines a new undo scope,
and all descendent nodes of the element, excluding elements with and descendent nodes of elements with undoscope
attribute,
will be managed by a new UndoManager
.
An undo scope host is a document, or an element with undoscope
attribute that is either
an editing host
or not editable.
The undoscope
attribute is a
boolean attribute
that controls the default undo scope of an element. It is to separate undo transaction histories of multiple editable regions without scripts.
Using undoscope
content attribute, authors can easily set text fields in a widget to have a separate undo transaction histories for example.
When the undoscope
content attribute is added to an editing host
or an element that is not editable, the user agent must define new undo scope for the element,
and create a new UndoManager
to manage any DOM changes made to all descendent nodes of
the element excluding undo scope hosts and their descendents.
When the undoscope
content attribute is removed from an editing host
or an element that is not editable, the user agent must remove all entries in the undo transaction history
of the corresponding undo scope without unapplying or reapplying them and
disconnect the corresponding UndoManager
for the scope.
After the removal, the node from which the content attribute is removed and their descendent nodes, excluding undo scope hosts and their descendents,
belong to the undo scope of the closest ancestor with
the undoscope
content attribute or of the document.
contenteditable
content attribute does not define a new undo scope
and all editing hosts share the same UndoManager
by default.
And the undoscope
content attribute on an editable element is ignored
except on editing hosts.
When the contenteditable
content attribute is set to true
on an element,
the user agent must disconnect the UndoManager
s
of all descendent undo scope hosts of the element that have become
editable due to the change
as if the undoscope
content attribute was removed from those nodes.
Conversely, when the contenteditable
content attribute is removed from an element or set to false
, the user agent must behave as if
undoscope
content attribute is removed and added back to all descendent nodes of the element that have become
non-editable due to the change.
In the following example, the first child element of the container becomes editable when the container's
contentEditable
IDL is set to true, resulting in the first child element's UndoManager
to be disconnected.
The second child element's UndoManager
isn't disconnected
because the second child did not become editable as a result of the assignment.
<div id="container"> <div undoscope>This will be editable</div> <div contenteditable="false" undoscope>This will remain not editable.</div> </div> <script> var container = document.getElementById('container'); var children = container.getElementsByTagName('*'); children[0].undoManager.transact({executeAutomatic: function () {}}); children[1].undoManager.transact({executeAutomatic: function () {}}); container.contentEditable = true; alert(children[0].undoManager); // Alerts null alert(children[1].undoManager.length); // Alerts 1 </script>
partial interface Element { attribute boolean undoScope; };
undoScope
Returns true
if the element is an undo scope host and false
otherwise.
UndoManager
interfaceTo manage transaction entries in the undo transaction history, the UndoManager
interface can be used:
interface UndoManager { void transact(in DOMTransaction transaction, in boolean merge); void undo(); void redo(); getter DOMTransaction[] item(in unsigned long index); readonly attribute unsigned long length; readonly attribute unsigned long position; void clearUndo(); void clearRedo(); };
undoManager
Returns the UndoManager
object.
undoManager
Returns the UndoManager
object.
transact(transaction,
merge)
Clears entries above the current undo position, applies transaction, and adds it to the beginning of the first entry in undo transaction history, or of a new undo transaction history if merge is set to false.
undo()
Unapplies all DOM transactions in the entry immediately after
the current position in the reverse order and increments
position
by 1
if position
<
length
.
redo()
Reapplies all DOM transactions in the entry immediately before
the current position and decrements position
by 1
if position
> 0
position
Returns the number of the current entry in the undo transaction history. (Entries at and past this point are redo entries.)
length
Returns the number of entries in the undo transaction history.
item
(index)Returns the entry with index index in the undo transaction history.
Returns null if index is out of range.
clearUndo()
Removes entries in the undo transaction history before
position
and
resets position
to 0.
clearRedo()
Removes entries in the undo transaction history after
position
.
UndoManager
objects represent and manage their node's
undo transaction history.
The object's supported property indices are the numbers in the range zero to length-1, unless the length is zero, in which case there are no supported property indices.
The transact(transaction, merge)
will
UndoManager
is already in the process of applying,
unapplying, or reapplying a DOM transaction,
or the UndoManager
had been disconnected, then throw
INVALID_ACCESS_ERR
and stop.UndoManager
if the UndoManager
had not already been disconnected.The undo()
will
UndoManager
is already in the process of applying,
unapplying, or reapplying a DOM transaction,
or the UndoManager
had been disconnected, then throw
INVALID_ACCESS_ERR
and stop.position
≤ length
, stop.UndoManager
if the UndoManager
had not already been disconnected.The redo()
will
UndoManager
is already in the process of applying,
unapplying, or reapplying a DOM transaction,
or the UndoManager
had been disconnected, then throw
INVALID_ACCESS_ERR
and stop.position
≤ 0, stop.UndoManager
if the UndoManager
had not already been disconnected.The item(n)
method must return a new array representing the nth entry in the undo transaction history if
0 ≤ n < length
, or null otherwise.
Being able to access an arbitrary element in the undo transaction history is needed to allow scripts to determine whether new DOM transaction and the last DOM transaction should being to the same entry or not.
The position
attribute must return the index of the undo position in the undo transaction history.
If there are no DOM transactions to undo, then the value must be same as
length
attribute.
If there are no DOM transactions to redo, then the value must be zero.
The length
attribute must return the number of entries in the undo transaction history.
This is the length.
The clearUndo()
method must throw INVALID_ACCESS_ERR
if any UndoManager
is already in the process of applying,
unapplying, or reapplying a DOM transaction,
or the UndoManager
had been disconnected,
otherwise it must remove all entries in the undo transaction history before the undo position,
and move the undo position to the top (set position to zero).
The clearRedo()
method must throw INVALID_ACCESS_ERR
if any UndoManager
is already in the process of applying,
unapplying, or reapplying a DOM transaction,
or the UndoManager
had been disconnected,
otherwise it must remove all entries in the undo transaction history after the undo position.
The active undo manager is the UndoManager
of the focused node in the document.
If no node has focus, then it's assumed to be of the document.
To disconnect an UndoManager means to deprive the ability to add or remove entries in
the undo transaction history of the UndoManager
.
Once the UndoManager
is disconnected, transact()
,
undo()
, redo()
, clearUndo()
,
and clearRedo()
will all throw
INVALID_ACCESS_ERR
.
Each entry in the UndoManager consists of one or more DOM transactions, all of which are unapplied and reapplied togehter in one undo or redo.
Because item()
returns new array on each call,
modifying the array does not have any effect on the sequence of DOM transactions of the entry,
and two return values of item()
are alwys different objects.
document.undoManager.transact(...); document.undoManager.transact(..., true); document.undoManager.transact(..., true); alert(document.undoManager.item(0).length); // Alerts 3 document.undoManager.item(0).pop(); alert(document.undoManager.item(0).length); // Still alerts 3 alert(document.undoManager.item(0) === document.undoManager.item(0)); // Alerts false
A typical use case for having multiple DOM transactions in one entry is for typing multiple letters, spaces, and new lines that must be undone or redone in one step.
In the following example, letters "o" and "k" are inserted by two automatic DOM transactions that form one entry in the undo transaction history of the UndoManager. A br element and string "hi" are then inserted by another two automatic DOM transactions to form entry in the undo transaction history. All transactions have the label "Typing".
// 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. myEditor.undoManager.transact({executeAutomatic: function () { insert(document.createTextNode('o')); }, label: 'Typing'}); myEditor.undoManager.transact({executeAutomatic: function () { insert(document.createTextNode('k')); }, label: 'Typing'}, true); myEditor.undoManager.transact({executeAutomatic: function () { insert(document.createElement('br')); }, label: 'Typing'}); myEditor.undoManager.transact({executeAutomatic: function () { insert(document.createTextNode('hi')); }, label: 'Typing'}), true);
When the first undo is executed immediately after this code is ran, the last two transactions are unapplied, 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".
Because Mac OS X and other frameworks expect applications to provide an array of undo items, simply dispatching undo and redo events and having scripts manage undo transaction history would not let the user agent populate the native UI properly.
partial interface Element { attribute UndoManager undoManager; };
undoManager
Returns the UndoManager
object associated with the element's undo scope
if the element is an undo scope host, or null
otherwise.
partial interface Document { attribute UndoManager undoManager; };
undoManager
Returns the UndoManager
object associated with the document.
The undoManager
IDL attribute of
Document
and
Element
interfaces must return the object implementing
the UndoManager
interface for the undo scope if the node is an undo scope host.
If the node is not an undo scope host, it must return null
.
When the user invokes an undo operation, or when the
execCommand()
method is called with
the undo command, the user agent must perform an undo operation on the active undo manager by calling
the undo()
method.
When the user invokes a redo operation, or when the
execCommand()
method is called with
the redo command, the user agent must perform an redo operation on the active undo manager by calling
the redo()
method.
A DOM transaction is an ordered set of DOM changes associated with a unique undo scope host that can be applied, unapplied, or reapplied.
To apply a DOM transaction means to make the associated DOM changes under the associated undo scope host. And to unapply and to reapply a DOM transaction means, respectively, to revert and to remake the associated DOM changes under the associated undo scope host.
A DOM transaction can be unapplied or reapplied if it appears, respectively, immediately after or immediately before
the undo position in the associated UndoManager
's undo transaction history.
DOM changes of a node is a sequence s1, s2, ... sn where each si with 1 ≤ i ≤ n is either one of:
textarea
element's
value
IDL attribute
or input
elment's
value
IDL attribute.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. If two DOM states of a node are equal, then the node and all its descendent nodes must be identical.
To revert DOM changes of the sequence s1, s2, ... sn, revert each si with 1 ≤ i ≤ n in the reverse order sn, sn-1, ... s1 as specified below:
To revert inserting a node into a parent before a child, run these steps:
To revert removing a node from a parent, let child be the next sibling of node before the removal, and run these steps:
To revert replacing data of a node with an offset, count, and data, let replacedData be the substringed data with node, offset, and count before the replacement, and run these steps:
length
attribute is less than offset, terminate these steps.To revert changing an attribute whose namespace is namespace and local name is localName to value, let oldValue be the content attribute value before the change, oldPrefix be the namespace prefix before the change, and change the attribute to oldValue and set the namespace prefix to oldPrefix.
To revert appending an attribute whose namespace is namespace and local name is localName to a node and setting the namespace prefix to prefix, run these steps.
To revert removing an attribute whose namespace is namespace and local name is localName from a node, let oldValue be the content attribute value and oldPrefix be the namespace prefix both before the removal, and run these steps.
To revert setting textarea
element's value
IDL attribute
and input
element's
value
IDL attribute
to value, set element's value
IDL attribute to
the raw value of the textarea element
and the value of the input element
before the setting respectively.
To reapply DOM changes of the sequence s1, s2, ... sn, reapply each si with 1 ≤ i ≤ n in the same order s1, s2, ... sn as specified below:
To reapply inserting a node into a parent before a child, run these steps:
To reapply removing a node from a parent, let child be the next sibling of node before the removal, and run these steps:
To reapply replacing data of a node with an offset, count, and data, and run these steps:
length
attribute is less than offset, terminate these steps.To reapply changing an attribute whose namespace is namespace and local name is localName to value, let prefix be the namespace prefix after the change, and change the attribute to value and set the namespace prefix to prefix.
To reapply appending an attribute whose namespace is namespace and local name is localName with value as the content attribute value to a node and setting the namespace prefix to prefix, run these steps:
To reapply removing an attribute whose namespace is namespace and local name is localName from a node, run these steps.
To reapply setting textarea
element's value
IDL attribute
or input
elment's
value
IDL attribute
to value, set element's value
IDL attribute to value.
DOMTransaction
interface[NoInterfaceObject] interface DOMTransaction { attribute DOMString label; attribute Function? executeAutomatic; attribute Function? execute; attribute Function? undo; attribute Function? redo; };
The DOMTransaction
interface is to be implemented by content scripts that implement a DOM transaction.
label
attribute must return null
or a string that
describes the semantics of the transaction such as "Inserting text" or "Deleting selection".
The user agent may expose this string or a part of this string through its native UI such as menu bar or context menu.
When there are multiple transactions in a single entry of
the undo transaction history, the user agent that doesn't support displaying
multiple labels for each entry must use the label of the first transaction in the sequence of the entry.
executeAutomatic()
, execute()
,
undo()
, and redo()
are attributes that must be supported,
as IDL attributes, by objects implementing the DOMTransaction
interface.
Any changes made to the value of executeAutomatic
,
execute
, undo
,
or redo
attributes will take effect immediately. In the following example,
execute
and undo
attributes are modified:
document.undoManager.transact({ execute: function () { this.execute = function () { alert('foo'); } alert('bar'); }, undo: function () { alert('baz'); } }); // alerts 'bar' document.undoManager.item(0)[0].undo = function() { alert('foobar'); } docuemnt.undoManager.undo(); // alerts 'foobar'
executeAutomatic
attribute must return a valid function if the transaction is a automatic DOM transaction,
and undefined
if it is a manual DOM transaction immediately before the transaction is applied.
Any changes made to the value of the executeAutomatic
attribute while the transaction is being applied or after the transaction had been applied
should not change the type of the DOM transaction.
All DOM changes made in execute
or
executeAutomatic
take effect immediately.
Sometimes, this disconnects the undoManager to which it belongs.
var scope = document.createElement('div'); scope.undoScope = true; document.body.appendChild(scope); scope.undoManager.transact({executeAutomatic: function () { scope.appendChild("foo"); alert(scope.textContent); // "foo" scope.undoScope = false; }}); scope.undoManager.undo(); // Throws an error because undoManager returns null.
An automatic DOM transaction is a DOM transaction where DOM changes are tracked by the user agent and the logic to unapply or reapply the transaction is implicitly created by the user agent.
When an automatic DOM transaction is applied,
the user agent must call the function returned by the executeAutomatic
attribute if the attribute returns a valid function object.
All DOM changes made by the method in the corresponding undo scope of the UndoManager
must be tracked by the user agent.
All DOM changes made outside of the undo scope take effect immediately
but are ignored for the purpose of undo and redo.
In the following example, undo()
will only remove "bar" and "foo"
remains in the body.
var scope = document.createElement('div'); scope.undoScope = true; document.body.appendChild(scope); scope.undoManager.transact({executeAutomatic: function () { document.body.appendChild("foo"); scope.appendChild("bar"); }}); scope.undoManager.undo(); alert(document.body.textContent); // Alerts "foo".
When an automatic DOM transaction is unapplied,
the user agent must revert DOM changes made inside the undo scope of the the UndoManager
while applying the transaction, and call the function returned by the undo
attribute if the attribute returns a valid function object.
When an automatic DOM transaction is reapplied,
the user agent must reapply DOM changes made inside the undo scope of the the UndoManager
while applying the transaction.
The user agent must then call the function returned by the redo
attribute if the attribute returns a valid function object.
The user agent must also restore selection after unapplying or reapplying an automatic DOM transaction in accordance to user agent's platform convention.
The user agent must implement user editing actions and drag and drop as automatic DOM transactions, and any application defined automatic DOM transactions must be compatible with user editing actions.
In an automatic DOM transaction, execute
attribute is ignored.
Authors should not modify nodes that are used by automatic DOM transactions in reverting or reapplying DOM changes as it will interfere with the user agent's attempt to unapply or reapply automatic DOM transactions.
In the following example, the user agent terminates steps early while reverting
the insertion of
the text node " world"
in the first call to undo()
and doesn't make any DOM changes.
var b = document.createTextNode("b"); b.appendChild(document.createTextNode("hello")); document.body.appendChild(b); document.undoManager.transact({ executeAutomatic: function () { document.body.appendChild(document.createTextNode(" world")); }}); b.appendChild(document.body.lastChild); document.undoManager.undo(); // No-op.
On the other hand, if we store the DOM state as done below, then the call to
undo()
will successfully remove the the text node from the body.
document.undoManager.redo(); // No-op. document.body.appendChild(b.lastChild); document.undoManager.undo(); // " world" is removed from document.body
A manual DOM transaction is a DOM transaction where the logic to apply, unapply, or reapply the transaction is explicitly defined by an application. It provides a way to communicate with user agent's undo transaction history, e.g. to populate user agent's undo menu.
When a manual DOM transaction is applied, the user agent must call
the function returned by the execute
if the attribute returns a valid function object.
When a manual DOM transaction is unapplied, the user agent must call
the function returned by the undo
attribute if the attribute returns a valid function object.
When a manual DOM transaction is reapplied, the user agent must call
the function returned by the redo
attribute if the attribute returns a valid function object.
In a manual DOM transaction, executeAutomatic
attribute is ignored.
Manual DOM transactions may be incompatible with automatic DOM transactions, in particular, with user editing actions if manual DOM transaction mutates nodes that are dependent on by automatic DOM transactions.
Manual DOM transactions will let authors populate items in the undo transaction history. In particular, this will let the user agent to fill native UIs such as menu bars to display undoable actions.
function drawLine(start, end, style) { document.undoManager.transact({ execute: function () { // Draw a line on canvas }, undo: function () { // Undraw a line }, redo: function () { this.execute(); }, 'Draw a line' }); }
In this example, drawLine() will add a new entry to the document's undo transaction history and the user agent can communicate the existence of this undoable action via UIs such as context menu and menubars.
When a new DOM transaction is applied by transact()
method to an undo transaction history
of a UndoManager
, the user agent must fire a DOM transaction event using
the TransactionEvent
interface.
When a DOM transaction is unapplied or reapplied though undo()
method or
redo()
method, of a UndoManager
, the user agent must fire
an undo event and a redo event respectively.
DOMTransactionEvent
interface[Constructor(DOMString type, optional EventInit eventInitDict)] interface DOMTransactionEvent : Event { readonly attribute Object transaction; };
transaction
Returns the transaction object that triggered this event.
The transaction
attribute of
the DOMTransactionEvent
interface must return the object that implements
the DOMTransactionEvent
interface that triggered the event.
When the user agent is required to fire a DOM transaction event for a DOM transaction t at an undo scope host h, the user agent must run the following steps:
DOMTransactionEvent
object and initialize it to have the name "DOMTransaction
",
to bubble, to not cancelable, and to have the transaction
attribute initialized to t.DOMTransactionEvent
object at the node h.When the user agent is required to fire an undo event and fire a redo event for a DOM transaction t at an undo scope host h, the user agent must run the following steps:
DOMTransactionEvent
object and initialize it to have the name "undo
" and
"redo
" respectively, to bubble, to not cancelable, and to have the transaction
attribute initialized to t.TransactionEvent
object at the node h.The target node is always set to a undo scope host or a node that was a undo scope host immediately before t was applied, unapplied, or reapplied.