1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 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) 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 * Creates an empty calItem view used to display read-only calendar items. 26 * @constructor 27 * @class 28 * Simple read-only view of an appointment or task. It looks more or less like a 29 * message - the notes have their own area at the bottom, and everything else 30 * goes into a header section at the top. 31 * 32 * @author Parag Shah 33 * @author Conrad Damon 34 * 35 * @param {DwtComposite} parent the parent widget 36 * @param {constant} posStyle the positioning style 37 * @param {ZmController} controller the owning controller 38 * 39 * @extends ZmMailMsgView 40 * 41 * @private 42 */ 43 ZmCalItemView = function(parent, posStyle, controller, id) { 44 if (arguments.length == 0) return; 45 46 params = {parent: parent, posStyle: posStyle, controller: controller}; 47 if (id) { 48 params.id = id; 49 } 50 ZmMailMsgView.call(this, params); 51 }; 52 53 ZmCalItemView.prototype = new ZmMailMsgView; 54 ZmCalItemView.prototype.constructor = ZmCalItemView; 55 56 // Public methods 57 58 ZmCalItemView.prototype.isZmCalItemView = true; 59 ZmCalItemView.prototype.toString = function() { return "ZmCalItemView"; }; 60 61 ZmCalItemView.prototype.getController = 62 function() { 63 return this._controller; 64 }; 65 66 // Following public overrides are a hack to allow this view to pretend it's a list view, 67 // as well as a calendar view 68 ZmCalItemView.prototype.getSelection = 69 function() { 70 return [this._calItem]; 71 }; 72 73 ZmCalItemView.prototype.getSelectionCount = 74 function() { 75 return 1; 76 }; 77 78 ZmCalItemView.prototype.needsRefresh = 79 function() { 80 return false; 81 }; 82 83 ZmCalItemView.prototype.addSelectionListener = function() {}; 84 ZmCalItemView.prototype.addActionListener = function() {}; 85 ZmCalItemView.prototype.handleActionPopdown = function(ev) {}; 86 87 ZmCalItemView.prototype.getTitle = 88 function() { 89 // override 90 }; 91 92 ZmCalItemView.prototype.set = 93 function(calItem, prevView, mode) { 94 if (this._calItem == calItem) { return; } 95 96 // So that Close button knows which view to go to 97 // condition introduced to avoid irrelevant view being persisted as previous view 98 var viewMgr = this._controller._viewMgr; 99 this._prevView = prevView || (viewMgr && (calItem.folderId != ZmFolder.ID_TRASH) ? 100 viewMgr.getCurrentViewName() : this._prevView); 101 102 this.reset(); 103 this._calItem = this._item = calItem; 104 this._mode = mode; 105 this._renderCalItem(calItem, true); 106 }; 107 108 ZmCalItemView.prototype.reset = 109 function() { 110 ZmMailMsgView.prototype.reset.call(this); 111 this._calItem = this._item = null; 112 }; 113 114 ZmCalItemView.prototype.close = function() {}; // override 115 ZmCalItemView.prototype.move = function() {}; // override 116 ZmCalItemView.prototype.changeReminder = function() {}; // override 117 118 119 // Private / protected methods 120 121 ZmCalItemView.prototype._renderCalItem = 122 function(calItem, renderButtons) { 123 this._lazyCreateObjectManager(); 124 125 var subs = this._getSubs(calItem); 126 var closeBtnCellId = this._htmlElId + "_closeBtnCell"; 127 var editBtnCellId = this._htmlElId + "_editBtnCell"; 128 this._hdrTableId = this._htmlElId + "_hdrTable"; 129 130 var calendar = calItem.getFolder(); 131 var isReadOnly = calendar.isReadOnly(); 132 subs.allowEdit = !isReadOnly && (appCtxt.get(ZmSetting.CAL_APPT_ALLOW_ATTENDEE_EDIT) || calItem.isOrg); 133 134 var el = this.getHtmlElement(); 135 el.innerHTML = AjxTemplate.expand("calendar.Appointment#ReadOnlyView", subs); 136 var offlineHandler = appCtxt.webClientOfflineHandler; 137 if (offlineHandler) { 138 var linkIds = [ZmCalItem.ATT_LINK_IMAGE, ZmCalItem.ATT_LINK_MAIN, ZmCalItem.ATT_LINK_DOWNLOAD]; 139 var getLinkIdCallback = this._getAttachmentLinkId.bind(this); 140 offlineHandler._handleAttachmentsForOfflineMode(calItem.getAttachments(), getLinkIdCallback, linkIds); 141 } 142 143 if (renderButtons) { 144 // add the close button 145 this._closeButton = new DwtButton({parent:this, className:"DwtToolbarButton"}); 146 this._closeButton.setImage("Close"); 147 this._closeButton.setText(ZmMsg.close); 148 this._closeButton.addSelectionListener(new AjxListener(this, this.close)); 149 this._closeButton.reparentHtmlElement(closeBtnCellId); 150 151 if (document.getElementById(editBtnCellId)) { 152 // add the save button for reminders and move select 153 this._editButton = new DwtButton({parent:this, className:"DwtToolbarButton"}); 154 this._editButton.setImage("Edit"); 155 this._editButton.setText(ZmMsg.edit); 156 this._editButton.addSelectionListener(new AjxListener(this, this.edit)); 157 var calendar = calItem && appCtxt.getById(calItem.folderId); 158 var isTrash = calendar && calendar.id == ZmOrganizer.ID_TRASH; 159 this._editButton.setEnabled(!isTrash); 160 this._editButton.reparentHtmlElement(editBtnCellId); 161 } 162 } 163 164 // content/body 165 var hasHtmlPart = (calItem.notesTopPart && calItem.notesTopPart.getContentType() == ZmMimeTable.MULTI_ALT); 166 var mode = (hasHtmlPart && appCtxt.get(ZmSetting.VIEW_AS_HTML)) 167 ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN; 168 169 var bodyPart = calItem.getNotesPart(mode); 170 if (bodyPart) { 171 this._msg = this._msg || this._calItem._currentlyLoaded; 172 if (mode === ZmMimeTable.TEXT_PLAIN) { 173 bodyPart = AjxStringUtil.convertToHtml(bodyPart); 174 } 175 this._makeIframeProxy({container: el, html:bodyPart, isTextMsg:(mode == ZmMimeTable.TEXT_PLAIN)}); 176 } 177 }; 178 179 ZmCalItemView.prototype._getSubs = 180 function(calItem) { 181 // override 182 }; 183 184 ZmCalItemView.prototype._getTimeString = 185 function(calItem) { 186 // override 187 }; 188 189 ZmCalItemView.prototype._setAttachmentLinks = 190 function() { 191 // do nothing since calItem view renders attachments differently 192 }; 193 194 // returns true if given dates are w/in a single day 195 ZmCalItemView.prototype._isOneDayAppt = 196 function(sd, ed) { 197 var start = new Date(sd.getTime()); 198 var end = new Date(ed.getTime()); 199 200 start.setHours(0, 0, 0, 0); 201 end.setHours(0, 0, 0, 0); 202 203 return start.valueOf() == end.valueOf(); 204 }; 205 206 207 208 ZmCalItemView.prototype._getAttachString = 209 function(calItem) { 210 var str = []; 211 var j = 0; 212 213 var attachList = calItem.getAttachments(); 214 if (attachList) { 215 var getLinkIdCallback = this._getAttachmentLinkId.bind(this); 216 for (var i = 0; i < attachList.length; i++) { 217 str[j++] = ZmApptViewHelper.getAttachListHtml(calItem, attachList[i], false, getLinkIdCallback); 218 } 219 } 220 221 return str.join(""); 222 }; 223 224 ZmCalItemView.rfc822Callback = 225 function(invId, partId) { 226 AjxDispatcher.require("MailCore", false); 227 ZmMailMsgView.rfc822Callback(invId, partId); 228 }; 229 230 /** 231 * Creates an empty appointment view. 232 * @constructor 233 * @class 234 * Simple read-only view of an appointment. It looks more or less like a message - 235 * the notes have their own area at the bottom, and everything else goes into a 236 * header section at the top. 237 * 238 * @author Parag Shah 239 * @author Conrad Damon 240 * 241 * @param {DwtComposite} parent the parent widget 242 * @param {constant} posStyle the positioning style 243 * @param {ZmController} controller the owning controller 244 * 245 * @extends ZmCalItemView 246 * 247 * @private 248 */ 249 ZmApptView = function(parent, posStyle, controller) { 250 251 ZmCalItemView.call(this, parent, posStyle, controller); 252 }; 253 254 ZmApptView.prototype = new ZmCalItemView; 255 ZmApptView.prototype.constructor = ZmApptView; 256 257 ZmApptView.prototype.isZmApptView = true; 258 ZmApptView.prototype.toString = function() { return "ZmApptView"; }; 259 260 // Public methods 261 262 ZmApptView.prototype.getTitle = 263 function() { 264 return [ZmMsg.zimbraTitle, ZmMsg.appointment].join(": "); 265 }; 266 267 ZmApptView.prototype.edit = 268 function(ev) { 269 var item = this._calItem; 270 271 if(!item.isOrg && !(this._editWarningDialog && this._editWarningDialog.isPoppedUp())){ 272 var msgDialog = this._editWarningDialog = appCtxt.getMsgDialog(); 273 msgDialog.setMessage(ZmMsg.attendeeEditWarning, DwtMessageDialog.WARNING_STYLE); 274 msgDialog.popup(); 275 msgDialog.registerCallback(DwtDialog.OK_BUTTON, this.edit, this); 276 return; 277 }else if(this._editWarningDialog){ 278 this._editWarningDialog.popdown(); 279 this._editWarningDialog.reset(); 280 } 281 282 var mode = ZmCalItem.MODE_EDIT; 283 if (item.isRecurring()) { 284 mode = this._mode || ZmCalItem.MODE_EDIT_SINGLE_INSTANCE; 285 } 286 item.setViewMode(mode); 287 var app = this._controller._app; 288 app.getApptComposeController().show(item, mode); 289 }; 290 291 ZmApptView.prototype.setBounds = 292 function(x, y, width, height) { 293 // dont reset the width! 294 ZmMailMsgView.prototype.setBounds.call(this, x, y, Dwt.DEFAULT, height); 295 }; 296 297 ZmApptView.prototype._renderCalItem = 298 function(calItem) { 299 300 this._lazyCreateObjectManager(); 301 302 var subs = this._getSubs(calItem); 303 subs.subject = AjxStringUtil.htmlEncode(subs.subject); 304 305 this._hdrTableId = this._htmlElId + "_hdrTable"; 306 307 var calendar = calItem.getFolder(); 308 var isReadOnly = calendar.isReadOnly() || calendar.isInTrash(); 309 subs.allowEdit = !isReadOnly && (appCtxt.get(ZmSetting.CAL_APPT_ALLOW_ATTENDEE_EDIT) || calItem.isOrg); 310 311 var el = this.getHtmlElement(); 312 el.innerHTML = AjxTemplate.expand("calendar.Appointment#ReadOnlyView", subs); 313 var offlineHandler = appCtxt.webClientOfflineHandler; 314 if (offlineHandler) { 315 var linkIds = [ZmCalItem.ATT_LINK_IMAGE, ZmCalItem.ATT_LINK_MAIN, ZmCalItem.ATT_LINK_DOWNLOAD]; 316 var getLinkIdCallback = this._getAttachmentLinkId.bind(this); 317 offlineHandler._handleAttachmentsForOfflineMode(calItem.getAttachments(), getLinkIdCallback, linkIds); 318 } 319 320 // Set tab name as Appointment subject 321 var subject = AjxStringUtil.trim(calItem.getName()); 322 if (subject) { 323 var tabButtonText = subject.substring(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT); 324 appCtxt.getAppViewMgr().setTabTitle(this._controller.getCurrentViewId(), tabButtonText); 325 } 326 327 this._createBubbles(); 328 329 var selParams = {parent: this, id: Dwt.getNextId('ZmNeedActionSelect_')}; 330 var statusSelect = new DwtSelect(selParams); 331 332 var ptst = {}; 333 ptst[ZmCalBaseItem.PSTATUS_NEEDS_ACTION] = ZmMsg.ptstMsgNeedsAction; 334 ptst[ZmCalBaseItem.PSTATUS_ACCEPT] = ZmMsg.ptstMsgAccepted; 335 ptst[ZmCalBaseItem.PSTATUS_TENTATIVE] = ZmMsg.ptstMsgTentative; 336 ptst[ZmCalBaseItem.PSTATUS_DECLINED] = ZmMsg.ptstMsgDeclined; 337 338 this._ptst = ptst; 339 //var statusMsgs = {}; 340 var calItemPtst = calItem.ptst || ZmCalBaseItem.PSTATUS_ACCEPT; 341 342 var data = null; 343 for (var stat in ptst) { 344 //stat = ptst[index]; 345 if (stat === ZmCalBaseItem.PSTATUS_NEEDS_ACTION && calItemPtst !== ZmCalBaseItem.PSTATUS_NEEDS_ACTION) { continue; } 346 data = new DwtSelectOptionData(stat, ZmCalItem.getLabelForParticipationStatus(stat), false, null, ZmCalItem.getParticipationStatusIcon(stat), Dwt.getNextId('ZmNeedActionOption_' + stat + '_')); 347 statusSelect.addOption(data); 348 if (stat == calItemPtst){ 349 statusSelect.setSelectedValue(stat); 350 } 351 } 352 if (isReadOnly) { statusSelect.setEnabled(false); } 353 354 this._statusSelect = statusSelect; 355 this._origPtst = calItemPtst; 356 statusSelect.reparentHtmlElement(this._htmlElId + "_responseActionSelectCell"); 357 statusSelect.addChangeListener(new AjxListener(this, this._statusSelectListener)); 358 359 this._statusMsgEl = document.getElementById(this._htmlElId + "_responseActionMsgCell"); 360 this._statusMsgEl.innerHTML = ptst[calItemPtst]; 361 362 // content/body 363 var hasHtmlPart = (calItem.notesTopPart && calItem.notesTopPart.getContentType() == ZmMimeTable.MULTI_ALT); 364 var mode = (hasHtmlPart && appCtxt.get(ZmSetting.VIEW_AS_HTML)) 365 ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN; 366 367 var bodyPart = calItem.getNotesPart(mode); 368 if (bodyPart) { 369 this._msg = this._msg || this._calItem._currentlyLoaded; 370 if (mode === ZmMimeTable.TEXT_PLAIN) { 371 bodyPart = AjxStringUtil.convertToHtml(bodyPart); 372 } 373 this._makeIframeProxy({container: el, html:bodyPart, isTextMsg:(mode == ZmMimeTable.TEXT_PLAIN)}); 374 } 375 }; 376 377 ZmApptView.prototype._getSubs = 378 function(calItem) { 379 var subject = calItem.getName(); 380 var location = calItem.location; 381 var equipment = calItem.getAttendeesText(ZmCalBaseItem.EQUIPMENT, true); 382 var isException = calItem._orig.isException; 383 var dateStr = this._getTimeString(calItem); 384 385 this._clearBubbles(); 386 var reqAttendees = this._getAttendeesByRoleCollapsed(calItem.getAttendees(ZmCalBaseItem.PERSON), ZmCalBaseItem.PERSON, ZmCalItem.ROLE_REQUIRED); 387 var optAttendees = this._getAttendeesByRoleCollapsed(calItem.getAttendees(ZmCalBaseItem.PERSON), ZmCalBaseItem.PERSON, ZmCalItem.ROLE_OPTIONAL); 388 var hasAttendees = reqAttendees || optAttendees; 389 390 var organizer, obo; 391 var recurStr = calItem.isRecurring() ? calItem.getRecurBlurb() : null; 392 var attachStr = this._getAttachString(calItem); 393 394 if (hasAttendees) { // I really don't know why this check here but it's the way it was before so keeping it. (I just renamed the var) 395 organizer = new AjxEmailAddress(calItem.getOrganizer(), null, calItem.getOrganizerName()); 396 397 var sender = calItem.message.getAddress(AjxEmailAddress.SENDER); 398 var from = calItem.message.getAddress(AjxEmailAddress.FROM); 399 var address = sender || from; 400 if (!organizer && address) { 401 organizer = address.toString(); 402 } 403 if (sender && organizer) { 404 obo = from ? new AjxEmailAddress(from.toString()) : organizer; 405 } 406 } 407 408 organizer = organizer && this._getBubbleHtml(organizer); 409 obo = obo && this._getBubbleHtml(obo); 410 411 return { 412 id: this._htmlElId, 413 subject: subject, 414 location: location, 415 equipment: equipment, 416 isException: isException, 417 dateStr: dateStr, 418 isAttendees: hasAttendees, 419 reqAttendees: reqAttendees, 420 optAttendees: optAttendees, 421 org: organizer, 422 obo: obo, 423 recurStr: recurStr, 424 attachStr: attachStr, 425 folder: appCtxt.getTree(ZmOrganizer.CALENDAR).getById(calItem.folderId), 426 folderLabel: ZmMsg.calendar, 427 reminderLabel: ZmMsg.reminder, 428 alarm: calItem.alarm, 429 isAppt: true, 430 _infoBarId: this._infoBarId 431 }; 432 }; 433 434 /** 435 * Creates a string of attendees by role. If an item doesn't have a name, its address is used. 436 * 437 * calls common code from mail msg view to get the collapse/expand "show more" funcitonality for large lists. 438 * 439 * @param list [array] list of attendees (ZmContact or ZmResource) 440 * @param type [constant] attendee type 441 * @param role [constant] attendee role 442 */ 443 ZmApptView.prototype._getAttendeesByRoleCollapsed = function(list, type, role) { 444 445 if (!(list && list.length)) { 446 return ""; 447 } 448 var attendees = ZmApptViewHelper.getAttendeesArrayByRole(list, role); 449 450 var emails = []; 451 for (var i = 0; i < attendees.length; i++) { 452 var att = attendees[i]; 453 emails.push(new AjxEmailAddress(att.getEmail(), type, att.getFullName(), att.getFullName(), att.isGroup(), att.canExpand)); 454 } 455 456 var options = {}; 457 options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS); 458 var addressInfo = this.getAddressesFieldHtmlHelper(emails, options, role); 459 return addressInfo.html; 460 }; 461 462 ZmApptView.prototype._getTimeString = 463 function(calItem) { 464 var sd = calItem._orig.startDate; 465 var ed = calItem._orig.endDate; 466 var tz = AjxMsg[AjxTimezone.DEFAULT] || AjxTimezone.getServerId(AjxTimezone.DEFAULT) 467 468 if (calItem.isRecurring() && this._mode == ZmCalItem.MODE_EDIT_SERIES) { 469 sd = calItem.startDate; 470 ed = calItem.endDate; 471 var seriesTZ = calItem.getTimezone(); 472 473 //convert to client timezone if appt's timezone differs 474 if(seriesTZ != AjxTimezone.getServerId(AjxTimezone.DEFAULT)) { 475 var offset1 = AjxTimezone.getOffset(AjxTimezone.DEFAULT, sd); 476 var offset2 = AjxTimezone.getOffset(AjxTimezone.getClientId(seriesTZ), sd); 477 sd.setTime(sd.getTime() + (offset1 - offset2)*60*1000); 478 ed.setTime(ed.getTime() + (offset1 - offset2)*60*1000); 479 calItem.setTimezone(AjxTimezone.getServerId(AjxTimezone.DEFAULT)); 480 } 481 } 482 483 var isAllDay = calItem.isAllDayEvent(); 484 var isMultiDay = calItem.isMultiDay(); 485 if (isAllDay && isMultiDay) { 486 var endDate = new Date(ed.getTime()); 487 ed.setDate(endDate.getDate()-1); 488 } 489 490 var pattern = isAllDay ? 491 (isMultiDay ? ZmMsg.apptTimeAllDayMulti : ZmMsg.apptTimeAllDay) : 492 (isMultiDay ? ZmMsg.apptTimeInstanceMulti : ZmMsg.apptTimeInstance); 493 var params = [sd, ed, tz]; 494 495 return AjxMessageFormat.format(pattern, params); 496 }; 497 498 ZmApptView.prototype.set = 499 function(appt, mode) { 500 this.reset(); 501 this._calItem = this._item = appt; 502 this._mode = mode; 503 this._renderCalItem(appt, false); 504 }; 505 506 ZmApptView.prototype.reEnableDesignMode = 507 function() { 508 509 }; 510 511 ZmApptView.prototype.isDirty = 512 function() { 513 var retVal = false, 514 value = this._statusSelect.getValue(); 515 if(this._origPtst != value) { 516 retVal = true; 517 } 518 return retVal; 519 }; 520 521 ZmApptView.prototype.isValid = 522 function() { 523 // No fields to validate 524 return true; 525 } 526 527 ZmApptView.prototype.setOrigPtst = 528 function(value) { 529 this._origPtst = value; 530 this._statusSelectListener(); 531 }; 532 533 ZmApptView.prototype.cleanup = 534 function() { 535 return false; 536 }; 537 538 ZmApptView.prototype.close = 539 function() { 540 this._controller._closeView(); 541 }; 542 543 ZmApptView.prototype.getOpValue = 544 function() { 545 var value = this._statusSelect.getValue(), 546 statusToOp = {}; 547 statusToOp[ZmCalBaseItem.PSTATUS_NEEDS_ACTION] = null; 548 statusToOp[ZmCalBaseItem.PSTATUS_ACCEPT] = ZmOperation.REPLY_ACCEPT; 549 statusToOp[ZmCalBaseItem.PSTATUS_TENTATIVE] = ZmOperation.REPLY_TENTATIVE; 550 statusToOp[ZmCalBaseItem.PSTATUS_DECLINED] = ZmOperation.REPLY_DECLINE; 551 return statusToOp[value]; 552 }; 553 554 ZmApptView.prototype._statusSelectListener = 555 function() { 556 var saveButton = this.getController().getCurrentToolbar().getButton(ZmOperation.SAVE), 557 value = this._statusSelect.getValue(); 558 saveButton.setEnabled(this._origPtst != value); 559 this._statusMsgEl.innerHTML = this._ptst[value]; 560 }; 561 562 ZmApptView.prototype._getDialogXY = 563 function() { 564 var loc = Dwt.toWindow(this.getHtmlElement(), 0, 0); 565 return new DwtPoint(loc.x + ZmApptComposeView.DIALOG_X, loc.y + ZmApptComposeView.DIALOG_Y); 566 }; 567