blob: 749561c8810677376932904a5fe48ec181e63e7b [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxUndoManager
*
* Implements a command history. When changing the graph model, an
* <mxUndoableChange> object is created at the start of the transaction (when
* model.beginUpdate is called). All atomic changes are then added to this
* object until the last model.endUpdate call, at which point the
* <mxUndoableEdit> is dispatched in an event, and added to the history inside
* <mxUndoManager>. This is done by an event listener in
* <mxEditor.installUndoHandler>.
*
* Each atomic change of the model is represented by an object (eg.
* <mxRootChange>, <mxChildChange>, <mxTerminalChange> etc) which contains the
* complete undo information. The <mxUndoManager> also listens to the
* <mxGraphView> and stores it's changes to the current root as insignificant
* undoable changes, so that drilling (step into, step up) is undone.
*
* This means when you execute an atomic change on the model, then change the
* current root on the view and click undo, the change of the root will be
* undone together with the change of the model so that the display represents
* the state at which the model was changed. However, these changes are not
* transmitted for sharing as they do not represent a state change.
*
* Example:
*
* When adding an undo manager to a graph, make sure to add it
* to the model and the view as well to maintain a consistent
* display across multiple undo/redo steps.
*
* (code)
* var undoManager = new mxUndoManager();
* var listener = function(sender, evt)
* {
* undoManager.undoableEditHappened(evt.getProperty('edit'));
* };
* graph.getModel().addListener(mxEvent.UNDO, listener);
* graph.getView().addListener(mxEvent.UNDO, listener);
* (end)
*
* The code creates a function that informs the undoManager
* of an undoable edit and binds it to the undo event of
* <mxGraphModel> and <mxGraphView> using
* <mxEventSource.addListener>.
*
* Event: mxEvent.CLEAR
*
* Fires after <clear> was invoked. This event has no properties.
*
* Event: mxEvent.UNDO
*
* Fires afer a significant edit was undone in <undo>. The <code>edit</code>
* property contains the <mxUndoableEdit> that was undone.
*
* Event: mxEvent.REDO
*
* Fires afer a significant edit was redone in <redo>. The <code>edit</code>
* property contains the <mxUndoableEdit> that was redone.
*
* Event: mxEvent.ADD
*
* Fires after an undoable edit was added to the history. The <code>edit</code>
* property contains the <mxUndoableEdit> that was added.
*
* Constructor: mxUndoManager
*
* Constructs a new undo manager with the given history size. If no history
* size is given, then a default size of 100 steps is used.
*/
function mxUndoManager(size)
{
this.size = (size != null) ? size : 100;
this.clear();
};
/**
* Extends mxEventSource.
*/
mxUndoManager.prototype = new mxEventSource();
mxUndoManager.prototype.constructor = mxUndoManager;
/**
* Variable: size
*
* Maximum command history size. 0 means unlimited history. Default is
* 100.
*/
mxUndoManager.prototype.size = null;
/**
* Variable: history
*
* Array that contains the steps of the command history.
*/
mxUndoManager.prototype.history = null;
/**
* Variable: indexOfNextAdd
*
* Index of the element to be added next.
*/
mxUndoManager.prototype.indexOfNextAdd = 0;
/**
* Function: isEmpty
*
* Returns true if the history is empty.
*/
mxUndoManager.prototype.isEmpty = function()
{
return this.history.length == 0;
};
/**
* Function: clear
*
* Clears the command history.
*/
mxUndoManager.prototype.clear = function()
{
this.history = [];
this.indexOfNextAdd = 0;
this.fireEvent(new mxEventObject(mxEvent.CLEAR));
};
/**
* Function: canUndo
*
* Returns true if an undo is possible.
*/
mxUndoManager.prototype.canUndo = function()
{
return this.indexOfNextAdd > 0;
};
/**
* Function: undo
*
* Undoes the last change.
*/
mxUndoManager.prototype.undo = function()
{
while (this.indexOfNextAdd > 0)
{
var edit = this.history[--this.indexOfNextAdd];
edit.undo();
if (edit.isSignificant())
{
this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
break;
}
}
};
/**
* Function: canRedo
*
* Returns true if a redo is possible.
*/
mxUndoManager.prototype.canRedo = function()
{
return this.indexOfNextAdd < this.history.length;
};
/**
* Function: redo
*
* Redoes the last change.
*/
mxUndoManager.prototype.redo = function()
{
var n = this.history.length;
while (this.indexOfNextAdd < n)
{
var edit = this.history[this.indexOfNextAdd++];
edit.redo();
if (edit.isSignificant())
{
this.fireEvent(new mxEventObject(mxEvent.REDO, 'edit', edit));
break;
}
}
};
/**
* Function: undoableEditHappened
*
* Method to be called to add new undoable edits to the <history>.
*/
mxUndoManager.prototype.undoableEditHappened = function(undoableEdit)
{
this.trim();
if (this.size > 0 &&
this.size == this.history.length)
{
this.history.shift();
}
this.history.push(undoableEdit);
this.indexOfNextAdd = this.history.length;
this.fireEvent(new mxEventObject(mxEvent.ADD, 'edit', undoableEdit));
};
/**
* Function: trim
*
* Removes all pending steps after <indexOfNextAdd> from the history,
* invoking die on each edit. This is called from <undoableEditHappened>.
*/
mxUndoManager.prototype.trim = function()
{
if (this.history.length > this.indexOfNextAdd)
{
var edits = this.history.splice(this.indexOfNextAdd,
this.history.length - this.indexOfNextAdd);
for (var i = 0; i < edits.length; i++)
{
edits[i].die();
}
}
};