1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 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) 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 28 /** 29 * Creates a dialog with various trees so a user can select a folder. 30 * @class 31 * This class represents choose folder dialog. 32 * 33 * @param {DwtControl} shell the parent 34 * @param {String} className the class name 35 * 36 * @extends ZmDialog 37 */ 38 ZmChooseFolderDialog = function(parent, className) { 39 var newButton = new DwtDialog_ButtonDescriptor(ZmChooseFolderDialog.NEW_BUTTON, ZmMsg._new, DwtDialog.ALIGN_LEFT); 40 var params = {parent:parent, className:className, extraButtons:[newButton], id:"ChooseFolderDialog"}; 41 ZmDialog.call(this, params); 42 43 this._createControls(); 44 this._setNameField(this._inputDivId); 45 this.registerCallback(ZmChooseFolderDialog.NEW_BUTTON, this._showNewDialog, this); 46 this._changeListener = new AjxListener(this, this._folderTreeChangeListener); 47 48 this._treeView = {}; 49 this._creatingFolder = false; 50 this._treeViewListener = new AjxListener(this, this._treeViewSelectionListener); 51 52 this._multiAcctOverviews = {}; 53 }; 54 55 ZmChooseFolderDialog.prototype = new ZmDialog; 56 ZmChooseFolderDialog.prototype.constructor = ZmChooseFolderDialog; 57 58 ZmChooseFolderDialog.prototype.isZmChooseFolderDialog = true; 59 ZmChooseFolderDialog.prototype.toString = function() { return "ZmChooseFolderDialog"; }; 60 61 ZmChooseFolderDialog.NEW_BUTTON = ++DwtDialog.LAST_BUTTON; 62 63 64 /** 65 * Since this dialog is intended for use in a variety of situations, we need to be 66 * able to create different sorts of overviews based on what the calling function 67 * wants. By default, we show the folder tree view. 68 * 69 * @param {Hash} params a hash of parameters 70 * @param {Object} params.data a array of items, a folder, an item or <code>null</code> 71 * @param {Array} params.treeIds a list of trees to show 72 * @param {String} params.overviewId the overview ID 73 * @param {Hash} params.omit a list IDs to not show 74 * @param {String} params.title the dialog title 75 * @param {String} params.description the description of what the user is selecting 76 * @param {Boolean} params.skipReadOnly if <code>true</code>, read-only folders will not be displayed 77 * @param {Boolean} params.skipRemote if <code>true</code>, remote folders (mountpoints) will not be displayed 78 * @param {Boolean} params.hideNewButton if <code>true</code>, new button will not be shown 79 * @param {Boolean} params.noRootSelect if <code>true</code>, do not make root tree item(s) selectable 80 * @params {Boolean} params.showDrafts if <code>true</code>, drafts folder will not be omited 81 */ 82 ZmChooseFolderDialog.prototype.popup = 83 function(params) { 84 85 this._keyPressedInField = false; //see comment in _handleKeyUp 86 87 // use reasonable defaults 88 params = params || {}; 89 90 // create an omit list for each account 91 // XXX: does this need to happen more then once??? 92 var omitPerAcct = {}; 93 var accounts = appCtxt.accountList.visibleAccounts; 94 for (var i = 0; i < accounts.length; i++) { 95 var acct = accounts[i]; 96 97 if (params.forceSingle && acct != appCtxt.getActiveAccount()) { 98 continue; 99 } 100 101 var omit = omitPerAcct[acct.id] = params.omit || {}; 102 103 omit[ZmFolder.ID_DRAFTS] = !params.showDrafts; 104 omit[ZmFolder.ID_OUTBOX] = true; 105 omit[ZmFolder.ID_SYNC_FAILURES] = true; 106 omit[ZmFolder.ID_DLS] = true; 107 108 var folderTree = appCtxt.getFolderTree(acct); 109 110 // omit any folders that are read only 111 if (params.skipReadOnly || params.skipRemote || appCtxt.isOffline) { 112 var folders = folderTree.asList({includeRemote : true}); 113 for (var i = 0; i < folders.length; i++) { 114 var folder = folders[i]; 115 116 // if skipping read-only, 117 if (params.skipReadOnly && folder.link && folder.isReadOnly()) { 118 omit[folder.id] = true; 119 continue; 120 } 121 122 // if skipping remote folders, 123 if (params.skipRemote && folder.isRemote()) { 124 omit[folders[i].id] = true; 125 } 126 } 127 } 128 } 129 130 if (this.setTitle) { 131 this.setTitle(params.title || ZmMsg.chooseFolder); 132 } 133 var descCell = document.getElementById(this._folderDescDivId); 134 if (descCell) { 135 descCell.innerHTML = params.description || ""; 136 } 137 138 var treeIds = this._treeIds = (params.treeIds && params.treeIds.length) 139 ? params.treeIds : [ZmOrganizer.FOLDER]; 140 141 // New button doesn't make sense if we're only showing saved searches 142 var searchOnly = (treeIds.length == 1 && treeIds[0] == ZmOrganizer.SEARCH); 143 var newButton = this._getNewButton(); 144 if (newButton) { 145 newButton.setVisible(!searchOnly && !params.hideNewButton && !appCtxt.isExternalAccount() && !appCtxt.isWebClientOffline()); 146 } 147 148 this._data = params.data; 149 150 var omitParam = {}; 151 if (appCtxt.multiAccounts) { 152 omitParam[ZmOrganizer.ID_ZIMLET] = true; 153 omitParam[ZmOrganizer.ID_ALL_MAILBOXES] = true; 154 } else { 155 omitParam = omitPerAcct[appCtxt.accountList.mainAccount.id]; 156 } 157 158 var popupParams = { 159 treeIds: treeIds, 160 omit: omitParam, 161 omitPerAcct: omitPerAcct, 162 fieldId: this._folderTreeDivId, 163 overviewId: params.overviewId, 164 noRootSelect: params.noRootSelect, 165 treeStyle: params.treeStyle || DwtTree.SINGLE_STYLE, // we don't want checkboxes! 166 appName: params.appName, 167 selectable: false, 168 forceSingle: params.forceSingle 169 }; 170 171 // make sure the requisite packages are loaded 172 var treeIdMap = {}; 173 for (var i = 0; i < treeIds.length; i++) { 174 treeIdMap[treeIds[i]] = true; 175 } 176 177 this._acceptFolderMatch = params.acceptFolderMatch; 178 179 // TODO: Refactor packages so that we don't have to bring in so much 180 // TODO: code just do make sure this dialog works. 181 // TODO: I opened bug 34447 for this performance enhancement. 182 var pkg = []; 183 if (treeIdMap[ZmOrganizer.BRIEFCASE]) pkg.push("BriefcaseCore","Briefcase"); 184 if (treeIdMap[ZmOrganizer.CALENDAR]) pkg.push("MailCore","CalendarCore","Calendar"); 185 if (treeIdMap[ZmOrganizer.ADDRBOOK]) pkg.push("ContactsCore","Contacts"); 186 if (treeIdMap[ZmOrganizer.FOLDER]) pkg.push("MailCore","Mail"); 187 if (treeIdMap[ZmOrganizer.TASKS]) pkg.push("TasksCore","Tasks"); 188 189 AjxDispatcher.require(pkg, true, new AjxCallback(this, this._doPopup, [popupParams])); 190 }; 191 192 ZmChooseFolderDialog.prototype._getNewButton = 193 function () { 194 return this.getButton(ZmChooseFolderDialog.NEW_BUTTON); 195 }; 196 197 ZmChooseFolderDialog.prototype._doPopup = 198 function(params) { 199 var ov = this._setOverview(params, params.forceSingle); 200 201 if (appCtxt.multiAccounts && !params.forceSingle) { 202 // ov is an overview container, and overviewId is the containerId 203 this._multiAcctOverviews[params.overviewId] = ov; 204 for (var i in this._multiAcctOverviews) { 205 this._multiAcctOverviews[i].setVisible(i == params.overviewId); 206 } 207 208 var overviews = ov.getOverviews(); 209 for (var i in overviews) { 210 var overview = overviews[i]; 211 // zimlet overview resets folder list 212 // need to stop resetting folder list for each overview 213 if (overview._treeIds[0].toLowerCase() != "zimlet") { 214 this._resetTree(params.treeIds, overview); 215 } 216 } 217 218 ov.expandAccountOnly(appCtxt.getActiveAccount()); 219 220 } else { 221 this._resetTree(params.treeIds, ov); 222 } 223 224 if (this.isZmDialog) { 225 this._focusElement = this._inputField; 226 this._inputField.setValue(""); 227 ZmDialog.prototype.popup.call(this); 228 } 229 }; 230 231 232 ZmChooseFolderDialog.prototype.getOverviewId = 233 function(part, appName) { 234 return appCtxt.getOverviewId([this.toString(), part, appName], null); 235 }; 236 237 ZmChooseFolderDialog.prototype.getOverviewId = function(appName) { 238 239 return appCtxt.getOverviewId([this.toString(), appName], null); 240 }; 241 242 ZmChooseFolderDialog.prototype._resetTree = 243 function(treeIds, overview) { 244 245 var account = overview.account || appCtxt.getActiveAccount() || appCtxt.accountList.mainAccount; 246 var acctTreeView = this._treeView[account.id] = {}; 247 var folderTree = appCtxt.getFolderTree(account); 248 249 for (var i = 0; i < treeIds.length; i++) { 250 var treeId = treeIds[i]; 251 var treeView = acctTreeView[treeId] = overview.getTreeView(treeId, true); 252 if (!treeView) { continue; } 253 254 // bug #18533 - always make sure header item is visible in "MoveTo" dialog 255 var headerItem = treeView.getHeaderItem(); 256 if (treeIds.length > 1) { 257 if (treeId == ZmId.ORG_FOLDER) { 258 headerItem.setText(ZmMsg.mailFolders); 259 } 260 else if (treeId == ZmId.ORG_BRIEFCASE) { 261 headerItem.setText(ZmMsg.briefcaseFolders); 262 } 263 } 264 headerItem.setVisible(true, true); 265 266 // expand root item 267 var ti = treeView.getTreeItemById(folderTree.root.id); 268 ti.setExpanded(true); 269 270 // bug fix #13159 (regression of #10676) 271 // - small hack to get selecting Trash folder working again 272 var trashId = ZmOrganizer.getSystemId(ZmOrganizer.ID_TRASH, account); 273 var ti = treeView.getTreeItemById(trashId); 274 if (ti) { 275 ti.setData(ZmTreeView.KEY_TYPE, treeId); 276 } 277 278 treeView.removeSelectionListener(this._treeViewListener); 279 treeView.addSelectionListener(this._treeViewListener); 280 } 281 282 folderTree.removeChangeListener(this._changeListener); 283 // this listener has to be added after folder tree view is set 284 // (so that it comes after the view's standard change listener) 285 folderTree.addChangeListener(this._changeListener); 286 287 this._loadFolders(); 288 this._resetTreeView(true); 289 }; 290 291 ZmChooseFolderDialog.prototype.reset = 292 function() { 293 var descCell = document.getElementById(this._folderDescDivId); 294 descCell.innerHTML = ""; 295 ZmDialog.prototype.reset.call(this); 296 this._data = this._treeIds = null; 297 this._creatingFolder = false; 298 }; 299 300 ZmChooseFolderDialog.prototype._contentHtml = 301 function() { 302 this._inputDivId = this._htmlElId + "_inputDivId"; 303 this._folderDescDivId = this._htmlElId + "_folderDescDivId"; 304 this._folderTreeDivId = this._htmlElId + "_folderTreeDivId"; 305 306 return AjxTemplate.expand("share.Widgets#ZmChooseFolderDialog", {id:this._htmlElId}); 307 }; 308 309 ZmChooseFolderDialog.prototype._createControls = 310 function() { 311 this._inputField = new DwtInputField({parent: this}); 312 document.getElementById(this._inputDivId).appendChild(this._inputField.getHtmlElement()); 313 this._inputField.addListener(DwtEvent.ONKEYUP, new AjxListener(this, this._handleKeyUp)); 314 //this._inputField.addListener(DwtEvent.ONKEYDOWN, new AjxListener(this, this._handleKeyDown)); 315 // unfortunately there's no onkeydown generally set for input fields so above line does not work 316 this._inputField.setHandler(DwtEvent.ONKEYDOWN, AjxCallback.simpleClosure(this._handleKeyDown, this)); 317 }; 318 319 ZmChooseFolderDialog.prototype._showNewDialog = 320 function() { 321 var itemType = this._getOverview().getSelected(true); 322 var newType = itemType || this._treeIds[0]; 323 var ftc = this._opc.getTreeController(newType); 324 var dialog = ftc._getNewDialog(); 325 dialog.reset(); 326 dialog.registerCallback(DwtDialog.OK_BUTTON, this._newCallback, this, [ftc, dialog]); 327 dialog.popup(); 328 }; 329 330 ZmChooseFolderDialog.prototype._newCallback = 331 function(ftc, dialog, params) { 332 ftc._doCreate(params); 333 dialog.popdown(); 334 this._creatingFolder = true; 335 }; 336 337 // After the user creates a folder, select it and optionally move items to it. 338 ZmChooseFolderDialog.prototype._folderTreeChangeListener = 339 function(ev) { 340 if (ev.event == ZmEvent.E_CREATE && this._creatingFolder) { 341 var organizers = ev.getDetail("organizers") || (ev.source && [ev.source]); 342 var org = organizers[0]; 343 if (org) { 344 var tv = this._treeView[org.getAccount().id][org.type]; 345 tv.setSelected(org, true); 346 if (this._moveOnFolderCreate && !ev.shiftKey && !ev.ctrlKey) { 347 tv._itemClicked(tv.getTreeItemById(org.id), ev); 348 } 349 } 350 this._creatingFolder = false; 351 } 352 this._loadFolders(); 353 }; 354 355 ZmChooseFolderDialog.prototype._okButtonListener = 356 function(ev) { 357 var tgtFolder = this._getOverview().getSelected(false); 358 var tgtType = this._getOverview().getSelected(true); 359 var folderList = (tgtFolder && (!(tgtFolder instanceof Array))) 360 ? [tgtFolder] : tgtFolder; 361 362 var msg = (!folderList || (folderList && folderList.length == 0)) 363 ? ZmMsg.noTargetFolder : null; 364 365 // check for valid target 366 if (!msg && this._data) { 367 for (var i = 0; i < folderList.length; i++) { 368 var folder = folderList[i]; 369 //Note - although this case is checked in mayContain, I do not change mayContain since mayContain is complicated and returns a boolean and used from DnD and is overridden a bunch of times. 370 //Only here we need the special message (see bug 82064) so I settle for that for now. 371 if (this._data.isZmFolder && !folder.isInTrash() && folder.hasChild(this._data.name) && !this._acceptFolderMatch) { 372 msg = ZmMsg.folderAlreadyExistsInDestination; 373 break; 374 } 375 else if (folder.mayContain && !folder.mayContain(this._data, tgtType, this._acceptFolderMatch)) { 376 if (this._data.isZmFolder) { 377 msg = ZmMsg.badTargetFolder; 378 } else { 379 var items = AjxUtil.toArray(this._data); 380 for (var i = 0; i < items.length; i++) { 381 var item = items[i]; 382 if (!item) { 383 continue; 384 } 385 if (item.isDraft && (folder.nId != ZmFolder.ID_TRASH && folder.nId != ZmFolder.ID_DRAFTS && folder.rid != ZmFolder.ID_DRAFTS)) { 386 // can move drafts into Trash or Drafts 387 msg = ZmMsg.badTargetFolderForDraftItem; 388 break; 389 } else if ((folder.nId == ZmFolder.ID_DRAFTS || folder.rid == ZmFolder.ID_DRAFTS) && !item.isDraft) { 390 // only drafts can be moved into Drafts 391 msg = ZmMsg.badItemForDraftsFolder; 392 break; 393 } 394 } 395 if(!msg) { 396 msg = ZmMsg.badTargetFolderItems; 397 } 398 } 399 break; 400 } 401 } 402 } 403 404 if (msg) { 405 this._showError(msg); 406 } else { 407 DwtDialog.prototype._buttonListener.call(this, ev, [tgtFolder]); 408 } 409 }; 410 411 ZmChooseFolderDialog.prototype._getTabGroupMembers = 412 function() { 413 return AjxUtil.collapseList([this._inputField, this._overview[this._curOverviewId]]); 414 }; 415 416 ZmChooseFolderDialog.prototype._loadFolders = 417 function() { 418 this._folders = []; 419 420 for (var accountId in this._treeView) { 421 var treeViews = this._treeView[accountId]; 422 423 for (var type in treeViews) { 424 var treeView = treeViews[type]; 425 if (!treeView) { continue; } 426 427 var items = treeView.getTreeItemList(); 428 for (var i = 0, len = items.length; i < len; i++) { 429 var ti = items[i]; 430 if (!ti.getData) { //not sure if this could happen but it was here before my refactoring. 431 continue; 432 } 433 var folder = items[i].getData(Dwt.KEY_OBJECT); 434 if (!folder || folder.nId == ZmOrganizer.ID_ROOT) { 435 continue; 436 } 437 var name = folder.getName(false, null, true, true).toLowerCase(); 438 var path = "/" + folder.getPath(false, false, null, true).toLowerCase(); 439 this._folders.push({id: folder.id, type: type, name: name, path: path, accountId: accountId}); 440 } 441 } 442 } 443 }; 444 445 ZmChooseFolderDialog.prototype._handleKeyDown = 446 function(ev) { 447 this._keyPressedInField = true; //see comment in _handleKeyUp 448 }; 449 450 ZmChooseFolderDialog.prototype._handleKeyUp = 451 function(ev) { 452 453 // this happens in the case of SearchFolder when the keyboard shortcut "s" was released when this 454 // field was in focus but it does not affect the field since it was not pressed here. 455 // (Bug 52983) 456 // in other words, the sequence that caused the bug is: 457 // 1. "s" keyDown triggering ZmDialog.prototype.popup() 458 // 2. ZmDialog.prototype.popup setting focus on the input field 459 // 3. "s" keyUp called triggering ZmChooseFolderDialog.prototype._handleKeyUp. 460 // Note that this is reset to false in the popup only, since only one time we need this protection, and it's the simplest. 461 if (!this._keyPressedInField) { 462 return; 463 } 464 465 var key = DwtKeyEvent.getCharCode(ev); 466 if (key === DwtKeyEvent.KEY_TAB) { 467 return; 468 } 469 else if (key === DwtKeyEvent.KEY_ARROW_DOWN) { 470 this._overview[this._curOverviewId].focus(); 471 return; 472 } 473 474 var num = 0, firstMatch, matches = []; 475 var value = this._inputField.getValue().toLowerCase(); 476 for (var i = 0, len = this._folders.length; i < len; i++) { 477 var folderInfo = this._folders[i]; 478 var treeView = this._treeView[folderInfo.accountId][folderInfo.type]; 479 var ti = treeView.getTreeItemById(folderInfo.id); 480 if (ti) { 481 var testPath = "/" + value.replace(/^\//, ""); 482 var path = folderInfo.path; 483 if (folderInfo.name.indexOf(value) == 0 || 484 (path.indexOf(testPath) == 0 && (path.substr(testPath.length).indexOf("/") == -1))) { 485 486 matches.push(ti); 487 var activeAccountId = appCtxt.getActiveAccount().id; 488 //choose the FIRST of active account folders. 489 if (!firstMatch || (folderInfo.accountId == activeAccountId 490 && firstMatch.accountId != activeAccountId)) { 491 firstMatch = folderInfo; 492 } 493 } 494 } 495 } 496 497 // now that we know which folders match, hide all items and then show 498 // the matches, expanding their parent chains as needed 499 this._resetTreeView(false); 500 501 for (var i = 0, len = matches.length; i < len; i++) { 502 var ti = matches[i]; 503 ti._tree._expandUp(ti); 504 ti.setVisible(true); 505 } 506 507 if (firstMatch) { 508 var tv = this._treeView[firstMatch.accountId][firstMatch.type]; 509 var ov = this._getOverview(); 510 if (ov) { 511 ov.deselectAllTreeViews(); 512 } 513 tv.setSelected(appCtxt.getById(firstMatch.id), true, true); 514 if (appCtxt.multiAccounts) { 515 var ov = this._getOverview(); 516 for (var h in ov._headerItems) { 517 ov._headerItems[h].setExpanded((h == firstMatch.accountId), false, false); 518 } 519 } 520 } 521 }; 522 523 ZmChooseFolderDialog.prototype._resetTreeView = 524 function(visible) { 525 for (var i = 0, len = this._folders.length; i < len; i++) { 526 var folderInfo = this._folders[i]; 527 var tv = this._treeView[folderInfo.accountId][folderInfo.type]; 528 var ti = tv.getTreeItemById(folderInfo.id); 529 if (ti) { 530 ti.setVisible(visible); 531 ti.setChecked(false, true); 532 } 533 } 534 }; 535 536 ZmChooseFolderDialog.prototype._getOverview = 537 function() { 538 var ov; 539 if (appCtxt.multiAccounts) { 540 ov = this._opc.getOverviewContainer(this._curOverviewId); 541 } 542 543 // this covers the case where we're in multi-account mode, but dialog was 544 // popped up in "forceSingle" mode 545 return (ov || ZmDialog.prototype._getOverview.call(this)); 546 }; 547 548 ZmChooseFolderDialog.prototype._treeViewSelectionListener = 549 function(ev) { 550 if (ev.detail != DwtTree.ITEM_SELECTED && 551 ev.detail != DwtTree.ITEM_DBL_CLICKED) 552 { 553 return; 554 } 555 var treeItem = ev.item; 556 if (treeItem && !treeItem.isSelectionEnabled()) { 557 // Selection should have been blocked, but only allow a double click if the item can be selected 558 return; 559 } 560 561 if (this._getOverview() instanceof ZmAccountOverviewContainer) { 562 if (ev.detail == DwtTree.ITEM_DBL_CLICKED && 563 ev.item instanceof DwtHeaderTreeItem) 564 { 565 return; 566 } 567 568 var oc = this._opc.getOverviewContainer(this._curOverviewId); 569 var overview = oc.getOverview(ev.item.getData(ZmTreeView.KEY_ID)); 570 oc.deselectAll(overview); 571 } 572 573 var organizer = ev.item && ev.item.getData(Dwt.KEY_OBJECT); 574 if (organizer.id == ZmFolder.ID_LOAD_FOLDERS) { 575 return; 576 } 577 var value = organizer ? organizer.getName(null, null, true) : ev.item.getText(); 578 this._inputField.setValue(value); 579 if (ev.detail == DwtTree.ITEM_DBL_CLICKED || ev.enter) { 580 this._okButtonListener(); 581 } 582 }; 583 584 ZmChooseFolderDialog.prototype._enterListener = 585 function(ev) { 586 this._okButtonListener.call(this, ev); 587 }; 588