summaryrefslogtreecommitdiff
path: root/src/js/util/mxUndoManager.js
blob: 2cb93cb6cf7496e00f1ede40b5aa13d959a26da5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/**
 * $Id: mxUndoManager.js,v 1.30 2011-10-05 06:39:19 gaudenz Exp $
 * Copyright (c) 2006-2010, JGraph Ltd
 */
/**
 * 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();
		}
	}
};