1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the operation class.
 27  * 
 28  */
 29 
 30 /**
 31  * Creates the operation object.
 32  * @class
 33  * This class provides the idea of an "operation", which is a user-initiated action
 34  * exposed through a button or menu item. Many operations (such as Delete) are shared
 35  * across applications/controllers. An operation gets defined by specifying its name,
 36  * tool tip, and image. Then controllers can simply select which operations they'd like
 37  * to support.
 38  * <br/>
 39  * <br/>
 40  * The two primary clients of this class are {@link ZmButtonToolBar} and {@link ZmActionMenu}. Clients 
 41  * should support {@link #createOp} and {@link #getOp} methods. See the two aforementioned clients for
 42  * examples.
 43  * 
 44  * @author Conrad Damon
 45  */
 46 ZmOperation = function() {};
 47 
 48 // Special operations
 49 ZmOperation.NONE 					= "NONE";		// no operations or menu items
 50 ZmOperation.SEP 					= "SEP";		// separator
 51 ZmOperation.SPACER 					= "SPACER";		// spacer (toolbar)
 52 ZmOperation.FILLER 					= "FILLER";		// filler (toolbar)
 53 
 54 // suffix for disabled image
 55 ZmOperation.DIS = "Dis";
 56 
 57 // text and icons displayed for operation
 58 ZmOperation.SETUP = {};
 59 
 60 // special-purpose operations
 61 ZmOperation.SETUP[ZmOperation.NONE]		= {};	// means no operations (as opposed to a default set)
 62 ZmOperation.SETUP[ZmOperation.SEP]		= {};	// a thin vertical or horizontal bar
 63 ZmOperation.SETUP[ZmOperation.SPACER]	= {};	// empty space of a given size
 64 ZmOperation.SETUP[ZmOperation.FILLER]	= {};	// expandable space (for right-align in toolbars)
 65 
 66 // preconditions for operations - no automatic checking is done, so a client
 67 // of this class has to check them on its own if it wants
 68 ZmOperation.PRECONDITION = {};
 69 
 70 // code to run after an operation has been created - typically used to create
 71 // a menu for a button
 72 ZmOperation.CALLBACK = {};
 73 
 74 /**
 75  * Defines the aspects of an operation, and the ID that refers to it.
 76  * 
 77  * @param {String}	op		the name of the operation
 78  * @param {hash}	params:
 79  *      @param {String}	text		the msg key for button or menu item text
 80  *      @param {String}	tooltip	the msg key for tooltip text
 81  *      @param {String}	image		the icon class for the button or menu item
 82  *      @param {String}	disImage	the disabled version of image; defaults to image + "Dis"
 83  *      @param {Boolean|String|Function}    precondition (overrides setting if present)
 84  * @param {constant}	precondition	must evaluate to true for this operation not to be filtered out
 85  * @param {AjxCallback}	callback	the callback to run after this operation has been created
 86  */
 87 ZmOperation.registerOp = function(op, params, precondition, callback) {
 88 
 89 	ZmOperation[op] = op;
 90 	ZmOperation.SETUP[op] = params || {};
 91 	if (precondition)	{ ZmOperation.PRECONDITION[op]	= precondition; }
 92 	if (callback)	    { ZmOperation.CALLBACK[op]	    = callback; }
 93 };
 94 
 95 
 96 ZmOperation.KEY_ID		= "opId";
 97 ZmOperation.MENUITEM_ID	= "menuItemId";
 98 
 99 ZmOperation.NEW_ITEM_OPS	= [];
100 ZmOperation.NEW_ITEM_KEY	= {};
101 ZmOperation.NEW_ORG_OPS		= [];
102 ZmOperation.NEW_ORG_KEY		= {};
103 
104 // Static hash of operation IDs ad descriptors
105 ZmOperation._operationDesc = {};
106 
107 /**
108  * Initializes and creates standard operations.
109  * 
110  */
111 ZmOperation.initialize =
112 function() {
113 	ZmOperation.registerOp(ZmId.OP_ACTIONS_MENU, {textKey:"actions", tooltipKey:"", textPrecedence:40});
114 	
115 	ZmOperation.registerOp(ZmId.OP_ATTACHMENT, {textKey:"addAttachment", tooltipKey:"attachmentTooltip", image:"Attachment", shortcut:ZmKeyMap.ATTACHMENT, showImageInToolbar: true});
116 	ZmOperation.registerOp(ZmId.OP_CALL, {image:"Telephone"});
117 	ZmOperation.registerOp(ZmId.OP_CANCEL, {textKey:"cancel", tooltipKey:"cancelTooltip", image:"Cancel", shortcut:ZmKeyMap.CANCEL});
118 	ZmOperation.registerOp(ZmId.OP_CHECK_ALL, {textKey:"checkAll", image:"Check"});
119 	ZmOperation.registerOp(ZmId.OP_CLEAR_ALL, {textKey:"clearAll", image:"Cancel"});
120 	ZmOperation.registerOp(ZmId.OP_CLOSE, {textKey:"close", tooltipKey:"closeTooltip", image:"Close", shortcut:ZmKeyMap.CANCEL});
121 	ZmOperation.registerOp(ZmId.OP_COMPOSE_FORMAT, {textKey:"format", tooltipKey:"formatTooltip", image:"SwitchFormat", shortcut:ZmKeyMap.HTML_FORMAT}, ZmSetting.HTML_COMPOSE_ENABLED);
122 	ZmOperation.registerOp(ZmId.OP_CONTACTGROUP_MENU, {textKey: "AB_CONTACT_GROUP", tooltipKey:"contactGroupTooltip", image:"Group"}, null,
123 		AjxCallback.simpleClosure(function(parent) {
124 			ZmOperation.addDeferredMenu(ZmOperation.addContactGroupMenu, parent, true);
125 	}));
126 	ZmOperation.registerOp(ZmId.OP_COPY, {textKey:"copy", image:"Copy"});
127 	ZmOperation.registerOp(ZmId.OP_DELETE, {textKey:"del", tooltipKey:"deleteTooltip", image:"Delete", shortcut:ZmKeyMap.DEL, textPrecedence:60});
128 	ZmOperation.registerOp(ZmId.OP_DELETE_WITHOUT_SHORTCUT, {textKey:"del", tooltipKey:"deleteTooltip", image:"Delete", textPrecedence:60});
129 	ZmOperation.registerOp(ZmId.OP_DETACH, {textKey:"detach", tooltipKey:"detach", image:"OpenInNewWindow", showImageInToolbar: true});
130     ZmOperation.registerOp(ZmId.OP_DETACH_WIN, {textKey:"detach", image:"OpenInNewWindow"});
131 	ZmOperation.registerOp(ZmId.OP_EDIT, {textKey:"edit", tooltipKey:"editTooltip", image:"Edit", shortcut:ZmKeyMap.EDIT});
132 	ZmOperation.registerOp(ZmId.OP_EDIT_AS_NEW, {textKey:"editAsNew", tooltipKey:"editTooltip", image:"Edit", shortcut:ZmKeyMap.EDIT});
133 	ZmOperation.registerOp(ZmId.OP_EDIT_PROPS, {textKey:"editProperties", tooltipKey:"editPropertiesTooltip", image:"Properties"});
134 	ZmOperation.registerOp(ZmId.OP_EXPAND, {textKey:"expand", image:"Plus"});
135 	ZmOperation.registerOp(ZmId.OP_EXPAND_ALL, {textKey:"expandAll", image:"Plus"});
136 //	ZmOperation.registerOp(ZmId.OP_EXPORT_FOLDER, {textKey:"exportFolder", image:"MailExport"});
137 	ZmOperation.registerOp(ZmId.OP_EMPTY_FOLDER,{textKey:"emptyFolder",image:"EmptyFolder"});
138 	ZmOperation.registerOp(ZmId.OP_FORMAT_HTML, {textKey: "formatAsHtml"}, ZmSetting.HTML_COMPOSE_ENABLED);
139 	ZmOperation.registerOp(ZmId.OP_FORMAT_TEXT, {textKey: "formatAsText"}, ZmSetting.HTML_COMPOSE_ENABLED);
140 	ZmOperation.registerOp(ZmId.OP_FORMAT_MORE_OPTIONS, {textKey: "moreComposeOptions"});
141     ZmOperation.registerOp(ZmId.OP_GROUPBY, {textKey:"groupBy"});
142     ZmOperation.registerOp(ZmId.OP_GROUPBY_NONE, {textKey:"groupByNone"});
143     ZmOperation.registerOp(ZmId.OP_GROUPBY_DATE, {textKey:"groupByDate"});
144     ZmOperation.registerOp(ZmId.OP_GROUPBY_FROM, {textKey:"groupByFrom"});
145     ZmOperation.registerOp(ZmId.OP_GROUPBY_PRIORITY, {textKey:"groupByPriority"});
146     ZmOperation.registerOp(ZmId.OP_GROUPBY_SIZE, {textKey:"groupBySize"});
147     ZmOperation.registerOp(ZmId.OP_GROUPBY_TAG,  {textKey:"groupByTag"});
148 	ZmOperation.registerOp(ZmId.OP_GO_TO_URL, {image:"URL", textKey:"goToUrlAlt"});
149 //	ZmOperation.registerOp(ZmId.OP_IMPORT_FOLDER, {textKey:"importFolder", image:"MailImport"});
150 	ZmOperation.registerOp(ZmId.OP_MARK_ALL_READ, {textKey:"markAllRead", image:"ReadMessage"});
151 //	ZmOperation.registerOp(ZmId.OP_MOUNT_FOLDER, {textKey:"mountFolder", image:"Folder"});
152 	ZmOperation.registerOp(ZmId.OP_MOVE, {textKey:"move", tooltipKey:"moveTooltip", image:"MoveToFolder", textPrecedence:40, showImageInToolbar: true}); //todo - remove
153 	ZmOperation.registerOp(ZmId.OP_MOVE_MENU, {textKey: "move", tooltipKey:"moveTooltip", image:"MoveToFolder", showImageInToolbar: true }, null,
154 		AjxCallback.simpleClosure(function(parent) {
155 			ZmOperation.addDeferredMenu(ZmOperation.addMoveMenu, parent, true);
156 		}));
157 
158     ZmOperation.registerOp(ZmId.OP_MUTE_CONV, {textKey:"muteConv", tooltipKey:"muteConvTooltip", image:"Mute", shortcut:ZmKeyMap.MUTE_UNMUTE_CONV});
159 	ZmOperation.registerOp(ZmId.OP_NEW_FOLDER, {textKey:"newFolder", tooltipKey:"newFolderTooltip", image:"NewFolder", shortcut:ZmKeyMap.NEW_FOLDER}, ZmSetting.USER_FOLDERS_ENABLED);
160 	ZmOperation.registerOp(ZmId.OP_NEW_MENU, {textKey:"_new", shortcut:ZmKeyMap.NEW, textPrecedence:100}, null,
161 		AjxCallback.simpleClosure(function(parent) {
162 			ZmOperation.addDeferredMenu(ZmOperation.addNewMenu, parent);
163 		}));
164 	ZmOperation.registerOp(ZmId.OP_NEW_TAG, {textKey:"newTag", tooltipKey:"newTagTooltip", image:"NewTag", shortcut:ZmKeyMap.NEW_TAG}, ZmSetting.TAGGING_ENABLED);
165     ZmOperation.registerOp(ZmId.OP_NOTIFY, {textKey: "notify", image:"Feedback"});
166 	ZmOperation.registerOp(ZmId.OP_OPEN_IN_TAB, {textKey:"openInTab", image:"OpenInNewTab"});
167 	ZmOperation.registerOp(ZmId.OP_OPTS, {textKey:"options", tooltipKey:"options", image:"ContextMenu"});
168 	ZmOperation.registerOp(ZmId.OP_PAGE_BACK, {image:"LeftArrow", shortcut:ZmKeyMap.PREV_PAGE});
169 	ZmOperation.registerOp(ZmId.OP_PAGE_FORWARD, {image:"RightArrow", shortcut:ZmKeyMap.NEXT_PAGE});
170 	ZmOperation.registerOp(ZmId.OP_PRINT, {textKey:"print", tooltipKey:"printTooltip", image:"Print", shortcut:ZmKeyMap.PRINT, textPrecedence:30, showImageInToolbar: true}, ZmSetting.PRINT_ENABLED);
171     ZmOperation.registerOp(ZmId.OP_PRIORITY_FILTER, {image:"Priority", textKey:"activityStream"}, ZmSetting.PRIORITY_INBOX_ENABLED);
172 	ZmOperation.registerOp(ZmId.OP_FIND_SHARES, {image:"Group", textKey:"findShares"}, ZmSetting.SHARING_ENABLED);
173 
174 	//ZmOperation.registerOp(ZmId.OP_QUICK_COMMANDS, {textKey:"quickCommands", image:"QuickCommand"});
175 	ZmOperation.registerOp(ZmId.OP_RECOVER_DELETED_ITEMS, {textKey:"recoverDeletedItems", image:"Trash", tooltipKey:"recoverDeletedItems"}, ZmSetting.DUMPSTER_ENABLED);
176     ZmOperation.registerOp(ZmId.OP_REFRESH, {textKey:"refresh", tooltipKey:"refreshTooltip"});
177 	ZmOperation.registerOp(ZmId.OP_RENAME_FOLDER, {textKey:"renameFolder", image:"Rename"});
178 	ZmOperation.registerOp(ZmId.OP_RENAME_SEARCH, {textKey:"renameSearch", image:"Rename"});
179 	ZmOperation.registerOp(ZmId.OP_RENAME_TAG, {textKey:"renameTag", image:"Rename"}, ZmSetting.TAGGING_ENABLED);
180 	ZmOperation.registerOp(ZmId.OP_SAVE, {textKey:"save", image:"Save", shortcut:ZmKeyMap.SAVE});
181 	ZmOperation.registerOp(ZmId.OP_SEARCH, {textKey:"findEmailFromSender", image:"Search"}, ZmSetting.SEARCH_ENABLED);
182     ZmOperation.registerOp(ZmId.OP_SEARCH_TO, {textKey:"findEmailToSender", image:"Search"}, ZmSetting.SEARCH_ENABLED);
183     ZmOperation.registerOp(ZmId.OP_SEARCH_MENU, {textKey:"findEmails", image:"Search"}, ZmSetting.SEARCH_ENABLED,
184 		AjxCallback.simpleClosure(function(parent) {
185 			ZmOperation.addDeferredMenu(ZmOperation.addSearchMenu, parent, true);
186 		}));
187 	ZmOperation.registerOp(ZmId.OP_SEND, {textKey:"send", tooltipKey:"sendTooltip", image:"Send", shortcut:ZmKeyMap.SEND});
188     ZmOperation.registerOp(ZmId.OP_FREE_BUSY_LINK, {textKey:"freeBusyLink", tooltipKey:"freeBusyLinkTooltip", image:"Send"});
189     ZmOperation.registerOp(ZmId.OP_SEND_FB_HTML, {textKey:"sendHTMLLink", tooltipKey:"freeBusyLinkTooltip"});
190     ZmOperation.registerOp(ZmId.OP_SEND_FB_ICS, {textKey:"sendICSLink", tooltipKey:"freeBusyLinkTooltip"});
191     ZmOperation.registerOp(ZmId.OP_SEND_FB_ICS_EVENT, {textKey:"sendICSEventLink", tooltipKey:"freeBusyLinkTooltip"});
192     ZmOperation.registerOp(ZmId.OP_SHARE, {textKey:"share", tooltipKey:"shareTooltip"}, ZmSetting.SHARING_ENABLED);
193 	ZmOperation.registerOp(ZmId.OP_SHARE_ACCEPT, {textKey:"acceptShare", image:"Check"}, ZmSetting.SHARING_ENABLED);
194 	ZmOperation.registerOp(ZmId.OP_SHARE_DECLINE, {textKey:"declineShare", image:"Cancel"}, ZmSetting.SHARING_ENABLED);
195 	ZmOperation.registerOp(ZmId.OP_SUBSCRIBE_APPROVE, {textKey:"dlApprove", image:"Check"});
196 	ZmOperation.registerOp(ZmId.OP_SUBSCRIBE_REJECT, {textKey:"dlReject", image:"Cancel"});
197 	ZmOperation.registerOp(ZmId.OP_SHARE_FOLDER, {textKey:"shareFolder", image:"SharedMailFolder"});
198 	ZmOperation.registerOp(ZmId.OP_SHOW_ALL_ITEM_TYPES, {textKey:"showAllItemTypes", image:"Globe"});
199     ZmOperation.registerOp(ZmId.OP_SORT_ASC, {textKey:"sortAscending"});
200     ZmOperation.registerOp(ZmId.OP_SORT_DESC, {textKey:"sortDescending"});
201 	ZmOperation.registerOp(ZmId.OP_SPELL_CHECK, {textKey:"spellCheck", image:"SpellCheck", tooltipKey:"spellCheckTooltip", shortcut:ZmKeyMap.SPELLCHECK, showImageInToolbar: true}, ZmSetting.SPELL_CHECK_ENABLED);
202 	ZmOperation.registerOp(ZmId.OP_SYNC, {textKey:"reload", image:"Refresh", shortcut:ZmKeyMap.REFRESH});
203     ZmOperation.registerOp(ZmId.OP_SYNC_ALL, {textKey:"checkAllFeed", image:"Refresh"});
204 	ZmOperation.registerOp(ZmId.OP_SYNC_OFFLINE_FOLDER, {textKey:"syncOfflineFolderOff", image:"Refresh"}, ZmSetting.OFFLINE_ENABLED); /* offline only */
205 	ZmOperation.registerOp(ZmId.OP_TAG, null, ZmSetting.TAGGING_ENABLED);
206 	ZmOperation.registerOp(ZmId.OP_TAG_COLOR_MENU, {textKey:"tagColor", image:"TagStack"}, ZmSetting.TAGGING_ENABLED,
207 		AjxCallback.simpleClosure(function(parent) {
208 			ZmOperation.addDeferredMenu(ZmOperation.addColorMenu, parent);
209 		}));
210 	ZmOperation.registerOp(ZmId.OP_TAG_MENU, {textKey: "tag", tooltipKey:"tagTooltip", image:"Tag", showImageInToolbar: true }, ZmSetting.TAGGING_ENABLED,
211 		AjxCallback.simpleClosure(function(parent) {
212 			ZmOperation.addDeferredMenu(ZmOperation.addTagMenu, parent, true);
213 		}));
214 	// placeholder for toolbar text
215 	ZmOperation.registerOp(ZmId.OP_TEXT);
216 	// XXX: need new icon? -
217 	//      Undelete is stupid. We should either add it for all items types (not just contacts) or just kill it
218     ZmOperation.registerOp(ZmId.OP_UNDELETE, {textKey:"undelete", tooltipKey:"undelete", image:"MoveToFolder"});
219     ZmOperation.registerOp(ZmId.OP_UNMUTE_CONV, {textKey:"unmuteConv", tooltipKey:"unmuteConvTooltip", image:"Unmute", shortcut:ZmKeyMap.MUTE_UNMUTE_CONV});
220 	ZmOperation.registerOp(ZmId.OP_VIEW, {textKey:"view", image:"SplitView"});
221 	ZmOperation.registerOp(ZmId.OP_VIEW_MENU, {tooltipKey:"viewTooltip", textKey:"view", image:"SplitPane", textPrecedence:80, showImageInToolbar: true, showTextInToolbar: true});
222 	ZmOperation.registerOp(ZmId.OP_ZIMLET, {image:"ZimbraIcon"});
223 
224 	// invites - needed for both Mail and Calendar
225 	ZmOperation.registerOp(ZmId.OP_ACCEPT_PROPOSAL, {textKey:"replyAccept", image:"Check"});
226 	ZmOperation.registerOp(ZmId.OP_DECLINE_PROPOSAL, {textKey:"replyDecline", image:"Cancel"});
227 	ZmOperation.registerOp(ZmId.OP_CAL_REPLY, {textKey:"reply", tooltipKey:"replyTooltip", image:"Reply", shortcut:ZmKeyMap.REPLY});
228 	ZmOperation.registerOp(ZmId.OP_CAL_REPLY_ALL, {textKey:"replyAll", tooltipKey:"replyAllTooltip", image:"ReplyAll", shortcut:ZmKeyMap.REPLY_ALL});
229 	ZmOperation.registerOp(ZmId.OP_REPLY_ACCEPT, {textKey:"replyAccept", image:"Check", showTextInToolbar: true, showImageInToolbar: true});
230 	ZmOperation.registerOp(ZmId.OP_REPLY_ACCEPT_NOTIFY, {textKey:"notifyOrganizerLabel", image:"Check"});
231 	ZmOperation.registerOp(ZmId.OP_REPLY_ACCEPT_IGNORE, {textKey:"dontNotifyOrganizerLabel", image:"Check"});
232 	ZmOperation.registerOp(ZmId.OP_REPLY_CANCEL);
233 	ZmOperation.registerOp(ZmId.OP_REPLY_DECLINE, {textKey:"replyDecline", image:"Cancel", showTextInToolbar: true, showImageInToolbar: true});
234 	ZmOperation.registerOp(ZmId.OP_REPLY_DECLINE_NOTIFY, {textKey:"notifyOrganizerLabel", image:"Cancel"});
235 	ZmOperation.registerOp(ZmId.OP_REPLY_DECLINE_IGNORE, {textKey:"dontNotifyOrganizerLabel", image:"Cancel"});
236 	ZmOperation.registerOp(ZmId.OP_REPLY_MODIFY);
237 	ZmOperation.registerOp(ZmId.OP_REPLY_NEW_TIME, {textKey:"replyNewTime", image:"NewTime"});
238 	ZmOperation.registerOp(ZmId.OP_REPLY_TENTATIVE, {textKey:"replyTentative", image:"QuestionMark", showTextInToolbar: true, showImageInToolbar: true});
239 	ZmOperation.registerOp(ZmId.OP_REPLY_TENTATIVE_NOTIFY, {textKey:"notifyOrganizerLabel", image:"QuestionMark"});
240 	ZmOperation.registerOp(ZmId.OP_REPLY_TENTATIVE_IGNORE, {textKey:"dontNotifyOrganizerLabel", image:"QuestionMark"});
241 
242 	// Compose Options - used by Calendar and Mail
243 	ZmOperation.registerOp(ZmId.OP_COMPOSE_OPTIONS, {textKey:"options", image:"Preferences"});
244 
245 	ZmOperation.NEW_ORG_OPS.push(ZmOperation.NEW_FOLDER, ZmOperation.NEW_TAG);
246 	ZmOperation.NEW_ORG_KEY[ZmOperation.NEW_FOLDER]	= "folder";
247 	ZmOperation.NEW_ORG_KEY[ZmOperation.NEW_TAG]	= "tag";
248 };
249 
250 /**
251  * Creates operation descriptors for the given operation IDs,
252  * then creates the appropriate widget for each operation based on the type of
253  * the parent. If it's a toolbar, then buttons are created. If it's a menu, menu items are
254  * created.
255  * <p>
256  * To override or add properties to a particular operation, pass in a hash of properties and
257  * values as a value in overrides, with the operation ID as the key.
258  * </p>
259  *
260  * @param {DwtComposite}	parent		the containing widget (toolbar or menu)
261  * @param {Array}	operations	a list of operation IDs
262  * @param {Hash}	overrides		a hash of overrides by op ID
263  *
264  * @returns	{Hash}	a hash of operations by ID
265  */
266 ZmOperation.createOperations =
267 function(parent, operations, overrides) {
268 	var obj = new ZmOperation();
269 	return obj._createOperations(parent, operations, overrides);
270 }
271 
272 /**
273  * Done through an object so that we can have more than one invocation going
274  * without sharing memory (eg, creating New submenu).
275  * 
276  * @private
277  */
278 ZmOperation.prototype._createOperations =
279 function(parent, operations, overrides) {
280 	if (operations == ZmOperation.NONE) {
281 		operations = null;
282 	}
283 	overrides = overrides || {};
284 
285 	var opHash = {};
286 	if (operations && operations.length) {
287 		for (var i = 0; i < operations.length; i++) {
288 			var id = operations[i];
289 			ZmOperation.defineOperation(id, overrides[id]);
290 			ZmOperation.addOperation(parent, id, opHash, null, overrides[id] && overrides[id].htmlElId);
291 		}
292 	}
293 
294 	return opHash;
295 };
296 
297 /**
298 * Creates an operation descriptor. The ID of an existing operation can be passed
299 * in to use as a base, with overridden properties passed in a hash. A new operation
300 * can be defined by passing its properties in a hash.
301 *
302 * @param {String}	baseId		the ID of an existing operation
303 * @param {Hash}	overrides	the property overrides for the operation
304 */
305 ZmOperation.defineOperation =
306 function(baseId, overrides) {
307 	var id = (overrides && overrides.id) || (baseId && baseId.id) || baseId || Dwt.getNextId();
308 	var textKey = (overrides && overrides.textKey) || ZmOperation.getProp(baseId, "textKey");
309 	var text = textKey && ZmMsg[textKey];
310 	var tooltipKey = (overrides && overrides.tooltipKey) || ZmOperation.getProp(baseId, "tooltipKey");
311 	var tooltip = tooltipKey && ZmMsg[tooltipKey];
312 	var image = ZmOperation.getProp(baseId, "image");
313 	var showImageInToolbar = ZmOperation.getProp(baseId, "showImageInToolbar");
314 	var showTextInToolbar = ZmOperation.getProp(baseId, "showTextInToolbar");
315 	var disImage = ZmOperation.getProp(baseId, "disImage");
316 	var enabled = (overrides && (overrides.enabled !== false));
317 	var style = ZmOperation.getProp(baseId, "style");
318 	var shortcut = ZmOperation.getProp(baseId, "shortcut");
319 
320     var opDesc = {id:id, text:text, image:image, showImageInToolbar:showImageInToolbar, showTextInToolbar:showTextInToolbar, disImage:disImage, enabled:enabled,
321 				  tooltip:tooltip, style:style, shortcut:shortcut};
322 	if (overrides) {
323 		for (var i in overrides) {
324 			opDesc[i] = overrides[i];
325 		}
326 	}
327 
328 	ZmOperation._operationDesc[id] = opDesc;
329 	
330 	return opDesc;
331 };
332 
333 /**
334  * Gets the value of a given property for a given operation.
335  *
336  * @param {String}	id		the operation ID
337  * @param {String}	prop	the name of an operation property
338  * 
339  * @return	{Object}	the value
340  */
341 ZmOperation.getProp =
342 function(id, prop) {
343 	var value = null;
344 	var setup = ZmOperation.SETUP[id];
345 	if (setup) {
346 		value = setup[prop];
347 		if (!value && (prop == "disImage") && setup.image) {
348 			value = setup.image;
349 		}
350 	}
351 
352 	return value;
353 };
354 
355 /**
356  * Checks if the operation is a separator or spacer.
357  * 
358  * @param	{String}	id		the id
359  * @return	{Boolean}	<code>true</code> if the operation is a spacer
360  */
361 ZmOperation.isSep =
362 function(id) {
363 	return (id == ZmOperation.SEP || id == ZmOperation.SPACER || id == ZmOperation.FILLER);
364 };
365 
366 /**
367  * Adds the operation.
368  * 
369  * @param {DwtComposite}	parent		the containing widget (toolbar or menu)
370  * @param	{String}		id		the id
371  * @param	{Hash}		opHash		a hash
372  * @param	{String}	[index]		the index
373  */
374 ZmOperation.addOperation =
375 function(parent, id, opHash, index, htmlElId) {
376 
377 	var opDesc = ZmOperation._operationDesc[id] || ZmOperation.defineOperation(id);
378 
379 	if (id == ZmOperation.SEP) {
380         if (parent instanceof DwtMenu) {
381             parent.createSeparator(index);
382         }
383         else {
384             parent.addSeparator(null, index);
385         }
386     } else if (id == ZmOperation.SPACER) {	// toolbar only
387 		parent.addSpacer(null, index);
388 	} else if (id == ZmOperation.FILLER) {	// toolbar only
389 		parent.addFiller(null, index);
390 	} else {
391 		if (index != null) {
392 			opDesc.index = index;
393 		}
394 		opHash[id] = parent.createOp(id, opDesc, htmlElId);
395 	}
396 	var callback = ZmOperation.CALLBACK[id];
397 	if (callback) {
398 		callback.run(opHash[id]);
399 	}
400 };
401 
402 /**
403  * Adds a deferred menu.
404  * 
405  * @param	{function}	addMenuFunc		the add menu function
406  * @param {DwtComposite}	parent		the containing widget (toolbar or menu)
407  */
408 ZmOperation.addDeferredMenu =
409 function(addMenuFunc, parent /* ... */) {
410     var args = [parent];
411     for (var i = 2; i < arguments.length; i++) {
412         args.push(arguments[i]);
413     }
414 	var callback = new AjxCallback(null, addMenuFunc, args);
415 	parent.setMenu(callback);
416 };
417 
418 /**
419  * Removes the operation.
420  * 
421  * @param {DwtComposite}	parent		the containing widget (toolbar or menu)
422  * @param	{String}	id		the id
423  * @param	{Hash}	opHash		a hash
424  */
425 ZmOperation.removeOperation =
426 function(parent, id, opHash) {
427 	var op = parent.getOp(id);
428 	if (op) {
429 		op.dispose();
430 		delete opHash[id];
431 	}
432 };
433 
434 /**
435  * Replaces the attributes of one operation with those of another, wholly or in part.
436  *
437  * @param {DwtComposite}	parent		the parent widget
438  * @param {String}	oldOp		the ID of operation to replace
439  * @param {String}	newOp		the ID of new operation to get replacement attributes from
440  * @param {String}	text		the new text (overrides text of newOp)
441  * @param {String}	image		the new image (overrides image of newOp)
442  * @param {String}	disImage	the new disabled image (overrides that of newOp)
443  */
444 ZmOperation.setOperation =
445 function(parent, oldOp, newOp, text, image, disImage) {
446 	var op = parent.getOp(oldOp);
447 	if (!op) return;
448 
449 	op.setText(text ? text : ZmMsg[ZmOperation.getProp(newOp, "textKey")]);
450 	op.setImage(image ? image : ZmOperation.getProp(newOp, "image"));
451 };
452 
453 /**
454  * Takes a list of operations and removes any who have a corresponding setting that's
455  * not set. Also deals with the fact that you don't want a separator or a spacer unless
456  * there's stuff on either side of it.
457  * 
458  * @param	{Array}	list		a list of {ZmOperation} objects
459  * @return	{Array}		a list of {ZmOperation} objects
460  */
461 ZmOperation.filterOperations =
462 function(list) {
463 	var newList = [];
464 	if (!(list && list.length)) { return newList; }
465 	
466 	// remove disabled operations
467 	for (var i = 0; i < list.length; i++) {
468 		var op = list[i];
469 		if (!op) {
470 			continue;
471 		}
472 		if (appCtxt.checkPrecondition(ZmOperation.PRECONDITION[op])) {
473 			newList.push(op);
474 		}
475 	}
476 	// reduce multiple consecutive separators to the first one
477 	var newList1 = [];
478 	var gotSep = false;
479 	for (var i = 0; i < newList.length; i++) {
480 		var op = newList[i];
481 		if (op == ZmOperation.SEP || op == ZmOperation.SPACER) {
482 			if (!gotSep) {
483 				newList1.push(op);
484 			}
485 			gotSep = true;
486 		} else {
487 			newList1.push(op);
488 			gotSep = false;
489 		}
490 	}
491 	// remove separator at beginning or end
492 	if (newList1 && newList1.length) {
493 		if (newList1[0] == ZmOperation.SEP || newList1[0] == ZmOperation.SPACER) {
494 			newList1.shift();
495 		}
496 		var i = newList1.length - 1;
497 		if (newList1[i] == ZmOperation.SEP || newList1[i] == ZmOperation.SPACER || newList1[i] == ZmOperation.FILLER) {
498 			newList1.pop();
499 		}
500 	}
501 	
502 	return newList1;
503 };
504 
505 /**
506  * Adds a "New" submenu. Overrides are used because we don't want "New" at the
507  * beginning of each label.
508  *
509  * @param {DwtComposite}		parent		the parent widget
510  * @return	{ZmActionMenu}	the menu
511  */
512 ZmOperation.addNewMenu =
513 function(parent) {
514 	var list = ZmOperation.NEW_ITEM_OPS;
515 	list.push(ZmOperation.SEP);
516 	list = list.concat(ZmOperation.NEW_ORG_OPS);
517 
518 	var overrides = {};
519 	for (var i = 0; i < list.length; i++) {
520 		var op = list[i];
521 		var htmlElId = parent._htmlElId + "_" + op;
522 		overrides[op] = {htmlElId: htmlElId};
523 		var textKey = ZmOperation.NEW_ITEM_KEY[op] || ZmOperation.NEW_ORG_KEY[op];
524 		if (textKey) {
525 			overrides[op].textKey = textKey;
526 		}
527 	}
528 
529 	var menu = new ZmActionMenu({parent:parent, menuItems:list, overrides:overrides});
530 	parent.setMenu(menu);
531 
532 	return menu;
533 };
534 
535 /**
536  * Adds a "Search" submenu for searching from/to sender/recipient.
537  *
538  * @param {DwtComposite}	parent		parent widget (a toolbar or action menu)
539  * @return	{ZmActionMenu}	the menu
540  */
541 ZmOperation.addSearchMenu =
542 function(parent) {
543 	var list = [ZmOperation.SEARCH, ZmOperation.SEARCH_TO];
544 
545 	var menu = new ZmActionMenu({parent:parent, menuItems:list});
546 	parent.setMenu(menu);
547 
548 	return menu;
549 };
550 
551 /**
552  * Adds a contact group menu for creating a contacts from the contact list
553  * @param {DwtComposite}    parent  parent widget (a toolbar or action menu)
554  * @return {ZmActionMenu) the menu
555  */
556 ZmOperation.addContactGroupMenu =
557 function(parent) {
558 	var contactGroupMenu = new ZmContactGroupMenu(parent);
559 	parent.setMenu(contactGroupMenu);
560 	return contactGroupMenu;
561 };
562 
563 /**
564  * Adds a "Tag" submenu for tagging items.
565  *
566  * @param {DwtComposite}	parent		parent widget (a toolbar or action menu)
567  * @return	{ZmTagMenu}	the menu
568  */
569 ZmOperation.addTagMenu =
570 function(parent) {
571 	var tagMenu = new ZmTagMenu(parent);
572 	parent.setMenu(tagMenu);
573 	return tagMenu;
574 };
575 
576 
577 /**
578 * Adds a "Move" submenu for tagging items.
579 *
580 * @param {DwtComposite}	parent		parent widget (a toolbar or action menu)
581 * @return	{ZmTagMenu}	the menu
582 */
583 ZmOperation.addMoveMenu =
584 function(parent) {
585 	var moveMenu = new DwtMenu(parent); //this is a dummy menu just so the drop-down would appear
586 	parent.setMenu(moveMenu);
587 	return moveMenu;
588 };
589 
590 /**
591  * Adds a color submenu for choosing tag color.
592  *
593  * @param {DwtComposite}	parent		parent widget (a toolbar or action menu)
594  * @param {boolean} hideNoFill True to hide the no-fill/use-default option.
595  * @return	{ZmPopupMenu}	the menu
596  */
597 ZmOperation.addColorMenu =
598 function(parent, hideNoFill) {
599     var menu = new ZmColorMenu({parent:parent,image:"Tag",hideNone:true,hideNoFill:hideNoFill});
600     parent.setMenu(menu);
601     return menu;
602 };
603 
604 /**
605  * Gets the tooltip for the operation with the given ID. If the operation has a shortcut associated
606  * with it, a shortcut hint is appended to the end of the tooltip.
607  *
608  * @param {String}	id		the operation ID
609  * @param {String}	keyMap	the key map (for resolving shortcut)
610  * @param {String}	tooltip	the tooltip override
611  * @return	{String}	the tool tip
612  */
613 ZmOperation.getToolTip =
614 function(id, keyMap, tooltip) {
615 	var opDesc = ZmOperation._operationDesc[id] || ZmOperation.defineOperation(id);
616 	tooltip = tooltip || opDesc.tooltip;
617 	var sc = tooltip && opDesc.shortcut && appCtxt.getShortcutHint(keyMap, opDesc.shortcut);
618 	return sc ? [tooltip, sc].join("") : tooltip;
619 };
620