1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines a base controller class.
 27  *
 28  */
 29 
 30 /**
 31  * This class is a base class for any controller that manages items such as messages, contacts,
 32  * appointments, tasks, etc. It handles operations that can be performed on those items such as
 33  * move, delete, tag, print, etc.
 34  *
 35  * @author Conrad Damon
 36  *
 37  * @param {DwtControl}					container					the containing shell
 38  * @param {ZmApp}						app							the containing application
 39  * @param {constant}					type						type of controller (typically a view type)				
 40  * @param {string}						sessionId					the session id
 41  * @param {ZmSearchResultsController}	searchResultsController		containing controller
 42  * 
 43  * @extends		ZmController
 44  */
 45 ZmBaseController = function(container, app, type, sessionId, searchResultsController) {
 46 
 47 	if (arguments.length == 0) { return; }
 48 	ZmController.apply(this, arguments);
 49 
 50 	this.setSessionId(sessionId, type || this.getDefaultViewType(), searchResultsController);
 51 	
 52     //this._refreshQuickCommandsClosure = this._refreshQuickCommands.bind(this);
 53     //this._quickCommandMenuHandlerClosure = this._quickCommandMenuHandler.bind(this);
 54 
 55 	// hashes keyed by view type
 56 	this._view		= {};
 57 	this._toolbar	= {};	// ZmButtonToolbar
 58 	this._tabGroups = {};	// DwtTabGroup
 59 
 60 	this._tagList = appCtxt.getTagTree();
 61 	if (this._tagList) {
 62 		this._boundTagChangeListener = this._tagChangeListener.bind(this);
 63 		this._tagList.addChangeListener(this._boundTagChangeListener);
 64 	}
 65 
 66 	// create a listener for each operation
 67 	this._listeners = {};
 68 	this._listeners[ZmOperation.NEW_MENU]		= this._newListener.bind(this);
 69 	this._listeners[ZmOperation.TAG_MENU]		= this._tagButtonListener.bind(this);
 70 	this._listeners[ZmOperation.MOVE_MENU]		= this._moveButtonListener.bind(this);
 71 	this._listeners[ZmOperation.ACTIONS_MENU]	= this._actionsButtonListener.bind(this);
 72 	this._listeners[ZmOperation.TAG]			= this._tagListener.bind(this);
 73 	this._listeners[ZmOperation.PRINT]			= this._printListener.bind(this);
 74 	this._listeners[ZmOperation.DELETE]			= this._deleteListener.bind(this);
 75 	this._listeners[ZmOperation.DELETE_WITHOUT_SHORTCUT]			= this._deleteListener.bind(this);
 76 	this._listeners[ZmOperation.CLOSE]			= this._backListener.bind(this);
 77 	this._listeners[ZmOperation.MOVE]			= this._moveListener.bind(this);
 78 	this._listeners[ZmOperation.SEARCH]			= this._searchListener.bind(this);
 79 	this._listeners[ZmOperation.NEW_MESSAGE]	= this._composeListener.bind(this);
 80 	this._listeners[ZmOperation.CONTACT]		= this._contactListener.bind(this);
 81 	this._listeners[ZmOperation.VIEW]			= this._viewMenuItemListener.bind(this);
 82 	this._listeners[ZmOperation.GO_TO_URL]		= this._goToUrlListener.bind(this);
 83 
 84 	// TODO: do this better - avoid referencing specific apps
 85 	if (window.ZmImApp) {
 86 		this._listeners[ZmOperation.IM] = ZmImApp.getImMenuItemListener();
 87 	}
 88 
 89 	/**
 90 	 * List of toolbar operations to enable on Zero/no selection
 91 	 * - Default is only enable ZmOperation.NEW_MENU
 92 	 */
 93 	this.operationsToEnableOnZeroSelection = [ZmOperation.NEW_MENU];
 94 
 95 	/**
 96 	 * List of toolbar operations to enable when multiple items are selected
 97 	 * - Default is to enable: ZmOperation.NEW_MENU, ZmOperation.TAG_MENU, ZmOperation.DELETE, ZmOperation.MOVE,
 98 	 * 						ZmOperation.MOVE_MENU, ZmOperation.FORWARD & ZmOperation.ACTIONS_MENU
 99 	 */
100 	this.operationsToEnableOnMultiSelection = [ZmOperation.NEW_MENU, ZmOperation.TAG_MENU, ZmOperation.DELETE,
101 												ZmOperation.MOVE, ZmOperation.MOVE_MENU, ZmOperation.FORWARD,
102 												ZmOperation.ACTIONS_MENU];
103 	/**
104 	 * List of toolbar operations to *disable*
105 	 * Default is to enable-all
106 	 */
107 	this.operationsToDisableOnSingleSelection = [];
108 };
109 
110 ZmBaseController.prototype = new ZmController;
111 ZmBaseController.prototype.constructor = ZmBaseController;
112 
113 ZmBaseController.prototype.isZmBaseController = true;
114 ZmBaseController.prototype.toString = function() { return "ZmBaseController"; };
115 
116 
117 
118 // public methods
119 
120 /**
121  * Sets the session id, view id, and tab id. Notes whether this controller is being
122  * used to display search results.
123  *
124  * @param {string}						sessionId					the session id
125  * @param {string}						type						the type
126  * @param {ZmSearchResultsController}	searchResultsController		owning controller
127  */
128 ZmBaseController.prototype.setSessionId =
129 function(sessionId, type, searchResultsController) {
130 
131 	ZmController.prototype.setSessionId.apply(this, arguments);
132 	this.searchResultsController = searchResultsController;
133 	this.isSearchResults = Boolean(searchResultsController);
134 };
135 
136 /**
137  * Gets the current view object.
138  * 
139  * @return	{DwtComposite}	the view object
140  */
141 ZmBaseController.prototype.getCurrentView =
142 function() {
143 	return this._view[this._currentViewId];
144 };
145 
146 /**
147  * Returns the view used to display a single item, if any.
148  */
149 ZmBaseController.prototype.getItemView = function() {
150 	return null;
151 };
152 
153 /**
154  * Gets the current tool bar.
155  * 
156  * @return	{ZmButtonToolbar}		the toolbar
157  */
158 ZmBaseController.prototype.getCurrentToolbar =
159 function() {
160 	return this._toolbar[this._currentViewId];
161 };
162 
163 /**
164  * Returns the list of items to be acted upon.
165  */
166 ZmBaseController.prototype.getItems = function() {};
167 
168 /**
169  * Returns the number of items to be acted upon.
170  */
171 ZmBaseController.prototype.getItemCount = function() {};
172 
173 /**
174  * Handles a shortcut.
175  * 
176  * @param	{constant}	actionCode		the action code
177  * @return	{Boolean}	<code>true</code> if the action is handled
178  */
179 ZmBaseController.prototype.handleKeyAction =
180 function(actionCode, ev) {
181 
182 	DBG.println(AjxDebug.DBG3, "ZmBaseController.handleKeyAction");
183     var isExternalAccount = appCtxt.isExternalAccount();
184 
185 	switch (actionCode) {
186 
187 		case ZmKeyMap.MOVE:
188             if (isExternalAccount) { break; }
189 			var items = this.getItems();
190 			if (items && items.length) {
191 				this._moveListener();
192 			}
193 			break;
194 
195 		case ZmKeyMap.PRINT:
196 			if (appCtxt.get(ZmSetting.PRINT_ENABLED) && !appCtxt.isWebClientOffline()) {
197 				this._printListener();
198 			}
199 			break;
200 
201 		case ZmKeyMap.TAG:
202             if (isExternalAccount) { break; }
203 			var items = this.getItems();
204 			if (items && items.length && (appCtxt.getTagTree().size() > 0)) {
205 				var dlg = appCtxt.getPickTagDialog();
206 				ZmController.showDialog(dlg, new AjxCallback(this, this._tagSelectionCallback, [items, dlg]));
207 			}
208 			break;
209 
210 		case ZmKeyMap.UNTAG:
211             if (isExternalAccount) { break; }
212 			if (appCtxt.get(ZmSetting.TAGGING_ENABLED)) {
213 				var items = this.getItems();
214 				if (items && items.length) {
215 					this._doRemoveAllTags(items);
216 				}
217 			}
218 			break;
219 
220 		default:
221 			return ZmController.prototype.handleKeyAction.apply(this, arguments);
222 	}
223 	return true;
224 };
225 
226 /**
227  * Returns true if this controller's view is currently being displayed (possibly within a search results tab)
228  */
229 ZmBaseController.prototype.isCurrent =
230 function() {
231 	return (this._currentViewId == appCtxt.getCurrentViewId());
232 };
233 
234 ZmBaseController.prototype.supportsDnD =
235 function() {
236 	return !appCtxt.isExternalAccount();
237 };
238 
239 // abstract protected methods
240 
241 // Creates the view element
242 ZmBaseController.prototype._createNewView	 		= function() {};
243 
244 // Populates the view with data
245 ZmBaseController.prototype._setViewContents			= function(view) {};
246 
247 // Returns text for the tag operation
248 ZmBaseController.prototype._getTagMenuMsg 			= function(num) {};
249 
250 // Returns text for the move dialog
251 ZmBaseController.prototype._getMoveDialogTitle		= function(num) {};
252 
253 // Returns a list of desired toolbar operations
254 ZmBaseController.prototype._getToolBarOps 			= function() {};
255 
256 // Returns a list of secondary (non primary) toolbar operations
257 ZmBaseController.prototype._getSecondaryToolBarOps 	= function() {};
258 
259 // Returns a list of buttons that align to the right, like view and detach
260 ZmBaseController.prototype._getRightSideToolBarOps 	= function() {};
261 
262 
263 // private and protected methods
264 
265 /**
266  * Creates basic elements and sets the toolbar and action menu.
267  * 
268  * @private
269  */
270 ZmBaseController.prototype._setup =
271 function(view) {
272 	this._initialize(view);
273 	this._resetOperations(this._toolbar[view], 0);
274 };
275 
276 /**
277  * Creates the basic elements: toolbar, list view, and action menu.
278  *
279  * @private
280  */
281 ZmBaseController.prototype._initialize =
282 function(view) {
283 	this._initializeToolBar(view);
284 	this._initializeView(view);
285 	this._initializeTabGroup(view);
286 };
287 
288 // Below are functions that return various groups of operations, for cafeteria-style
289 // operation selection.
290 
291 /**
292  * @private
293  */
294 ZmBaseController.prototype._standardToolBarOps =
295 function() {
296 	return [ZmOperation.DELETE, ZmOperation.MOVE_MENU, ZmOperation.PRINT];
297 };
298 
299 /**
300  * Initializes the toolbar buttons and listeners.
301  * 
302  * @private
303  */
304 ZmBaseController.prototype._initializeToolBar =
305 function(view, className) {
306 
307 	if (this._toolbar[view]) { return; }
308 
309 	var buttons = this._getToolBarOps();
310 	var secondaryButtons = this._getSecondaryToolBarOps() || [];
311 	var rightSideButtons = this._getRightSideToolBarOps() || [];
312 	if (!(buttons || secondaryButtons)) { return; }
313 
314 	var tbParams = {
315 		parent:				this._container,
316 		buttons:			buttons,
317 		secondaryButtons:	secondaryButtons,
318 		rightSideButtons: 	rightSideButtons,
319 		overrides:          this._getButtonOverrides(buttons.concat(secondaryButtons).concat(rightSideButtons)),
320 		context:			view,
321 		controller:			this,
322 		refElementId:		ZmId.SKIN_APP_TOP_TOOLBAR,
323 		addTextElement:		true,
324 		className:			className
325 	};
326 	var tb = this._toolbar[view] = new ZmButtonToolBar(tbParams);
327 
328 	var text = tb.getButton(ZmOperation.TEXT);
329 	if (text) {
330 		text.addClassName("itemCountText");
331 	}
332 
333 	var button;
334 	for (var i = 0; i < tb.opList.length; i++) {
335 		button = tb.opList[i];
336 		if (this._listeners[button]) {
337 			tb.addSelectionListener(button, this._listeners[button]);
338 		}
339 	}
340 
341 	button = tb.getButton(ZmOperation.TAG_MENU);
342 	if (button) {
343 		button.noMenuBar = true;
344 		this._setupTagMenu(tb);
345 	}
346 
347 	button = tb.getButton(ZmOperation.MOVE_MENU);
348 	if (button) {
349 		button.noMenuBar = true;
350 		this._setupMoveMenu(tb);
351 	}
352 
353 
354 	// add the selection listener for when user clicks on the little drop-down arrow (unfortunately we have to do that here separately) It is done for the main button area in a generic way to all toolbar buttons elsewhere
355 	var actionsButton = tb.getActionsButton();
356 	if (actionsButton) {
357 		actionsButton.addDropDownSelectionListener(this._listeners[ZmOperation.ACTIONS_MENU]);
358 	}
359 
360 	var actionsMenu = tb.getActionsMenu();
361 	if (actionsMenu) {
362 		this._setSearchMenu(actionsMenu, true);
363 	}	
364 
365 	appCtxt.notifyZimlets("initializeToolbar", [this._app, tb, this, view], {waitUntilLoaded:true});
366 };
367 
368 ZmBaseController.prototype._getButtonOverrides = function(buttons) {};
369 
370 /**
371  * Initializes the view and its listeners.
372  * 
373  * @private
374  */
375 ZmBaseController.prototype._initializeView =
376 function(view) {
377 
378 	if (this._view[view]) { return; }
379 
380 	this._view[view] = this._createNewView(view);
381 	this._view[view].addSelectionListener(this._listSelectionListener.bind(this));
382 	this._view[view].addActionListener(this._listActionListener.bind(this));
383 };
384 
385 // back-compatibility (bug 60073)
386 ZmBaseController.prototype._initializeListView = ZmBaseController.prototype._initializeView;
387 
388 /**
389  * Sets up tab groups (focus ring).
390  * 
391  * @private
392  */
393 ZmBaseController.prototype._initializeTabGroup = function(view) {
394 
395 	if (this._tabGroups[view]) {
396         return;
397     }
398 
399 	this._tabGroups[view] = this._createTabGroup();
400 	this._tabGroups[view].newParent(appCtxt.getRootTabGroup());
401 	this._tabGroups[view].addMember(this._toolbar[view].getTabGroupMember());
402     this._tabGroups[view].addMember(this._view[view].getTabGroupMember());
403 };
404 
405 /**
406  * Creates the desired application view.
407  *
408  * @param params		[hash]			hash of params:
409  *        view			[constant]		view ID
410  *        elements		[array]			array of view components
411  *        controller	[ZmController]	controller responsible for this view
412  *        isAppView		[boolean]*		this view is a top-level app view
413  *        clear			[boolean]*		if true, clear the hidden stack of views
414  *        pushOnly		[boolean]*		if true, don't reset the view's data, just swap the view in
415  *        noPush		[boolean]*		if true, don't push the view, just set its contents
416  *        isTransient	[boolean]*		this view doesn't go on the hidden stack
417  *        stageView		[boolean]*		stage the view rather than push it
418  *        tabParams		[hash]*			button params; view is opened in app tab instead of being stacked
419  *        
420  * @private
421  */
422 ZmBaseController.prototype._setView =
423 function(params) {
424 
425 	var view = params.view;
426 	
427 	// create the view (if we haven't yet)
428 	if (!this._appViews[view]) {
429 		// view management callbacks
430 		var callbacks = {};
431 		callbacks[ZmAppViewMgr.CB_PRE_HIDE]		= this._preHideCallback.bind(this);
432 		callbacks[ZmAppViewMgr.CB_PRE_UNLOAD]	= this._preUnloadCallback.bind(this);
433 		callbacks[ZmAppViewMgr.CB_POST_HIDE]	= this._postHideCallback.bind(this);
434 		callbacks[ZmAppViewMgr.CB_POST_REMOVE]	= this._postRemoveCallback.bind(this);
435 		callbacks[ZmAppViewMgr.CB_PRE_SHOW]		= this._preShowCallback.bind(this);
436 		callbacks[ZmAppViewMgr.CB_POST_SHOW]	= this._postShowCallback.bind(this);
437 
438 		params.callbacks = callbacks;
439 		params.viewId = view;
440 		params.controller = this;
441 		this._app.createView(params);
442 		this._appViews[view] = true;
443 	}
444 
445 	// populate the view
446 	if (!params.pushOnly) {
447 		this._setViewContents(view);
448 	}
449 
450 	// push the view
451 	if (params.stageView) {
452 		this._app.stageView(view);
453 	} else if (!params.noPush) {
454 		return (params.clear ? this._app.setView(view) : this._app.pushView(view));
455 	}
456 };
457 
458 
459 
460 // Operation listeners
461 
462 /**
463  * Tag button has been pressed. We don't tag anything (since no tag has been selected),
464  * we just show the dynamic tag menu.
465  * 
466  * @private
467  */
468 ZmBaseController.prototype._tagButtonListener =
469 function(ev) {
470 	var toolbar = this._toolbar[this._currentViewId];
471 	if (ev.item.parent == toolbar) {
472 		this._setTagMenu(toolbar);
473 	}
474 };
475 
476 /**
477  * Move button has been pressed. We don't move anything (since no folder has been selected),
478  * we just show the dynamic move menu.
479  *
480  * @private
481  */
482 ZmBaseController.prototype._moveButtonListener =
483 function(ev, list) {
484 	this._pendingActionData = list || this.getItems();
485 
486 	var toolbar = this._toolbar[this._currentViewId];
487 
488 	var moveButton = toolbar.getOp(ZmOperation.MOVE_MENU);
489 	if (!moveButton) {
490 		return;
491 	}
492 	if (!this._moveButtonInitialized) {
493 		this._moveButtonInitialized = true;
494 		appCtxt.getShell().setBusy(true);
495 		this._setMoveButton(moveButton);
496 		appCtxt.getShell().setBusy(false);
497 	}
498 	else {
499 		//need to update this._data so the chooser knows from which folder we are trying to move.
500 		this._folderChooser.updateData(this._getMoveParams(this._folderChooser).data);
501 	}
502 	var newButton = this._folderChooser._getNewButton();
503 	if (newButton) {
504 		newButton.setVisible(!appCtxt.isWebClientOffline());
505 	}
506 	moveButton.popup();
507 	moveButton.getMenu().getHtmlElement().style.width = "auto"; //reset the width so it's dynamic. without this it is set to 0, and in any case even if it was set to some other > 0 value, it needs to be dynamic due to collapse/expand (width changes)
508 	this._folderChooser.focus();
509 };
510 
511 /**
512  * Actions button has been pressed.
513  * @private
514  */
515 ZmBaseController.prototype._actionsButtonListener =
516 function(ev) {
517 	var menu = this.getCurrentToolbar().getActionsMenu();
518 	menu.parent.popup();	
519 };
520 
521 
522 /**
523  * Tag/untag items.
524  * 
525  * @private
526  */
527 ZmBaseController.prototype._tagListener =
528 function(ev, items) {
529 
530 	if (this.isCurrent()) {
531 		var menuItem = ev.item;
532 		var tagEvent = menuItem.getData(ZmTagMenu.KEY_TAG_EVENT);
533 		var tagAdded = menuItem.getData(ZmTagMenu.KEY_TAG_ADDED);
534 		items = items || this.getItems();
535 
536 		if (tagEvent == ZmEvent.E_TAGS && tagAdded) {
537 			this._doTag(items, menuItem.getData(Dwt.KEY_OBJECT), true);
538 		} else if (tagEvent == ZmEvent.E_CREATE) {
539 			this._pendingActionData = items;
540 			var newTagDialog = appCtxt.getNewTagDialog();
541 			if (!this._newTagCb) {
542 				this._newTagCb = new AjxCallback(this, this._newTagCallback);
543 			}
544 			ZmController.showDialog(newTagDialog, this._newTagCb);
545 			newTagDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, newTagDialog);
546 		} else if (tagEvent == ZmEvent.E_TAGS && !tagAdded) {
547 			//remove tag
548 			this._doTag(items, menuItem.getData(Dwt.KEY_OBJECT), false);
549 		} else if (tagEvent == ZmEvent.E_REMOVE_ALL) {
550 			// bug fix #607
551 			this._doRemoveAllTags(items);
552 		}
553 	}
554 };
555 
556 /**
557  * Called after tag selection via dialog.
558  * 
559  * @private
560  */
561 ZmBaseController.prototype._tagSelectionCallback =
562 function(items, dialog, tag) {
563 	if (tag) {
564 		this._doTag(items, tag, true);
565 	}
566 	dialog.popdown();
567 };
568 
569 /**
570  * overload if you want to print in a different way.
571  * 
572  * @private
573  */
574 ZmBaseController.prototype._printListener =
575 function(ev) {
576 	var items = this.getItems();
577     if (items && items[0]) {
578 	    window.open(items[0].getRestUrl(), "_blank");
579 	}
580 };
581 
582 ZmBaseController.prototype._backListener =
583 function(ev) {
584 	this._app.popView();
585 };
586 
587 /**
588  * Delete one or more items.
589  * 
590  * @private
591  */
592 ZmBaseController.prototype._deleteListener =
593 function(ev) {
594 	this._doDelete(this.getItems(), ev.shiftKey);
595 };
596 
597 /**
598  * Move button has been pressed, show the dialog.
599  * 
600  * @private
601  */
602 ZmBaseController.prototype._moveListener =
603 function(ev, list) {
604 
605 	this._pendingActionData = list || this.getItems();
606 	var moveToDialog = appCtxt.getChooseFolderDialog();
607 	if (!this._moveCb) {
608 		this._moveCb = new AjxCallback(this, this._moveCallback);
609 	}
610 	ZmController.showDialog(moveToDialog, this._moveCb, this._getMoveParams(moveToDialog));
611 	moveToDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, moveToDialog);
612 };
613 
614 /**
615  * @protected
616  */
617 ZmBaseController.prototype._getMoveParams =
618 function(dlg) {
619 
620 	var org = ZmApp.ORGANIZER[this._app._name] || ZmOrganizer.FOLDER;
621 	return {
622 		overviewId:		dlg.getOverviewId(this._app._name),
623 		data:			this._pendingActionData,
624 		treeIds:		[org],
625 		title:			this._getMoveDialogTitle(this._pendingActionData.length, this._pendingActionData),
626 		description:	ZmMsg.targetFolder,
627 		treeStyle:		DwtTree.SINGLE_STYLE,
628 		noRootSelect: 	true, //I don't think you can ever use the "move" dialog to move anything to a root folder... am I wrong?
629 		appName:		this._app._name
630 	};
631 };
632 
633 /**
634  * Switch to selected view.
635  * 
636  * @private
637  */
638 ZmBaseController.prototype._viewMenuItemListener =
639 function(ev) {
640 	if (ev.detail == DwtMenuItem.CHECKED || ev.detail == DwtMenuItem.UNCHECKED) {
641 		this.switchView(ev.item.getData(ZmOperation.MENUITEM_ID));
642 	}
643 };
644 
645 
646 // new organizer callbacks
647 
648 /**
649  * Created a new tag, now apply it.
650  * 
651  * @private
652  */
653 ZmBaseController.prototype._tagChangeListener =
654 function(ev) {
655 
656 	// only process if current view is this view!
657 	if (this.isCurrent()) {
658 		if (ev.type == ZmEvent.S_TAG && ev.event == ZmEvent.E_CREATE && this._pendingActionData) {
659 			var tag = ev.getDetail("organizers")[0];
660 			this._doTag(this._pendingActionData, tag, true);
661 			this._pendingActionData = null;
662 			this._menuPopdownActionListener();
663 		}
664 	}
665 };
666 
667 /**
668  * Move stuff to a new folder.
669  * 
670  * @private
671  */
672 ZmBaseController.prototype._moveCallback =
673 function(folder) {
674 	this._doMove(this._pendingActionData, folder);
675 	this._clearDialog(appCtxt.getChooseFolderDialog());
676 	this._pendingActionData = null;
677 };
678 
679 /**
680  * Move stuff to a new folder. 
681  *
682  * @private
683  */
684 ZmBaseController.prototype._moveMenuCallback =
685 function(moveButton, folder) {
686 	this._doMove(this._pendingActionData, folder);
687 	moveButton.getMenu().popdown();
688 	this._pendingActionData = null;
689 };
690 
691 // Data handling
692 
693 // Actions on items are performed through their containing list
694 ZmBaseController.prototype._getList =
695 function(items) {
696 
697 	items = AjxUtil.toArray(items);
698 	var item = items[0];
699 	return item && item.list;
700 };
701 
702 // callback (closure) to run when an action has completely finished
703 ZmBaseController.prototype._getAllDoneCallback = function() {};
704 
705 /**
706  * Shows the given summary as status toast.
707  *
708  * @param {String}		summary						the text that summarizes the recent action
709  * @param {ZmAction}	actionLogItem				the logged action for possible undoing
710  * @param {boolean}		showToastOnParentWindow		the toast message should be on the parent window (since the child window is being closed)
711  */
712 ZmBaseController.showSummary =
713 function(summary, actionLogItem, showToastOnParentWindow) {
714 	
715 	if (!summary) {
716 		return;
717 	}
718 	var ctxt = showToastOnParentWindow ? parentAppCtxt : appCtxt;
719 	var actionController = ctxt.getActionController();
720 	var undoLink = actionLogItem && actionController && actionController.getUndoLink(actionLogItem);
721 	if (undoLink && actionController) {
722 		actionController.onPopup();
723 		ctxt.setStatusMsg({msg: summary + undoLink, transitions: actionController.getStatusTransitions()});
724 	} else {
725 		ctxt.setStatusMsg(summary);
726 	}
727 };
728 
729 /**
730  * Flag/unflag an item
731  * 
732  * @private
733  */
734 ZmBaseController.prototype._doFlag =
735 function(items, on) {
736 
737 	items = AjxUtil.toArray(items);
738 	if (!items.length) { return; }
739 
740 	if (items[0].isZmItem) {
741 		if (on !== true && on !== false) {
742 			on = !items[0].isFlagged;
743 		}
744 		var items1 = [];
745 		for (var i = 0; i < items.length; i++) {
746 			if (items[i].isFlagged != on) {
747 				items1.push(items[i]);
748 			}
749 		}
750 	} else {
751 		items1 = items;
752 	}
753 
754 	var params = {items:items1, op:"flag", value:on};
755     params.actionTextKey = on ? 'actionFlag' : 'actionUnflag';
756 	var list = params.list = this._getList(params.items);
757 	this._setupContinuation(this._doFlag, [on], params);
758 	list.flagItems(params);
759 };
760 
761 // TODO: shouldn't this be in ZmMailItemController?
762 ZmBaseController.prototype._doMsgPriority = 
763 function(items, on) {
764 	items = AjxUtil.toArray(items);
765 	if (!items.length) { return; }
766 
767 	if (items[0].isZmItem) {
768 		if (on !== true && on !== false) {
769 			on = !items[0].isPriority;
770 		}
771 		var items1 = [];
772 		for (var i = 0; i < items.length; i++) {
773 			if (items[i].isPriority != on) {
774 				items1.push(items[i]);
775 			}
776 		}
777 	} else {
778 		items1 = items;
779 	}
780 
781 	var params = {items:items1, op:"priority", value:on};
782     params.actionTextKey = on ? 'actionMsgPriority' : 'actionUnMsgPriority';
783 	var list = params.list = this._getList(params.items);
784 	this._setupContinuation(this._doMsgPriority, [on], params);
785 	list.flagItems(params);	
786 };
787 
788 /**
789  * Tag/untag items
790  * 
791  * @private
792  */
793 ZmBaseController.prototype._doTag =
794 function(items, tag, doTag) {
795 
796 	items = AjxUtil.toArray(items);
797 	if (!items.length) { return; }
798 
799 	//see bug 79756 as well as this bug, bug 98316.
800 	for (var i = 0; i < items.length; i++) {
801 		if (items[i].cloneOf) {
802 			items[i] = items[i].cloneOf;
803 		}
804 	}
805 
806 	var params = {items:items, tag:tag, doTag:doTag};
807 	var list = params.list = this._getList(params.items);
808 	this._setupContinuation(this._doTag, [tag, doTag], params);
809 	list.tagItems(params);
810 };
811 
812 /**
813  * Remove all tags for given items
814  * 
815  * @private
816  */
817 ZmBaseController.prototype._doRemoveAllTags =
818 function(items) {
819 
820 	items = AjxUtil.toArray(items);
821 	if (!items.length) { return; }
822 
823 	//see bug 79756 as well as this bug.
824 	for (var i = 0; i < items.length; i++) {
825 		if (items[i].cloneOf) {
826 			items[i] = items[i].cloneOf;
827 		}
828 	}
829 	var params = {items:items};
830 	var list = params.list = this._getList(params.items);
831 	this._setupContinuation(this._doRemoveAllTags, null, params);
832 	list.removeAllTags(params);
833 };
834 
835 /**
836 * Deletes one or more items from the list.
837 *
838 * @param items			[Array]			list of items to delete
839 * @param hardDelete		[boolean]*		if true, physically delete items
840 * @param attrs			[Object]*		additional attrs for SOAP command
841 * @param confirmDelete  [Boolean]       user already confirmed hard delete (see ZmBriefcaseController.prototype._doDelete and ZmBriefcaseController.prototype._doDelete2) 
842 * 
843 * @private
844 */
845 ZmBaseController.prototype._doDelete =
846 function(items, hardDelete, attrs, confirmDelete) {
847 
848 	items = AjxUtil.toArray(items);
849 	if (!items.length) { return; }
850 
851 	// If the initial set of deletion items is incomplete (we will be using continuation) then if its deletion
852 	// from the trash folder mark it as a hardDelete.  Otherwise, upon continuation the items will be moved
853 	// (Trash to Trash) instead of deleted.
854 	var folder = this._getSearchFolder();
855 	var inTrashFolder = (folder && folder.nId == ZmFolder.ID_TRASH);
856 	if (inTrashFolder) {
857 		hardDelete = true;
858 	}
859 
860 	var params = {
861 		items:			items,
862 		hardDelete:		hardDelete,
863 		attrs:			attrs,
864 		childWin:		appCtxt.isChildWindow && window,
865 		closeChildWin:	appCtxt.isChildWindow,
866 		confirmDelete:	confirmDelete
867 	};
868 	var allDoneCallback = this._getAllDoneCallback();
869 	var list = params.list = this._getList(params.items);
870 	this._setupContinuation(this._doDelete, [hardDelete, attrs, true], params, allDoneCallback);
871 	
872 	if (!hardDelete) {
873 		var anyScheduled = false;
874 		for (var i=0, cnt=items.length; i<cnt; i++) {
875 			if (items[i] && items[i].isScheduled) {
876 				anyScheduled = true;
877 				break;
878 			}
879 		}
880 		if (anyScheduled) {
881 			params.noUndo = true;
882 			this._popupScheduledWarningDialog(list.deleteItems.bind(list, params));
883 		} else {
884 			list.deleteItems(params);
885 		}
886 	} else {
887 		list.deleteItems(params);
888 	}
889 };
890 
891 /**
892  * Moves a list of items to the given folder. Any item already in that folder is excluded.
893  *
894  * @param {Array}	items		a list of items to move
895  * @param {ZmFolder}	folder		the destination folder
896  * @param {Object}	attrs		the additional attrs for SOAP command
897  * @param {Boolean}		isShiftKey	<code>true</code> if forcing a copy action
898  * @param {Boolean}		noUndo	<code>true</code> undo not allowed
899  * @private
900  */
901 ZmBaseController.prototype._doMove =
902 function(items, folder, attrs, isShiftKey, noUndo) {
903 
904 	items = AjxUtil.toArray(items);
905 	if (!items.length) { return; }
906 
907 	var move = [];
908 	var copy = [];
909 	if (items[0].isZmItem) {
910 		for (var i = 0; i < items.length; i++) {
911 			var item = items[i];
912 			if (!item.folderId || (item.folderId != folder.id || (attrs && attrs.op == "recover"))) {
913 				if (!this._isItemMovable(item, isShiftKey, folder)) {
914 					copy.push(item);
915 				} else {
916 					move.push(item);
917 				}
918 			}
919 		}
920 	} else {
921 		move = items;
922 	}
923 
924 	var params = {folder:folder, attrs:attrs, noUndo: noUndo};
925     params.errorCallback = this._actionErrorCallback.bind(this);
926 
927 	var allDoneCallback = this._getAllDoneCallback();
928 	if (move.length) {
929 		params.items = move;
930 		var list = params.list = this._getList(params.items);
931 		this._setupContinuation(this._doMove, [folder, attrs, isShiftKey], params, allDoneCallback);
932 
933 		if (folder.isInTrash()) {
934 			var anyScheduled = false;
935 			var mItems = AjxUtil.toArray(move);
936 			for (var i=0, cnt=mItems.length; i<cnt; i++) {
937 				if (mItems[i] && mItems[i].isScheduled) {
938 					anyScheduled = true;
939 					break;
940 				}
941 			}
942 			if (anyScheduled) {
943 				params.noUndo = true;
944 				this._popupScheduledWarningDialog(list.moveItems.bind(list, params));
945 			} else {
946 				list.moveItems(params);
947 			}
948 		}
949 		else if (folder.id == appCtxt.get(ZmSetting.MAIL_ACTIVITYSTREAM_FOLDER) && items.length == 1) { 
950 			list.moveItems(params);
951 			var activityStreamDialog = appCtxt.getActivityStreamFilterDialog();
952 			activityStreamDialog.setFields(items[0]);
953 			activityStreamDialog.popup();
954 		}
955 		else if (items.length == 1 && folder.id == ZmFolder.ID_INBOX) {
956 			list.moveItems(params);
957 			var fromFolder = appCtxt.getById(items[0].folderId);
958 			if (fromFolder && fromFolder.id == appCtxt.get(ZmSetting.MAIL_ACTIVITYSTREAM_FOLDER)) { 
959 				var activityStreamDialog = appCtxt.getActivityToInboxFilterDialog();
960 				activityStreamDialog.setFields(items[0]);
961 				activityStreamDialog.popup();
962 			}
963 		}
964 		else {
965 			list.moveItems(params);
966 		}
967 	}
968 
969 	if (copy.length) {
970 		params.items = copy;
971 		var list = params.list = this._getList(params.items);
972 		this._setupContinuation(this._doMove, [folder, attrs, isShiftKey], params, allDoneCallback, true);
973 		list.copyItems(params);
974 	}
975 };
976 
977 ZmBaseController.prototype._actionErrorCallback =
978 function(ex){
979     return false;
980 };
981 
982 ZmBaseController.prototype._popupScheduledWarningDialog =
983 function(callback) {
984 	var dialog = appCtxt.getOkCancelMsgDialog();
985 	dialog.reset();
986 	dialog.setMessage(ZmMsg.moveScheduledMessageWarning, DwtMessageDialog.WARNING_STYLE);
987 	dialog.registerCallback(DwtDialog.OK_BUTTON, this._scheduledWarningDialogListener.bind(this, callback, dialog));
988 	dialog.associateEnterWithButton(DwtDialog.OK_BUTTON);
989 	dialog.popup(null, DwtDialog.OK_BUTTON);
990 };
991 
992 ZmBaseController.prototype._scheduledWarningDialogListener =
993 function(callback, dialog) {
994 	dialog.popdown()
995 	callback();
996 };
997 
998 /**
999  * Decides whether an item is movable
1000  *
1001  * @param {Object}	item			the item to be checked
1002  * @param {Boolean}		isShiftKey	<code>true</code> if forcing a copy (not a move)
1003  * @param {ZmFolder}	folder		the folder this item belongs under
1004  * 
1005  * @private
1006  */
1007 ZmBaseController.prototype._isItemMovable =
1008 function(item, isShiftKey, folder) {
1009 	return (!isShiftKey && !item.isReadOnly() && !folder.isReadOnly());
1010 };
1011 
1012 /**
1013  * Modify an item.
1014  * 
1015  * @private
1016  */
1017 ZmBaseController.prototype._doModify =
1018 function(item, mods) {
1019 	var list = this._getList(item);
1020 	list.modifyItem(item, mods);
1021 };
1022 
1023 /**
1024  * Create an item. We need to be passed a list since we may not have one.
1025  * 
1026  * @private
1027  */
1028 ZmBaseController.prototype._doCreate =
1029 function(list, args) {
1030 	list.create(args);
1031 };
1032 
1033 // Miscellaneous
1034 
1035 
1036 /**
1037  * Add listener to tag menu
1038  * 
1039  * @private
1040  */
1041 ZmBaseController.prototype._setupTagMenu =
1042 function(parent, listener) {
1043 	if (!parent) return;
1044 	var tagMenu = parent.getTagMenu();
1045 	listener = listener || this._listeners[ZmOperation.TAG];
1046 	if (tagMenu) {
1047 		tagMenu.addSelectionListener(listener);
1048 	}
1049 	if (parent.isZmButtonToolBar) {
1050 		var tagButton = parent.getOp(ZmOperation.TAG_MENU);
1051 		if (tagButton) {
1052 			tagButton.addDropDownSelectionListener(this._listeners[ZmOperation.TAG_MENU]);
1053 		}
1054 	}
1055 };
1056 
1057 /**
1058  * setup the move menu
1059  *
1060  * @private
1061  */
1062 ZmBaseController.prototype._setupMoveMenu =
1063 function(parent) {
1064 	if (!parent) {
1065 		return;
1066 	}
1067 	if (!parent.isZmButtonToolBar) {
1068 		return;
1069 	}
1070 	var moveButton = parent.getOp(ZmOperation.MOVE_MENU);
1071 	if (moveButton) {
1072 		moveButton.addDropDownSelectionListener(this._listeners[ZmOperation.MOVE_MENU]);
1073 	}
1074 };
1075 
1076 /**
1077  * Dynamically build the tag menu based on selected items and their tags.
1078  * 
1079  * @private
1080  */
1081 ZmBaseController.prototype._setTagMenu =
1082 function(parent, items) {
1083 
1084 	if (!parent) { return; }
1085 
1086 	var tagOp = parent.getOp(ZmOperation.TAG_MENU);
1087 	if (tagOp) {
1088 		var tagMenu = parent.getTagMenu();
1089 		if (!tagMenu) { return; }
1090 
1091 		// dynamically build tag menu add/remove lists
1092 		items = items || AjxUtil.toArray(this.getItems());
1093 
1094 		for (var i=0; i<items.length; i++) {
1095 			if (items[i].cloneOf) {
1096 				items[i] = items[i].cloneOf;
1097 			}
1098 		}
1099 
1100 		var account = (appCtxt.multiAccounts && items.length == 1) ? items[0].getAccount() : null;
1101 
1102 		// fetch tag tree from appctxt (not cache) for multi-account case
1103 		tagMenu.set(items, appCtxt.getTagTree(account));
1104 		if (parent.isZmActionMenu) {
1105 			tagOp.setText(this._getTagMenuMsg(items.length, items));
1106 		}
1107 		else {
1108 			tagMenu.parent.popup();
1109 
1110 			// bug #17584 - we currently don't support creating new tags in new window
1111 			if (appCtxt.isChildWindow || appCtxt.isWebClientOffline()) {
1112 				var mi = tagMenu.getMenuItem(ZmTagMenu.MENU_ITEM_ADD_ID);
1113 				if (mi) {
1114 					mi.setVisible(false);
1115 				}
1116 			}
1117 		}
1118 	}
1119 };
1120 
1121 /**
1122  * copied some from ZmCalendarApp.createMiniCalButton
1123  * initializes the move button with {@link ZmFolderChooser} as the menu.
1124  *
1125  * @param	{DwtButton}	the button
1126  */
1127 ZmBaseController.prototype._setMoveButton =
1128 function(moveButton) {
1129 
1130 	// create menu for button
1131 	var moveMenu = new DwtMenu({parent: moveButton, style:DwtMenu.CALENDAR_PICKER_STYLE, id: "ZmMoveButton_" + this.getCurrentViewId()});
1132 	moveMenu.getHtmlElement().style.width = "auto"; //make it dynamic  (so expanding long named sub-folders would expand width. (plus right now it sets it to 0 due to some styles)
1133 	moveButton.setMenu(moveMenu, true);
1134 
1135 	var chooser = this._folderChooser = new ZmFolderChooser({parent:moveMenu});
1136 	var moveParams = this._getMoveParams(chooser);
1137 	moveParams.overviewId += this._currentViewId; //so it works when switching views (cuz the tree has a listener and the tree is shared unless it's different ID). maybe there's a different way to solve this.
1138 	chooser.setupFolderChooser(moveParams, this._moveMenuCallback.bind(this, moveButton));
1139 
1140 	return moveButton;
1141 };
1142 
1143 /**
1144  * Resets the available operations on a toolbar or action menu.
1145  * 
1146  * @param {DwtControl}	parent		toolbar or action menu
1147  * @param {number}		num			number of items selected currently
1148  * @private
1149  */
1150 ZmBaseController.prototype._resetOperations =
1151 function(parent, num) {
1152 
1153 	if (!parent) { return; }
1154 
1155 	if (num == 0) {
1156 		parent.enableAll(false);
1157 		parent.enable(this.operationsToEnableOnZeroSelection, true);
1158 	} else if (num == 1) {
1159 		parent.enableAll(true);
1160 		parent.enable(this.operationsToDisableOnSingleSelection, false);
1161 	} else if (num > 1) {
1162 		parent.enableAll(false);
1163 		parent.enable(this.operationsToEnableOnMultiSelection, true);
1164     }
1165 
1166 	// bug: 41758 - don't allow shared items to be tagged
1167 	var folder = (num > 0) && this._getSearchFolder();
1168 	if (folder && folder.isReadOnly()) {
1169 		parent.enable(ZmOperation.TAG_MENU, false);
1170 	}
1171     //this._resetQuickCommandOperations(parent);
1172 };
1173 
1174 /**
1175  * Resets a single operation on a toolbar or action menu.
1176  * 
1177  * @param {DwtControl}	parent		toolbar or action menu
1178  * @param {number}		num			number of items selected currently
1179  * @param {constant}	op			operation
1180  * @private
1181  */
1182 ZmBaseController.prototype._resetOperation = function(parent, num, op) {};
1183 
1184 /**
1185  * Resets the available options on the toolbar.
1186  * 
1187  * @private
1188  */
1189 ZmBaseController.prototype._resetToolbarOperations =
1190 function() {
1191 	this._resetOperations(this._toolbar[this._currentViewId], this.getItemCount());
1192 };
1193 
1194 
1195 /**
1196  * @private
1197  */
1198 ZmBaseController.prototype._getDefaultFocusItem =
1199 function() {
1200 	return this.getCurrentView();
1201 };
1202 
1203 /**
1204  * Sets a callback that shows a summary of what was done. The first three arguments are
1205  * provided for overriding classes that want to apply an action to an extended list of
1206  * items (retrieved via successive search, for example).
1207  *
1208  * @param {function}	actionMethod		the controller action method
1209  * @param {Array}		args				an arg list for above (except for items arg)
1210  * @param {Hash}		params				the params that will be passed to list action method
1211  * @param {closure}		allDoneCallback		the callback to run after all items processed
1212  * 
1213  * @private
1214  */
1215 ZmBaseController.prototype._setupContinuation =
1216 function(actionMethod, args, params, allDoneCallback) {
1217 	params.finalCallback = this._continueAction.bind(this, {allDoneCallback:allDoneCallback});
1218 };
1219 
1220 /**
1221  * Runs the "all done" callback and shows a summary of what was done.
1222  *
1223  * @param {Hash}		params				a hash of parameters
1224  * @param {closure}	 	allDoneCallback		the callback to run when we're all done
1225  * 
1226  * @private
1227  */
1228 ZmBaseController.prototype._continueAction =
1229 function(params) {
1230 
1231 	if (params.allDoneCallback) {
1232 		params.allDoneCallback();
1233 	}
1234 	ZmBaseController.showSummary(params.actionSummary, params.actionLogItem, params.closeChildWin);
1235 };
1236 
1237 
1238 
1239 ZmBaseController.prototype._bubbleSelectionListener = function(ev) {
1240 
1241 	this._actionEv = ev;
1242 	var bubble = ev.item;
1243 	if (ev.detail === DwtEvent.ONDBLCLICK) {
1244 		this._actionEv.bubble = bubble;
1245 		this._actionEv.address = bubble.addrObj || bubble.address;
1246 		this._composeListener(ev);
1247 	}
1248 	else {
1249 		var view = this.getItemView(),
1250 			bubbleList = view && view._bubbleList;
1251 
1252 		if (bubbleList && bubbleList.selectAddressText) {
1253 			bubbleList.selectAddressText();
1254 		}
1255 	}
1256 };
1257 
1258 ZmBaseController.prototype._bubbleActionListener = function(ev, addr) {
1259 
1260 	this._actionEv = ev;
1261 	var bubble = this._actionEv.bubble = ev.item,
1262 		address = this._actionEv.address = addr || bubble.addrObj || bubble.address,
1263 		menu = this._getBubbleActionMenu();
1264 
1265 	if (menu) {
1266 		menu.enable(
1267 			[
1268 				ZmOperation.CONTACT,
1269 				ZmOperation.ADD_TO_FILTER_RULE
1270 			],
1271 			!appCtxt.isWebClientOffline()
1272 		);
1273 		this._loadContactForMenu(menu, address, ev);
1274 	}
1275 };
1276 
1277 ZmBaseController.prototype._getBubbleActionMenu = function() {
1278 
1279 	if (this._bubbleActionMenu) {
1280 		return this._bubbleActionMenu;
1281 	}
1282 
1283 	var menuItems = this._getBubbleActionMenuOps();
1284 	var menu = this._bubbleActionMenu = new ZmActionMenu({
1285 		parent:     this._shell,
1286 		menuItems:  menuItems,
1287 		controller: this,
1288 		id:         ZmId.create({
1289 			componentType:  ZmId.WIDGET_MENU,
1290 			componentName:  this._currentViewId,
1291 			app:            this._app
1292 		})
1293 	});
1294 
1295 	if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
1296 		this._setSearchMenu(menu, false);
1297 	}
1298 
1299 	if (appCtxt.get(ZmSetting.FILTERS_ENABLED) && this._setAddToFilterMenu) {
1300 		this._setAddToFilterMenu(menu);
1301 	}
1302 
1303 	menu.addPopdownListener(this._bubbleMenuPopdownListener.bind(this));
1304 
1305 	for (var i = 0; i < menuItems.length; i++) {
1306 		var menuItem = menuItems[i];
1307 		if (this._listeners[menuItem]) {
1308 			menu.addSelectionListener(menuItem, this._listeners[menuItem]);
1309 		}
1310 	}
1311 
1312 	menu.setVisible(true);
1313 	var clipboard = appCtxt.getClipboard();
1314 	if (clipboard) {
1315 		clipboard.init(menu.getOp(ZmOperation.COPY), {
1316 			onMouseDown:    this._clipCopy.bind(this),
1317 			onComplete:     this._clipCopyComplete.bind(this)
1318 		});
1319 	}
1320 
1321 	return menu;
1322 };
1323 
1324 ZmBaseController.prototype._getBubbleActionMenuOps = function() {
1325 
1326 	var ops = [];
1327 	if (AjxClipboard.isSupported()) {
1328 		// we use Zero Clipboard (a Flash hack) to copy address
1329 		ops.push(ZmOperation.COPY);
1330 	}
1331 	ops.push(ZmOperation.SEARCH_MENU);
1332 	ops.push(ZmOperation.NEW_MESSAGE);
1333 	ops.push(ZmOperation.CONTACT);
1334 	ops.push(ZmOperation.GO_TO_URL);
1335 
1336 	if (appCtxt.get(ZmSetting.FILTERS_ENABLED) && this._filterListener) {
1337 		ops.push(ZmOperation.ADD_TO_FILTER_RULE);
1338 	}
1339 
1340 	return ops;
1341 };
1342 
1343 // Copies address text from the active bubble to the clipboard.
1344 ZmBaseController.prototype._clipCopy = function(clip) {
1345 	clip.setText(this._actionEv.address + AjxEmailAddress.SEPARATOR);
1346 };
1347 
1348 ZmBaseController.prototype._clipCopyComplete = function(clip) {
1349 	this._bubbleActionMenu.popdown();
1350 };
1351 
1352 // This will get called before the menu item listener. If that causes issues,
1353 // we can run this function on a timer.
1354 ZmBaseController.prototype._bubbleMenuPopdownListener = function() {
1355 
1356 	var itemView = this.getItemView(),
1357 		bubbleList = itemView && itemView._bubbleList,
1358 		bubble = this._actionEv && this._actionEv.bubble;
1359 
1360 	if (bubbleList) {
1361 		bubbleList.clearRightSelection();
1362 		if (bubble) {
1363 			bubble.setClassName(bubbleList._normalClass);
1364 		}
1365 	}
1366 	this._actionEv.bubble = null;
1367 };
1368 
1369 // handle click on an address (or "Select All") in popup DL expansion list
1370 ZmBaseController.prototype._dlAddrSelected = function(match, ev) {
1371 	this._actionEv.address = match;
1372 	this._composeListener(ev);
1373 };
1374 
1375 ZmBaseController.prototype._loadContactForMenu = function(menu, address, ev, imItem) {
1376 
1377 	var ac = window.parentAppCtxt || appCtxt;
1378 	var contactsApp = ac.getApp(ZmApp.CONTACTS),
1379 		address = address.isAjxEmailAddress ? address : new AjxEmailAddress(address),
1380 		email = address.getAddress();
1381 
1382 	if (!email) {
1383 		return;
1384 	}
1385 
1386 	// first check if contact is cached, and no server call is needed
1387 	var contact = contactsApp.getContactByEmail(email);
1388 	if (contact) {
1389 		this._handleResponseGetContact(menu, address, ev, imItem, contact);
1390 		return;
1391 	}
1392 
1393 	var op = menu.getOp(ZmOperation.CONTACT);
1394 	if (op) {
1395 		op.setText(ZmMsg.loading);
1396 	}
1397 	if (imItem) {
1398 		if (ZmImApp.updateImMenuItemByAddress(imItem, address, false)) {
1399 			imItem.setText(ZmMsg.loading);
1400 		}
1401 		else {
1402 			imItem = null;	// done updating item, didn't need server call
1403 		}
1404 	}
1405 	menu.popup(0, ev.docX || ev.item.getXW(), ev.docY || ev.item.getYH());
1406 	var respCallback = this._handleResponseGetContact.bind(this, menu, address, ev, imItem);
1407 	contactsApp.getContactByEmail(email, respCallback);
1408 };
1409 
1410 ZmBaseController.prototype._handleResponseGetContact = function(menu, address, ev, imItem, contact) {
1411 
1412 	this._actionEv.contact = contact;
1413 	this._setContactText(contact, menu);
1414 
1415 	if (imItem) {
1416 		if (contact) {
1417 			ZmImApp.updateImMenuItemByContact(imItem, contact, address);
1418 		}
1419 		else {
1420 			ZmImApp.handleResponseGetContact(imItem, address, true);
1421 		}
1422 	}
1423 	menu.popup(0, ev.docX || ev.item.getXW(), ev.docY || ev.item.getYH());
1424 };
1425 
1426 /**
1427  * Sets text to "add" or "edit" based on whether a participant is a contact or not.
1428  * contact - the contact (or null)
1429  * extraMenu - see ZmMailListController.prototype._setContactText
1430  *
1431  * @private
1432  */
1433 ZmBaseController.prototype._setContactText = function(contact, menu) {
1434 	ZmBaseController.setContactTextOnMenu(contact, menu || this._actionMenu);
1435 };
1436 
1437 /**
1438  * Sets text to "add" or "edit" based on whether a participant is a contact or not.
1439  * contact - the contact (or null)
1440  * menus - array of one or more menus
1441  *
1442  * @private
1443  */
1444 ZmBaseController.setContactTextOnMenu = function(contact, menu) {
1445 
1446 	if (!menu) {
1447 		return;
1448 	}
1449 
1450 	var newOp = ZmOperation.EDIT_CONTACT;
1451 	var newText = null; //no change ("edit contact")
1452 
1453 	if (contact && contact.isDistributionList()) {
1454 		newText = ZmMsg.AB_EDIT_DL;
1455 	}
1456 	else if (contact && contact.isGroup()) {
1457 		newText = ZmMsg.AB_EDIT_GROUP;
1458 	}
1459 	else if (!contact || contact.isGal) {
1460 		// if there's no contact, or it's a GAL contact - there's no "edit" - just "add".
1461 		newText = ZmMsg.AB_ADD_CONTACT;
1462 		newOp = ZmOperation.NEW_CONTACT;
1463 	}
1464 
1465 	ZmOperation.setOperation(menu, ZmOperation.CONTACT, newOp, newText);
1466 
1467 	if (appCtxt.isWebClientOffline()) {
1468 		menu.enable(ZmOperation.CONTACT, false);
1469 	}
1470 };
1471 
1472 /**
1473  * Add listener to search menu
1474  *
1475  * @param parent
1476  */
1477 ZmBaseController.prototype._setSearchMenu = function(parent, isToolbar) {
1478 
1479 	var searchMenu = parent && parent.getSearchMenu && parent.getSearchMenu();
1480 	if (!searchMenu) {
1481 		return;
1482 	}
1483 	searchMenu.addSelectionListener(ZmOperation.SEARCH, this._searchListener.bind(this, AjxEmailAddress.FROM, isToolbar));
1484 	searchMenu.addSelectionListener(ZmOperation.SEARCH_TO, this._searchListener.bind(this, AjxEmailAddress.TO, isToolbar));
1485 
1486 	if (this.getSearchFromText()) {
1487 		searchMenu.getMenuItem(ZmOperation.SEARCH).setText(this.getSearchFromText());
1488 	}
1489 	if (this.getSearchToText()) {
1490 		searchMenu.getMenuItem(ZmOperation.SEARCH_TO).setText(this.getSearchToText());
1491 	}
1492 };
1493 
1494 /**
1495  * From Search based on email address.
1496  *
1497  * @private
1498  */
1499 ZmBaseController.prototype._searchListener = function(addrType, isToolbar, ev) {
1500 
1501 	var folder = this._getSearchFolder(),
1502 		item = this._actionEv.item,
1503 		address = this._actionEv.address,
1504 		name;
1505 
1506 	if (item && item.isZmMailMsg && folder && folder.isOutbound()) {
1507 		/* sent/drafts search from all recipients */
1508 		var toAddrs = item.getAddresses(AjxEmailAddress.TO).getArray(),
1509 			ccAddrs = item.getAddresses(AjxEmailAddress.CC).getArray();
1510 
1511 		name = toAddrs.concat(ccAddrs);
1512 	}
1513 	else if (address) {
1514 		name = address.isAjxEmailAddress ? address.getAddress() : address;
1515 	}
1516 
1517 	if (name) {
1518         var ac = window.parentAppCtxt || window.appCtxt;
1519 		var srchCtlr = ac.getSearchController();
1520 		if (addrType === AjxEmailAddress.FROM) {
1521 			srchCtlr.fromSearch(name);
1522 		}
1523 		else if (addrType === AjxEmailAddress.TO) {
1524 			srchCtlr.toSearch(name);
1525 		}
1526 	}
1527 };
1528 
1529 /**
1530  * Compose message to participant.
1531  *
1532  * @private
1533  */
1534 ZmBaseController.prototype._composeListener = function(ev, addr) {
1535 
1536 	var addr = addr || (this._actionEv && this._actionEv.address),
1537 		email = addr && addr.toString();
1538 
1539 	if (email) {
1540 		AjxDispatcher.run("Compose", {
1541 			action:         ZmOperation.NEW_MESSAGE,
1542 			inNewWindow:    this._app._inNewWindow(ev),
1543 			toOverride:     email + AjxEmailAddress.SEPARATOR
1544 		});
1545 	}
1546 };
1547 
1548 /**
1549  * If there's a contact for the participant, edit it, otherwise add it.
1550  *
1551  * @private
1552  */
1553 ZmBaseController.prototype._contactListener = function(ev) {
1554 	var loadCallback = this._handleLoadContactListener.bind(this);
1555 	AjxDispatcher.require(["ContactsCore", "Contacts"], false, loadCallback, null, true);
1556 };
1557 
1558 /**
1559  * @private
1560  */
1561 ZmBaseController.prototype._handleLoadContactListener = function() {
1562 
1563 	var cc = appCtxt.isChildWindow ? window.parentAppCtxt.getApp(ZmApp.CONTACTS).getContactController() :
1564 									AjxDispatcher.run("GetContactController");
1565 	var contact = this._actionEv.contact;
1566 	if (contact) {
1567 		if (contact.isDistributionList()) {
1568 			this._editListener(this._actionEv, contact);
1569 			return;
1570 		}
1571 		if (contact.isLoaded) {
1572 			var isDirty = contact.isGal;
1573 			cc.show(contact, isDirty);
1574 		} else {
1575 			var callback = this._loadContactCallback.bind(this);
1576 			contact.load(callback);
1577 		}
1578 	} else {
1579 		var contact = cc._createNewContact(this._actionEv);
1580 		cc.show(contact, true);
1581 	}
1582 	if (appCtxt.isChildWindow) {
1583 		window.close();
1584 	}
1585 };
1586 
1587 ZmBaseController.prototype.getSearchFromText = function() {
1588 	return null;
1589 };
1590 
1591 ZmBaseController.prototype.getSearchToText = function() {
1592 	return null;
1593 };
1594 
1595 ZmBaseController.prototype._createNewContact = function(ev) {
1596 	var contact = new ZmContact(null);
1597 	contact.initFromEmail(ev.address);
1598 	return contact;
1599 };
1600 
1601 ZmBaseController.prototype._loadContactCallback = function(resp, contact) {
1602 	AjxDispatcher.run("GetContactController").show(contact);
1603 };
1604 
1605 ZmBaseController.prototype._getSearchFolder = function() {
1606 	var id = this._getSearchFolderId();
1607 	return id && appCtxt.getById(id);
1608 };
1609 
1610 /**
1611  * This method gets overridden if folder id is retrieved another way
1612  *
1613  * @param {boolean}		allowComplex	if true, search can have other terms aside from the folder term
1614  * @private
1615  */
1616 ZmBaseController.prototype._getSearchFolderId = function(allowComplex) {
1617 	var s = this._activeSearch && this._activeSearch.search;
1618 	return s && (allowComplex || s.isSimple()) && s.folderId;
1619 };
1620 
1621 ZmBaseController.prototype._goToUrlListener = function(ev) {
1622 	var addr = this._getAddress(this._actionEv.address);
1623 	var parts = addr.split("@");
1624 	if (!parts.length) {
1625 		return;
1626 	}
1627 	var domain = parts[1];
1628 	var pieces = domain.split(".");
1629 	var url = "http://" + (pieces.length <= 2 ? "www." + domain : domain);
1630 	window.open(url, "_blank");
1631 
1632 };
1633 
1634 ZmBaseController.prototype._getAddress = function(obj) {
1635 	return obj.isAjxEmailAddress ? obj.address : obj;
1636 };
1637