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  * Does nothing.
 26  * @constructor
 27  * @class
 28  * This static class provides utility functions for dealing with appointments
 29  * and their related forms and views.
 30  *
 31  * @author Parag Shah
 32  * @author Conrad Damon
 33  *
 34  * - Helper methods shared by several views associated w/ creating new appointments.
 35  *   XXX: move to new files when fully baked!
 36  *   
 37  * @private
 38  */
 39 ZmApptViewHelper = function() {
 40 };
 41 
 42 ZmApptViewHelper.REPEAT_OPTIONS = [
 43 	{ label: ZmMsg.none, 				value: "NON", 	selected: true 	},
 44 	{ label: ZmMsg.everyDay, 			value: "DAI", 	selected: false },
 45 	{ label: ZmMsg.everyWeek, 			value: "WEE", 	selected: false },
 46 	{ label: ZmMsg.everyMonth, 			value: "MON", 	selected: false },
 47 	{ label: ZmMsg.everyYear, 			value: "YEA", 	selected: false },
 48 	{ label: ZmMsg.custom, 				value: "CUS", 	selected: false }];
 49 
 50 
 51 ZmApptViewHelper.SHOWAS_OPTIONS = [
 52 	{ label: ZmMsg.free, 				value: "F", 	selected: false },
 53 	{ label: ZmMsg.organizerTentative, 		value: "T", 	selected: false },
 54 	{ label: ZmMsg.busy, 				value: "B", 	selected: true  },
 55 	{ label: ZmMsg.outOfOffice,			value: "O", 	selected: false }
 56 ];
 57 
 58 /**
 59  * returns the label of the option specified by it's value. This is used in calendar.Appointment#Tooltip template
 60  *
 61  * @param value
 62  * returns the label
 63  */
 64 ZmApptViewHelper.getShowAsOptionLabel =
 65 function(value) {
 66 
 67 	for (var i = 0; i < ZmApptViewHelper.SHOWAS_OPTIONS.length; i++) {
 68 		var option = ZmApptViewHelper.SHOWAS_OPTIONS[i];
 69 		if (option.value == value) {
 70 			return option.label;
 71 		}
 72 	}
 73 };
 74 
 75 
 76 /**
 77  * Gets an object with the indices of the currently selected time fields.
 78  *
 79  * @param {ZmApptEditView}	tabView		the edit/tab view containing time widgets
 80  * @param {Hash}	dateInfo	a hash of date info to fill in
 81  */
 82 ZmApptViewHelper.getDateInfo =
 83 function(tabView, dateInfo) {
 84 	dateInfo.startDate = tabView._startDateField.value;
 85 	dateInfo.endDate = tabView._endDateField.value;
 86     var tzoneSelect = tabView._tzoneSelect || tabView._tzoneSelectStart;
 87     dateInfo.timezone = tzoneSelect ? tzoneSelect.getValue() : "";
 88     if (tabView._allDayCheckbox && tabView._allDayCheckbox.checked) {
 89 		dateInfo.showTime = false;
 90 
 91         //used by DwtTimeInput - advanced time picker
 92         dateInfo.startTimeStr = dateInfo.endTimeStr = null;
 93 
 94         //used by DwtTimeSelect
 95         dateInfo.startHourIdx = dateInfo.startMinuteIdx = dateInfo.startAmPmIdx =
 96 		dateInfo.endHourIdx = dateInfo.endMinuteIdx = dateInfo.endAmPmIdx = null;
 97 
 98         dateInfo.isAllDay = true;
 99     } else {
100 		dateInfo.showTime = true;
101 
102         if(tabView._startTimeSelect instanceof DwtTimeSelect) {
103             dateInfo.startHourIdx = tabView._startTimeSelect.getSelectedHourIdx();
104             dateInfo.startMinuteIdx = tabView._startTimeSelect.getSelectedMinuteIdx();
105             dateInfo.startAmPmIdx = tabView._startTimeSelect.getSelectedAmPmIdx();
106             dateInfo.endHourIdx = tabView._endTimeSelect.getSelectedHourIdx();
107             dateInfo.endMinuteIdx = tabView._endTimeSelect.getSelectedMinuteIdx();
108             dateInfo.endAmPmIdx = tabView._endTimeSelect.getSelectedAmPmIdx();
109         }else {
110             dateInfo.startHourIdx = dateInfo.startMinuteIdx = dateInfo.startAmPmIdx =
111             dateInfo.endHourIdx = dateInfo.endMinuteIdx = dateInfo.endAmPmIdx = null;            
112         }
113 
114         if(tabView._startTimeSelect instanceof DwtTimeInput) {
115             dateInfo.startTimeStr = tabView._startTimeSelect.getTimeString();
116             dateInfo.endTimeStr = tabView._endTimeSelect.getTimeString();
117         }else {
118             dateInfo.startTimeStr = dateInfo.endTimeStr = null;
119         }
120 
121         dateInfo.isAllDay = false;
122 	}
123 };
124 
125 ZmApptViewHelper.handleDateChange = 
126 function(startDateField, endDateField, isStartDate, skipCheck, oldStartDate) {
127 	var needsUpdate = false;
128 	var sd = AjxDateUtil.simpleParseDateStr(startDateField.value);
129 	var ed = AjxDateUtil.simpleParseDateStr(endDateField.value);
130 
131 	// if start date changed, reset end date if necessary
132 	if (isStartDate) {
133 		// if date was input by user and it's foobar, reset to today's date
134 		if (!skipCheck) {
135 			if (sd == null || isNaN(sd)) {
136 				sd = new Date();
137 			}
138 			// always reset the field value in case user entered date in wrong format
139 			startDateField.value = AjxDateUtil.simpleComputeDateStr(sd);
140 		}
141 
142 		if (ed.valueOf() < sd.valueOf()) {
143 			endDateField.value = startDateField.value;
144         }else if(oldStartDate != null) {
145             var delta = ed.getTime() - oldStartDate.getTime();
146             var newEndDate = new Date(sd.getTime() + delta);
147             endDateField.value = AjxDateUtil.simpleComputeDateStr(newEndDate);
148         }
149 		needsUpdate = true;
150 	} else {
151 		// if date was input by user and it's foobar, reset to today's date
152 		if (!skipCheck) {
153 			if (ed == null || isNaN(ed)) {
154 				ed = new Date();
155 			}
156 			// always reset the field value in case user entered date in wrong format
157 			endDateField.value = AjxDateUtil.simpleComputeDateStr(ed);
158 		}
159 
160 		// otherwise, reset start date if necessary
161 		if (sd.valueOf() > ed.valueOf()) {
162 			startDateField.value = endDateField.value;
163 			needsUpdate = true;
164 		}
165 	}
166 
167 	return needsUpdate;
168 };
169 
170 ZmApptViewHelper.getApptToolTipText =
171 function(origAppt, controller) {
172     if(origAppt._toolTip) {
173         return origAppt._toolTip;
174     }
175     var appt = ZmAppt.quickClone(origAppt);
176     var organizer = appt.getOrganizer();
177 	var sentBy = appt.getSentBy();
178 	var userName = appCtxt.get(ZmSetting.USERNAME);
179 	if (sentBy || (organizer && organizer != userName)) {
180 		organizer = (appt.message && appt.message.invite && appt.message.invite.getOrganizerName()) || organizer;
181 		if (sentBy) {
182 			var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
183 			var contact = contactsApp && contactsApp.getContactByEmail(sentBy);
184 			sentBy = (contact && contact.getFullName()) || sentBy;
185 		}
186 	} else {
187 		organizer = null;
188 		sentBy = null;
189 	}
190 
191 	var params = {
192 		appt: appt,
193 		cal: (appt.folderId != ZmOrganizer.ID_CALENDAR && controller) ? controller.getCalendar() : null,
194 		organizer: organizer,
195 		sentBy: sentBy,
196 		when: appt.getDurationText(false, false),
197 		location: appt.getLocation(),
198 		width: "250",
199         hideAttendees: true
200 	};
201 
202 	var toolTip = origAppt._toolTip = AjxTemplate.expand("calendar.Appointment#Tooltip", params);
203     return toolTip;
204 };
205 
206 
207 ZmApptViewHelper.getDayToolTipText =
208 function(date, list, controller, noheader, emptyMsg, isMinical, getSimpleToolTip) {
209 	
210 	if (!emptyMsg) {
211 		emptyMsg = ZmMsg.noAppts;
212 	}
213 
214 	var html = new AjxBuffer();
215 
216 	var formatter = DwtCalendar.getDateFullFormatter();	
217 	var title = formatter.format(date);
218 	
219 	html.append("<div>");
220 
221 	html.append("<table cellpadding='0' cellspacing='0' border='0'>");
222 	if (!noheader) html.append("<tr><td><div class='calendar_tooltip_month_day_label'>", title, "</div></td></tr>");
223 	html.append("<tr><td>");
224 	html.append("<table cellpadding='1' cellspacing='0' border='0'>");
225 	
226 	var size = list ? list.size() : 0;
227 
228 	var useEmptyMsg = true;
229 	var dateTime = date.getTime();
230 	for (var i = 0; i < size; i++) {
231 		var ao = list.get(i);
232 		var isAllDay = ao.isAllDayEvent();
233 		if (isAllDay || getSimpleToolTip) {
234 			// Multi-day "appts/all day events" will be broken up into one sub-appt per day, so only show
235 			// the one that matches the selected date
236 			var apptDate = new Date(ao.startDate.getTime());
237 			apptDate.setHours(0,0,0,0);
238 			if (apptDate.getTime() != dateTime) continue;
239 		}
240 
241 		if (isAllDay && !getSimpleToolTip) {
242 			useEmptyMsg = false;
243 			if(!isMinical && ao.toString() == "ZmAppt") {
244 				html.append("<tr><td><div class=appt>");
245 				html.append(ZmApptViewHelper.getApptToolTipText(ao, controller));
246 				html.append("</div></td></tr>");
247 			}
248 			else {
249 				//DBG.println("AO    "+ao);
250 				var widthField = AjxEnv.isIE ? "width:500px;" : "min-width:300px;";
251 				html.append("<tr><td><div style='" + widthField + "' class=appt>");
252 				html.append(ZmApptViewHelper._allDayItemHtml(ao, Dwt.getNextId(), controller, true, true));
253 				html.append("</div></td></tr>");
254 			}
255 		}
256 		else {
257 			useEmptyMsg = false;
258 			if (!isMinical && ao.toString() == "ZmAppt") {
259 				html.append("<tr><td><div class=appt>");
260 				html.append(ZmApptViewHelper.getApptToolTipText(ao, controller));
261 				html.append("</div></td></tr>");
262 			}
263 			else {
264 				var color = ZmCalendarApp.COLORS[controller.getCalendarColor(ao.folderId)];
265 				var isNew = ao.status == ZmCalBaseItem.PSTATUS_NEEDS_ACTION;
266 				html.append("<tr><td class='calendar_month_day_item'><div class='", color, isNew ? "DarkC" : "C", "'>");
267 				if (isNew) html.append("<b>");
268 				
269 				var dur; 
270 				if (isAllDay) {
271 					dur = ao._orig.getDurationText(false, false, true)
272 				} 
273 				else {
274 					//html.append("• ");
275 					//var dur = ao.getShortStartHour();
276 					dur = getSimpleToolTip ? ao._orig.getDurationText(false,false,true) : ao.getDurationText(false,false);
277 				}
278 				html.append(dur);
279 				if (dur != "") {
280 					html.append(" ");
281 					if (isAllDay) { 
282 						html.append("- "); 
283 					}
284 				}   
285 				html.append(AjxStringUtil.htmlEncode(ao.getName()));
286 				
287 				if (isNew) html.append("</b>");
288 				html.append("</div>");
289 				html.append("</td></tr>");
290 			}
291 		}
292 	}
293 	if (useEmptyMsg) {
294 		html.append("<tr><td>"+emptyMsg+"</td></tr>");
295 	}
296 	html.append("</table>");
297 	html.append("</td></tr></table>");
298 	html.append("</div>");
299 
300 	return html.toString();
301 };
302 
303 /**
304  * Returns a list of calendars based on certain conditions. Especially useful
305  * for multi-account
306  *
307  * @param folderSelect	[DwtSelect]		DwtSelect object to populate
308  * @param folderRow		[HTMLElement]	Table row element to show/hide
309  * @param calendarOrgs	[Object]		Hash map of calendar ID to calendar owner
310  * @param calItem		[ZmCalItem]		a ZmAppt or ZmTask object
311  */
312 ZmApptViewHelper.populateFolderSelect =
313 function(folderSelect, folderRow, calendarOrgs, calItem) {
314 	// get calendar folders (across all accounts)
315 	var org = ZmOrganizer.ITEM_ORGANIZER[calItem.type];
316 	var data = [];
317 	var folderTree;
318 	var accounts = appCtxt.accountList.visibleAccounts;
319 	for (var i = 0; i < accounts.length; i++) {
320 		var acct = accounts[i];
321 
322 		var appEnabled = ZmApp.SETTING[ZmItem.APP[calItem.type]];
323 		if ((appCtxt.isOffline && acct.isMain) ||
324 			!appCtxt.get(appEnabled, null, acct))
325 		{
326 			continue;
327 		}
328 
329 		folderTree = appCtxt.getFolderTree(acct);
330 		data = data.concat(folderTree.getByType(org));
331 	}
332 
333 	// add the local account last for multi-account
334 	if (appCtxt.isOffline) {
335 		folderTree = appCtxt.getFolderTree(appCtxt.accountList.mainAccount);
336 		data = data.concat(folderTree.getByType(org));
337 	}
338 
339 	folderSelect.clearOptions();
340     
341 	for (var i = 0; i < data.length; i++) {
342 		var cal = data[i];
343 		var acct = cal.getAccount();
344 
345 		if (cal.noSuchFolder || cal.isFeed() || (cal.link && cal.isReadOnly()) || cal.isInTrash()) { continue; }
346 
347 		if (appCtxt.multiAccounts &&
348 			cal.nId == ZmOrganizer.ID_CALENDAR &&
349 			acct.isCalDavBased())
350 		{
351 			continue;
352 		}
353 
354         var id = cal.link ? cal.getRemoteId() : cal.id;
355 		calendarOrgs[id] = cal.owner;
356 
357 		// bug: 28363 - owner attribute is not available for shared sub folders
358 		if (cal.isRemote() && !cal.owner && cal.parent && cal.parent.isRemote()) {
359 			calendarOrgs[id] = cal.parent.getOwner();
360 		}
361 
362 		var selected = ((calItem.folderId == cal.id) || (calItem.folderId == id));
363 		var icon = appCtxt.multiAccounts ? acct.getIcon() : cal.getIconWithColor();
364 		var name = AjxStringUtil.htmlDecode(appCtxt.multiAccounts
365 			? ([cal.getName(), " (", acct.getDisplayName(), ")"].join(""))
366 			: cal.getName());
367 		var option = new DwtSelectOption(id, selected, name, null, null, icon);
368 		folderSelect.addOption(option, selected);
369 	}
370 
371     ZmApptViewHelper.folderSelectResize(folderSelect);
372     //todo: new ui hide folder select if there is only one folder
373 };
374 
375 /**
376  * Takes a string, AjxEmailAddress, or contact/resource and returns
377  * a ZmContact or a ZmResource. If the attendee cannot be found in
378  * contacts, locations, or equipment, a new contact or
379  * resource is created and initialized.
380  *
381  * @param item			[object]		string, AjxEmailAddress, ZmContact, or ZmResource
382  * @param type			[constant]*		attendee type
383  * @param strictText	[boolean]*		if true, new location will not be created from free text
384  * @param strictEmail	[boolean]*		if true, new attendee will not be created from email address
385  */
386 ZmApptViewHelper.getAttendeeFromItem =
387 function(item, type, strictText, strictEmail, checkForAvailability) {
388 
389 	if (!item || !type) return null;
390 
391 	if (type == ZmCalBaseItem.LOCATION && !ZmApptViewHelper._locations) {
392 		if (!appCtxt.get(ZmSetting.GAL_ENABLED)) {
393 			//if GAL is disabled then user does not have permission to load locations.
394 			return null;
395 		}
396 		var locations = ZmApptViewHelper._locations = appCtxt.getApp(ZmApp.CALENDAR).getLocations();
397         if(!locations.isLoaded) {
398             locations.load();
399         }
400 
401 	}
402 	if (type == ZmCalBaseItem.EQUIPMENT && !ZmApptViewHelper._equipment) {
403 		if (!appCtxt.get(ZmSetting.GAL_ENABLED)) {
404 			//if GAL is disabled then user does not have permission to load equipment.
405 			return null;
406 		}
407 		var equipment = ZmApptViewHelper._equipment = appCtxt.getApp(ZmApp.CALENDAR).getEquipment();
408         if(!equipment.isLoaded) {
409             equipment.load();
410         }                
411 	}
412 	
413 	var attendee = null;
414 	if (item.type == ZmItem.CONTACT || item.type == ZmItem.GROUP || item.type == ZmItem.RESOURCE) {
415 		// it's already a contact or resource, return it as is
416 		attendee = item;
417 	} else if (item instanceof AjxEmailAddress) {
418 		var addr = item.getAddress();
419 		// see if we have this contact/resource by checking email address
420 		attendee = ZmApptViewHelper._getAttendeeFromAddr(addr, type);
421 
422 		// Bug 7837: preserve the email address as it was typed
423 		//           instead of using the contact's primary email.
424 		if (attendee && (type === ZmCalBaseItem.PERSON || type === ZmCalBaseItem.GROUP)) {
425 			attendee = AjxUtil.createProxy(attendee);
426 			attendee._inviteAddress = addr;
427 			attendee.getEmail = function() {
428 				return this._inviteAddress || this.constructor.prototype.getEmail.apply(this);
429 			};
430 		}
431 
432 		if (!checkForAvailability && !attendee && !strictEmail) {
433 			// AjxEmailAddress has name and email, init a new contact/resource from those
434 			if (type === ZmCalBaseItem.PERSON) {
435 				attendee = new ZmContact(null, null, ZmItem.CONTACT);
436 			}
437 			else if (type === ZmCalBaseItem.GROUP) {
438 				attendee = new ZmContact(null, null, ZmItem.GROUP);
439 			}
440 			else {
441 				attendee = new ZmResource(type);
442 			}
443 			attendee.initFromEmail(item, true);
444 		}
445 		attendee.canExpand = item.canExpand;
446 		var ac = window.parentAppCtxt || window.appCtxt;
447 		ac.setIsExpandableDL(addr, attendee.canExpand);
448 	} else if (typeof item == "string") {
449 		item = AjxStringUtil.trim(item);	// trim white space
450 		item = item.replace(/;$/, "");		// trim separator
451 		// see if it's an email we can use for lookup
452 	 	var email = AjxEmailAddress.parse(item);
453 	 	if (email) {
454 	 		var addr = email.getAddress();
455 	 		// is it a contact/resource we already know about?
456 			attendee = ZmApptViewHelper._getAttendeeFromAddr(addr, type);
457 			if (!checkForAvailability && !attendee && !strictEmail) {
458 				if (type === ZmCalBaseItem.PERSON || type === ZmCalBaseItem.FORWARD) {
459 					attendee = new ZmContact(null, null, ZmItem.CONTACT);
460 				}
461 				else if (type === ZmCalBaseItem.GROUP) {
462 					attendee = new ZmContact(null, null, ZmItem.GROUP);
463 				}
464 				else if (type === ZmCalBaseItem.LOCATION) {
465 					attendee = new ZmResource(null, ZmApptViewHelper._locations, ZmCalBaseItem.LOCATION);
466 				}
467 				else if (type === ZmCalBaseItem.EQUIPMENT) {
468 					attendee = new ZmResource(null, ZmApptViewHelper._equipment, ZmCalBaseItem.EQUIPMENT);
469 				}
470 				attendee.initFromEmail(email, true);
471 			} else if (attendee && (type === ZmCalBaseItem.PERSON || type === ZmCalBaseItem.GROUP)) {
472 				// remember actual address (in case it's email2 or email3)
473 				attendee._inviteAddress = addr;
474                 attendee.getEmail = function() {
475 				    return this._inviteAddress || this.constructor.prototype.getEmail.apply(this);
476 			    };
477 			}
478 		}
479 	}
480 	return attendee;
481 };
482 
483 ZmApptViewHelper._getAttendeeFromAddr =
484 function(addr, type) {
485 
486 	var attendee = null;
487 	if (type === ZmCalBaseItem.PERSON || type === ZmCalBaseItem.GROUP || type === ZmCalBaseItem.FORWARD) {
488 		var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
489 		attendee = contactsApp && contactsApp.getContactByEmail(addr);
490 	} else if (type == ZmCalBaseItem.LOCATION) {
491         attendee = ZmApptViewHelper._locations.getResourceByEmail(addr);
492 	} else if (type == ZmCalBaseItem.EQUIPMENT) {
493 		attendee = ZmApptViewHelper._equipment.getResourceByEmail(addr);
494 	}
495 	return attendee;
496 };
497 
498 /**
499  * Returns a AjxEmailAddress for the organizer.
500  *
501  * @param organizer	[string]*		organizer's email address
502  * @param account	[ZmAccount]*	organizer's account
503  */
504 ZmApptViewHelper.getOrganizerEmail =
505 function(organizer, account) {
506 	var orgAddress = organizer ? organizer : appCtxt.get(ZmSetting.USERNAME, null, account);
507 	var orgName = (orgAddress == appCtxt.get(ZmSetting.USERNAME, null, account))
508 		? appCtxt.get(ZmSetting.DISPLAY_NAME, null, account) : null;
509 	return new AjxEmailAddress(orgAddress, null, orgName);
510 };
511 
512 ZmApptViewHelper.getAddressEmail =
513 function(email, isIdentity) {
514 	var orgAddress = email ? email : appCtxt.get(ZmSetting.USERNAME);
515 	var orgName;
516     if(email == appCtxt.get(ZmSetting.USERNAME)){
517         orgName = appCtxt.get(ZmSetting.DISPLAY_NAME);
518     }else{
519         //Identity
520         var iCol = appCtxt.getIdentityCollection(),
521             identity = iCol ? iCol.getIdentityBySendAddress(orgAddress) : "";
522         if(identity){
523             orgName = identity.sendFromDisplay;
524         }
525     }
526     return new AjxEmailAddress(orgAddress, null, orgName);    
527 };
528 
529 /**
530 * Creates a string from a list of attendees/locations/resources. If an item
531 * doesn't have a name, its address is used.
532 *
533 * @param list					[array]			list of attendees (ZmContact or ZmResource)
534 * @param type					[constant]		attendee type
535 * @param includeDisplayName		[boolean]*		if true, include location info in parens (ZmResource)
536 * @param includeRole		    [boolean]*		if true, include attendee role
537 */
538 ZmApptViewHelper.getAttendeesString = 
539 function(list, type, includeDisplayName, includeRole) {
540 	if (!(list && list.length)) return "";
541 
542 	var a = [];
543 	for (var i = 0; i < list.length; i++) {
544 		var attendee = list[i];
545 		var text = ZmApptViewHelper.getAttendeesText(attendee, type);
546 		if (includeDisplayName && list.length == 1) {
547 			var displayName = attendee.getAttr(ZmResource.F_locationName);
548 			if (displayName) {
549 				text = [text, " (", displayName, ")"].join("");
550 			}
551 		}
552         if(includeRole) {
553             text += " " + (attendee.getParticipantRole() || ZmCalItem.ROLE_REQUIRED);
554         }
555 		a.push(text);
556 	}
557 
558 	return a.join(ZmAppt.ATTENDEES_SEPARATOR);
559 };
560 
561 ZmApptViewHelper.getAttendeesText =
562 function(attendee, type, shortForm) {
563 
564     //give preference to lookup email is the attendee object is located by looking up email address
565     var lookupEmailObj = attendee.getLookupEmail(true);
566     if(lookupEmailObj) {
567 		return lookupEmailObj.toString(shortForm || (type && type !== ZmCalBaseItem.PERSON && type !== ZmCalBaseItem.GROUP));
568 	}
569 
570     return attendee.getAttendeeText(type, shortForm);
571 };
572 
573 /**
574 * Creates a string of attendees by role. If an item
575 * doesn't have a name, its address is used.
576 *
577 * calls common code from mail msg view to get the collapse/expand "show more" funcitonality for large lists.
578 *
579 * @param list					[array]			list of attendees (ZmContact or ZmResource)
580 * @param type					[constant]		attendee type
581 * @param role      		        [constant]      attendee role
582 * @param count                  [number]        number of attendees to be returned
583 */
584 ZmApptViewHelper.getAttendeesByRoleCollapsed =
585 function(list, type, role, objectManager, htmlElId) {
586     if (!(list && list.length)) return "";
587 	var attendees = ZmApptViewHelper.getAttendeesArrayByRole(list, role);
588 
589 	var emails = [];
590 	for (var i = 0; i < attendees.length; i++) {
591 		var att = attendees[i];
592 		emails.push(new AjxEmailAddress(att.getEmail(), type, att.getFullName(), att.getFullName()));
593 	}
594 
595 	var options = {};
596 	options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS);
597 	var addressInfo = ZmMailMsgView.getAddressesFieldHtmlHelper(emails, options,
598 		role, objectManager, htmlElId);
599 	return addressInfo.html;
600 };
601 
602 /**
603 * Creates a string of attendees by role. this allows to show only count elements, with "..." appended.
604 *
605 * @param list					[array]			list of attendees (ZmContact or ZmResource)
606 * @param type					[constant]		attendee type
607 * @param role      		        [constant]      attendee role
608 * @param count                  [number]        number of attendees to be returned
609 */
610 ZmApptViewHelper.getAttendeesByRole =
611 function(list, type, role, count) {
612     if (!(list && list.length)) return "";
613 
614 	var res = [];
615 
616 	var attendees = ZmApptViewHelper.getAttendeesArrayByRole(list, role);
617 	for (var i = 0; i < attendees.length; i++) {
618 		if (count && i > count) {
619 			res.push(" ...");
620 			break;
621 		}
622 		if (i > 0) {
623 			res.push(ZmAppt.ATTENDEES_SEPARATOR);
624 		}
625 		res.push(attendees[i].getAttendeeText(type));
626 	}
627 	return res.join("");
628 };
629 
630 
631 
632 /**
633 * returns array of attendees by role.
634 *
635 * @param list					[array]			list of attendees (ZmContact or ZmResource)
636 * @param role      		        [constant]      attendee role
637 */
638 ZmApptViewHelper.getAttendeesArrayByRole =
639 function(list, role, count) {
640 
641     if (!(list && list.length)) {
642 	    return [];
643     }
644 
645     var a = [];
646     for (var i = 0; i < list.length; i++) {
647         var attendee = list[i];
648         var attendeeRole = attendee.getParticipantRole() || ZmCalItem.ROLE_REQUIRED;
649         if (attendeeRole === role){
650             a.push(attendee);
651         }
652     }
653 	return a;
654 };
655 
656 ZmApptViewHelper._allDayItemHtml =
657 function(appt, id, controller, first, last) {
658 	var isNew = appt.ptst == ZmCalBaseItem.PSTATUS_NEEDS_ACTION;
659 	var isAccepted = appt.ptst == ZmCalBaseItem.PSTATUS_ACCEPT;
660 	var calendar = appt.getFolder();
661     AjxDispatcher.require(["MailCore", "CalendarCore", "Calendar"]);
662 
663     var tagNames  = appt.getVisibleTags();
664     var tagIcon = last ? appt.getTagImageFromNames(tagNames) : null;
665 
666     var fba = isNew ? ZmCalBaseItem.PSTATUS_NEEDS_ACTION : appt.fba;
667     var headerColors = ZmApptViewHelper.getApptColor(isNew, calendar, tagNames, "header");
668     var headerStyle  = ZmCalBaseView._toColorsCss(headerColors.appt);
669     var bodyColors   = ZmApptViewHelper.getApptColor(isNew, calendar, tagNames, "body");
670     var bodyStyle    = ZmCalBaseView._toColorsCss(bodyColors.appt);
671 
672     var borderLeft  = first ? "" : "border-left:0;";
673     var borderRight = last  ? "" : "border-right:0;";
674 
675     var newState = isNew ? "_new" : "";
676 	var subs = {
677 		id:           id,
678 		headerStyle:  headerStyle,
679 		bodyStyle:    bodyStyle,
680 		newState:     newState,
681 		name:         first ? AjxStringUtil.htmlEncode(appt.getName()) : " ",
682 //		tag: isNew ? "NEW" : "",		//  HACK: i18n
683 		starttime:    appt.getDurationText(true, true),
684 		endtime:      (!appt._fanoutLast && (appt._fanoutFirst || (appt._fanoutNum > 0))) ? "" : ZmCalBaseItem._getTTHour(appt.endDate),
685 		location:     AjxStringUtil.htmlEncode(appt.getLocation()),
686 		status:       appt.isOrganizer() ? "" : appt.getParticipantStatusStr(),
687 		icon:         first && appt.isPrivate() ? "ReadOnly" : null,
688         showAsColor:  first ? ZmApptViewHelper._getShowAsColorFromId(fba) : "",
689         showAsClass:  first ? "" : "appt_allday" + newState + "_name",
690         boxBorder:    ZmApptViewHelper.getBoxBorderFromId(fba),
691         borderLeft:   borderLeft,
692         borderRight:  borderRight,
693         tagIcon:      tagIcon
694 	};
695     ZmApptViewHelper.setupCalendarColor(last, headerColors, tagNames, subs, "headerStyle", null, 1, 1);
696     return AjxTemplate.expand("calendar.Calendar#calendar_appt_allday", subs);
697 };
698 
699 ZmApptViewHelper._getShowAsColorFromId =
700 function(id) {
701     var color = "#4AA6F1";
702 	switch(id) {
703         case ZmCalBaseItem.PSTATUS_NEEDS_ACTION: color = "#FF3300"; break;
704 		case "F": color = "#FFFFFF"; break;
705 		case "B": color = "#4AA6F1"; break;
706 		case "T": color = "#BAE0E3"; break;
707 		case "O": color = "#7B5BAC"; break;
708 	}
709     var colorCss = Dwt.createLinearGradientCss("#FFFFFF", color, "v");
710     if (!colorCss) {
711         colorCss = "background-color: " + color + ";";
712     }
713     return colorCss;
714 };
715 
716 ZmApptViewHelper.getBoxBorderFromId =
717 function(id) {
718 	switch(id) {
719 		case "F": return "ZmSchedulerApptBorder-free";
720         case ZmCalBaseItem.PSTATUS_NEEDS_ACTION:
721 		case "B": return "ZmSchedulerApptBorder-busy";
722 		case "T": return "ZmSchedulerApptBorder-tentative";
723 		case "O": return "ZmSchedulerApptBorder-outOfOffice";
724 	}
725 	return "ZmSchedulerApptBorder-busy";
726 };
727 
728 /**
729  * Returns a list of attendees with the given role.
730  *
731  * @param	{array}		list		list of attendees
732  * @param	{constant}	role		defines the role of the attendee (required/optional)
733  *
734  * @return	{array}	a list of attendees
735  */
736 ZmApptViewHelper.filterAttendeesByRole =
737 function(list, role) {
738 
739 	var result = [];
740 	for (var i = 0; i < list.length; i++) {
741 		var attendee = list[i];
742 		var attRole = attendee.getParticipantRole() || ZmCalItem.ROLE_REQUIRED;
743 		if (attRole == role){
744 			result.push(attendee);
745 		}
746 	}
747 	return result;
748 };
749 
750 ZmApptViewHelper.getApptColor =
751 function(deeper, calendar, tagNames, segment) {
752     var colors = ZmCalBaseView._getColors(calendar.rgb || ZmOrganizer.COLOR_VALUES[calendar.color]);
753     var calColor = deeper ? colors.deeper[segment] : colors.standard[segment];
754     var apptColor = calColor;
755     if (tagNames && (tagNames.length == 1)) {
756 		var tagList = appCtxt.getAccountTagList(calendar);
757 
758         var tag = tagList.getByNameOrRemote(tagNames[0]);
759         if(tag){apptColor = { bgcolor: tag.getColor() };}
760     }
761     return {calendar:calColor, appt:apptColor};
762 };
763 
764 ZmApptViewHelper.setupCalendarColor =
765 function(last, colors, tagNames, templateData, colorParam, clearParam, peelTopOffset, peelRightOffset, div) {
766     var colorCss = Dwt.createLinearGradientCss("#FFFFFF", colors.appt.bgcolor, "v");
767     if (colorCss) {
768         templateData[colorParam] = colorCss;
769         if (clearParam) {
770             templateData[clearParam] = null;
771         }
772     }
773     if (last && tagNames && (tagNames.length == 1)) {
774         if (!colorCss) {
775             // Can't use the gradient color.  IE masking doesn't work properly for tags on appts;
776             // Since the color is already set in the background, just print the overlay image
777             var match = templateData.tagIcon.match(AjxImg.RE_COLOR);
778             if (match) {
779                 templateData.tagIcon = (match && match[1]) + "Overlay";
780             }
781         }
782         // Tag color has been applied to the appt.  Add the calendar peel image
783         templateData.peelIcon  = "Peel,color=" + colors.calendar.bgcolor;
784         templateData.peelTop   = peelTopOffset;
785         templateData.peelRight = peelRightOffset;
786     }
787 };
788 
789 /**
790  * Gets the attach list as HTML.
791  * 
792  * @param {ZmCalItem}	calItem			calendar item
793  * @param {Object}		attach			a generic Object contain meta info about the attachment
794  * @param {Boolean}		hasCheckbox		<code>true</code> to insert a checkbox prior to the attachment
795  * @return	{String}	the HTML
796  * 
797  * TODO: replace string onclick handlers with funcs
798  */
799 ZmApptViewHelper.getAttachListHtml =
800 function(calItem, attach, hasCheckbox, getLinkIdCallback) {
801 	var msgFetchUrl = appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI);
802 
803 	// gather meta data for this attachment
804 	var mimeInfo = ZmMimeTable.getInfo(attach.ct);
805 	var icon = mimeInfo ? mimeInfo.image : "GenericDoc";
806 	var size = attach.s;
807 	var sizeText;
808 	if (size != null) {
809 		if (size < 1024)		sizeText = size + " B";
810 		else if (size < 1024^2)	sizeText = Math.round((size/1024) * 10) / 10 + " KB";
811 		else 					sizeText = Math.round((size / (1024*1024)) * 10) / 10 + " MB";
812 	}
813 
814 	var html = [];
815 	var i = 0;
816 
817 	// start building html for this attachment
818 	html[i++] = "<table border=0 cellpadding=0 cellspacing=0><tr>";
819 	if (hasCheckbox) {
820 		html[i++] = "<td width=1%><input type='checkbox' checked value='";
821 		html[i++] = attach.part;
822 		html[i++] = "' name='";
823 		html[i++] = ZmCalItem.ATTACHMENT_CHECKBOX_NAME;
824 		html[i++] = "'></td>";
825 	}
826 
827 	var hrefRoot = ["href='", msgFetchUrl, "&id=", calItem.invId, "&part=", attach.part].join("");
828 	html[i++] = "<td width=20><a target='_blank' class='AttLink' ";
829 	if (getLinkIdCallback) {
830 		var imageLinkId = getLinkIdCallback(attach.part, ZmCalItem.ATT_LINK_IMAGE);
831 		html[i++] = "id='";
832 		html[i++] = imageLinkId;
833 		html[i++] = "' ";
834 	}
835 	html[i++] = hrefRoot;
836 	html[i++] = "'>";
837 	html[i++] = AjxImg.getImageHtml(icon);
838 
839 	html[i++] = "</a></td><td><a target='_blank' class='AttLink' ";
840 
841 	if (appCtxt.get(ZmSetting.MAIL_ENABLED) && attach.ct == ZmMimeTable.MSG_RFC822) {
842 		html[i++] = " href='javascript:;' onclick='ZmCalItemView.rfc822Callback(";
843 		html[i++] = '"';
844 		html[i++] = calItem.invId;
845 		html[i++] = '"';
846 		html[i++] = ",\"";
847 		html[i++] = attach.part;
848 		html[i++] = "\"); return false;'";
849 	} else {
850 		html[i++] = hrefRoot;
851 		html[i++] = "'";
852 	}
853 	if (getLinkIdCallback) {
854 		var mainLinkId = getLinkIdCallback(attach.part, ZmCalItem.ATT_LINK_MAIN);
855 		html[i++] = " id='";
856 		html[i++] = mainLinkId;
857 		html[i++] = "'";
858 	}
859 	html[i++] = ">";
860 	html[i++] = AjxStringUtil.htmlEncode(attach.filename);
861 	html[i++] = "</a>";
862 
863 	var addHtmlLink = (appCtxt.get(ZmSetting.VIEW_ATTACHMENT_AS_HTML) &&
864 					   attach.body == null && ZmMimeTable.hasHtmlVersion(attach.ct));
865 
866 	if (sizeText || addHtmlLink) {
867 		html[i++] = " (";
868 		if (sizeText) {
869 			html[i++] = sizeText;
870 			html[i++] = ") ";
871 		}
872 		var downloadLinkId = "";
873 		if (getLinkIdCallback) {
874 			downloadLinkId = getLinkIdCallback(attach.part, ZmCalItem.ATT_LINK_DOWNLOAD);
875 		}
876 		if (addHtmlLink) {
877 			html[i++] = "<a style='text-decoration:underline' target='_blank' class='AttLink' ";
878 			if (getLinkIdCallback) {
879 				html[i++] = "id='";
880 				html[i++] = downloadLinkId;
881 				html[i++] = "' ";
882 			}
883 			html[i++] = hrefRoot;
884 			html[i++] = "&view=html'>";
885 			html[i++] = ZmMsg.preview;
886 			html[i++] = "</a> ";
887 		}
888 		if (attach.ct != ZmMimeTable.MSG_RFC822) {
889 			html[i++] = "<a style='text-decoration:underline' class='AttLink' onclick='ZmZimbraMail.unloadHackCallback();' ";
890 			if (getLinkIdCallback) {
891 				html[i++] = " id='";
892 				html[i++] = downloadLinkId;
893 				html[i++] = "' ";
894 			}
895 			html[i++] = hrefRoot;
896 			html[i++] = "&disp=a'>";
897 			html[i++] = ZmMsg.download;
898 			html[i++] = "</a>";
899 		}
900 	}
901 
902 	html[i++] = "</td></tr></table>";
903 
904 	// Provide lookup id and label for offline mode
905 	if (!attach.mid) {
906 		attach.mid = calItem.invId;
907 		attach.label = attach.filename;
908 	}
909 
910 	return html.join("");
911 };
912 
913 /**
914  * @param {DwtSelect} folderSelect
915  *
916  * TODO: set the width for folderSelect once the image icon gets loaded if any
917  */
918 ZmApptViewHelper.folderSelectResize =
919 function(folderSelect) {
920 
921     var divEl = folderSelect._containerEl,
922         childNodes,
923         img;
924 
925     if (divEl) {
926         childNodes = divEl.childNodes[0];
927         if (childNodes) {
928             img = childNodes.getElementsByTagName("img")[0];
929             if (img) {
930                 img.onload = function() {
931                     divEl.style.width = childNodes.offsetWidth || "auto";// offsetWidth doesn't work in IE if the element or one of its parents has display:none
932                     img.onload = "";
933                 }
934             }
935         }
936     }
937 };
938