mxUndoManager.js 5.66 KB
/**
 * 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();
		}
	}
};