1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines a tree controller. 27 * 28 */ 29 30 /** 31 * Creates a tree controller. 32 * @class 33 * This class is a base class for controllers for organizers. Those are 34 * represented by trees, both as data and visually. This class uses the support provided by 35 * {@link ZmOperation}. Each type of organizer has a singleton tree controller which manages all 36 * the tree views of that type. 37 * 38 * @author Conrad Damon 39 * 40 * @param {constant} type the type of organizer we are displaying/controlling 41 * 42 * @extends ZmController 43 */ 44 ZmTreeController = function(type) { 45 46 if (arguments.length == 0) { return; } 47 48 ZmController.call(this, null); 49 50 this.type = type; 51 this._opc = appCtxt.getOverviewController(); 52 53 // common listeners 54 this._listeners = {}; 55 this._listeners[ZmOperation.DELETE] = this._deleteListener.bind(this); 56 this._listeners[ZmOperation.DELETE_WITHOUT_SHORTCUT] = this._deleteListener.bind(this); 57 this._listeners[ZmOperation.MOVE] = this._moveListener.bind(this); 58 this._listeners[ZmOperation.EXPAND_ALL] = this._expandAllListener.bind(this); 59 this._listeners[ZmOperation.MARK_ALL_READ] = this._markAllReadListener.bind(this); 60 this._listeners[ZmOperation.SYNC] = this._syncListener.bind(this); 61 this._listeners[ZmOperation.SYNC_ALL] = this._syncAllListener.bind(this); 62 this._listeners[ZmOperation.EDIT_PROPS] = this._editPropsListener.bind(this); 63 this._listeners[ZmOperation.EMPTY_FOLDER] = this._emptyListener.bind(this); 64 this._listeners[ZmOperation.FIND_SHARES] = this._findSharesListener.bind(this); 65 this._listeners[ZmOperation.OPEN_IN_TAB] = this._openInTabListener.bind(this); 66 67 // drag-and-drop 68 this._dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE); 69 this._dragSrc.addDragListener(this._dragListener.bind(this)); 70 this._dropTgt = new DwtDropTarget(ZmTreeController.DROP_SOURCES[type]); 71 this._dropTgt.addDropListener(this._dropListener.bind(this)); 72 73 this._treeView = {}; // hash of tree views of this type, by overview ID 74 this._hideEmpty = {}; // which tree views to hide if they have no data 75 this._dataTree = {}; // data tree per account 76 77 this._treeSelectionShortcutDelay = ZmTreeController.TREE_SELECTION_SHORTCUT_DELAY; 78 }; 79 80 ZmTreeController.prototype = new ZmController; 81 ZmTreeController.prototype.constructor = ZmTreeController; 82 83 ZmTreeController.COLOR_CLASS = {}; 84 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_ORANGE] = "OrangeBg"; 85 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_BLUE] = "BlueBg"; 86 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_CYAN] = "CyanBg"; 87 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_GREEN] = "GreenBg"; 88 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_PURPLE] = "PurpleBg"; 89 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_RED] = "RedBg"; 90 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_YELLOW] = "YellowBg"; 91 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_PINK] = "PinkBg"; 92 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_GRAY] = "Gray"; // not GrayBg so it doesn't blend in 93 94 // time that selection via up/down arrow must remain on an item to trigger a search 95 ZmTreeController.TREE_SELECTION_SHORTCUT_DELAY = 750; 96 97 // valid sources for drop target for different tree controllers 98 ZmTreeController.DROP_SOURCES = {}; 99 100 // interval of retrying empty folder (seconds) 101 ZmTreeController.EMPTY_FOLDER_RETRY_INTERVAL = 5; 102 103 // the maximum number of trials of empty folder 104 ZmTreeController.EMPTY_FOLDER_MAX_TRIALS = 6; 105 106 // Abstract protected methods 107 108 // Enables/disables operations based on the given organizer ID 109 ZmTreeController.prototype.resetOperations = function() {}; 110 111 // Returns a list of desired header action menu operations 112 ZmTreeController.prototype._getHeaderActionMenuOps = function() {}; 113 114 // Returns a list of desired action menu operations 115 ZmTreeController.prototype._getActionMenuOps = function() {}; 116 117 // Returns the dialog for organizer creation 118 ZmTreeController.prototype._getNewDialog = function() {}; 119 120 // Returns the dialog for renaming an organizer 121 ZmTreeController.prototype._getRenameDialog = function() {}; 122 123 // Method that is run when a tree item is left-clicked 124 ZmTreeController.prototype._itemClicked = function() {}; 125 126 // Method that is run when a tree item is dbl-clicked 127 ZmTreeController.prototype._itemDblClicked = function() {}; 128 129 // Handles a drop event 130 ZmTreeController.prototype._dropListener = function() {}; 131 132 // Returns an appropriate title for the "Move To" dialog 133 ZmTreeController.prototype._getMoveDialogTitle = function() {}; 134 135 /** 136 * @private 137 */ 138 ZmTreeController.prototype._resetOperation = 139 function(parent, id, text, image, enabled, visible) { 140 var op = parent && parent.getOp(id); 141 if (!op) return; 142 143 if (text) op.setText(text); 144 if (image) op.setImage(image); 145 if (enabled != null) op.setEnabled(enabled); 146 if (visible != null) op.setVisible(visible); 147 }; 148 149 /** 150 * @private 151 */ 152 ZmTreeController.prototype._resetButtonPerSetting = 153 function(parent, op, isSupported) { 154 var button = parent.getOp(op); 155 if (button) { 156 if (isSupported) { 157 button.setVisible(true); 158 if (appCtxt.isOffline && !appCtxt.getActiveAccount().isZimbraAccount) { 159 button.setEnabled(false); 160 } 161 } else { 162 button.setVisible(false); 163 } 164 } 165 }; 166 167 ZmTreeController.prototype._enableRecoverDeleted = 168 function (parent, isTrash) { 169 op = parent.getOp(ZmOperation.RECOVER_DELETED_ITEMS); 170 if (!op) { 171 return; 172 } 173 var featureEnabled = appCtxt.get(ZmSetting.DUMPSTER_ENABLED); 174 op.setVisible(featureEnabled && isTrash); 175 op.setEnabled(isTrash); 176 }; 177 178 ZmTreeController.prototype._findSharesListener = 179 function(ev) { 180 var folder = this._getActionedOrganizer(ev); 181 var account = folder.getAccount(); 182 183 if (appCtxt.multiAccounts && account && account.isZimbraAccount) { 184 appCtxt.accountList.setActiveAccount(account); 185 } 186 var dialog = appCtxt.getShareSearchDialog(); 187 var addCallback = this._handleAddShare.bind(this); 188 dialog.popup(folder.type, addCallback); 189 }; 190 191 ZmTreeController.prototype._handleAddShare = function () { 192 var dialog = appCtxt.getShareSearchDialog(); 193 var shares = dialog.getShares(); 194 dialog.popdown(); 195 if (shares.length === 0) { 196 return; 197 } 198 199 AjxDispatcher.require("Share"); 200 var requests = []; 201 for (var i = 0; i < shares.length; i++) { 202 var share = shares[i]; 203 requests.push({ 204 _jsns: "urn:zimbraMail", 205 link: { 206 l: ZmOrganizer.ID_ROOT, 207 name: share.defaultMountpointName, 208 view: share.view, 209 zid: share.ownerId, 210 rid: share.folderId 211 } 212 }); 213 } 214 215 var params = { 216 jsonObj: { 217 BatchRequest: { 218 _jsns: "urn:zimbra", 219 CreateMountpointRequest: requests 220 } 221 }, 222 asyncMode: true 223 }; 224 appCtxt.getAppController().sendRequest(params); 225 }; 226 227 // Opens a view of the given organizer in a search tab 228 ZmTreeController.prototype._openInTabListener = function(ev) { 229 this._itemClicked(this._getActionedOrganizer(ev), true); 230 }; 231 232 233 234 235 236 // Public methods 237 238 /** 239 * Returns a string representation of the object. 240 * 241 * @return {String} a string representation of the object 242 */ 243 ZmTreeController.prototype.toString = 244 function() { 245 return "ZmTreeController"; 246 }; 247 248 /** 249 * Displays the tree of this type. 250 * 251 * @param {Hash} params a hash of parameters 252 * @param {constant} params.overviewId the overview ID 253 * @param {Boolean} params.showUnread if <code>true</code>, unread counts will be shown 254 * @param {Object} params.omit a hash of organizer IDs to ignore 255 * @param {Object} params.include a hash of organizer IDs to include 256 * @param {Boolean} params.forceCreate if <code>true</code>, tree view will be created 257 * @param {String} params.app the app that owns the overview 258 * @param {Boolean} params.hideEmpty if <code>true</code>, don't show header if there is no data 259 * @param {Boolean} params.noTooltips if <code>true</code>, don't show tooltips for tree items 260 */ 261 ZmTreeController.prototype.show = 262 function(params) { 263 var id = params.overviewId; 264 this._hideEmpty[id] = params.hideEmpty; 265 266 if (!this._treeView[id] || params.forceCreate) { 267 this._treeViewCreated = false; 268 this._treeView[id] = null; 269 this._treeView[id] = this.getTreeView(id, true); 270 } 271 272 // bug fix #24241 - for offline, zimlet tree is re-used across accounts 273 var isMultiAccountZimlet = (appCtxt.multiAccounts && this.type == ZmOrganizer.ZIMLET); 274 var account = isMultiAccountZimlet 275 ? appCtxt.accountList.mainAccount 276 : (this.type == ZmOrganizer.VOICE ? id : params.account); // HACK for voice app 277 var dataTree = this.getDataTree(account); 278 279 if (dataTree) { 280 params.dataTree = dataTree; 281 var setting = ZmOrganizer.OPEN_SETTING[this.type]; 282 params.collapsed = (!isMultiAccountZimlet && (!(!setting || (appCtxt.get(setting, null, account) !== false)))); // yikes! 283 284 var overview = this._opc.getOverview(id); 285 286 if (overview && overview.showNewButtons && this.type != ZmOrganizer.ZIMLET && this.type != ZmId.ORG_PREF_PAGE ) { 287 this._setupOptButton(params); 288 } 289 290 this._treeView[id].set(params); 291 this._checkTreeView(id); 292 } 293 294 if (!this._treeViewCreated) { 295 this._treeViewCreated = true; 296 this._postSetup(id, params.account); 297 } 298 return this._treeView[id]; 299 }; 300 301 /** 302 * Gets the tree view for the given overview. 303 * 304 * @param {constant} overviewId the overview ID 305 * @param {Boolean} force if <code>true</code>, force tree view creation 306 * @return {ZmTreeView} the tree view 307 */ 308 ZmTreeController.prototype.getTreeView = 309 function(overviewId, force) { 310 // TODO: What side-effects will this have in terms of the _postSetup??? 311 if (force && !this._treeView[overviewId]) { 312 this._treeView[overviewId] = this._setup(overviewId); 313 } 314 return this._treeView[overviewId]; 315 }; 316 317 /** 318 * Clears the tree view for the given overview. 319 * 320 * @param {constant} overviewId the overview ID 321 * 322 */ 323 ZmTreeController.prototype.clearTreeView = 324 function(overviewId) { 325 // TODO: remove change listener if last tree view cleared 326 if (this._treeView[overviewId]) { 327 this._treeView[overviewId].dispose(); 328 delete this._treeView[overviewId]; 329 } 330 }; 331 332 /** 333 * Gets the controller drop target. 334 * 335 * @return {DwtDropTarget} the drop target 336 */ 337 ZmTreeController.prototype.getDropTarget = 338 function() { 339 return this._dropTgt; 340 }; 341 342 /** 343 * Gets the data tree. 344 * 345 * @param {ZmZimbraAccount} account the account 346 * @return {Object} the data tree 347 */ 348 ZmTreeController.prototype.getDataTree = 349 function(account) { 350 account = account || appCtxt.getActiveAccount(); 351 var dataTree = this._dataTree[account.id]; 352 if (!dataTree) { 353 dataTree = this._dataTree[account.id] = appCtxt.getTree(this.type, account); 354 if (dataTree) { 355 dataTree.addChangeListener(this._getTreeChangeListener()); 356 } 357 } 358 return dataTree; 359 }; 360 361 /** 362 * Dispose of this controller. Removes the tree change listener. 363 * called when ZmComposeController is disposed (new window). 364 * If the change listener stayed we would get exceptions since this window will no longer exist. 365 * 366 */ 367 ZmTreeController.prototype.dispose = 368 function() { 369 var account = appCtxt.getActiveAccount(); 370 var dataTree = this._dataTree[account.id]; 371 if (!dataTree) { 372 return; 373 } 374 dataTree.removeChangeListener(this._getTreeChangeListener()); 375 }; 376 377 378 379 ZmTreeController.prototype.setVisibleIfExists = 380 function(parent, opId, visible) { 381 var op = parent.getOp(opId); 382 if (!op) { 383 return; 384 } 385 op.setVisible(visible); 386 }; 387 388 // Private and protected methods 389 390 /** 391 * Sets up the params for the new button in the header item 392 * 393 * @param {Hash} params a hash of parameters 394 * 395 * @private 396 */ 397 ZmTreeController.prototype._setupOptButton = 398 function(params) { 399 var tooltipKey = ZmOperation.getProp(ZmOperation.OPTIONS, "tooltipKey"); 400 params.optButton = { 401 image: ZmOperation.getProp(ZmOperation.OPTIONS, "image"), 402 tooltip: tooltipKey ? ZmMsg[tooltipKey] : null, 403 callback: new AjxCallback(this, this._dispOpts) 404 }; 405 }; 406 407 /** 408 * Shows options for header item 409 * 410 * @param {Hash} params a hash of parameters 411 * 412 * @private 413 */ 414 415 ZmTreeController.prototype._dispOpts = 416 function(ev){ 417 418 var treeItem = ev.dwtObj; 419 420 var type = treeItem && treeItem.getData(ZmTreeView.KEY_TYPE); 421 if (!type) { return; } 422 423 var actionMenu = this._getHeaderActionMenu(ev); 424 if (actionMenu) { 425 actionMenu.popup(0, ev.docX, ev.docY); 426 } 427 }; 428 429 ZmTreeController.prototype._getTreeChangeListener = 430 function() { 431 if (!this._dataChangeListener) { 432 this._dataChangeListener = appCtxt.isChildWindow ? AjxCallback.simpleClosure(this._treeChangeListener, this) : new AjxListener(this, this._treeChangeListener); 433 } 434 return this._dataChangeListener; 435 }; 436 437 /** 438 * Performs initialization. 439 * 440 * @param overviewId [constant] overview ID 441 */ 442 ZmTreeController.prototype._setup = 443 function(overviewId) { 444 var treeView = this._initializeTreeView(overviewId); 445 if (this._opc.getOverview(overviewId).actionSupported) { 446 this._initializeActionMenus(); 447 } 448 return treeView; 449 }; 450 451 /** 452 * Performs any little fixups after the tree view is first created 453 * and shown. 454 * 455 * @param {constant} overviewId the overview ID 456 * @param {ZmZimbraAccount} account the current account 457 * 458 * @private 459 */ 460 ZmTreeController.prototype._postSetup = 461 function(overviewId, account) { 462 463 var treeView = this.getTreeView(overviewId); 464 if (!treeView.isCheckedStyle && !ZmOrganizer.HAS_COLOR[this.type]) { return; } 465 466 var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT, account); 467 var rootTreeItem = treeView.getTreeItemById(rootId); 468 if (!rootTreeItem) { return; } 469 if (treeView.isCheckedStyle) { 470 rootTreeItem.showCheckBox(false); 471 } 472 var treeItems = rootTreeItem.getItems(); 473 for (var i = 0; i < treeItems.length; i++) { 474 this._fixupTreeNode(treeItems[i], null, treeView, true); 475 } 476 }; 477 478 /** 479 * Takes care of the tree item's color and/or checkbox. 480 * 481 * @param {DwtTreeItem} treeItem the tree item 482 * @param {ZmOrganizer} organizer the organizer it represents 483 * @param {ZmTreeView} treeView the tree view this organizer belongs to 484 * 485 * @private 486 */ 487 ZmTreeController.prototype._fixupTreeNode = 488 function(treeItem, organizer, treeView, skipNotify) { 489 if (treeItem._isSeparator) { return; } 490 organizer = organizer || treeItem.getData(Dwt.KEY_OBJECT); 491 if (organizer) { 492 if (ZmOrganizer.HAS_COLOR[this.type]) { 493 this._setTreeItemColor(treeItem, organizer); 494 } 495 if (treeView.isCheckedStyle) { 496 if ((organizer.type == this.type && treeView.isCheckedStyle) || 497 organizer.nId == ZmOrganizer.ID_TRASH || organizer.nId == ZmOrganizer.ID_DRAFTS) { 498 treeItem.setChecked(organizer.isChecked, true); 499 } else { 500 treeItem.showCheckBox(false); 501 treeItem.enableSelection(true); 502 } 503 } 504 505 // set expand state per user's prefs 506 this._expandTreeItem(treeItem, skipNotify); 507 } 508 var treeItems = treeItem.getItems(); 509 for (var i = 0; i < treeItems.length; i++) { 510 this._fixupTreeNode(treeItems[i], null, treeView, skipNotify); 511 } 512 }; 513 514 ZmTreeController.prototype._expandTreeItem = 515 function(treeItem, skipNotify) { 516 var expanded = appCtxt.get(ZmSetting.FOLDERS_EXPANDED); 517 var folderId = treeItem.getData(Dwt.KEY_ID); 518 var parentTi = treeItem.parent; 519 520 // only expand if the parent is also expanded 521 if (expanded[folderId] && 522 parentTi && (parentTi instanceof DwtTreeItem) && parentTi.getExpanded()) 523 { 524 treeItem.setExpanded(true, null, skipNotify); 525 } 526 }; 527 528 ZmTreeController.prototype._expandTreeItems = 529 function(treeItem) { 530 if (treeItem._isSeparator) { return; } 531 532 this._expandTreeItem(treeItem); 533 534 // recurse! 535 var treeItems = treeItem.getItems(); 536 for (var i = 0; i < treeItems.length; i++) { 537 this._expandTreeItems(treeItems[i]); 538 } 539 }; 540 541 /** 542 * Sets the background color of the tree item. 543 * 544 * @param treeItem [DwtTreeItem] tree item 545 * @param organizer [ZmOrganizer] organizer it represents 546 */ 547 ZmTreeController.prototype._setTreeItemColor = 548 function(treeItem, organizer) { 549 treeItem.setImage(organizer.getIconWithColor()); 550 }; 551 552 ZmTreeController.prototype._getTreeItemColorClassName = 553 function(treeItem, organizer) { 554 if (!treeItem || !organizer) { return null; } 555 if (organizer.isInTrash()) { return null; } 556 557 // a color value of 0 means DEFAULT 558 var color = organizer.color 559 ? organizer.color 560 : ZmOrganizer.DEFAULT_COLOR[organizer.type]; 561 562 return (color && (color != ZmOrganizer.C_NONE)) 563 ? ZmTreeController.COLOR_CLASS[color] : ""; 564 }; 565 566 /** 567 * Lazily creates a tree view of this type, using options from the overview. 568 * 569 * @param {constant} overviewId the overview ID 570 * 571 * @private 572 */ 573 ZmTreeController.prototype._initializeTreeView = 574 function(overviewId) { 575 var overview = this._opc.getOverview(overviewId); 576 var params = { 577 parent: overview, 578 parentElement: overview.getTreeParent(this.type), 579 overviewId: overviewId, 580 type: this.type, 581 headerClass: overview.headerClass, 582 dragSrc: (overview.dndSupported ? this._dragSrc : null), 583 dropTgt: (overview.dndSupported ? this._dropTgt : null), 584 treeStyle: overview.treeStyle, 585 isCheckedByDefault: overview.isCheckedByDefault, 586 allowedTypes: this._getAllowedTypes(), 587 allowedSubTypes: this._getAllowedSubTypes() 588 }; 589 params.id = ZmId.getTreeId(overviewId, params.type); 590 if (params.type && params.type.match(/TASK|ADDRBOOK|FOLDER|BRIEFCASE|CALENDAR|PREF_PAGE/) && 591 (!params.headerClass || params.headerClass == "overviewHeader")){ 592 params.headerClass = "FirstOverviewHeader overviewHeader"; 593 } 594 var treeView = this._createTreeView(params); 595 treeView.addSelectionListener(new AjxListener(this, this._treeViewListener)); 596 treeView.addTreeListener(new AjxListener(this, this._treeListener)); 597 598 return treeView; 599 }; 600 601 /** 602 * @private 603 */ 604 ZmTreeController.prototype._createTreeView = 605 function(params) { 606 return new ZmTreeView(params); 607 }; 608 609 /** 610 * Creates up to two action menus, one for the tree view's header item, and 611 * one for the rest of the items. Note that each of these two menus is a 612 * singleton, shared among the tree views of this type. 613 * 614 * @private 615 */ 616 ZmTreeController.prototype._initializeActionMenus = 617 function() { 618 var obj = this; 619 var func = this._createActionMenu; 620 621 var ops = this._getHeaderActionMenuOps(); 622 if (!this._headerActionMenu && ops) { 623 var args = [this._shell, ops]; 624 this._headerActionMenu = new AjxCallback(obj, func, args); 625 } 626 var ops = this._getActionMenuOps(); 627 if (!this._actionMenu && ops) { 628 var args = [this._shell, ops]; 629 this._actionMenu = new AjxCallback(obj, func, args); 630 } 631 }; 632 633 /** 634 * Instantiates the header action menu if necessary. 635 * 636 * @private 637 */ 638 ZmTreeController.prototype._getHeaderActionMenu = 639 function(ev) { 640 if (this._headerActionMenu instanceof AjxCallback) { 641 var callback = this._headerActionMenu; 642 this._headerActionMenu = callback.run(); 643 } 644 return this._headerActionMenu; 645 }; 646 647 /** 648 * Instantiates the action menu if necessary. 649 * 650 * @private 651 */ 652 ZmTreeController.prototype._getActionMenu = 653 function(ev, item) { 654 var controller = this; 655 656 // special case - search folder. might have moved under a regular folder 657 if (item && item.type == ZmOrganizer.SEARCH) { 658 controller = this._opc.getTreeController(ZmOrganizer.SEARCH); 659 } 660 661 if (controller._actionMenu instanceof AjxCallback) { 662 var callback = controller._actionMenu; 663 controller._actionMenu = callback.run(); 664 } 665 return controller._actionMenu; 666 }; 667 668 /** 669 * Creates and returns an action menu, and sets its listeners. 670 * 671 * @param {DwtControl} parent the menu parent widget 672 * @param {Array} menuItems the list of menu items 673 * 674 * @private 675 */ 676 ZmTreeController.prototype._createActionMenu = 677 function(parent, menuItems) { 678 if (!menuItems) return; 679 680 var map = appCtxt.getCurrentController() && appCtxt.getCurrentController().getKeyMapName(); 681 var id = map ? ("ZmActionMenu_" + map):Dwt.getNextId("ZmActionMenu_") 682 id = (map && this.type) ? id + "_" + this.type : id; 683 var actionMenu = new ZmActionMenu({parent:parent, menuItems:menuItems, id: id}); 684 685 menuItems = actionMenu.opList; 686 for (var i = 0; i < menuItems.length; i++) { 687 var menuItem = menuItems[i]; 688 if (this._listeners[menuItem]) { 689 actionMenu.addSelectionListener(menuItem, this._listeners[menuItem]); 690 } 691 } 692 actionMenu.addPopdownListener(new AjxListener(this, this._menuPopdownActionListener)); 693 694 return actionMenu; 695 }; 696 697 /** 698 * Determines which types of organizer may be displayed at the top level. By default, 699 * the tree shows its own type. 700 * 701 * @private 702 */ 703 ZmTreeController.prototype._getAllowedTypes = 704 function() { 705 var types = {}; 706 types[this.type] = true; 707 return types; 708 }; 709 710 /** 711 * Determines which types of organizer may be displayed below the top level. By default, 712 * the tree shows its own type. 713 * 714 * @private 715 */ 716 ZmTreeController.prototype._getAllowedSubTypes = 717 function() { 718 var types = {}; 719 types[this.type] = true; 720 return types; 721 }; 722 723 // Actions 724 725 /** 726 * Creates a new organizer and adds it to the tree of that type. 727 * 728 * @param {Hash} params a hash of parameters 729 * @param {constant} params.type the type of organizer 730 * @param {ZmOrganizer} params.parent parent of the new organizer 731 * @param {String} params.name the name of the new organizer 732 * 733 * @private 734 */ 735 ZmTreeController.prototype._doCreate = 736 function(params) { 737 params.type = this.type; 738 var funcName = ZmOrganizer.CREATE_FUNC[this.type]; 739 if (funcName) { 740 var func = eval(funcName); 741 return func(params); 742 } 743 }; 744 745 /** 746 * Deletes an organizer and removes it from the tree. 747 * 748 * @param {ZmOrganizer} organizer the organizer to delete 749 */ 750 ZmTreeController.prototype._doDelete = 751 function(organizer) { 752 organizer._delete(); 753 }; 754 755 /** 756 * 757 * @param {ZmOrganizer} organizer the organizer 758 * @param {int} trialCounter the number of trials of empty folder 759 * @param {AjxException} ex the exception 760 * 761 * @private 762 */ 763 ZmTreeController.prototype._doEmpty = 764 function(organizer, trialCounter, ex) { 765 var recursive = false; 766 var timeout = ZmTreeController.EMPTY_FOLDER_RETRY_INTERVAL; 767 var noBusyOverlay = true; 768 if (!trialCounter) { 769 trialCounter = 1; 770 } 771 var errorCallback = this._doEmptyErrorHandler.bind(this, organizer, trialCounter); 772 organizer.empty(recursive, null, this._doEmptyHandler.bind(this, organizer), timeout, errorCallback, noBusyOverlay); 773 }; 774 775 /** 776 * 777 * @param {ZmOrganizer} organizer the organizer 778 * @param {int} trialCounter the number of trials of empty folder 779 * @param {AjxException} ex the exception 780 * 781 * @private 782 */ 783 ZmTreeController.prototype._doEmptyErrorHandler = 784 function(organizer, trialCounter, ex) { 785 if (ex) { 786 if (ex.code == ZmCsfeException.SVC_ALREADY_IN_PROGRESS) { 787 appCtxt.setStatusMsg(ZmMsg.emptyFolderAlreadyInProgress); 788 return true; 789 } else if(ex.code != AjxException.CANCELED) { 790 return false; 791 } 792 } 793 794 if (trialCounter > ZmTreeController.EMPTY_FOLDER_MAX_TRIALS -1){ 795 appCtxt.setStatusMsg(ZmMsg.emptyFolderNoResponse, ZmStatusView.LEVEL_CRITICAL); 796 return true; 797 } 798 trialCounter++; 799 this._doEmpty(organizer, trialCounter); 800 }; 801 802 ZmTreeController.prototype._doEmptyHandler = 803 function(organizer) { 804 appCtxt.setStatusMsg({msg: AjxMessageFormat.format(ZmMsg.folderEmptied, organizer.getName())}); 805 var ctlr = appCtxt.getCurrentController(); 806 if (!ctlr || !ctlr._getSearchFolderId || !ctlr.getListView) { 807 return; 808 } 809 var folderId = ctlr._getSearchFolderId(); 810 if (folderId !== organizer.id) { 811 return; 812 } 813 var view = ctlr.getListView(); 814 view._resetList(); 815 view._setNoResultsHtml(); 816 }; 817 818 /** 819 * Renames an organizer. 820 * 821 * @param {ZmOrganizer} organizer the organizer to rename 822 * @param {String} name the new name of the organizer 823 * 824 * @private 825 */ 826 ZmTreeController.prototype._doRename = 827 function(organizer, name) { 828 organizer.rename(name); 829 }; 830 831 /** 832 * Moves an organizer to a new folder. 833 * 834 * @param {ZmOrganizer} organizer the organizer to move 835 * @param {ZmFolder} folder the target folder 836 * 837 * @private 838 */ 839 ZmTreeController.prototype._doMove = 840 function(organizer, folder) { 841 organizer.move(folder); 842 }; 843 844 /** 845 * Marks an organizer's items as read. 846 * 847 * @param {ZmOrganizer} organizer the organizer 848 * 849 * @private 850 */ 851 ZmTreeController.prototype._doMarkAllRead = 852 function(organizer) { 853 organizer.markAllRead(); 854 }; 855 856 /** 857 * Syncs an organizer to its feed (URL). 858 * 859 * @param {ZmOrganizer} organizer the organizer 860 * 861 * @private 862 */ 863 ZmTreeController.prototype._doSync = 864 function(organizer) { 865 organizer.sync(); 866 }; 867 868 // Listeners 869 870 /** 871 * Handles left and right mouse clicks. A left click generates a selection event. 872 * If selection is supported for the overview, some action (typically a search) 873 * will be performed. A right click generates an action event, which pops up an 874 * action menu if supported. 875 * 876 * @param {DwtUiEvent} ev the UI event 877 * 878 * @private 879 */ 880 ZmTreeController.prototype._treeViewListener = function(ev) { 881 882 if (ev.detail !== DwtTree.ITEM_ACTIONED && ev.detail !== DwtTree.ITEM_SELECTED && ev.detail !== DwtTree.ITEM_DBL_CLICKED) { 883 return; 884 } 885 886 var treeItem = ev.item; 887 888 var type = treeItem.getData(ZmTreeView.KEY_TYPE); 889 if (!type) { 890 return; 891 } 892 893 var item = treeItem.getData(Dwt.KEY_OBJECT); 894 if (item) { 895 this._actionedOrganizer = item; 896 if (item.noSuchFolder) { 897 var folderTree = appCtxt.getFolderTree(); 898 if (folderTree) { 899 folderTree.handleDeleteNoSuchFolder(item); 900 } 901 return; 902 } 903 if (item && item.type === ZmOrganizer.SEARCH) { 904 var controller = this._opc.getTreeController(ZmOrganizer.SEARCH); 905 if (controller) { 906 controller._actionedOrganizer = item; 907 controller._actionedOverviewId = treeItem.getData(ZmTreeView.KEY_ID); 908 } 909 } 910 } 911 912 var id = treeItem.getData(Dwt.KEY_ID); 913 var overviewId = this._actionedOverviewId = treeItem.getData(ZmTreeView.KEY_ID); 914 var overview = this._opc.getOverview(overviewId); 915 if (!overview) { 916 return; 917 } 918 919 if (ev.detail === DwtTree.ITEM_ACTIONED) { 920 // right click 921 if (overview.actionSupported) { 922 var actionMenu = this.getItemActionMenu(ev, item); 923 if (actionMenu) { 924 this.resetOperations(actionMenu, type, id); 925 actionMenu.popup(0, ev.docX, ev.docY); 926 } 927 } 928 } 929 else if ((ev.detail === DwtTree.ITEM_SELECTED) && item) { 930 if (appCtxt.multiAccounts && (item instanceof ZmOrganizer)) { 931 this._handleMultiAccountItemSelection(ev, overview, treeItem, item); 932 } 933 else { 934 this._handleItemSelection(ev, overview, treeItem, item); 935 } 936 } 937 else if ((ev.detail === DwtTree.ITEM_DBL_CLICKED) && item) { 938 this._itemDblClicked(item); 939 } 940 }; 941 942 ZmTreeController.prototype.getItemActionMenu = function(ev, item) { 943 var actionMenu = (item.nId == ZmOrganizer.ID_ROOT || item.isDataSource(ZmAccount.TYPE_IMAP)) 944 ? this._getHeaderActionMenu(ev) 945 : this._getActionMenu(ev, item); 946 return actionMenu; 947 } 948 949 /** 950 * @private 951 */ 952 ZmTreeController.prototype._handleItemSelection = 953 function(ev, overview, treeItem, item) { 954 // left click or selection via shortcut 955 overview.itemSelected(treeItem); 956 957 if (ev.kbNavEvent) { 958 Dwt.scrollIntoView(treeItem._itemDiv, overview.getHtmlElement()); 959 ZmController.noFocus = true; 960 } 961 962 if (overview._treeSelectionShortcutDelayActionId) { 963 AjxTimedAction.cancelAction(overview._treeSelectionShortcutDelayActionId); 964 } 965 966 if ((overview.selectionSupported || item._showFoldersCallback) && !treeItem._isHeader) { 967 if (ev.kbNavEvent) { 968 // for shortcuts, process selection via Enter immediately; selection via up/down keys 969 // is delayed (or can be disabled by setting the delay to 0) 970 if (ev.enter || this._treeSelectionShortcutDelay) { 971 var action = new AjxTimedAction(this, ZmTreeController.prototype._treeSelectionTimedAction, [item, overview]); 972 overview._treeSelectionShortcutDelayActionId = AjxTimedAction.scheduleAction(action, this._treeSelectionShortcutDelay); 973 } 974 } else { 975 if ((appCtxt.multiAccounts && (item instanceof ZmOrganizer)) || 976 (item.type == ZmOrganizer.VOICE)) 977 { 978 appCtxt.getCurrentApp().getOverviewContainer().deselectAll(overview); 979 980 // set the active account based on the item clicked 981 var account = item.account || appCtxt.accountList.mainAccount; 982 appCtxt.accountList.setActiveAccount(account); 983 } 984 985 this._itemSelected(item); 986 } 987 } 988 }; 989 990 /** 991 * @private 992 */ 993 ZmTreeController.prototype._itemSelected = 994 function(item) { 995 if (item && item._showFoldersCallback) { 996 item._showFoldersCallback.run(); 997 } else { 998 this._itemClicked(item); 999 } 1000 1001 }; 1002 1003 /** 1004 * Allows subclass to overload in case something needs to be done before 1005 * processing tree item selection in a multi-account environment. Otherwise, 1006 * do the normal tree item selection. 1007 * 1008 * @private 1009 */ 1010 ZmTreeController.prototype._handleMultiAccountItemSelection = 1011 function(ev, overview, treeItem, item) { 1012 this._handleItemSelection(ev, overview, treeItem, item); 1013 }; 1014 1015 /** 1016 * @private 1017 */ 1018 ZmTreeController.prototype._treeSelectionTimedAction = 1019 function(item, overview) { 1020 if (overview._treeSelectionShortcutDelayActionId) { 1021 AjxTimedAction.cancelAction(overview._treeSelectionShortcutDelayActionId); 1022 } 1023 this._itemSelected(item); 1024 }; 1025 1026 /** 1027 * Propagates a change in tree state to other trees of the same type in app overviews. 1028 * 1029 * @param {ZmTreeEvent} ev a tree event 1030 * 1031 * @private 1032 */ 1033 ZmTreeController.prototype._treeListener = 1034 function(ev) { 1035 var treeItem = ev && ev.item; 1036 var overviewId = treeItem && treeItem._tree && treeItem._tree.overviewId; 1037 var overview = appCtxt.getOverviewController().getOverview(overviewId); 1038 var acct = overview.account; 1039 if (appCtxt.multiAccounts && acct) { 1040 appCtxt.accountList.setActiveAccount(acct); 1041 } 1042 1043 // persist expand/collapse state for folders 1044 var isExpand = ev.detail == DwtTree.ITEM_EXPANDED; 1045 var folderId = (ev.detail == DwtTree.ITEM_COLLAPSED || isExpand) 1046 ? treeItem.getData(Dwt.KEY_ID) : null; 1047 1048 if (folderId && !treeItem._isHeader) { 1049 var setExpanded = appCtxt.get(ZmSetting.FOLDERS_EXPANDED, folderId) || false; //I think it's set as undefined if "false" in ZmSetting.prototype.setValue) 1050 if (typeof(setExpanded) == "string") {//I can't figure out why it becomes a string sometimes. That's nasty. 1051 setExpanded = (setExpanded === "true"); 1052 } 1053 //setting in case of skipImplicit is still causing problems (the fix to bug 72590 was not good enough), since even if this "set" is not persisting, 1054 //future ones (collapse/expand in the mail tab) would cause it to save implicitly, which is not what we want. 1055 //so I simply do not call "set" in case of skipImplicit. Might want to change the name of this variable slightly, but not sure to what. 1056 if (!overview.skipImplicit && setExpanded !== isExpand) { //set only if changed (ZmSetting.prototype.setValue is supposed to not send a request if no change, but it might have bugs) 1057 appCtxt.set(ZmSetting.FOLDERS_EXPANDED, isExpand, folderId); 1058 } 1059 1060 // check if any of this treeItem's children need to be expanded as well 1061 if (isExpand) { 1062 this._expandTreeItems(treeItem); 1063 } 1064 } 1065 1066 // only handle events that come from headers in app overviews 1067 if (!(ev && ev.detail && overview && overview.isAppOverview && treeItem._isHeader)) { return; } 1068 1069 var settings = appCtxt.getSettings(acct); 1070 var setting = settings.getSetting(ZmOrganizer.OPEN_SETTING[this.type]); 1071 if (setting) { 1072 setting.setValue(ev.detail == DwtTree.ITEM_EXPANDED); 1073 } 1074 }; 1075 1076 /** 1077 * Handles changes to the underlying model. The change is propagated to 1078 * all the tree views known to this controller. 1079 * 1080 * @param {ZmEvent} ev a change event 1081 * 1082 * @private 1083 */ 1084 ZmTreeController.prototype._treeChangeListener = 1085 function(ev) { 1086 this._evHandled = {}; 1087 for (var overviewId in this._treeView) { 1088 this._changeListener(ev, this._treeView[overviewId], overviewId); 1089 } 1090 }; 1091 1092 /** 1093 * Handles a change event for one tree view. 1094 * 1095 * @param {ZmEvent} ev a change event 1096 * @param {ZmTreeView} treeView a tree view 1097 * @param {constant} overviewId overview ID 1098 * 1099 * @private 1100 */ 1101 ZmTreeController.prototype._changeListener = 1102 function(ev, treeView, overviewId) { 1103 if (this._evHandled[overviewId]) { return; } 1104 if (!treeView.allowedTypes[ev.type] && !treeView.allowedSubTypes[ev.type]) { return; } 1105 1106 var organizers = ev.getDetail("organizers"); 1107 if (!organizers && ev.source) { 1108 organizers = [ev.source]; 1109 } 1110 1111 // handle one organizer at a time 1112 for (var i = 0; i < organizers.length; i++) { 1113 var organizer = organizers[i]; 1114 1115 var node = treeView.getTreeItemById(organizer.id); 1116 // Note: source tree handles moves - it will have node 1117 if (!node && (ev.event != ZmEvent.E_CREATE)) { continue; } 1118 1119 var fields = ev.getDetail("fields"); 1120 if (ev.event == ZmEvent.E_DELETE) { 1121 if (organizer.nId == ZmFolder.ID_TRASH || organizer.nId == ZmFolder.ID_SPAM) { 1122 node.setText(organizer.getName(false)); // empty Trash or Junk 1123 } else { 1124 node.dispose(); 1125 } 1126 this._checkTreeView(overviewId); 1127 this._evHandled[overviewId] = true; 1128 } else if (ev.event == ZmEvent.E_CREATE || ev.event == ZmEvent.E_MOVE) { 1129 // for multi-account, make sure this organizer applies to the given overview 1130 if (appCtxt.multiAccounts) { 1131 var overview = this._opc.getOverview(overviewId); 1132 if (overview && overview.account != organizer.getAccount()) { 1133 continue; 1134 } 1135 } 1136 var parentNode = this._getParentNode(organizer, ev, overviewId); 1137 var idx = parentNode ? ZmTreeView.getSortIndex(parentNode, organizer, eval(ZmTreeView.COMPARE_FUNC[organizer.type])) : null; 1138 if (parentNode && (ev.event == ZmEvent.E_CREATE)) { 1139 // parent's tree controller should handle creates - root is shared by all folder types 1140 var type = ((organizer.parent.nId == ZmOrganizer.ID_ROOT) || organizer.parent.isRemoteRoot()) ? ev.type : organizer.parent.type; 1141 if (type !== this.type || !treeView._isAllowed(organizer.parent, organizer)) { 1142 continue; 1143 } 1144 if (organizer.isOfflineGlobalSearch) { 1145 appCtxt.getApp(ZmApp.MAIL).getOverviewContainer().addSearchFolder(organizer); 1146 return; 1147 } else { 1148 node = this._addNew(treeView, parentNode, organizer, idx); // add to new parent 1149 } 1150 this.createDataSource(organizer); 1151 } else if (ev.event == ZmEvent.E_MOVE) { 1152 var selectedItem = treeView.getSelected(); 1153 if (AjxUtil.isArray1(selectedItem)) { //make sure this tree is not a checked style one (no idea where we have that, but see the getSelected code 1154 selectedItem = null; 1155 } 1156 node.dispose(); 1157 if (parentNode) { 1158 node = this._addNew(treeView, parentNode, organizer, idx); // add to new parent 1159 } 1160 //highlight the current chosen one again, in case it was moved, thus losing selection 1161 if (!treeView.getSelected() && selectedItem) { //if item was selected but now it is not 1162 treeView.setSelected(selectedItem.id, true, true); 1163 } 1164 } 1165 if (parentNode) { 1166 parentNode.setExpanded(true); // so that new node is visible 1167 1168 this._fixupTreeNode(node, organizer, treeView); 1169 } 1170 this._checkTreeView(overviewId); 1171 this._evHandled[overviewId] = true; 1172 } else if (ev.event == ZmEvent.E_MODIFY) { 1173 if (!fields) { return; } 1174 if (fields[ZmOrganizer.F_TOTAL] || fields[ZmOrganizer.F_SIZE] || fields[ZmOrganizer.F_UNREAD] || fields[ZmOrganizer.F_NAME]) { 1175 node.setToolTipContent(organizer.getToolTip(true)); 1176 if (appCtxt.multiAccounts && organizer.type == ZmOrganizer.FOLDER) { 1177 appCtxt.getApp(ZmApp.MAIL).getOverviewContainer().updateTooltip(organizer.nId); 1178 } 1179 } 1180 1181 if (fields[ZmOrganizer.F_NAME] || 1182 fields[ZmOrganizer.F_UNREAD] || 1183 fields[ZmOrganizer.F_FLAGS] || 1184 fields[ZmOrganizer.F_COLOR] || 1185 ((organizer.nId == ZmFolder.ID_DRAFTS || organizer.rid == ZmFolder.ID_DRAFTS || 1186 organizer.nId == ZmOrganizer.ID_OUTBOX) && fields[ZmOrganizer.F_TOTAL])) 1187 { 1188 this._updateOverview({ 1189 organizer: organizer, 1190 node: node, 1191 fields: fields, 1192 treeView: treeView, 1193 overviewId: overviewId, 1194 ev: ev 1195 }); 1196 1197 this._evHandled[overviewId] = true; 1198 } 1199 } 1200 } 1201 }; 1202 1203 /** 1204 * Handle an organizer change by updating the tree view. For example, a name change requires sorting. 1205 * 1206 * @param params hash hash of params: 1207 * 1208 * organizer ZmOrganizer organizer that changed 1209 * node DwtTreeItem organizer node in tree view 1210 * fields hash changed fields 1211 * treeView ZmTreeView tree view for this organizer type 1212 * overviewId string ID of containing overview 1213 * ev ZmEvent change event 1214 * 1215 * @private 1216 */ 1217 ZmTreeController.prototype._updateOverview = function(params) { 1218 1219 var org = params.organizer, 1220 node = params.node, 1221 parentNode = this._getParentNode(org, params.ev, params.overviewId); 1222 1223 node.setText(org.getName(params.treeView._showUnread)); 1224 1225 // If the name changed, re-sort the containing list 1226 if (params.fields && params.fields[ZmOrganizer.F_NAME]) { 1227 if (parentNode && (parentNode.getNumChildren() > 1)) { 1228 var nodeSelected = node._selected; 1229 // remove and re-insert the node (if parent has more than one child) 1230 node.dispose(); 1231 var idx = ZmTreeView.getSortIndex(parentNode, org, eval(ZmTreeView.COMPARE_FUNC[org.type])); 1232 node = params.treeView._addNew(parentNode, org, idx); 1233 if (nodeSelected) { 1234 //if it was selected, re-select it so it is highlighted. No need for notifications. 1235 params.treeView.setSelected(org, true); 1236 } 1237 } else { 1238 node.setDndText(org.getName()); 1239 } 1240 appCtxt.getAppViewMgr().updateTitle(); 1241 } 1242 1243 // A folder aggregates unread status of its descendents, so propagate up parent chain 1244 if (params.fields[ZmOrganizer.F_UNREAD]) { 1245 var parent = org.parent; 1246 while (parent && parentNode && parent.nId != ZmOrganizer.ID_ROOT) { 1247 parentNode.setText(parent.getName(params.treeView._showUnread)); 1248 parentNode = this._getParentNode(parent, params.ev, params.overviewId); 1249 parent = parent.parent; 1250 } 1251 } 1252 1253 // Miscellaneous cleanup (color, selection) 1254 this._fixupTreeNode(node, org, params.treeView); 1255 }; 1256 1257 ZmTreeController.prototype._getParentNode = function(organizer, ev, overviewId) { 1258 1259 if (organizer.parent) { 1260 // if node being moved to root, we assume new parent must be the container of its type 1261 var type = (organizer.parent.nId == ZmOrganizer.ID_ROOT) ? ev.type : null; 1262 return this._opc.getOverview(overviewId).getTreeItemById(organizer.parent.id, type); 1263 } 1264 }; 1265 1266 /** 1267 * Makes a request to add a new item to the tree, returning true if the item was 1268 * actually added, or false if it was omitted. 1269 * 1270 * @param {ZmTreeView} treeView the tree view 1271 * @param {DwtTreeItem} parentNode the node under which to add the new one 1272 * @param {ZmOrganizer} organizer the organizer for the new node 1273 * @param {int} idx the position at which to add the new node 1274 * 1275 * @private 1276 */ 1277 ZmTreeController.prototype._addNew = 1278 function(treeView, parentNode, organizer, idx) { 1279 return treeView._addNew(parentNode, organizer, idx); 1280 }; 1281 1282 /** 1283 * Pops up the appropriate "New ..." dialog. 1284 * 1285 * @param {DwtUiEvent} ev the UI event 1286 * @param {ZmZimbraAccount} account used by multi-account mailbox (optional) 1287 * 1288 * @private 1289 */ 1290 ZmTreeController.prototype._newListener = 1291 function(ev, account) { 1292 this._pendingActionData = this._getActionedOrganizer(ev); 1293 var newDialog = this._getNewDialog(); 1294 if (!this._newCb) { 1295 this._newCb = new AjxCallback(this, this._newCallback); 1296 } 1297 if (this._pendingActionData && !appCtxt.getById(this._pendingActionData.id)) { 1298 this._pendingActionData = appCtxt.getFolderTree(account).root; 1299 } 1300 1301 if (!account && appCtxt.multiAccounts) { 1302 var ov = this._opc.getOverview(this._actionedOverviewId); 1303 account = ov && ov.account; 1304 } 1305 1306 ZmController.showDialog(newDialog, this._newCb, this._pendingActionData, account); 1307 newDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, newDialog); 1308 }; 1309 1310 ZmTreeController.prototype.createDataSource = 1311 function(organizer) { 1312 //override 1313 }; 1314 1315 /** 1316 * Pops up the appropriate "Rename ..." dialog. 1317 * 1318 * @param {DwtUiEvent} ev the UI event 1319 * 1320 * @private 1321 */ 1322 ZmTreeController.prototype._renameListener = 1323 function(ev) { 1324 this._pendingActionData = this._getActionedOrganizer(ev); 1325 var renameDialog = this._getRenameDialog(); 1326 if (!this._renameCb) { 1327 this._renameCb = new AjxCallback(this, this._renameCallback); 1328 } 1329 ZmController.showDialog(renameDialog, this._renameCb, this._pendingActionData); 1330 renameDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, renameDialog); 1331 }; 1332 1333 /** 1334 * Deletes an organizer. 1335 * 1336 * @param {DwtUiEvent} ev the UI event 1337 * 1338 * @private 1339 */ 1340 ZmTreeController.prototype._deleteListener = 1341 function(ev) { 1342 this._doDelete(this._getActionedOrganizer(ev)); 1343 }; 1344 1345 /** 1346 * @private 1347 */ 1348 ZmTreeController.prototype._emptyListener = 1349 function(ev) { 1350 this._doEmpty(this._getActionedOrganizer(ev)); 1351 }; 1352 1353 /** 1354 * Moves an organizer into another folder. 1355 * 1356 * @param {DwtUiEvent} ev the UI event 1357 * 1358 * @private 1359 */ 1360 ZmTreeController.prototype._moveListener = 1361 function(ev) { 1362 this._pendingActionData = this._getActionedOrganizer(ev); 1363 var moveToDialog = appCtxt.getChooseFolderDialog(); 1364 if (!this._moveCb) { 1365 this._moveCb = new AjxCallback(this, this._moveCallback); 1366 } 1367 ZmController.showDialog(moveToDialog, this._moveCb, this._getMoveParams(moveToDialog)); 1368 moveToDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, moveToDialog); 1369 }; 1370 1371 /** 1372 * @private 1373 */ 1374 ZmTreeController.prototype._getMoveParams = 1375 function(dlg) { 1376 var omit = {}; 1377 omit[ZmFolder.ID_SPAM] = true; 1378 return { 1379 data: this._pendingActionData, 1380 treeIds: [this.type], 1381 overviewId: dlg.getOverviewId(appCtxt.getCurrentAppName() + '_' + this.type), 1382 omit: omit, 1383 title: AjxStringUtil.htmlEncode(this._getMoveDialogTitle()), 1384 description: ZmMsg.targetFolder, 1385 appName: ZmOrganizer.APP[this.type] 1386 }; 1387 }; 1388 1389 /** 1390 * Expands the tree below the action'd node. 1391 * 1392 * @param {DwtUiEvent} ev the UI event 1393 * 1394 * @private 1395 */ 1396 ZmTreeController.prototype._expandAllListener = 1397 function(ev) { 1398 var organizer = this._getActionedOrganizer(ev); 1399 var treeView = this.getTreeView(this._actionedOverviewId); 1400 var ti = treeView.getTreeItemById(organizer.id); 1401 window.duringExpandAll = true; 1402 ti.setExpanded(true, true); 1403 window.duringExpandAll = false; 1404 if (window.afterExpandAllCallback) { 1405 window.afterExpandAllCallback(); //save the explicit setting now after all was expanded - so only one request instead of many 1406 window.afterExpandAllCallback = null; 1407 } 1408 }; 1409 1410 /** 1411 * Mark's an organizer's contents as read. 1412 * 1413 * @param {DwtUiEvent} ev the UI event 1414 * 1415 * @private 1416 */ 1417 ZmTreeController.prototype._markAllReadListener = 1418 function(ev) { 1419 this._doMarkAllRead(this._getActionedOrganizer(ev)); 1420 }; 1421 1422 /** 1423 * Syncs all the organizers to its feed (URL). 1424 * 1425 * @param {DwtUiEvent} ev the UI event 1426 * 1427 * @private 1428 */ 1429 ZmTreeController.prototype._syncAllListener = 1430 function(ev) { 1431 // Loop over all the TreeViews 1432 for (var overviewId in this._treeView) { 1433 var treeView = this.getTreeView(overviewId); 1434 var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT, appCtxt.getActiveAccount()); 1435 var rootTreeItem = treeView.getTreeItemById(rootId); 1436 var treeItems = rootTreeItem && rootTreeItem.getItems(); 1437 if (treeItems) { 1438 for (var i = 0; i < treeItems.length; i++) { 1439 var ti = treeItems[i]; 1440 var folder = ti && ti.getData && ti.getData(Dwt.KEY_OBJECT); 1441 if (folder && (folder.isFeed() || folder.hasFeeds())) { 1442 this._syncFeeds(folder); 1443 } 1444 } 1445 } 1446 } 1447 }; 1448 1449 /** 1450 * Syncs an organizer to its feed (URL). 1451 * 1452 * @param {DwtUiEvent} ev the UI event 1453 * 1454 * @private 1455 */ 1456 ZmTreeController.prototype._syncListener = 1457 function(ev) { 1458 this._syncFeeds(this._getActionedOrganizer(ev)); 1459 }; 1460 1461 /** 1462 * @private 1463 */ 1464 ZmTreeController.prototype._syncFeeds = 1465 function(f) { 1466 if (f.isFeed()) { 1467 this._doSync(f); 1468 } else if (f.hasFeeds()) { 1469 var a = f.children.getArray(); 1470 var sz = f.children.size(); 1471 for (var i = 0; i < sz; i++) { 1472 if (a[i].isFeed() || (a[i].hasFeeds && a[i].hasFeeds())) { 1473 this._syncFeeds(a[i]); 1474 } 1475 } 1476 } 1477 }; 1478 1479 /** 1480 * Brings up a dialog for editing organizer properties. 1481 * 1482 * @param {DwtUiEvent} ev the UI event 1483 * 1484 * @private 1485 */ 1486 ZmTreeController.prototype._editPropsListener = 1487 function(ev) { 1488 var folderPropsDialog = appCtxt.getFolderPropsDialog(); 1489 folderPropsDialog.popup(this._getActionedOrganizer(ev)); 1490 }; 1491 1492 /** 1493 * Handles a drag event by setting the source data. 1494 * 1495 * @param {DwtDragEvent} ev a drag event 1496 * 1497 * @private 1498 */ 1499 ZmTreeController.prototype._dragListener = 1500 function(ev) { 1501 switch (ev.action) { 1502 case DwtDragEvent.SET_DATA: 1503 ev.srcData = {data:ev.srcControl.getData(Dwt.KEY_OBJECT), controller:this}; 1504 break; 1505 } 1506 }; 1507 1508 /** 1509 * Called when a dialog we opened is closed. Sets the style of the actioned 1510 * tree item from "actioned" back to its normal state. 1511 * 1512 * @private 1513 */ 1514 ZmTreeController.prototype._menuPopdownActionListener = 1515 function() { 1516 if (this._pendingActionData) { return; } 1517 1518 var treeView = this.getTreeView(this._actionedOverviewId); 1519 if (this._actionedOrganizer && (treeView.getSelected() != this._actionedOrganizer)) { 1520 var ti = treeView.getTreeItemById(this._actionedOrganizer.id); 1521 if (ti) { 1522 ti._setActioned(false); 1523 } 1524 } 1525 }; 1526 1527 // Callbacks 1528 1529 /** 1530 * Called when a "New ..." dialog is submitted to create the organizer. 1531 * 1532 * @param {Hash} params a hash of parameters 1533 * @param {ZmOrganizer} params.organizer the parent organizer 1534 * @param {String} params.name the name of the new organizer 1535 * 1536 * @private 1537 */ 1538 ZmTreeController.prototype._newCallback = 1539 function(params) { 1540 this._doCreate(params); 1541 this._clearDialog(this._getNewDialog()); 1542 }; 1543 1544 /** 1545 * Called when a "Rename ..." dialog is submitted to rename the organizer. 1546 * 1547 * @param {ZmOrganizer} organizer the organizer 1548 * @param {String} name the new name of the organizer 1549 * 1550 * @private 1551 */ 1552 ZmTreeController.prototype._renameCallback = 1553 function(organizer, name) { 1554 this._doRename(organizer, name); 1555 this._clearDialog(this._getRenameDialog()); 1556 }; 1557 1558 /** 1559 * Called when a "Move To ..." dialog is submitted to move the organizer. 1560 * 1561 * @param {ZmFolder} folder the target folder 1562 * 1563 * @private 1564 */ 1565 ZmTreeController.prototype._moveCallback = 1566 function(folder) { 1567 this._doMove(this._pendingActionData, folder); 1568 this._clearDialog(appCtxt.getChooseFolderDialog()); 1569 }; 1570 1571 /** 1572 * Called if a user has agreed to go ahead and delete an organizer. 1573 * 1574 * @param {ZmOrganizer} organizer the organizer to delete 1575 * 1576 * @private 1577 */ 1578 ZmTreeController.prototype._deleteShieldYesCallback = 1579 function(organizer) { 1580 this._doDelete(organizer); 1581 this._clearDialog(this._deleteShield); 1582 }; 1583 1584 /** 1585 * @private 1586 */ 1587 ZmTreeController.prototype._emptyShieldYesCallback = 1588 function(organizer) { 1589 this._doEmpty(organizer); 1590 this._clearDialog(this._emptyShield); 1591 }; 1592 1593 /** 1594 * Prompts user before folder is emptied. 1595 * 1596 * @param {DwtUiEvent} ev the UI event 1597 * 1598 * @private 1599 */ 1600 1601 ZmTreeController.prototype._getEmptyShieldWarning = 1602 function(ev) { 1603 var organizer = this._pendingActionData = this._getActionedOrganizer(ev); 1604 var ds = this._emptyShield = appCtxt.getOkCancelMsgDialog(); 1605 ds.reset(); 1606 ds.registerCallback(DwtDialog.OK_BUTTON, this._emptyShieldYesCallback, this, organizer); 1607 ds.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, this._emptyShield); 1608 var msg = (organizer.nId != ZmFolder.ID_TRASH) 1609 ? (AjxMessageFormat.format(ZmMsg.confirmEmptyFolder, organizer.getName())) 1610 : ZmMsg.confirmEmptyTrashFolder; 1611 ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE); 1612 1613 var focusButtonId = (organizer.nId == ZmFolder.ID_TRASH || organizer.nId == ZmFolder.ID_SPAM) ? DwtDialog.OK_BUTTON : DwtDialog.CANCEL_BUTTON; 1614 ds.associateEnterWithButton(focusButtonId); 1615 ds.popup(null, focusButtonId); 1616 1617 if (!(organizer.nId == ZmFolder.ID_SPAM || organizer.isInTrash())) { 1618 var cancelButton = ds.getButton(DwtDialog.CANCEL_BUTTON); 1619 cancelButton.focus(); 1620 } 1621 }; 1622 1623 // Miscellaneous private methods 1624 1625 /** 1626 * Returns the organizer that's currently selected for action (via right-click). 1627 * Note: going up the object tree to find the actioned organizer will only work 1628 * for tree item events; it won't work for action menu item events, since action 1629 * menus are children of the shell. 1630 * 1631 * @param {DwtUiEvent} ev the UI event 1632 * 1633 * @private 1634 */ 1635 ZmTreeController.prototype._getActionedOrganizer = 1636 function(ev) { 1637 if (this._actionedOrganizer) { 1638 return this._actionedOrganizer; 1639 } 1640 1641 var obj = ev.item; 1642 while (obj) { 1643 var data = obj.getData(Dwt.KEY_OBJECT); 1644 if (data instanceof ZmOrganizer) { 1645 this._actionedOrganizer = data; 1646 return this._actionedOrganizer; 1647 } 1648 obj = obj.parent; 1649 } 1650 return null; 1651 }; 1652 1653 /** 1654 * Shows or hides the tree view. It is hidden only if there is no data, and we 1655 * have been told to hide empty tree views of this type. 1656 * 1657 * @param {constant} overviewId the overview ID 1658 * 1659 * @private 1660 */ 1661 ZmTreeController.prototype._checkTreeView = 1662 function(overviewId) { 1663 if (!overviewId || !this._treeView[overviewId]) { return; } 1664 1665 var account = this._opc.getOverview(overviewId).account; 1666 var dataTree = this.getDataTree(account); 1667 var hide = (ZmOrganizer.HIDE_EMPTY[this.type] && dataTree && (dataTree.size() == 0)); 1668 this._treeView[overviewId].setVisible(!hide); 1669 }; 1670