undo.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. /**
  7. * A simple undo stack manager.
  8. *
  9. * Actions are added along with the necessary code to
  10. * reverse the action.
  11. *
  12. * @param integer maxUndo Maximum number of undo steps.
  13. * defaults to 50.
  14. */
  15. function UndoStack(maxUndo) {
  16. this.maxUndo = maxUndo || 50;
  17. this._stack = [];
  18. }
  19. exports.UndoStack = UndoStack;
  20. UndoStack.prototype = {
  21. // Current index into the undo stack. Is positioned after the last
  22. // currently-applied change.
  23. _index: 0,
  24. // The current batch depth (see startBatch() for details)
  25. _batchDepth: 0,
  26. destroy: function () {
  27. this.uninstallController();
  28. delete this._stack;
  29. },
  30. /**
  31. * Start a collection of related changes. Changes will be batched
  32. * together into one undo/redo item until endBatch() is called.
  33. *
  34. * Batches can be nested, in which case the outer batch will contain
  35. * all items from the inner batches. This allows larger user
  36. * actions made up of a collection of smaller actions to be
  37. * undone as a single action.
  38. */
  39. startBatch: function () {
  40. if (this._batchDepth++ === 0) {
  41. this._batch = [];
  42. }
  43. },
  44. /**
  45. * End a batch of related changes, performing its action and adding
  46. * it to the undo stack.
  47. */
  48. endBatch: function () {
  49. if (--this._batchDepth > 0) {
  50. return;
  51. }
  52. // Cut off the end of the undo stack at the current index,
  53. // and the beginning to prevent a stack larger than maxUndo.
  54. let start = Math.max((this._index + 1) - this.maxUndo, 0);
  55. this._stack = this._stack.slice(start, this._index);
  56. let batch = this._batch;
  57. delete this._batch;
  58. let entry = {
  59. do: function () {
  60. for (let item of batch) {
  61. item.do();
  62. }
  63. },
  64. undo: function () {
  65. for (let i = batch.length - 1; i >= 0; i--) {
  66. batch[i].undo();
  67. }
  68. }
  69. };
  70. this._stack.push(entry);
  71. this._index = this._stack.length;
  72. entry.do();
  73. this._change();
  74. },
  75. /**
  76. * Perform an action, adding it to the undo stack.
  77. *
  78. * @param function toDo Called to perform the action.
  79. * @param function undo Called to reverse the action.
  80. */
  81. do: function (toDo, undo) {
  82. this.startBatch();
  83. this._batch.push({ do: toDo, undo });
  84. this.endBatch();
  85. },
  86. /*
  87. * Returns true if undo() will do anything.
  88. */
  89. canUndo: function () {
  90. return this._index > 0;
  91. },
  92. /**
  93. * Undo the top of the undo stack.
  94. *
  95. * @return true if an action was undone.
  96. */
  97. undo: function () {
  98. if (!this.canUndo()) {
  99. return false;
  100. }
  101. this._stack[--this._index].undo();
  102. this._change();
  103. return true;
  104. },
  105. /**
  106. * Returns true if redo() will do anything.
  107. */
  108. canRedo: function () {
  109. return this._stack.length > this._index;
  110. },
  111. /**
  112. * Redo the most recently undone action.
  113. *
  114. * @return true if an action was redone.
  115. */
  116. redo: function () {
  117. if (!this.canRedo()) {
  118. return false;
  119. }
  120. this._stack[this._index++].do();
  121. this._change();
  122. return true;
  123. },
  124. _change: function () {
  125. if (this._controllerWindow) {
  126. this._controllerWindow.goUpdateCommand("cmd_undo");
  127. this._controllerWindow.goUpdateCommand("cmd_redo");
  128. }
  129. },
  130. /**
  131. * ViewController implementation for undo/redo.
  132. */
  133. /**
  134. * Install this object as a command controller.
  135. */
  136. installController: function (controllerWindow) {
  137. this._controllerWindow = controllerWindow;
  138. controllerWindow.controllers.appendController(this);
  139. },
  140. /**
  141. * Uninstall this object from the command controller.
  142. */
  143. uninstallController: function () {
  144. if (!this._controllerWindow) {
  145. return;
  146. }
  147. this._controllerWindow.controllers.removeController(this);
  148. },
  149. supportsCommand: function (command) {
  150. return (command == "cmd_undo" ||
  151. command == "cmd_redo");
  152. },
  153. isCommandEnabled: function (command) {
  154. switch (command) {
  155. case "cmd_undo": return this.canUndo();
  156. case "cmd_redo": return this.canRedo();
  157. }
  158. return false;
  159. },
  160. doCommand: function (command) {
  161. switch (command) {
  162. case "cmd_undo": return this.undo();
  163. case "cmd_redo": return this.redo();
  164. default: return null;
  165. }
  166. },
  167. onEvent: function (event) {},
  168. };