undoredomanager.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Code for managing series of undo-redo actions in the form of
  16. * {@link goog.editor.plugins.UndoRedoState}s.
  17. *
  18. */
  19. goog.provide('goog.editor.plugins.UndoRedoManager');
  20. goog.provide('goog.editor.plugins.UndoRedoManager.EventType');
  21. goog.require('goog.editor.plugins.UndoRedoState');
  22. goog.require('goog.events');
  23. goog.require('goog.events.EventTarget');
  24. /**
  25. * Manages undo and redo operations through a series of {@code UndoRedoState}s
  26. * maintained on undo and redo stacks.
  27. *
  28. * @constructor
  29. * @extends {goog.events.EventTarget}
  30. */
  31. goog.editor.plugins.UndoRedoManager = function() {
  32. goog.events.EventTarget.call(this);
  33. /**
  34. * The maximum number of states on the undo stack at any time. Used to limit
  35. * the memory footprint of the undo-redo stack.
  36. * TODO(user) have a separate memory size based limit.
  37. * @type {number}
  38. * @private
  39. */
  40. this.maxUndoDepth_ = 100;
  41. /**
  42. * The undo stack.
  43. * @type {Array<goog.editor.plugins.UndoRedoState>}
  44. * @private
  45. */
  46. this.undoStack_ = [];
  47. /**
  48. * The redo stack.
  49. * @type {Array<goog.editor.plugins.UndoRedoState>}
  50. * @private
  51. */
  52. this.redoStack_ = [];
  53. /**
  54. * A queue of pending undo or redo actions. Stored as objects with two
  55. * properties: func and state. The func property stores the undo or redo
  56. * function to be called, the state property stores the state that method
  57. * came from.
  58. * @type {Array<Object>}
  59. * @private
  60. */
  61. this.pendingActions_ = [];
  62. };
  63. goog.inherits(goog.editor.plugins.UndoRedoManager, goog.events.EventTarget);
  64. /**
  65. * Event types for the events dispatched by undo-redo manager.
  66. * @enum {string}
  67. */
  68. goog.editor.plugins.UndoRedoManager.EventType = {
  69. /**
  70. * Signifies that he undo or redo stack transitioned between 0 and 1 states,
  71. * meaning that the ability to peform undo or redo operations has changed.
  72. */
  73. STATE_CHANGE: 'state_change',
  74. /**
  75. * Signifies that a state was just added to the undo stack. Events of this
  76. * type will have a {@code state} property whose value is the state that
  77. * was just added.
  78. */
  79. STATE_ADDED: 'state_added',
  80. /**
  81. * Signifies that the undo method of a state is about to be called.
  82. * Events of this type will have a {@code state} property whose value is the
  83. * state whose undo action is about to be performed. If the event is cancelled
  84. * the action does not proceed, but the state will still transition between
  85. * stacks.
  86. */
  87. BEFORE_UNDO: 'before_undo',
  88. /**
  89. * Signifies that the redo method of a state is about to be called.
  90. * Events of this type will have a {@code state} property whose value is the
  91. * state whose redo action is about to be performed. If the event is cancelled
  92. * the action does not proceed, but the state will still transition between
  93. * stacks.
  94. */
  95. BEFORE_REDO: 'before_redo'
  96. };
  97. /**
  98. * The key for the listener for the completion of the asynchronous state whose
  99. * undo or redo action is in progress. Null if no action is in progress.
  100. * @type {goog.events.Key}
  101. * @private
  102. */
  103. goog.editor.plugins.UndoRedoManager.prototype.inProgressActionKey_ = null;
  104. /**
  105. * Set the max undo stack depth (not the real memory usage).
  106. * @param {number} depth Depth of the stack.
  107. */
  108. goog.editor.plugins.UndoRedoManager.prototype.setMaxUndoDepth = function(
  109. depth) {
  110. this.maxUndoDepth_ = depth;
  111. };
  112. /**
  113. * Add state to the undo stack. This clears the redo stack.
  114. *
  115. * @param {goog.editor.plugins.UndoRedoState} state The state to add to the undo
  116. * stack.
  117. */
  118. goog.editor.plugins.UndoRedoManager.prototype.addState = function(state) {
  119. // TODO: is the state.equals check necessary?
  120. if (this.undoStack_.length == 0 ||
  121. !state.equals(this.undoStack_[this.undoStack_.length - 1])) {
  122. this.undoStack_.push(state);
  123. if (this.undoStack_.length > this.maxUndoDepth_) {
  124. this.undoStack_.shift();
  125. }
  126. // Clobber the redo stack.
  127. var redoLength = this.redoStack_.length;
  128. this.redoStack_.length = 0;
  129. this.dispatchEvent({
  130. type: goog.editor.plugins.UndoRedoManager.EventType.STATE_ADDED,
  131. state: state
  132. });
  133. // If the redo state had states on it, then clobbering the redo stack above
  134. // has caused a state change.
  135. if (this.undoStack_.length == 1 || redoLength) {
  136. this.dispatchStateChange_();
  137. }
  138. }
  139. };
  140. /**
  141. * Dispatches a STATE_CHANGE event with this manager as the target.
  142. * @private
  143. */
  144. goog.editor.plugins.UndoRedoManager.prototype.dispatchStateChange_ =
  145. function() {
  146. this.dispatchEvent(
  147. goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE);
  148. };
  149. /**
  150. * Performs the undo operation of the state at the top of the undo stack, moving
  151. * that state to the top of the redo stack. If the undo stack is empty, does
  152. * nothing.
  153. */
  154. goog.editor.plugins.UndoRedoManager.prototype.undo = function() {
  155. this.shiftState_(this.undoStack_, this.redoStack_);
  156. };
  157. /**
  158. * Performs the redo operation of the state at the top of the redo stack, moving
  159. * that state to the top of the undo stack. If redo undo stack is empty, does
  160. * nothing.
  161. */
  162. goog.editor.plugins.UndoRedoManager.prototype.redo = function() {
  163. this.shiftState_(this.redoStack_, this.undoStack_);
  164. };
  165. /**
  166. * @return {boolean} Wether the undo stack has items on it, i.e., if it is
  167. * possible to perform an undo operation.
  168. */
  169. goog.editor.plugins.UndoRedoManager.prototype.hasUndoState = function() {
  170. return this.undoStack_.length > 0;
  171. };
  172. /**
  173. * @return {boolean} Wether the redo stack has items on it, i.e., if it is
  174. * possible to perform a redo operation.
  175. */
  176. goog.editor.plugins.UndoRedoManager.prototype.hasRedoState = function() {
  177. return this.redoStack_.length > 0;
  178. };
  179. /**
  180. * Move a state from one stack to the other, performing the appropriate undo
  181. * or redo action.
  182. *
  183. * @param {Array<goog.editor.plugins.UndoRedoState>} fromStack Stack to move
  184. * the state from.
  185. * @param {Array<goog.editor.plugins.UndoRedoState>} toStack Stack to move
  186. * the state to.
  187. * @private
  188. */
  189. goog.editor.plugins.UndoRedoManager.prototype.shiftState_ = function(
  190. fromStack, toStack) {
  191. if (fromStack.length) {
  192. var state = fromStack.pop();
  193. // Push the current state into the redo stack.
  194. toStack.push(state);
  195. this.addAction_({
  196. type: fromStack == this.undoStack_ ?
  197. goog.editor.plugins.UndoRedoManager.EventType.BEFORE_UNDO :
  198. goog.editor.plugins.UndoRedoManager.EventType.BEFORE_REDO,
  199. func: fromStack == this.undoStack_ ? state.undo : state.redo,
  200. state: state
  201. });
  202. // If either stack transitioned between 0 and 1 in size then the ability
  203. // to do an undo or redo has changed and we must dispatch a state change.
  204. if (fromStack.length == 0 || toStack.length == 1) {
  205. this.dispatchStateChange_();
  206. }
  207. }
  208. };
  209. /**
  210. * Adds an action to the queue of pending undo or redo actions. If no actions
  211. * are pending, immediately performs the action.
  212. *
  213. * @param {Object} action An undo or redo action. Stored as an object with two
  214. * properties: func and state. The func property stores the undo or redo
  215. * function to be called, the state property stores the state that method
  216. * came from.
  217. * @private
  218. */
  219. goog.editor.plugins.UndoRedoManager.prototype.addAction_ = function(action) {
  220. this.pendingActions_.push(action);
  221. if (this.pendingActions_.length == 1) {
  222. this.doAction_();
  223. }
  224. };
  225. /**
  226. * Executes the action at the front of the pending actions queue. If an action
  227. * is already in progress or the queue is empty, does nothing.
  228. * @private
  229. */
  230. goog.editor.plugins.UndoRedoManager.prototype.doAction_ = function() {
  231. if (this.inProgressActionKey_ || this.pendingActions_.length == 0) {
  232. return;
  233. }
  234. var action = this.pendingActions_.shift();
  235. var e = {type: action.type, state: action.state};
  236. if (this.dispatchEvent(e)) {
  237. if (action.state.isAsynchronous()) {
  238. this.inProgressActionKey_ = goog.events.listen(
  239. action.state, goog.editor.plugins.UndoRedoState.ACTION_COMPLETED,
  240. this.finishAction_, false, this);
  241. action.func.call(action.state);
  242. } else {
  243. action.func.call(action.state);
  244. this.doAction_();
  245. }
  246. }
  247. };
  248. /**
  249. * Finishes processing the current in progress action, starting the next queued
  250. * action if one exists.
  251. * @private
  252. */
  253. goog.editor.plugins.UndoRedoManager.prototype.finishAction_ = function() {
  254. goog.events.unlistenByKey(/** @type {number} */ (this.inProgressActionKey_));
  255. this.inProgressActionKey_ = null;
  256. this.doAction_();
  257. };
  258. /**
  259. * Clears the undo and redo stacks.
  260. */
  261. goog.editor.plugins.UndoRedoManager.prototype.clearHistory = function() {
  262. if (this.undoStack_.length > 0 || this.redoStack_.length > 0) {
  263. this.undoStack_.length = 0;
  264. this.redoStack_.length = 0;
  265. this.dispatchStateChange_();
  266. }
  267. };
  268. /**
  269. * @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of
  270. * the undo stack without removing it from the stack.
  271. */
  272. goog.editor.plugins.UndoRedoManager.prototype.undoPeek = function() {
  273. return this.undoStack_[this.undoStack_.length - 1];
  274. };
  275. /**
  276. * @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of
  277. * the redo stack without removing it from the stack.
  278. */
  279. goog.editor.plugins.UndoRedoManager.prototype.redoPeek = function() {
  280. return this.redoStack_[this.redoStack_.length - 1];
  281. };