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 a new appointment view. The view does not display itself on construction. 26 * @constructor 27 * @class 28 * This class provides a form for creating/editing appointments. It is a tab view with 29 * five tabs: the appt form, a scheduling page, and three pickers (one each for finding 30 * attendees, locations, and equipment). The attendee data (people, locations, and 31 * equipment are all attendees) is maintained here centrally, since it is presented and 32 * can be modified in each of the five tabs. 33 * 34 * @author Parag Shah 35 * 36 * @param {DwtShell} parent the element that created this view 37 * @param {String} className class name for this view 38 * @param {ZmCalendarApp} calApp a handle to the owning calendar application 39 * @param {ZmApptComposeController} controller the controller for this view 40 * 41 * @extends DwtTabView 42 */ 43 ZmApptComposeView = function(parent, className, calApp, controller) { 44 45 className = className ? className : "ZmApptComposeView"; 46 var params = {parent:parent, className:className, posStyle:Dwt.ABSOLUTE_STYLE, id:Dwt.getNextId("APPT_COMPOSE_")}; 47 DwtComposite.call(this, params); 48 49 this.setScrollStyle(DwtControl.CLIP); 50 this._app = calApp; 51 this._controller = controller; 52 53 // centralized date info 54 this._dateInfo = {}; 55 56 // centralized attendee data 57 this._attendees = {}; 58 this._attendees[ZmCalBaseItem.PERSON] = new AjxVector(); // list of ZmContact 59 this._attendees[ZmCalBaseItem.LOCATION] = new AjxVector(); // list of ZmResource 60 this._attendees[ZmCalBaseItem.EQUIPMENT]= new AjxVector(); // list of ZmResource 61 62 // set of attendee keys (for preventing duplicates) 63 this._attendeeKeys = {}; 64 this._attendeeKeys[ZmCalBaseItem.PERSON] = {}; 65 this._attendeeKeys[ZmCalBaseItem.LOCATION] = {}; 66 this._attendeeKeys[ZmCalBaseItem.EQUIPMENT] = {}; 67 68 // Email to type map 69 this._attendeeType = {}; 70 71 // for attendees change events 72 this._evt = new ZmEvent(ZmEvent.S_CONTACT); 73 this._evtMgr = new AjxEventMgr(); 74 75 this._initialize(); 76 }; 77 78 // attendee operations 79 ZmApptComposeView.MODE_ADD = 1; 80 ZmApptComposeView.MODE_REMOVE = 2; 81 ZmApptComposeView.MODE_REPLACE = 3; 82 83 ZmApptComposeView.prototype = new DwtComposite; 84 ZmApptComposeView.prototype.constructor = ZmApptComposeView; 85 86 // Consts 87 88 // Message dialog placement 89 ZmApptComposeView.DIALOG_X = 50; 90 ZmApptComposeView.DIALOG_Y = 100; 91 92 //compose mode 93 ZmApptComposeView.CREATE = 1; 94 ZmApptComposeView.EDIT = 2; 95 ZmApptComposeView.FORWARD = 3; 96 ZmApptComposeView.PROPOSE_TIME = 4; 97 98 // Public methods 99 100 ZmApptComposeView.prototype.toString = 101 function() { 102 return "ZmApptComposeView"; 103 }; 104 105 ZmApptComposeView.prototype.getController = 106 function() { 107 return this._controller; 108 }; 109 110 ZmApptComposeView.prototype.set = 111 function(appt, mode, isDirty) { 112 113 var isForward = false; 114 115 //decides whether appt is being edited/forwarded/proposed new time 116 var apptComposeMode = ZmApptComposeView.EDIT; 117 118 119 //"mode" should always be set to one of ZmCalItem.MODE_EDIT/ZmCalItem.MODE_EDIT_INSTANCE/ZmCalItem.MODE_EDIT_SERIES/ZmCalItem.MODE_NEW 120 if(ZmCalItem.FORWARD_MAPPING[mode]) { 121 isForward = true; 122 this._forwardMode = mode; 123 mode = ZmCalItem.FORWARD_MAPPING[mode]; 124 apptComposeMode = ZmApptComposeView.FORWARD; 125 } else { 126 this._forwardMode = undefined; 127 } 128 129 this._proposeNewTime = (mode == ZmCalItem.MODE_PROPOSE_TIME); 130 131 if (this._proposeNewTime) { 132 mode = appt.viewMode || ZmCalItem.MODE_EDIT; 133 apptComposeMode = ZmApptComposeView.PROPOSE_TIME; 134 } 135 136 this._setData = [appt, mode, isDirty]; 137 this._dateInfo.timezone = appt.getTimezone(); 138 this._apptEditView.initialize(appt, mode, isDirty, apptComposeMode); 139 this._apptEditView.show(); 140 141 var editMode = !Boolean(this._forwardMode) && !this._proposeNewTime; 142 this._apptEditView.enableInputs(editMode); 143 this._apptEditView.enableSubjectField(!this._proposeNewTime); 144 145 var toolbar = this._controller.getToolbar(); 146 toolbar.enableAll(true); 147 toolbar.enable([ZmOperation.ATTACHMENT], editMode); 148 }; 149 150 ZmApptComposeView.prototype.cleanup = 151 function() { 152 // clear attendees lists 153 this._attendees[ZmCalBaseItem.PERSON] = new AjxVector(); 154 this._attendees[ZmCalBaseItem.LOCATION] = new AjxVector(); 155 this._attendees[ZmCalBaseItem.EQUIPMENT] = new AjxVector(); 156 157 this._attendeeKeys[ZmCalBaseItem.PERSON] = {}; 158 this._attendeeKeys[ZmCalBaseItem.LOCATION] = {}; 159 this._attendeeKeys[ZmCalBaseItem.EQUIPMENT] = {}; 160 161 this._apptEditView.cleanup(); 162 }; 163 164 ZmApptComposeView.prototype.preload = 165 function() { 166 this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 167 this._apptEditView.createHtml(); 168 }; 169 170 ZmApptComposeView.prototype.getComposeMode = 171 function() { 172 return this._apptEditView.getComposeMode(); 173 }; 174 175 // Sets the mode the editor should be in. 176 ZmApptComposeView.prototype.setComposeMode = 177 function(composeMode) { 178 if (composeMode == Dwt.TEXT || 179 (composeMode == Dwt.HTML && appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED))) 180 { 181 this._apptEditView.setComposeMode(composeMode); 182 } 183 }; 184 185 ZmApptComposeView.prototype.reEnableDesignMode = 186 function() { 187 this._apptEditView.reEnableDesignMode(); 188 }; 189 190 ZmApptComposeView.prototype.isDirty = 191 function() { 192 //if view is inactive or closed return false 193 if(this._controller.inactive) { 194 return false; 195 } 196 //drag and drop changed appts will be dirty even if nothing is changed 197 var apptEditView = this._apptEditView; 198 if( apptEditView && apptEditView._calItem && apptEditView._calItem.dndUpdate){ 199 return true; 200 } 201 return apptEditView.isDirty(); 202 }; 203 204 ZmApptComposeView.prototype.isReminderOnlyChanged = 205 function() { 206 return this._apptEditView ? this._apptEditView.isReminderOnlyChanged() : false; 207 }; 208 209 ZmApptComposeView.prototype.isValid = 210 function() { 211 return this._apptEditView.isValid(); 212 }; 213 214 /** 215 * Adds an attachment file upload field to the compose form. 216 * 217 */ 218 ZmApptComposeView.prototype.addAttachmentField = 219 function() { 220 this._apptEditView.addAttachmentField(); 221 }; 222 223 ZmApptComposeView.prototype.getAppt = 224 function(attId) { 225 return this.getCalItem(attId); 226 }; 227 228 ZmApptComposeView.prototype.getCalItem = 229 function(attId) { 230 return this._apptEditView.getCalItem(attId); 231 }; 232 233 ZmApptComposeView.prototype.getForwardAddress = 234 function() { 235 return this._apptEditView.getForwardAddress(); 236 }; 237 238 ZmApptComposeView.prototype.gotNewAttachments = 239 function() { 240 return this._apptEditView.gotNewAttachments(); 241 }; 242 243 ZmApptComposeView.prototype.getHtmlEditor = 244 function() { 245 return this._apptEditView.getHtmlEditor(); 246 }; 247 248 ZmApptComposeView.prototype.getNumLocationConflictRecurrence = 249 function() { 250 return this._apptEditView.getNumLocationConflictRecurrence(); 251 } 252 253 ZmApptComposeView.prototype.cancelLocationRequest = 254 function() { 255 return this._apptEditView.cancelLocationRequest(); 256 } 257 258 ZmApptComposeView.prototype.setLocationConflictCallback = 259 function(locationConflictCallback) { 260 this._locationConflictCallback = locationConflictCallback; 261 }; 262 263 /** 264 * Updates the set of attendees for this appointment, by adding attendees or by 265 * replacing the current list (with a clone of the one passed in). 266 * 267 * @param attendees [object] attendee(s) as string, array, or AjxVector 268 * @param type [constant] attendee type (attendee/location/equipment) 269 * @param mode [constant]* replace (default) or add 270 * @param index [int]* index at which to add attendee 271 * 272 * @private 273 */ 274 ZmApptComposeView.prototype.updateAttendees = 275 function(attendees, type, mode, index) { 276 attendees = (attendees instanceof AjxVector) ? attendees.getArray() : 277 (attendees instanceof Array) ? attendees : [attendees]; 278 mode = mode || ZmApptComposeView.MODE_REPLACE; 279 // Note whether any of the attendees changed. Needed to decide 280 // for Locations whether or not to check for conflicts 281 var changed = false; 282 var key; 283 if (mode == ZmApptComposeView.MODE_REPLACE) { 284 this._attendees[type] = new AjxVector(); 285 var oldKeys = this._attendeeKeys[type]; 286 this._attendeeKeys[type] = {}; 287 for (var i = 0; i < attendees.length; i++) { 288 var attendee = attendees[i]; 289 this._attendees[type].add(attendee); 290 key = this._addAttendeeKey(attendee, type); 291 this._attendeeType[key] = type; 292 if (key && !oldKeys[key]) { 293 // New key that was not in the old set 294 changed = true; 295 } 296 } 297 if ((type == ZmCalBaseItem.LOCATION) && this._locationConflictCallback) { 298 for (key in oldKeys) { 299 if (key && !this._attendeeKeys[type][key]) { 300 // Old location key that is not in the new set 301 changed = true; 302 break; 303 } 304 } 305 } 306 } else if (mode == ZmApptComposeView.MODE_ADD) { 307 for (var i = 0; i < attendees.length; i++) { 308 var attendee = attendees[i]; 309 key = this._getAttendeeKey(attendee); 310 this._attendeeType[key] = type; 311 if (!this._attendeeKeys[type][key] === true) { 312 this._attendees[type].add(attendee, index); 313 this._addAttendeeKey(attendee, type); 314 changed = true; 315 } 316 } 317 } else if (mode == ZmApptComposeView.MODE_REMOVE) { 318 for (var i = 0; i < attendees.length; i++) { 319 var attendee = attendees[i]; 320 key = this._removeAttendeeKey(attendee, type); 321 delete this._attendeeType[key]; 322 this._attendees[type].remove(attendee); 323 if (key) { 324 changed = true; 325 } 326 } 327 } 328 329 if (changed && (type == ZmCalBaseItem.LOCATION) && this._locationConflictCallback) { 330 this._locationConflictCallback.run(this._attendees[ZmCalBaseItem.LOCATION]); 331 } 332 }; 333 334 335 ZmApptComposeView.prototype.setApptMessage = 336 function(msg){ 337 this._apptEditView.setApptMessage(msg); 338 }; 339 340 ZmApptComposeView.prototype.isAttendeesEmpty = 341 function() { 342 return this._apptEditView.isAttendeesEmpty(); 343 }; 344 345 ZmApptComposeView.prototype.isOrganizer = 346 function() { 347 return this._apptEditView.isOrganizer(); 348 }; 349 350 ZmApptComposeView.prototype.getTitle = 351 function() { 352 return [ZmMsg.zimbraTitle, ZmMsg.appointment].join(": "); 353 }; 354 355 ZmApptComposeView.prototype._getAttendeeKey = 356 function(attendee) { 357 var email = attendee.getLookupEmail() || attendee.getEmail(); 358 var name = attendee.getFullName(); 359 return email ? email : name; 360 }; 361 362 ZmApptComposeView.prototype._addAttendeeKey = 363 function(attendee, type) { 364 var key = this._getAttendeeKey(attendee); 365 if (key) { 366 this._attendeeKeys[type][key] = true; 367 } 368 return key; 369 }; 370 371 ZmApptComposeView.prototype._removeAttendeeKey = 372 function(attendee, type) { 373 var key = this._getAttendeeKey(attendee); 374 if (key) { 375 delete this._attendeeKeys[type][key]; 376 } 377 return key; 378 }; 379 380 ZmApptComposeView.prototype.getAttendeeType = 381 function(email) { 382 return this._attendeeType[email]; 383 } 384 385 /** 386 * Adds a change listener. 387 * 388 * @param {AjxListener} listener a listener 389 */ 390 ZmApptComposeView.prototype.addChangeListener = 391 function(listener) { 392 return this._evtMgr.addListener(ZmEvent.L_MODIFY, listener); 393 }; 394 395 /** 396 * Removes the given change listener. 397 * 398 * @param {AjxListener} listener a listener 399 */ 400 ZmApptComposeView.prototype.removeChangeListener = 401 function(listener) { 402 return this._evtMgr.removeListener(ZmEvent.L_MODIFY, listener); 403 }; 404 405 ZmApptComposeView.prototype.showErrorMessage = 406 function(msg, style, cb, cbObj, cbArgs) { 407 var msgDialog = appCtxt.getMsgDialog(); 408 msgDialog.reset(); 409 style = style ? style : DwtMessageDialog.CRITICAL_STYLE 410 msgDialog.setMessage(msg, style); 411 msgDialog.popup(this._getDialogXY()); 412 msgDialog.registerCallback(DwtDialog.OK_BUTTON, cb, cbObj, cbArgs); 413 }; 414 415 ZmApptComposeView.prototype.showInvalidDurationMsg = 416 function(msg, style, cb, cbObj, cbArgs) { 417 var msgDlg = appCtxt.getMsgDialog(true); 418 msgDlg.setMessage(ZmMsg.timezoneConflictMsg,DwtMessageDialog.WARNING_STYLE); 419 msgDlg.setTitle(ZmMsg.timezoneConflictTitle); 420 msgDlg.popup(); 421 } 422 ZmApptComposeView.prototype.showInvalidDurationRecurrenceMsg = 423 function() { 424 var msgDlg = appCtxt.getMsgDialog(true); 425 msgDlg.setMessage(ZmMsg.durationRecurrenceError, DwtMessageDialog.WARNING_STYLE); 426 msgDlg.setTitle(ZmMsg.durationRecurrenceErrorTitle); 427 msgDlg.popup(); 428 } 429 430 // Private / Protected methods 431 432 ZmApptComposeView.prototype._initialize = 433 function() { 434 this._apptEditView = new ZmApptEditView(this, this._attendees, this._controller, this._dateInfo); 435 this._apptEditView.addRepeatChangeListener(new AjxListener(this, this._repeatChangeListener)); 436 this.addControlListener(new AjxListener(this, this._controlListener)); 437 438 // make the appointment edit view take up the full size of this view 439 var bounds = this.getInsetBounds(); 440 this._apptEditView.setSize(bounds.width, bounds.height); 441 }; 442 443 ZmApptComposeView.prototype.getApptEditView = 444 function() { 445 return this._apptEditView; 446 }; 447 448 ZmApptComposeView.prototype.getAttendees = 449 function(type) { 450 return this._attendees[type]; 451 }; 452 453 ZmApptComposeView.prototype._repeatChangeListener = 454 function(ev) { 455 456 }; 457 458 // Consistent spot to locate various dialogs 459 ZmApptComposeView.prototype._getDialogXY = 460 function() { 461 var loc = Dwt.toWindow(this.getHtmlElement(), 0, 0); 462 return new DwtPoint(loc.x + ZmApptComposeView.DIALOG_X, loc.y + ZmApptComposeView.DIALOG_Y); 463 }; 464 465 // Listeners 466 467 ZmApptComposeView.prototype._controlListener = 468 function(ev) { 469 if (ev && ev.type === DwtControlEvent.RESIZE) { 470 // make the appointment edit view take up the full size of this view 471 var bounds = this.getInsetBounds(); 472 this._apptEditView.setSize(bounds.width, bounds.height); 473 } 474 }; 475 476 ZmApptComposeView.prototype.deactivate = 477 function() { 478 this._controller.inactive = true; 479 480 //clear the free busy cache if the last tabbed compose view session is closed 481 //var activeComposeSesions = this._app.getNumSessionControllers(ZmId.VIEW_APPOINTMENT); 482 //if(activeComposeSesions == 0) this._app.getFreeBusyCache().clearCache(); 483 484 }; 485 486 ZmApptComposeView.prototype.checkIsDirty = 487 function(type, attribs){ 488 return this._apptEditView.checkIsDirty(type, attribs); 489 }; 490 491 ZmApptComposeView.prototype.areRecurringChangesDirty = function() { 492 return this._apptEditView.areRecurringChangesDirty(); 493 }; 494