230 lines
5.7 KiB
JavaScript
230 lines
5.7 KiB
JavaScript
|
/**
|
||
|
* 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
|
||
|
* 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();
|
||
|
}
|
||
|
}
|
||
|
};
|