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