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