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 an item. 27 */ 28 29 /** 30 * Creates an item. 31 * @class 32 * An item is a piece of data that may contain user content. Most items are taggable. Currently, 33 * the following things are items: conversation, message, attachment, appointment, and contact. 34 * <br/> 35 * <br/> 36 * An item typically appears in the context of a containing list. Its event handling 37 * is generally handled by the list so we avoid having the same listeners on each item. If we 38 * create a context where an item stands alone outside a list context, then the item will have 39 * its own listeners and do its own notification handling. 40 * 41 * @author Conrad Damon 42 * 43 * @param {constant} type type of object (conv, msg, etc) 44 * @param {int} id the unique id 45 * @param {ZmList} list a list that contains this item 46 * @param {Boolean} noCache if <code>true</code>, do not cache this item 47 * 48 * @extends ZmModel 49 */ 50 ZmItem = function(type, id, list, noCache) { 51 52 if (arguments.length == 0) { return; } 53 ZmModel.call(this, type); 54 55 this.type = type; 56 this.id = id; 57 this.list = list; 58 this._list = {}; 59 60 // number of views using this item 61 this.refCount = 0; 62 63 this.tags = []; 64 this.tagHash = {}; 65 this.folderId = 0; 66 67 // make sure the cached item knows which lists it is in, even if those other lists 68 // have separate instances of this item - propagate view IDs from currently cached item 69 var curItem = appCtxt.getById(id); 70 if (curItem) { 71 this._list = AjxUtil.hashCopy(curItem._list); 72 if (!list) { 73 // No list specified, preserve the previous list 74 this.list = curItem.list; 75 } 76 } 77 if (list) { 78 this._list[list.id] = true; 79 } 80 81 if (id && !noCache) { 82 appCtxt.cacheSet(id, this); 83 } 84 }; 85 86 ZmItem.prototype = new ZmModel; 87 ZmItem.prototype.constructor = ZmItem; 88 89 ZmItem.prototype.isZmItem = true; 90 ZmItem.prototype.toString = function() { return "ZmItem"; }; 91 92 93 ZmItem.APP = {}; // App responsible for item 94 ZmItem.MSG_KEY = {}; // Type names 95 ZmItem.ICON = {}; // Representative icons 96 ZmItem.RESULTS_LIST = {}; // Function for creating search results list 97 98 // fields that can be part of a displayed item 99 ZmItem.F_ACCOUNT = ZmId.FLD_ACCOUNT; 100 ZmItem.F_ATTACHMENT = ZmId.FLD_ATTACHMENT; 101 ZmItem.F_CAPACITY = ZmId.FLD_CAPACITY; 102 ZmItem.F_COMPANY = ZmId.FLD_COMPANY; 103 ZmItem.F_DATE = ZmId.FLD_DATE; 104 ZmItem.F_DEPARTMENT = ZmId.FLD_DEPARTMENT; 105 ZmItem.F_EMAIL = ZmId.FLD_EMAIL; 106 ZmItem.F_EXPAND = ZmId.FLD_EXPAND; 107 ZmItem.F_FILE_TYPE = ZmId.FLD_FILE_TYPE; 108 ZmItem.F_FLAG = ZmId.FLD_FLAG; 109 ZmItem.F_FOLDER = ZmId.FLD_FOLDER; 110 ZmItem.F_FRAGMENT = ZmId.FLD_FRAGMENT; 111 ZmItem.F_FROM = ZmId.FLD_FROM; 112 ZmItem.F_HOME_PHONE = ZmId.FLD_HOME_PHONE; 113 ZmItem.F_ID = ZmId.FLD_ID; 114 ZmItem.F_INDEX = ZmId.FLD_INDEX; 115 ZmItem.F_ITEM_ROW = ZmId.FLD_ITEM_ROW; 116 ZmItem.F_ITEM_ROW_3PANE = ZmId.FLD_ITEM_ROW_3PANE; 117 ZmItem.F_LOCATION = ZmId.FLD_LOCATION; 118 ZmItem.F_NAME = ZmId.FLD_NAME; 119 ZmItem.F_NOTES = ZmId.FLD_NOTES; 120 ZmItem.F_PARTICIPANT = ZmId.FLD_PARTICIPANT; 121 ZmItem.F_PCOMPLETE = ZmId.FLD_PCOMPLETE; 122 ZmItem.F_PRIORITY = ZmId.FLD_PRIORITY; 123 ZmItem.F_RECURRENCE = ZmId.FLD_RECURRENCE; 124 ZmItem.F_SELECTION = ZmId.FLD_SELECTION; 125 ZmItem.F_SELECTION_CELL = ZmId.FLD_SELECTION_CELL; 126 ZmItem.F_SIZE = ZmId.FLD_SIZE; 127 ZmItem.F_SORTED_BY = ZmId.FLD_SORTED_BY; // placeholder for 3-pane view 128 ZmItem.F_STATUS = ZmId.FLD_STATUS; 129 ZmItem.F_READ = ZmId.FLD_READ; 130 ZmItem.F_MUTE = ZmId.FLD_MUTE; 131 ZmItem.F_SUBJECT = ZmId.FLD_SUBJECT; 132 ZmItem.F_TAG = ZmId.FLD_TAG; 133 ZmItem.F_TAG_CELL = ZmId.FLD_TAG_CELL; 134 ZmItem.F_TO = ZmId.FLD_TO; 135 ZmItem.F_TYPE = ZmId.FLD_TYPE; 136 ZmItem.F_VERSION = ZmId.FLD_VERSION; 137 ZmItem.F_WORK_PHONE = ZmId.FLD_WORK_PHONE; 138 ZmItem.F_LOCK = ZmId.FLD_LOCK; 139 ZmItem.F_MSG_PRIORITY = ZmId.FLD_MSG_PRIORITY; 140 ZmItem.F_APP_PASSCODE_CREATED = ZmId.FLD_CREATED; 141 ZmItem.F_APP_PASSCODE_LAST_USED = ZmId.FLD_LAST_USED; 142 143 // Action requests for different items 144 ZmItem.SOAP_CMD = {}; 145 146 // Item fields (for modify events) 147 ZmItem.TAGS_FIELD = 1; 148 149 // Item flags 150 ZmItem.FLAG_ATTACH = "a"; 151 ZmItem.FLAG_FLAGGED = "f"; 152 ZmItem.FLAG_FORWARDED = "w"; 153 ZmItem.FLAG_ISDRAFT = "d"; 154 ZmItem.FLAG_ISSCHEDULED = "c"; 155 ZmItem.FLAG_ISSENT = "s"; 156 ZmItem.FLAG_READ_RECEIPT_SENT = "n"; 157 ZmItem.FLAG_REPLIED = "r"; 158 ZmItem.FLAG_UNREAD = "u"; 159 ZmItem.FLAG_MUTE = "("; 160 ZmItem.FLAG_LOW_PRIORITY = "?"; 161 ZmItem.FLAG_HIGH_PRIORITY = "!"; 162 ZmItem.FLAG_PRIORITY = "+"; //msg prioritization 163 ZmItem.FLAG_NOTE = "t"; //specially for notes 164 ZmItem.FLAG_OFFLINE_CREATED = "o"; 165 166 ZmItem.ALL_FLAGS = [ 167 ZmItem.FLAG_FLAGGED, 168 ZmItem.FLAG_ATTACH, 169 ZmItem.FLAG_UNREAD, 170 ZmItem.FLAG_MUTE, 171 ZmItem.FLAG_REPLIED, 172 ZmItem.FLAG_FORWARDED, 173 ZmItem.FLAG_ISSENT, 174 ZmItem.FLAG_READ_RECEIPT_SENT, 175 ZmItem.FLAG_ISDRAFT, 176 ZmItem.FLAG_ISSCHEDULED, 177 ZmItem.FLAG_HIGH_PRIORITY, 178 ZmItem.FLAG_LOW_PRIORITY, 179 ZmItem.FLAG_PRIORITY, 180 ZmItem.FLAG_NOTE, 181 ZmItem.FLAG_OFFLINE_CREATED 182 ]; 183 184 // Map flag to item property 185 ZmItem.FLAG_PROP = {}; 186 ZmItem.FLAG_PROP[ZmItem.FLAG_ATTACH] = "hasAttach"; 187 ZmItem.FLAG_PROP[ZmItem.FLAG_FLAGGED] = "isFlagged"; 188 ZmItem.FLAG_PROP[ZmItem.FLAG_FORWARDED] = "isForwarded"; 189 ZmItem.FLAG_PROP[ZmItem.FLAG_ISDRAFT] = "isDraft"; 190 ZmItem.FLAG_PROP[ZmItem.FLAG_ISSCHEDULED] = "isScheduled"; 191 ZmItem.FLAG_PROP[ZmItem.FLAG_ISSENT] = "isSent"; 192 ZmItem.FLAG_PROP[ZmItem.FLAG_READ_RECEIPT_SENT] = "readReceiptSent"; 193 ZmItem.FLAG_PROP[ZmItem.FLAG_REPLIED] = "isReplied"; 194 ZmItem.FLAG_PROP[ZmItem.FLAG_UNREAD] = "isUnread"; 195 ZmItem.FLAG_PROP[ZmItem.FLAG_MUTE] = "isMute"; 196 ZmItem.FLAG_PROP[ZmItem.FLAG_LOW_PRIORITY] = "isLowPriority"; 197 ZmItem.FLAG_PROP[ZmItem.FLAG_HIGH_PRIORITY] = "isHighPriority"; 198 ZmItem.FLAG_PROP[ZmItem.FLAG_PRIORITY] = "isPriority"; 199 ZmItem.FLAG_PROP[ZmItem.FLAG_NOTE] = "isNote"; 200 ZmItem.FLAG_PROP[ZmItem.FLAG_OFFLINE_CREATED] = "isOfflineCreated"; 201 202 // DnD actions this item is allowed 203 204 /** 205 * Defines the "move" action. 206 * 207 * @see #getDefaultDndAction 208 */ 209 ZmItem.DND_ACTION_MOVE = 1 << 0; 210 /** 211 * Defines the "copy" action. 212 * 213 * @see #getDefaultDndAction 214 */ 215 ZmItem.DND_ACTION_COPY = 1 << 1; 216 /** 217 * Defines the "move & copy" action. 218 * 219 * @see #getDefaultDndAction 220 */ 221 ZmItem.DND_ACTION_BOTH = ZmItem.DND_ACTION_MOVE | ZmItem.DND_ACTION_COPY; 222 223 /** 224 * Defines the notes separator which is used by items 225 * (such as calendar or share invites) that have notes. 226 * 227 */ 228 ZmItem.NOTES_SEPARATOR = "*~*~*~*~*~*~*~*~*~*"; 229 230 /** 231 * Registers an item and stores information about the given item type. 232 * 233 * @param {constant} item the item type 234 * @param {Hash} params a hash of parameters 235 * @param {constant} params.app the app that handles this item type 236 * @param {String} params.nameKey the message key for item name 237 * @param {String} params.icon the name of item icon class 238 * @param {String} params.soapCmd the SOAP command for acting on this item 239 * @param {String} params.itemClass the name of class that represents this item 240 * @param {String} params.node the SOAP response node for this item 241 * @param {constant} params.organizer the associated organizer 242 * @param {String} params.searchType the associated type in SearchRequest 243 * @param {function} params.resultsList the function that returns a {@link ZmList} for holding search results of this type 244 */ 245 ZmItem.registerItem = 246 function(item, params) { 247 if (params.app) { ZmItem.APP[item] = params.app; } 248 if (params.nameKey) { ZmItem.MSG_KEY[item] = params.nameKey; } 249 if (params.icon) { ZmItem.ICON[item] = params.icon; } 250 if (params.soapCmd) { ZmItem.SOAP_CMD[item] = params.soapCmd; } 251 if (params.itemClass) { ZmList.ITEM_CLASS[item] = params.itemClass; } 252 if (params.node) { ZmList.NODE[item] = params.node; } 253 if (params.organizer) { ZmOrganizer.ITEM_ORGANIZER[item] = params.organizer; } 254 if (params.searchType) { ZmSearch.TYPE[item] = params.searchType; } 255 if (params.resultsList) { ZmItem.RESULTS_LIST[item] = params.resultsList; } 256 257 if (params.node) { 258 ZmList.ITEM_TYPE[params.node] = item; 259 } 260 261 if (params.dropTargets) { 262 if (!ZmApp.DROP_TARGETS[params.app]) { 263 ZmApp.DROP_TARGETS[params.app] = {}; 264 } 265 ZmApp.DROP_TARGETS[params.app][item] = params.dropTargets; 266 } 267 }; 268 269 /** 270 * Gets an item id by taking a normalized id (or an item id) and returning the item id. 271 * 272 * @param {String} id the normalized id 273 * @return {String} the item id 274 */ 275 ZmItem.getItemId = 276 function(id) { 277 if (!id) { 278 return id; 279 } 280 if (!ZmItem.SHORT_ID_RE) { 281 var shell = DwtShell.getShell(window); 282 ZmItem.SHORT_ID_RE = new RegExp(appCtxt.get(ZmSetting.USERID) + ':', "gi"); 283 } 284 return id.replace(ZmItem.SHORT_ID_RE, ''); 285 }; 286 287 // abstract methods 288 /** 289 * Creates an item. 290 * 291 * @param {Hash} args the arguments 292 */ 293 ZmItem.prototype.create = function(args) {}; 294 /** 295 * Modifies an item. 296 * 297 * @param {Hash} mods the arguments 298 */ 299 ZmItem.prototype.modify = function(mods) {}; 300 301 /** 302 * Gets the item by id. 303 * 304 * @param {String} id an item id 305 * @return {ZmItem} the item 306 */ 307 ZmItem.prototype.getById = 308 function(id) { 309 if (id == this.id) { 310 return this; 311 } 312 }; 313 314 ZmItem.prototype.getAccount = 315 function() { 316 if (!this.account) { 317 var account; 318 319 if (this.folderId) { 320 var ac = window.parentAppCtxt || window.appCtxt; 321 var folder = ac.getById(this.folderId); 322 account = folder && folder.getAccount(); 323 } 324 325 if (!account) { 326 var parsed = ZmOrganizer.parseId(this.id); 327 account = parsed && parsed.account; 328 } 329 this.account = account; 330 } 331 return this.account; 332 }; 333 334 /** 335 * Clears the item. 336 * 337 */ 338 ZmItem.prototype.clear = function() { 339 340 // only clear data if no views are using this item 341 if (this.refCount <= 1) { 342 this._evtMgr.removeAll(ZmEvent.L_MODIFY); 343 if (this.tags.length) { 344 for (var i = 0; i < this.tags.length; i++) { 345 this.tags[i] = null; 346 } 347 this.tags = []; 348 } 349 for (var i in this.tagHash) { 350 this.tagHash[i] = null; 351 } 352 this.tagHash = {}; 353 } 354 355 this.refCount--; 356 }; 357 358 /** 359 * Caches the item. 360 * 361 * @return {Boolean} <code>true</code> if the item is placed into cache; <code>false</code> otherwise 362 */ 363 ZmItem.prototype.cache = 364 function(){ 365 if (this.id) { 366 appCtxt.cacheSet(this.id, this); 367 return true; 368 } 369 return false; 370 }; 371 372 /** 373 * Checks if the item has a given tag. 374 * 375 * @param {String} tagName tag name 376 * @return {Boolean} <code>true</code> is this item has the given tag. 377 */ 378 ZmItem.prototype.hasTag = 379 function(tagName) { 380 return (this.tagHash[tagName] == true); 381 }; 382 383 /** 384 * is it possible to add a tag to this item? 385 * @param tagName 386 * @returns {boolean} 387 */ 388 ZmItem.prototype.canAddTag = 389 function(tagName) { 390 return !this.hasTag(tagName); 391 }; 392 393 394 /** 395 * Gets the folder id that contains this item, if available. 396 * 397 * @return {String} the folder id or <code>null</code> for none 398 */ 399 ZmItem.prototype.getFolderId = 400 function() { 401 return this.folderId; 402 }; 403 404 /** 405 * @deprecated 406 * Use getRestUrl 407 * 408 * @private 409 * @see #getRestUrl 410 */ 411 ZmItem.prototype.getUrl = 412 function() { 413 return this.getRestUrl(); 414 }; 415 416 /** 417 * Gets the rest url for this item. 418 * 419 * @return {String} the url 420 */ 421 ZmItem.prototype.getRestUrl = 422 function() { 423 // return REST URL as seen by server 424 if (this.restUrl) { 425 return this.restUrl; 426 } 427 428 // if server doesn't tell us what URL to use, do our best to generate 429 var organizerType = ZmOrganizer.ITEM_ORGANIZER[this.type]; 430 var organizer = appCtxt.getById(this.folderId); 431 var url = organizer 432 ? ([organizer.getRestUrl(), "/", AjxStringUtil.urlComponentEncode(this.name)].join("")) 433 : null; 434 435 DBG.println(AjxDebug.DBG3, "NO REST URL FROM SERVER. GENERATED URL: " + url); 436 437 return url; 438 }; 439 440 /** 441 * Gets the appropriate tag image info for this item. 442 * 443 * @return {String} the tag image info 444 */ 445 ZmItem.prototype.getTagImageInfo = 446 function() { 447 return this.getTagImageFromNames(this.getVisibleTags()); 448 }; 449 450 /** 451 * @deprecated 452 * */ 453 ZmItem.prototype.getTagImageFromIds = 454 function(tagIds) { 455 var tagImageInfo; 456 457 if (!tagIds || tagIds.length == 0) { 458 tagImageInfo = "Blank_16"; 459 } else if (tagIds.length == 1) { 460 tagImageInfo = this.getTagImage(tagIds[0]); 461 } else { 462 tagImageInfo = "TagStack"; 463 } 464 465 return tagImageInfo; 466 }; 467 468 ZmItem.prototype.getVisibleTags = 469 function() { 470 if(!appCtxt.get(ZmSetting.TAGGING_ENABLED)){ 471 return []; 472 } 473 return this.tags; 474 //todo - do we need anything from this? 475 // var searchAll = appCtxt.getSearchController().searchAllAccounts; 476 // if (!searchAll && this.isShared()) { 477 // return []; 478 // } else { 479 // return this.tags; 480 // } 481 }; 482 483 ZmItem.prototype.getTagImageFromNames = 484 function(tags) { 485 486 if (!tags || tags.length == 0) { 487 return "Blank_16"; 488 } 489 if (tags.length == 1) { 490 return this.getTagImage(tags[0]); 491 } 492 493 return "TagStack"; 494 }; 495 496 497 ZmItem.prototype.getTagImage = 498 function(tagName) { 499 //todo - I don't think we need the qualified/normalized/whatever id anymore. 500 // var tagFullId = (!this.getAccount().isMain) 501 // ? ([this.getAccount().id, tagName].join(":")) 502 // : (ZmOrganizer.getSystemId(tagName)); 503 var tagList = appCtxt.getAccountTagList(this); 504 505 var tag = tagList.getByNameOrRemote(tagName); 506 return tag ? tag.getIconWithColor() : "Blank_16"; 507 }; 508 509 /** 510 * Gets the default action to use when dragging this item. This method 511 * is meant to be overloaded for items that are read-only and can only be copied. 512 * 513 * @param {Boolean} forceCopy If set, default DnD action is a copy 514 * @return {Object} the action 515 */ 516 ZmItem.prototype.getDefaultDndAction = 517 function(forceCopy) { 518 return (this.isReadOnly() || forceCopy) 519 ? ZmItem.DND_ACTION_COPY 520 : ZmItem.DND_ACTION_MOVE; 521 }; 522 523 /** 524 * Checks if this item is read-only. This method should be 525 * overloaded by the derived object to determine what "read-only" means. 526 * 527 * @return {Boolean} the read-only status 528 */ 529 ZmItem.prototype.isReadOnly = 530 function() { 531 return false; 532 }; 533 534 /** 535 * Checks if this item is shared. 536 * 537 * @return {Boolean} <code>true</code> if this item is shared (remote) 538 */ 539 ZmItem.prototype.isShared = 540 function() { 541 if (this._isShared == null) { 542 if (this.id === -1) { 543 this._isShared = false; 544 } else { 545 this._isShared = appCtxt.isRemoteId(this.id); 546 } 547 } 548 return this._isShared; 549 }; 550 551 // Notification handling 552 553 // For delete and modify notifications, we first apply the notification to this item. Then we 554 // see if the item is a member of any other lists. If so, we have those other copies of this 555 // item handle the notification as well. Each will notify through the list that created it. 556 557 ZmItem.prototype.notifyDelete = 558 function() { 559 this._notifyDelete(); 560 for (var listId in this._list) { 561 var list = appCtxt.getById(listId); 562 if (!list || (this.list && listId == this.list.id)) { continue; } 563 var ctlr = list.controller; 564 if (!ctlr || ctlr.inactive || (ctlr.getList().id != listId)) { continue; } 565 var doppleganger = list.getById(this.id); 566 if (doppleganger) { 567 doppleganger._notifyDelete(); 568 } 569 } 570 }; 571 572 ZmItem.prototype._notifyDelete = 573 function() { 574 this.deleteLocal(); 575 if (this.list) { 576 this.list.deleteLocal([this]); 577 } 578 this._notify(ZmEvent.E_DELETE); 579 }; 580 581 ZmItem.prototype.notifyModify = 582 function(obj, batchMode) { 583 this._notifyModify(obj, batchMode); 584 for (var listId in this._list) { 585 var list = listId ? appCtxt.getById(listId) : null; 586 if (!list || (this.list && (listId == this.list.id))) { continue; } 587 var ctlr = list.controller; 588 if (!ctlr || ctlr.inactive || (ctlr.getList().id != listId)) { continue; } 589 var doppleganger = list.getById(this.id); 590 if (doppleganger) { 591 doppleganger._notifyModify(obj, batchMode); 592 } 593 } 594 }; 595 596 /** 597 * Handles a modification notification. 598 * 599 * @param {Object} obj the item with the changed attributes/content 600 * @param {boolean} batchMode if true, return event type and don't notify 601 */ 602 ZmItem.prototype._notifyModify = 603 function(obj, batchMode) { 604 // empty string is meaningful here, it means no tags 605 if (obj.tn != null) { 606 this._parseTagNames(obj.tn); 607 this._notify(ZmEvent.E_TAGS); 608 } 609 // empty string is meaningful here, it means no flags 610 if (obj.f != null) { 611 var flags = this._getFlags(); 612 var origFlags = {}; 613 for (var i = 0; i < flags.length; i++) { 614 origFlags[flags[i]] = this[ZmItem.FLAG_PROP[flags[i]]]; 615 } 616 this._parseFlags(obj.f); 617 var changedFlags = []; 618 for (var i = 0; i < flags.length; i++) { 619 var on = this[ZmItem.FLAG_PROP[flags[i]]]; 620 if (origFlags[flags[i]] != on) { 621 changedFlags.push(flags[i]); 622 } 623 } 624 if (changedFlags.length) { 625 this._notify(ZmEvent.E_FLAGS, {flags: changedFlags}); 626 } 627 } 628 if (obj.l != null && obj.l != this.folderId) { 629 var details = {oldFolderId:this.folderId}; 630 this.moveLocal(obj.l); 631 if (this.list) { 632 this.list.moveLocal([this], obj.l); 633 } 634 if (batchMode) { 635 delete obj.l; // folder has been handled 636 return ZmEvent.E_MOVE; 637 } else { 638 this._notify(ZmEvent.E_MOVE, details); 639 } 640 } 641 }; 642 643 // Local change handling 644 645 /** 646 * Applies the given flag change to this item by setting a boolean property. 647 * 648 * @param {constant} flag the flag that changed 649 * @param {Boolean} on <code>true</code> if the flag is now set 650 */ 651 ZmItem.prototype.flagLocal = 652 function(flag, on) { 653 this[ZmItem.FLAG_PROP[flag]] = on; 654 }; 655 656 /** 657 * Sets the given flag change to this item. Both the flags string and the 658 * flag properties are affected. 659 * 660 * @param {constant} flag the flag that changed 661 * @param {Boolean} on <code>true</code> if the flag is now set 662 * 663 * @return {String} the new flags string 664 */ 665 ZmItem.prototype.setFlag = 666 function(flag, on) { 667 this.flagLocal(flag, on); 668 var flags = this.flags || ""; 669 if (on && flags.indexOf(flag) == -1) { 670 flags = flags + flag; 671 } else if (!on && flags.indexOf(flag) != -1) { 672 flags = flags.replace(flag, ""); 673 } 674 this.flags = flags; 675 676 return flags; 677 }; 678 679 /** 680 * Adds or removes the given tag for this item. 681 * 682 * @param {Object} tag tag name 683 * @param {Boolean} doTag <code>true</code> if tag is being added; <code>false</code> if it is being removed 684 * @return {Boolean} <code>true</code> to notify 685 */ 686 ZmItem.prototype.tagLocal = 687 function(tag, doTag) { 688 var bNotify = false; 689 if (doTag) { 690 if (!this.tagHash[tag]) { 691 bNotify = true; 692 this.tags.push(tag); 693 this.tagHash[tag] = true; 694 } 695 } else { 696 for (var i = 0; i < this.tags.length; i++) { 697 if (this.tags[i] == tag) { 698 this.tags.splice(i, 1); 699 delete this.tagHash[tag]; 700 bNotify = true; 701 break; 702 } 703 } 704 } 705 706 return bNotify; 707 }; 708 709 /** 710 * Removes all tags. 711 * 712 */ 713 ZmItem.prototype.removeAllTagsLocal = 714 function() { 715 this.tags = []; 716 for (var i in this.tagHash) { 717 delete this.tagHash[i]; 718 } 719 }; 720 721 /** 722 * Deletes local, in case an item wants to do something while being deleted. 723 */ 724 ZmItem.prototype.deleteLocal = function() {}; 725 726 /** 727 * Moves the item. 728 * 729 * @param {String} folderId 730 * @param {AjxCallback} callback the callback 731 * @param {AjxCallback} errorCallback the callback on error 732 * @return {Object} the result of the move 733 */ 734 ZmItem.prototype.move = 735 function(folderId, callback, errorCallback) { 736 return ZmItem.move(this.id, folderId, callback, errorCallback); 737 }; 738 739 /** 740 * Moves the item. 741 * 742 * @return {Object} the result of the move 743 */ 744 ZmItem.move = 745 function(itemId, folderId, callback, errorCallback, accountName) { 746 var json = { 747 ItemActionRequest: { 748 _jsns: "urn:zimbraMail", 749 action: { 750 id: itemId instanceof Array ? itemId.join() : itemId, 751 op: "move", 752 l: folderId 753 } 754 } 755 }; 756 757 var params = { 758 jsonObj: json, 759 asyncMode: Boolean(callback), 760 callback: callback, 761 errorCallback: errorCallback, 762 accountName: accountName 763 }; 764 return appCtxt.getAppController().sendRequest(params); 765 }; 766 767 /** 768 * Updates the folder for this item. 769 * 770 * @param {String} folderId the new folder ID 771 */ 772 ZmItem.prototype.moveLocal = 773 function(folderId) { 774 this.folderId = folderId; 775 }; 776 777 /** 778 * Takes a comma-separated list of tag IDs and applies the tags to this item. 779 * 780 * @private 781 */ 782 ZmItem.prototype._parseTags = 783 function(str) { 784 this.tags = []; 785 this.tagHash = {}; 786 if (str && str.length) { 787 var tags = str.split(","); 788 for (var i = 0; i < tags.length; i++) { 789 var tagId = Number(tags[i]); 790 if (tagId >= ZmOrganizer.FIRST_USER_ID[ZmOrganizer.TAG]) 791 this.tagLocal(tagId, true); 792 } 793 } 794 }; 795 796 /** 797 * Takes a comma-separated list of tag names and applies the tags to this item. 798 * 799 * @private 800 */ 801 ZmItem.prototype._parseTagNames = 802 function(str) { 803 this.tags = []; 804 this.tagHash = {}; 805 if (!str || !str.length) { 806 return; 807 } 808 809 // server escapes comma with backslash 810 str = str.replace(/\\,/g, "\u001D"); 811 var tags = str.split(","); 812 813 for (var i = 0; i < tags.length; i++) { 814 var tagName = tags[i].replace("\u001D", ","); 815 this.tagLocal(tagName, true); 816 } 817 }; 818 819 /** 820 * Takes a string of flag chars and applies them to this item. 821 * 822 * @private 823 */ 824 ZmItem.prototype._parseFlags = 825 function(str) { 826 this.flags = str; 827 for (var i = 0; i < ZmItem.ALL_FLAGS.length; i++) { 828 var flag = ZmItem.ALL_FLAGS[i]; 829 var on = (str && (str.indexOf(flag) != -1)) ? true : false; 830 this.flagLocal(flag, on); 831 } 832 }; 833 834 // Listener notification 835 836 /** 837 * Notify the list as well as this item. 838 * 839 * @private 840 */ 841 ZmItem.prototype._notify = 842 function(event, details) { 843 this._doNotify(event, details); 844 }; 845 846 ZmItem.prototype._setupNotify = 847 function() { 848 this._doNotify(); 849 } 850 851 ZmItem.prototype._doNotify = 852 function(event, details) { 853 if (this._evt) { 854 this._evt.item = this; 855 if (event != null) { 856 ZmModel.prototype._notify.call(this, event, details); 857 } 858 } else { 859 var idText = ""; 860 if (this.type && this.id) { 861 idText = ": item = " + this.type + "(" + this.id + ")"; 862 } 863 DBG.println(AjxDebug.DBG1, "ZmItem._doNotify, missing _evt" + idText); 864 } 865 if (this.list) { 866 this.list._evt.item = this; 867 this.list._evt.items = [this]; 868 if (event != null) { 869 if (details) { 870 details.items = [this]; 871 } else { 872 details = {items: [this]}; 873 } 874 this.list._notify(event, details); 875 } 876 } 877 }; 878 879 /** 880 * Returns a list of flags that apply to this type of item. 881 * 882 * @private 883 */ 884 ZmItem.prototype._getFlags = 885 function() { 886 return [ZmItem.FLAG_FLAGGED, ZmItem.FLAG_ATTACH]; 887 }; 888 889 /** 890 * Rename the item. 891 * 892 * @param {String} newName 893 * @param {AjxCallback} callback the callback 894 * @param {AjxCallback} errorCallback the callback on error 895 * @return {Object} the result of the move 896 */ 897 ZmItem.prototype.rename = 898 function(newName, callback, errorCallback) { 899 return ZmItem.rename(this.id, newName, callback, errorCallback); 900 }; 901 902 /** 903 * Rename the item. 904 * 905 * @return {Object} the result of the move 906 */ 907 ZmItem.rename = 908 function(itemId, newName, callback, errorCallback, accountName) { 909 var json = { 910 ItemActionRequest: { 911 _jsns: "urn:zimbraMail", 912 action: { 913 id: itemId instanceof Array ? itemId[0] : itemId, 914 op: "rename", 915 name: newName 916 } 917 } 918 }; 919 920 var params = { 921 jsonObj: json, 922 asyncMode: Boolean(callback), 923 callback: callback, 924 errorCallback: errorCallback, 925 accountName: accountName 926 }; 927 return appCtxt.getAppController().sendRequest(params); 928 }; 929 930 ZmItem.prototype.getSortedTags = 931 function() { 932 var numTags = this.tags && this.tags.length; 933 if (numTags) { 934 var tagList = appCtxt.getAccountTagList(this); 935 var ta = []; 936 for (var i = 0; i < numTags; i++) { 937 var tag = tagList.getByNameOrRemote(this.tags[i]); 938 //tag could be missing if this was called when deleting a whole tag (not just untagging one message). So this makes sure we don't have a null item. 939 if (!tag) { 940 continue; 941 } 942 ta.push(tag); 943 } 944 ta.sort(ZmTag.sortCompare); 945 return ta; 946 } 947 return null; 948 }; 949 950