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 * 27 * This file defines a folder tree controller. 28 * 29 */ 30 31 /** 32 * Creates a folder tree controller. 33 * @class 34 * This class controls a tree display of folders. 35 * 36 * @param {Constant} type the type of organizer we are displaying/controlling ({@link ZmOrganizer.FOLDER} or {@link ZmOrganizer.SEARCH}) 37 * @param {DwtDropTarget} dropTgt the drop target for this type 38 * 39 * @extends ZmTreeController 40 */ 41 ZmFolderTreeController = function(type, dropTgt) { 42 43 if (!arguments.length) { return; } 44 45 ZmTreeController.call(this, (type || ZmOrganizer.FOLDER)); 46 47 this._listeners[ZmOperation.NEW_FOLDER] = this._newListener.bind(this); 48 this._listeners[ZmOperation.PRIORITY_FILTER] = this._priorityFilterListener.bind(this); 49 this._listeners[ZmOperation.RENAME_FOLDER] = this._renameListener.bind(this); 50 this._listeners[ZmOperation.SHARE_FOLDER] = this._shareFolderListener.bind(this); 51 this._listeners[ZmOperation.EMPTY_FOLDER] = this._emptyListener.bind(this); 52 this._listeners[ZmOperation.RECOVER_DELETED_ITEMS] = this._recoverListener.bind(this); 53 this._listeners[ZmOperation.SYNC_OFFLINE_FOLDER] = this._syncOfflineFolderListener.bind(this); 54 }; 55 56 ZmFolderTreeController.prototype = new ZmTreeController; 57 ZmFolderTreeController.prototype.constructor = ZmFolderTreeController; 58 59 ZmFolderTreeController.prototype.isZmFolderTreeController = true; 60 ZmFolderTreeController.prototype.toString = function() { return "ZmFolderTreeController"; }; 61 62 // Public methods 63 64 /** 65 * Shows the folder tree with certain folders hidden. 66 * 67 * @param {Hash} params a hash of parameters 68 * @param {Array} params.omit an array of folder ids to omit 69 * @param {ZmAccount} params.account the account 70 */ 71 ZmFolderTreeController.prototype.show = 72 function(params) { 73 74 return ZmTreeController.prototype.show.call(this, params); 75 }; 76 77 /** 78 * Resets and enables/disables operations based on context. 79 * 80 * @param {DwtControl} parent the widget that contains the operations 81 * @param {int} id the id of the currently selected/activated organizer 82 */ 83 ZmFolderTreeController.prototype.resetOperations = 84 function(parent, type, id) { 85 86 var emptyText = ZmMsg.emptyFolder; //ZmMsg.empty + (ZmFolder.MSG_KEY[id]?" "+ZmFolder.MSG_KEY[id] : ""); 87 var folder = appCtxt.getById(id); 88 var hasContent = ((folder.numTotal > 0) || (folder.children && (folder.children.size() > 0))); 89 90 // disable empty folder option for inbox, sent and drafts: bug 66656 91 var isEmptyFolderAllowed = true; 92 var y = folder.rid; 93 if (y == ZmFolder.ID_ROOT || y == ZmFolder.ID_INBOX || y == ZmFolder.ID_SENT || y == ZmFolder.ID_DRAFTS) { 94 isEmptyFolderAllowed = false; 95 } 96 97 // user folder or Folders header 98 var nId = ZmOrganizer.normalizeId(id, this.type); 99 if (nId == ZmOrganizer.ID_ROOT || (!folder.isSystem() && !folder.isSystemEquivalent()) /*&& !folder.isSyncIssuesFolder()*/) { 100 var isShareVisible = (!folder.link || folder.isAdmin()); 101 if (appCtxt.isOffline) { 102 isShareVisible = !folder.getAccount().isMain && folder.getAccount().isZimbraAccount; 103 } 104 parent.enableAll(true); 105 var isSubFolderOfReadOnly = folder.parent && folder.parent.isReadOnly(); 106 parent.enable([ZmOperation.DELETE_WITHOUT_SHORTCUT, ZmOperation.MOVE, ZmOperation.EDIT_PROPS], !isSubFolderOfReadOnly); 107 parent.enable(ZmOperation.SYNC, folder.isFeed()/* || folder.hasFeeds()*/); 108 parent.enable(ZmOperation.SYNC_ALL, folder.isFeed() || folder.hasFeeds()); 109 parent.enable(ZmOperation.SHARE_FOLDER, isShareVisible); 110 parent.enable(ZmOperation.EMPTY_FOLDER, ((hasContent || folder.link) && isEmptyFolderAllowed && !appCtxt.isExternalAccount())); // numTotal is not set for shared folders 111 parent.enable(ZmOperation.RENAME_FOLDER, !(isSubFolderOfReadOnly || folder.isDataSource() || appCtxt.isExternalAccount())); // dont allow datasource'd folder to be renamed via overview 112 parent.enable(ZmOperation.NEW_FOLDER, !(folder.disallowSubFolder || appCtxt.isExternalAccount())); 113 114 if (folder.isRemote() && folder.isReadOnly()) { 115 parent.enable([ZmOperation.NEW_FOLDER, ZmOperation.MARK_ALL_READ, ZmOperation.EMPTY_FOLDER], false); 116 } 117 if (appCtxt.isExternalAccount()) { 118 parent.enable([ZmOperation.DELETE_WITHOUT_SHORTCUT, ZmOperation.MOVE], false); 119 } 120 } 121 // system folder 122 else { 123 if (folder.isSystemEquivalent()) { 124 nId = folder.getSystemEquivalentFolderId(); 125 } 126 parent.enableAll(false); 127 // can't create folders under Drafts or Junk 128 if (!folder.disallowSubFolder && 129 (nId == ZmFolder.ID_INBOX || 130 nId == ZmFolder.ID_SENT || 131 nId == ZmFolder.ID_TRASH)) 132 { 133 parent.enable(ZmOperation.NEW_FOLDER, true); 134 } 135 // "Empty" for Chats, Junk and Trash 136 if (nId == ZmFolder.ID_SPAM || 137 nId == ZmFolder.ID_TRASH || 138 nId == ZmFolder.ID_CHATS) 139 { 140 if (nId == ZmFolder.ID_SPAM) { 141 emptyText = ZmMsg.emptyJunk; 142 } else if (nId == ZmFolder.ID_TRASH) { 143 emptyText = ZmMsg.emptyTrash; 144 } 145 parent.enable(ZmOperation.EMPTY_FOLDER, hasContent); 146 } 147 // only allow Inbox and Sent system folders to be share-able for now 148 if (!folder.link && (nId == ZmFolder.ID_INBOX || nId == ZmFolder.ID_SENT || nId == ZmFolder.ID_DRAFTS)) { 149 parent.enable([ZmOperation.SHARE_FOLDER, ZmOperation.EDIT_PROPS], true); 150 } 151 if (appCtxt.multiAccounts) { 152 var isShareVisible = !folder.getAccount().isMain && folder.getAccount().isZimbraAccount; 153 if(nId == ZmFolder.ID_SPAM || nId == ZmFolder.ID_TRASH) { 154 isShareVisible = false; 155 } 156 parent.enable([ZmOperation.SHARE_FOLDER, ZmOperation.EDIT_PROPS], isShareVisible); 157 } 158 // bug fix #30435 - enable empty folder for sync failures folder 159 if (appCtxt.isOffline && nId == ZmOrganizer.ID_SYNC_FAILURES && hasContent) { 160 parent.enable(ZmOperation.EMPTY_FOLDER, true); 161 } 162 } 163 164 parent.enable(ZmOperation.OPEN_IN_TAB, true); 165 parent.enable(ZmOperation.EXPAND_ALL, (folder.size() > 0)); 166 if (nId != ZmOrganizer.ID_ROOT && !folder.isReadOnly()) { 167 // always enable for shared folders since we dont get this info from server 168 parent.enable(ZmOperation.MARK_ALL_READ, !folder.isRemoteRoot() && (folder.numUnread > 0 || folder.link)); 169 } 170 171 var op = parent.getOp(ZmOperation.EMPTY_FOLDER); 172 if (op) { 173 op.setText(emptyText); 174 } 175 176 var isTrash = (nId == ZmOrganizer.ID_TRASH); 177 // are there any external accounts associated to this folder? 178 var button = parent.getOp(ZmOperation.SYNC); 179 if (button) { 180 var syncAllButton = parent.getOp(ZmOperation.SYNC_ALL); 181 var hasFeeds = folder.hasFeeds(); 182 if (folder.isFeed()) { 183 button.setEnabled(true); 184 button.setVisible(true); 185 button.setText(ZmMsg.checkFeed); 186 if (syncAllButton) { 187 syncAllButton.setEnabled(true); 188 syncAllButton.setVisible(true); 189 syncAllButton.setText(ZmMsg.checkAllFeed); 190 } 191 } 192 else if (hasFeeds && !isTrash) { 193 if (syncAllButton){ 194 syncAllButton.setEnabled(true); 195 syncAllButton.setVisible(true); 196 syncAllButton.setText(ZmMsg.checkAllFeed); 197 } 198 } 199 else { 200 var isEnabled = appCtxt.get(ZmSetting.POP_ACCOUNTS_ENABLED) || appCtxt.get(ZmSetting.IMAP_ACCOUNTS_ENABLED); 201 if (!appCtxt.isOffline && isEnabled) { 202 var dsCollection = AjxDispatcher.run("GetDataSourceCollection"); 203 var dataSources = dsCollection.getItemsFor(ZmOrganizer.normalizeId(folder.id)); 204 if (dataSources.length > 0) { 205 button.setText(ZmMsg.checkExternalMail); 206 button.setEnabled(true); 207 button.setVisible(true); 208 } else { 209 button.setVisible(false); 210 } 211 } 212 else { 213 button.setVisible(false); 214 } 215 216 if ((!hasFeeds || isTrash) && syncAllButton) { 217 syncAllButton.setVisible(false); 218 } 219 } 220 } 221 222 button = parent.getOp(ZmOperation.SYNC_OFFLINE_FOLDER); 223 if (button) { 224 if (!folder.isOfflineSyncable) { 225 button.setVisible(false); 226 } else { 227 button.setVisible(true); 228 button.setEnabled(true); 229 var text = (folder.isOfflineSyncing) 230 ? ZmMsg.syncOfflineFolderOff : ZmMsg.syncOfflineFolderOn; 231 button.setText(text); 232 } 233 } 234 var priorityInboxEnabled = appCtxt.get(ZmSetting.PRIORITY_INBOX_ENABLED); 235 var priorityInboxOp = parent.getOp(ZmOperation.PRIORITY_FILTER); 236 if (priorityInboxOp) { 237 priorityInboxOp.setVisible(priorityInboxEnabled); 238 priorityInboxOp.setEnabled(priorityInboxEnabled); 239 } 240 this._enableRecoverDeleted(parent, isTrash); 241 242 // we always enable sharing in case we're in multi-mbox mode 243 this._resetButtonPerSetting(parent, ZmOperation.SHARE_FOLDER, appCtxt.get(ZmSetting.SHARING_ENABLED)); 244 }; 245 246 247 // Private methods 248 249 /** 250 * Returns ops available for "Folders" container. 251 * 252 * @private 253 */ 254 ZmFolderTreeController.prototype._getHeaderActionMenuOps = 255 function() { 256 if (appCtxt.isExternalAccount()) { 257 return [ZmOperation.EXPAND_ALL]; 258 } 259 return [ 260 ZmOperation.NEW_FOLDER, 261 ZmOperation.SEP, 262 ZmOperation.PRIORITY_FILTER, 263 ZmOperation.EXPAND_ALL, 264 ZmOperation.SYNC, 265 ZmOperation.FIND_SHARES 266 ]; 267 }; 268 269 /** 270 * Returns ops available for folder items. 271 * 272 * @private 273 */ 274 ZmFolderTreeController.prototype._getActionMenuOps = function() { 275 276 return [ 277 ZmOperation.NEW_FOLDER, 278 ZmOperation.SYNC, 279 ZmOperation.SYNC_ALL, 280 ZmOperation.MARK_ALL_READ, 281 ZmOperation.EMPTY_FOLDER, 282 ZmOperation.RECOVER_DELETED_ITEMS, 283 ZmOperation.SHARE_FOLDER, 284 ZmOperation.MOVE, 285 ZmOperation.DELETE_WITHOUT_SHORTCUT, 286 ZmOperation.RENAME_FOLDER, 287 ZmOperation.EDIT_PROPS, 288 ZmOperation.SYNC_OFFLINE_FOLDER, 289 ZmOperation.OPEN_IN_TAB, 290 ZmOperation.EXPAND_ALL 291 ]; 292 }; 293 294 /** 295 * @private 296 */ 297 ZmFolderTreeController.prototype._getAllowedSubTypes = 298 function() { 299 var types = {}; 300 types[ZmOrganizer.FOLDER] = true; 301 types[ZmOrganizer.SEARCH] = true; 302 return types; 303 }; 304 305 /** 306 * Returns a "New Folder" dialog. 307 * 308 * @private 309 */ 310 ZmFolderTreeController.prototype._getNewDialog = 311 function() { 312 return appCtxt.getNewFolderDialog(); 313 }; 314 315 /** 316 * Returns a "Rename Folder" dialog. 317 * 318 * @private 319 */ 320 ZmFolderTreeController.prototype._getRenameDialog = 321 function() { 322 return appCtxt.getRenameFolderDialog(); 323 }; 324 325 /** 326 * Called when a left click occurs (by the tree view listener). The folder that 327 * was clicked may be a search, since those can appear in Trash within the folder tree. The 328 * appropriate search will be performed. 329 * 330 * @param {ZmOrganizer} folder the folder or search that was clicked 331 * 332 * @private 333 */ 334 ZmFolderTreeController.prototype._itemClicked = function(folder, openInTab) { 335 336 // bug 41196 - turn off new mail notifier if inactive account folder clicked 337 if (appCtxt.isOffline) { 338 var acct = folder.getAccount(); 339 if (acct && acct.inNewMailMode) { 340 acct.inNewMailMode = false; 341 var allContainers = appCtxt.getOverviewController()._overviewContainer; 342 for (var i in allContainers) { 343 allContainers[i].updateAccountInfo(acct, true, true); 344 } 345 } 346 } 347 348 if (folder.type == ZmOrganizer.SEARCH) { 349 // if the clicked item is a search (within the folder tree), hand 350 // it off to the search tree controller 351 var stc = this._opc.getTreeController(ZmOrganizer.SEARCH); 352 stc._itemClicked(folder, openInTab); 353 } else if (folder.id == ZmFolder.ID_ATTACHMENTS) { 354 var attController = AjxDispatcher.run("GetAttachmentsController"); 355 attController.show(); 356 } 357 else { 358 var searchFor = ZmId.SEARCH_MAIL; 359 if (folder.isInTrash()) { 360 var app = appCtxt.getCurrentAppName(); 361 // if other apps add Trash to their folder tree, set appropriate type here: 362 if (app == ZmApp.CONTACTS) { 363 searchFor = ZmItem.CONTACT; 364 } 365 } 366 var sc = appCtxt.getSearchController(); 367 var acct = folder.getAccount(); 368 369 var sortBy = appCtxt.get(ZmSetting.SORTING_PREF, folder.nId); 370 if (!sortBy) { 371 sortBy = (sc.currentSearch && folder.nId == sc.currentSearch.folderId) ? null : ZmSearch.DATE_DESC; 372 } 373 else { 374 //user may have saved folder with From search then switched views; don't allow From sort in conversation mode 375 var groupMode = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy(); 376 if (groupMode == ZmItem.CONV && (sortBy == ZmSearch.NAME_ASC || sortBy == ZmSearch.NAME_DESC)) { 377 sortBy = appCtxt.get(ZmSetting.SORTING_PREF, appCtxt.getCurrentViewId()); //default to view preference 378 if (!sortBy) { 379 sortBy = ZmSearch.DATE_DESC; //default 380 } 381 appCtxt.set(ZmSetting.SORTING_PREF, sortBy, folder.nId); 382 } 383 } 384 var params = { 385 query: folder.createQuery(), 386 searchFor: searchFor, 387 getHtml: folder.nId == ZmFolder.ID_DRAFTS || appCtxt.get(ZmSetting.VIEW_AS_HTML), 388 types: folder.nId == ZmOrganizer.ID_SYNC_FAILURES ? [ZmItem.MSG] : null, // for Sync Failures folder, always show in traditional view 389 sortBy: sortBy, 390 accountName: acct && acct.name, 391 userInitiated: openInTab, 392 origin: ZmId.SEARCH 393 }; 394 395 sc.resetSearchAllAccounts(); 396 397 if (appCtxt.multiAccounts) { 398 // make sure we have permissions for this folder (in case an "external" 399 // server was down during account load) 400 if (folder.link && folder.perm == null) { 401 var folderTree = appCtxt.getFolderTree(acct); 402 if (folderTree) { 403 var callback = new AjxCallback(this, this._getPermissionsResponse, [params]); 404 folderTree.getPermissions({callback:callback, folderIds:[folder.id]}); 405 } 406 return; 407 } 408 409 if (appCtxt.isOffline && acct.hasNotSynced() && !acct.__syncAsked) { 410 acct.__syncAsked = true; 411 412 var dialog = appCtxt.getYesNoMsgDialog(); 413 dialog.registerCallback(DwtDialog.YES_BUTTON, this._syncAccount, this, [dialog, acct]); 414 dialog.setMessage(ZmMsg.neverSyncedAsk, DwtMessageDialog.INFO_STYLE); 415 dialog.popup(); 416 } 417 } 418 419 sc.search(params); 420 } 421 }; 422 423 /** 424 * @private 425 */ 426 ZmFolderTreeController.prototype._syncAccount = 427 function(dialog, account) { 428 dialog.popdown(); 429 account.sync(); 430 }; 431 432 /** 433 * @private 434 */ 435 ZmFolderTreeController.prototype._getPermissionsResponse = 436 function(params) { 437 appCtxt.getSearchController().search(params); 438 }; 439 440 441 // Actions 442 443 /** 444 * @private 445 */ 446 ZmFolderTreeController.prototype._doSync = 447 function(folder) { 448 var dsc = AjxDispatcher.run("GetDataSourceCollection"); 449 var nFid = ZmOrganizer.normalizeId(folder.id); 450 var dataSources = dsc.getItemsFor(nFid); 451 452 if (dataSources.length > 0) { 453 dsc.importMailFor(nFid); 454 } 455 else { 456 ZmTreeController.prototype._doSync.call(this, folder); 457 } 458 }; 459 460 /** 461 * @private 462 */ 463 ZmFolderTreeController.prototype._syncFeeds = 464 function(folder) { 465 if (!appCtxt.isOffline && folder && !folder.isFeed()) { 466 var dataSources = (appCtxt.get(ZmSetting.POP_ACCOUNTS_ENABLED) || appCtxt.get(ZmSetting.IMAP_ACCOUNTS_ENABLED)) 467 ? folder.getDataSources(null, true) : null; 468 469 if (dataSources) { 470 var dsc = AjxDispatcher.run("GetDataSourceCollection"); 471 dsc.importMail(dataSources); 472 return; 473 } 474 } 475 476 ZmTreeController.prototype._syncFeeds.call(this, folder); 477 }; 478 479 /** 480 * Adds the new item to the tree. 481 * 482 * @param {ZmTreeView} treeView a tree view 483 * @param {DwtTreeItem} parentNode the node under which to add the new one 484 * @param {ZmOrganizer} organizer the organizer for the new node 485 * @param {int} idx theposition at which to add the new node 486 * @return {DwtTreeItem} the resulting item 487 * 488 * @private 489 */ 490 ZmFolderTreeController.prototype._addNew = 491 function(treeView, parentNode, organizer, idx) { 492 if (ZmFolder.HIDE_ID[organizer.id]) { 493 return false; 494 } 495 return treeView._addNew(parentNode, organizer, idx); 496 }; 497 498 // Listeners 499 500 /** 501 * Deletes a folder. If the folder is in Trash, it is hard-deleted. Otherwise, it 502 * is moved to Trash (soft-delete). If the folder is Trash or Junk, it is emptied. 503 * A warning dialog will be shown before the Junk folder is emptied. 504 * 505 * @param {DwtUiEvent} ev the UI event 506 * 507 * @private 508 */ 509 ZmFolderTreeController.prototype._deleteListener = 510 function(ev) { 511 var organizer = this._getActionedOrganizer(ev); 512 513 // bug fix #35405 - accounts with disallowSubFolder flag set (eg Yahoo) do not support moving folder to Trash 514 var trashFolder = appCtxt.isOffline ? this.getDataTree().getById(ZmFolder.ID_TRASH) : null; 515 if (trashFolder && trashFolder.disallowSubFolder && organizer.numTotal > 0) { 516 var d = appCtxt.getMsgDialog(); 517 d.setMessage(ZmMsg.errorCannotDeleteFolder); 518 d.popup(); 519 return; 520 } 521 522 // TODO: not sure what SPAM is doing in here - can you delete it? 523 if (organizer.nId == ZmFolder.ID_SPAM || organizer.isInTrash() || (trashFolder && trashFolder.disallowSubFolder)) { 524 this._pendingActionData = organizer; 525 var ds = this._deleteShield = appCtxt.getOkCancelMsgDialog(); 526 ds.reset(); 527 ds.registerCallback(DwtDialog.OK_BUTTON, this._deleteShieldYesCallback, this, organizer); 528 ds.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, this._deleteShield); 529 var confirm; 530 if (organizer.type === ZmOrganizer.SEARCH) { 531 confirm = ZmMsg.confirmDeleteSavedSearch; 532 } 533 else if (organizer.nId == ZmFolder.ID_TRASH) { 534 confirm = ZmMsg.confirmEmptyTrashFolder; 535 } 536 else if (organizer.nId == ZmFolder.ID_SPAM) { 537 confirm = ZmMsg.confirmEmptyFolder; 538 } 539 else { 540 // TODO: should probably split out msgs by folder type 541 confirm = ZmMsg.confirmDeleteFolder; 542 } 543 var msg = AjxMessageFormat.format(confirm, organizer.getName()); 544 ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE); 545 ds.popup(); 546 } 547 else { 548 this._doMove(organizer, appCtxt.getById(ZmFolder.ID_TRASH)); 549 } 550 }; 551 552 /** 553 * Empties a folder. 554 * It removes all the items in the folder except sub-folders. 555 * If the folder is Trash, it empties even the sub-folders. 556 * A warning dialog will be shown before any folder is emptied. 557 * 558 * @param {DwtUiEvent} ev the UI event 559 * 560 * @private 561 */ 562 ZmFolderTreeController.prototype._emptyListener = 563 function(ev) { 564 this._getEmptyShieldWarning(ev); 565 }; 566 567 ZmFolderTreeController.prototype._recoverListener = 568 function(ev) { 569 appCtxt.getDumpsterDialog().popup(this._getSearchFor(), this._getSearchTypes()); 570 }; 571 572 ZmFolderTreeController.prototype._getSearchFor = 573 function(ev) { 574 return ZmId.SEARCH_MAIL; // Fallback value; subclasses should return differently 575 }; 576 577 ZmFolderTreeController.prototype._getSearchTypes = 578 function(ev) { 579 return [ZmItem.MSG]; // Fallback value; subclasses should return differently 580 }; 581 582 /** 583 * Toggles on/off flag for syncing IMAP folder with server. Only for offline use. 584 * 585 * @param {DwtUiEvent} ev the UI event 586 * 587 * @private 588 */ 589 ZmFolderTreeController.prototype._syncOfflineFolderListener = 590 function(ev) { 591 var folder = this._getActionedOrganizer(ev); 592 if (folder) { 593 folder.toggleSyncOffline(); 594 } 595 }; 596 597 /** 598 * Don't allow dragging of system folders. 599 * 600 * @param {DwtDragEvent} ev the drag event 601 * 602 * @private 603 */ 604 ZmFolderTreeController.prototype._dragListener = 605 function(ev) { 606 if (ev.action == DwtDragEvent.DRAG_START) { 607 var folder = ev.srcControl.getData(Dwt.KEY_OBJECT); 608 ev.srcData = {data:folder, controller:this}; 609 if (!(folder instanceof ZmFolder) || folder.isSystem() /*|| folder.isSyncIssuesFolder()*/) { 610 ev.operation = Dwt.DND_DROP_NONE; 611 } 612 } 613 }; 614 615 /** 616 * Handles the potential drop of something onto a folder. When something is dragged over 617 * a folder, returns true if a drop would be allowed. When something is actually dropped, 618 * performs the move. If items are being dropped, the source data is not the items 619 * themselves, but an object with the items (data) and their controller, so they can be 620 * moved appropriately. 621 * 622 * @param {DwtDropEvent} ev the drop event 623 * 624 * @private 625 */ 626 ZmFolderTreeController.prototype._dropListener = 627 function(ev) { 628 629 var dropFolder = ev.targetControl.getData(Dwt.KEY_OBJECT); 630 var data = ev.srcData.data; 631 var isShiftKey = (ev.shiftKey || ev.uiEvent.shiftKey); 632 633 if (ev.action == DwtDropEvent.DRAG_ENTER) { 634 if (!data) { 635 ev.doIt = false; 636 return; 637 } 638 var type = ev.targetControl.getData(ZmTreeView.KEY_TYPE); 639 if (data instanceof ZmFolder) { 640 ev.doIt = dropFolder.mayContain(data, type) && !dropFolder.disallowSubFolder; 641 } else if (data instanceof ZmTag) { 642 ev.doIt = false; // tags cannot be moved 643 } else { 644 if (this._dropTgt.isValidTarget(data)) { 645 ev.doIt = dropFolder.mayContain(data, type); 646 647 var action; 648 var actionData = AjxUtil.toArray(data); 649 650 // walk thru the array and find out what action is allowed 651 for (var i = 0; i < actionData.length; i++) { 652 if (actionData[i] instanceof ZmItem) { 653 action |= actionData[i].getDefaultDndAction(isShiftKey); 654 } 655 } 656 657 var plusDiv = document.getElementById(DwtId.DND_PLUS_ID); 658 if (action && plusDiv) { 659 // TODO - what if action is ZmItem.DND_ACTION_BOTH ?? 660 var isCopy = ((action & ZmItem.DND_ACTION_COPY) != 0); 661 Dwt.setVisibility(plusDiv, isCopy); 662 } 663 } else { 664 ev.doIt = false; 665 } 666 } 667 } else if (ev.action == DwtDropEvent.DRAG_DROP) { 668 if (data instanceof ZmFolder) { 669 this._doMove(data, dropFolder); 670 } else { 671 var ctlr = ev.srcData.controller; 672 var items = (data instanceof Array) ? data : [data]; 673 if (appCtxt.multiAccounts && !isShiftKey && !dropFolder.getAccount().isMain && 674 this._isMovingAcrossAccount(items, dropFolder)) 675 { 676 var dialog = appCtxt.getYesNoMsgDialog(); 677 dialog.registerCallback(DwtDialog.YES_BUTTON, this._continueMovingAcrossAccount, this, [dialog, ctlr, items, dropFolder]); 678 dialog.setMessage(ZmMsg.moveAcrossAccountWarning, DwtMessageDialog.WARNING_STYLE); 679 dialog.popup(); 680 } 681 else { 682 ctlr._doMove(items, dropFolder, null, isShiftKey); 683 } 684 } 685 } 686 }; 687 688 ZmFolderTreeController.prototype._isMovingAcrossAccount = 689 function(items, dropFolder) { 690 for (var i = 0; i < items.length; i++) { 691 var item = items[i]; 692 var itemAcct = item.getAccount(); 693 if (itemAcct && itemAcct != dropFolder.getAccount()) { 694 return true; 695 } 696 } 697 return false; 698 }; 699 700 ZmFolderTreeController.prototype._continueMovingAcrossAccount = 701 function(dialog, ctlr, items, dropFolder) { 702 dialog.popdown(); 703 ctlr._doMove(items, dropFolder); 704 }; 705 706 707 ZmTreeController.prototype._priorityFilterListener = 708 function(ev) { 709 var priorityFilterDialog = appCtxt.getPriorityMessageFilterDialog(); 710 ZmController.showDialog(priorityFilterDialog); 711 }; 712 713 /** 714 * @private 715 */ 716 ZmFolderTreeController.prototype._shareFolderListener = 717 function(ev) { 718 this._pendingActionData = this._getActionedOrganizer(ev); 719 appCtxt.getSharePropsDialog().popup(ZmSharePropsDialog.NEW, this._pendingActionData); 720 }; 721 722 // Miscellaneous 723 724 /** 725 * Returns a title for moving a folder. 726 * 727 * @return {String} the title 728 * @private 729 */ 730 ZmFolderTreeController.prototype._getMoveDialogTitle = 731 function() { 732 return AjxMessageFormat.format(ZmMsg.moveFolder, this._pendingActionData.name); 733 }; 734