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