1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2010, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2010, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 */ 27 28 /** 29 * @class 30 * Creates a stack of undoable actions (ZmAction objects) 31 * 32 * @param {int} [maxLength] The maximum size of the stack. Defaults to 0, meaning no limit 33 * 34 * Adding actions to a full stack will pop the oldest actions off 35 */ 36 ZmActionStack = function(maxLength) { 37 this._stack = []; 38 this._pointer = -1; 39 this._maxLength = maxLength || 0; // 0 means no limit 40 }; 41 42 ZmEvent.S_ACTION = "ACTION"; 43 44 ZmActionStack.validTypes = [ZmId.ORG_FOLDER, ZmId.ITEM_MSG, ZmId.ITEM_CONV, 45 ZmId.ITEM_CONTACT, ZmId.ITEM_GROUP, 46 ZmId.ITEM_BRIEFCASE, ZmId.ORG_BRIEFCASE, 47 ZmId.ITEM_TASK, ZmId.ORG_TASKS 48 ]; // Set ZmActionStack.validTypes to false to allow all item types 49 50 ZmActionStack.prototype.toString = function() { 51 return "ZmActionStack"; 52 }; 53 54 /** 55 * Logs a raw action, interpreting the params and creates a ZmAction object that is pushed onto the stack and returned 56 * 57 * @param {String} op operation to perform. Currently supported are "move", "trash", "spam" and "!spam" 58 * @param {Hash} [attrs] attributes for the operation. Pretty much the same as what the backend expects, e.g. "l" for the destination folderId of a move 59 60 * @param {String} [items] array of items to perform the action for. Valid types are specified in ZmActionStack.validTypes. Only one of [items],[item],[ids] or [id] should be specified; the first one found is used, ignoring the rest. 61 * @param {String} [item] item to perform the action for, if there is only one item. Accomplishes the same as putting the item in an array and giving it as [items] 62 * @param {String} [ids] array of ids of items to perform the action for. 63 * @param {String} [id] id of item to perform the action for, if there is only one. Accomplishes the same as putting the id in an array and giving it as [ids]. 64 */ 65 ZmActionStack.prototype.logAction = function(params) { 66 67 var op = params.op, 68 items = []; 69 70 if (params.items) { 71 for (var i = 0; i < params.items.length; i++) { 72 var item = params.items[i]; 73 if (item && this._isValidType(item.type)) { 74 items.push(item); 75 } 76 } 77 } 78 else if (params.item) { 79 if (params.item && this._isValidType(params.item.type)) { 80 items.push(params.item); 81 } 82 } 83 else if (params.ids) { 84 for (var i = 0; i < params.ids.length; i++) { 85 var item = appCtxt.getById(params.ids[i]); 86 if (item && this._isValidType(item.type)) { 87 items.push(item); 88 } 89 } 90 } 91 else if (params.id) { 92 var item = appCtxt.getById(params.id); 93 if (item && this._isValidType(item.type)) { 94 items.push(item); 95 } 96 } 97 98 var attrs = params.attrs; 99 100 // for a conv, create a list of undoable msg moves so msgs can be restored to their disparate original folders 101 for (var i = 0; i < items.length; i++) { 102 var item = items[i]; 103 if (item.type === ZmItem.CONV) { 104 var tcon = attrs && attrs.tcon; 105 for (var msgId in item.msgFolder) { 106 var folderId = item.msgFolder[msgId], 107 tconCode = ZmFolder.TCON_CODE[folderId]; 108 109 // if tcon kept us from moving a msg, no need to undo it 110 if (!tcon || tcon.indexOf(tconCode) === -1) { 111 items.push({ 112 isConvMsg: true, 113 id: msgId, 114 type: ZmItem.MSG, 115 folderId: folderId, 116 list: { type: ZmItem.MSG } // hack to expose item.list.type 117 }); 118 } 119 } 120 } 121 } 122 123 var multi = items.length > 1; 124 125 var action = null; 126 var folderId; 127 switch (op) { 128 case "trash": 129 folderId = ZmFolder.ID_TRASH; 130 break; 131 case "spam": 132 folderId = ZmFolder.ID_SPAM; 133 break; 134 case "move": 135 case "!spam": 136 folderId = attrs.l; 137 break; 138 } 139 140 var folder = appCtxt.getById(folderId); 141 if (folder && !folder.isRemote()) { // Enable undo only when destination folder exists (it should!!) and is not remote (bug #51656) 142 switch (op) { 143 case "trash": 144 case "move": 145 case "spam": 146 case "!spam": 147 for (var i = 0; i < items.length; i++) { 148 var item = items[i]; 149 var moveAction; 150 151 if (item instanceof ZmItem) { 152 if (!item.isShared()) { // Moving shared items is not undoable 153 moveAction = new ZmItemMoveAction(item, item.getFolderId(), folderId, op); 154 } 155 } 156 else if (item instanceof ZmOrganizer) { 157 if (!item.isRemote()) { // Moving remote organizers is not undoable 158 moveAction = new ZmOrganizerMoveAction(item, item.parent.id, folderId, op); 159 } 160 } 161 else if (item.isConvMsg) { 162 if (!appCtxt.isRemoteId(item.id)) { 163 moveAction = new ZmItemMoveAction(item, item.folderId, folderId, op); 164 } 165 } 166 if (moveAction) { 167 if (multi) { 168 if (!action) action = new ZmCompositeAction(folderId); 169 action.addAction(moveAction); 170 } else { 171 action = moveAction; 172 } 173 } 174 } 175 break; 176 } 177 if (action) { 178 this._push(action); 179 } 180 } 181 182 return action; 183 }; 184 185 /** 186 * Returns whether there are actions that can be undone 187 */ 188 ZmActionStack.prototype.canUndo = function() { 189 return this._pointer >= 0; 190 }; 191 192 /** 193 * Returns whether there are actions that can be redone 194 */ 195 ZmActionStack.prototype.canRedo = function() { 196 return this._pointer < this._stack.length - 1; 197 }; 198 199 200 /** 201 * Returns whether the next undo action has completed 202 */ 203 ZmActionStack.prototype.actionIsComplete = function() { 204 return this.canUndo() && this._current().getComplete(); 205 }; 206 207 /** 208 * Attaches a completion callback to the current action 209 */ 210 ZmActionStack.prototype.onComplete = function(callback) { 211 var action = this._current(); 212 if (action) { 213 action.onComplete(callback); 214 } 215 }; 216 217 /** 218 * Undoes the current action (if applicable) and moves the internal pointer 219 */ 220 ZmActionStack.prototype.undo = function() { 221 if (this.canUndo()) { 222 var action = this._pop(); 223 action.undo(); 224 } 225 }; 226 227 /** 228 * Redoes the current action (if applicable) and moves the internal pointer 229 */ 230 ZmActionStack.prototype.redo = function() { 231 if (this.canRedo()) { 232 var action = this._stack[++this._pointer]; 233 action.redo(); 234 } 235 }; 236 237 /** 238 * Puts an action into the stack at the current position 239 * If we're not at the top of the stack (ie. undoes have been performed), we kill all later actions (so redoing the undone actions is no longer possible) 240 */ 241 ZmActionStack.prototype._push = function(action) { 242 if (action && action instanceof ZmAction) { 243 var next = this._pointer + 1; 244 while (this._maxLength && next>=this._maxLength) { 245 // Stack size is reached, shift off actions until we're under the limit 246 this._stack.shift(); 247 next--; 248 } 249 this._stack[next] = action; 250 this._stack.length = next+1; // Kill all actions after pointer 251 this._pointer = next; 252 } 253 }; 254 255 /** 256 * Returns the action at the current position and moves the pointer 257 */ 258 ZmActionStack.prototype._pop = function() { 259 return this.canUndo() ? this._stack[this._pointer--] : null; 260 }; 261 262 /** 263 * Returns the action at the current position, does not move the pointer 264 */ 265 ZmActionStack.prototype._current = function() { 266 return this.canUndo() ? this._stack[this._pointer] : null; 267 }; 268 269 /** 270 * Returns true if the given type is valid. 271 */ 272 ZmActionStack.prototype._isValidType = function(type) { 273 return !ZmActionStack.validTypes || AjxUtil.indexOf(ZmActionStack.validTypes, type) !== -1; 274 }; 275