1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 contains the edit contact view classes.
 27  */
 28 
 29 /**
 30  * Creates the edit contact view.
 31  * @class
 32  * This class represents the edit contact view.
 33  * 
 34  * @param	{DwtComposite}	parent		the parent
 35  * @param	{ZmContactController}		controller		the controller
 36  * 
 37  * @extends		DwtForm
 38  */
 39 ZmEditContactView = function(parent, controller) {
 40 	if (arguments.length == 0) return;
 41 
 42 	var form = {
 43 		ondirty: this._handleDirty,
 44 		items: this.getFormItems()
 45 	};
 46 
 47 	var params = {
 48 		id: "editcontactform",
 49 		parent: parent,
 50 		className: "ZmEditContactView",
 51 		posStyle: DwtControl.ABSOLUTE_STYLE,
 52 		form: form
 53 	};
 54 	DwtForm.call(this, params);
 55 
 56 	// add details menu, if needed
 57 	var details = this.getControl("DETAILS");
 58 	if (details) {
 59 		var menu = this.__getDetailsMenu();
 60 		if (menu) {
 61 			details.setMenu(menu);
 62 			details.addSelectionListener(new AjxListener(details, details.popup, [menu]));
 63 		}
 64 		else {
 65 			this.setVisible("DETAILS", false);
 66 		}
 67 	}
 68 
 69 	// save other state
 70 	this._controller = controller;
 71 
 72 	ZmTagsHelper.setupListeners(this);
 73 
 74 	this._changeListener = new AjxListener(this, this._contactChangeListener);
 75 
 76 	this.setScrollStyle(Dwt.SCROLL);
 77 	this.clean = false;
 78 };
 79 
 80 ZmEditContactView.prototype = new DwtForm;
 81 ZmEditContactView.prototype.constructor = ZmEditContactView;
 82 
 83 /**
 84  * Returns a string representation of the object.
 85  * 
 86  * @return		{String}		a string representation of the object
 87  */
 88 ZmEditContactView.prototype.toString = function() {
 89 	return "ZmEditContactView";
 90 };
 91 
 92 // form information that you can override
 93 
 94 /**
 95  * Gets the form items.
 96  * 
 97  * @return	{Hash}	a hash of form items
 98  */
 99 ZmEditContactView.prototype.getFormItems = function() {
100 	if (!this._formItems) {
101 		this._formItems = [
102 			// debug
103 	//			{ id: "DEBUG", type: "DwtText", ignore:true },
104 			// header pseudo-items
105 			{ id: "FULLNAME", type: "DwtText", className: "contactHeader",
106 				getter: this._getFullName, ignore: true },
107 			// contact attribute fields
108 			{ id: "IMAGE", type: "ZmEditContactViewImage" },
109 			{ id: "ZIMLET_IMAGE", type: "DwtText" },
110 			{ id: "PREFIX", type: "DwtInputField", width: 38, tooltip: ZmMsg.namePrefix, hint: ZmMsg.AB_FIELD_prefix, visible: "get('SHOW_PREFIX')" },
111 			{ id: "FIRST", type: "DwtInputField", width: 95, tooltip: ZmMsg.firstName, hint: ZmMsg.AB_FIELD_firstName, visible: "get('SHOW_FIRST')", onblur: "this._controller.updateTabTitle()" },
112 			{ id: "MIDDLE", type: "DwtInputField", width: 95, tooltip: ZmMsg.middleName, hint: ZmMsg.AB_FIELD_middleName, visible: "get('SHOW_MIDDLE')" },
113 			{ id: "MAIDEN", type: "DwtInputField", width: 95, tooltip: ZmMsg.maidenName, hint: ZmMsg.AB_FIELD_maidenName, visible: "get('SHOW_MAIDEN')" },
114 			{ id: "LAST", type: "DwtInputField", width: 95, tooltip: ZmMsg.lastName, hint: ZmMsg.AB_FIELD_lastName, visible: "get('SHOW_LAST')" , onblur: "this._controller.updateTabTitle()"},
115 			{ id: "SUFFIX", type: "DwtInputField", width: 38, tooltip: ZmMsg.nameSuffix, hint: ZmMsg.AB_FIELD_suffix, visible: "get('SHOW_SUFFIX')" },
116 			{ id: "NICKNAME", type: "DwtInputField", width: 66, hint: ZmMsg.AB_FIELD_nickname, visible: "get('SHOW_NICKNAME')" },
117 			{ id: "COMPANY", type: "DwtInputField", width: 209, hint: ZmMsg.AB_FIELD_company, visible: "get('SHOW_COMPANY')", onblur: "this._controller.updateTabTitle()" },
118 			{ id: "TITLE", type: "DwtInputField", width: 209, hint: ZmMsg.AB_FIELD_jobTitle, visible: "get('SHOW_TITLE')" },
119 			{ id: "DEPARTMENT", type: "DwtInputField", width: 209, hint: ZmMsg.AB_FIELD_department, visible: "get('SHOW_DEPARTMENT')" },
120 			{ id: "NOTES", type: "DwtInputField", hint: ZmMsg.notes, width: "47em", rows:4 },
121             // phonetic name fields
122             { id: "PHONETIC_PREFIX", visible: "this.isVisible('PREFIX')", ignore:true },
123             { id: "PHONETIC_FIRST", type: "DwtInputField", width: 95, hint: ZmMsg.AB_FIELD_phoneticFirstName, visible: "this.isVisible('FIRST')" },
124             { id: "PHONETIC_MIDDLE", visible: "this.isVisible('MIDDLE')", ignore:true },
125             { id: "PHONETIC_MAIDEN", visible: "this.isVisible('MAIDEN')", ignore:true },
126             { id: "PHONETIC_LAST", type: "DwtInputField", width: 95, hint: ZmMsg.AB_FIELD_phoneticLastName, visible: "this.isVisible('LAST')" },
127             { id: "PHONETIC_SUFFIX", visible: "this.isVisible('SUFFIX')", ignore:true },
128             { id: "PHONETIC_COMPANY", type: "DwtInputField", width: 209, hint: ZmMsg.AB_FIELD_phoneticCompany, visible: "this.isVisible('COMPANY')" },
129 			// contact list fields
130 			{ id: "EMAIL", type: "ZmEditContactViewInputSelectRows", rowitem: {
131 				type: "ZmEditContactViewInputSelect", equals:ZmEditContactViewInputSelect.equals, params: {
132 					inputWidth: 352, tooltip: ZmMsg.email, hint: ZmMsg.emailAddrHint, options: this.getEmailOptions()
133 				}
134 			}, validator: ZmEditContactView.emailValidator },
135 			{ id: "PHONE", type: "ZmEditContactViewInputSelectRows", rowitem: {
136 				type: "ZmEditContactViewInputSelect", equals:ZmEditContactViewInputSelect.equals, params: {
137 					inputWidth: 351, tooltip: ZmMsg.phone, hint: ZmMsg.phoneNumberHint, options: this.getPhoneOptions()
138 				}
139 			} },
140 			{ id: "IM", type: "ZmEditContactViewInputSelectRows", rowitem: {
141 				type: "ZmEditContactViewIM", equals: ZmEditContactViewIM.equals, params: {
142 					inputWidth: 351, tooltip: ZmMsg.imShort, hint: ZmMsg.imScreenNameHint, options: this.getIMOptions()
143 				}
144 			} },
145 			{ id: "ADDRESS", type: "ZmEditContactViewInputSelectRows",
146 				rowtemplate: "abook.Contacts#ZmEditContactViewAddressRow",
147 				rowitem: { type: "ZmEditContactViewAddress", equals: ZmEditContactViewAddress.equals,
148 					params: { options: this.getAddressOptions() }
149 				}
150 			},
151 			{ id: "URL", type: "ZmEditContactViewInputSelectRows", rowitem: {
152 				type: "ZmEditContactViewInputSelect", equals:ZmEditContactViewInputSelect.equals, params: {
153 					inputWidth: 351, hint: ZmMsg.url, options: this.getURLOptions()
154 				}
155 			} },
156 			{ id: "OTHER", type: "ZmEditContactViewInputSelectRows", rowitem: {
157 				type: "ZmEditContactViewOther", equals:ZmEditContactViewInputSelect.equals, params: {
158 					inputWidth: 300,
159 					selectInputWidth: 112,
160 					hint: ZmMsg.date,
161 					options: this.getOtherOptions()
162 				}
163 			}, validator: ZmEditContactViewOther.validator },
164 			// other controls
165 			{ id: "DETAILS", type: "DwtButton",
166 				label: "\u00BB", // »
167 				tooltip: ZmMsg.chooseFields,
168 				ignore:true,
169 				className: "ZmEditContactViewDetailsButton",
170 				template: "abook.Contacts#ZmEditContactViewDetailsButton",
171 				onblur: "this._controller.updateTabTitle()"
172 			},
173 			{ id: "FILE_AS", type: "DwtSelect", onchange: this._handleFileAsChange, items: this.getFileAsOptions(), tooltip: ZmMsg.fileAs },
174 			{ id: "FOLDER", type: "DwtButton", image: "ContactsFolder", imageAltText: ZmMsg.location, tooltip: ZmMsg.location,
175 				enabled: "this._contact && !this._contact.isReadOnly()",
176 				onclick: this._handleFolderButton
177 			},
178 			{ id: "TAG", type: "DwtControl",
179 				enabled: "this._contact && !this._contact.isShared()",
180 				visible: "appCtxt.get(ZmSetting.TAGGING_ENABLED)"
181 			},
182 			{ id: "ACCOUNT", type: "DwtLabel",
183 				visible: "appCtxt.multiAccounts"
184 			},
185 			// NOTE: Return false onclick to prevent default action
186 			{ id: "VIEW_IMAGE", ignore: true, onclick: "open(get('IMAGE')) && false", visible: "get('IMAGE')" },
187 			{ id: "REMOVE_IMAGE", ignore: true, onclick: this._handleRemoveImage, visible: "get('IMAGE')" },
188 			// pseudo-items
189 			{ id: "JOB", notab: true, ignore:true, visible: "get('SHOW_TITLE') && get('SHOW_DEPARTMENT')" },
190 			{ id: "TITLE_DEPARTMENT_SEP", notab: true,
191 				ignore:true, visible: "get('SHOW_TITLE') && get('SHOW_DEPARTMENT')"
192 			}
193 		];
194 	}
195 	return this._formItems;
196 };
197 
198 /**
199  * validate the array of email addresses. (0, 1 or more, each from a row in the edit view)
200  * @param {Array} emails
201  * @returns {*}
202  */
203 ZmEditContactView.emailValidator = function(emails) {
204 	for (var i = 0; i < emails.length; i++) {
205 		var address = emails[i];
206 		if (address && !AjxEmailAddress.validateAddress(address)) {
207 			throw ZmMsg.invalidEmailAddress;
208 		}
209 	}
210 	return true;
211 };
212 
213 /**
214  * Gets the form item with the given id.
215  * <p>
216  * <strong>Note:</strong>
217  * This method is especially useful as a way to modify the default
218  * set of form items without redeclaring the entire form declaration.
219  *
220  * @param {String}	id        [string] Form item identifier.
221  * @param {Array}	[formItems] the list of form items. If not
222  *                           specified, the form items array returned
223  *                           by {@link #getFormItems} is used.
224  *                           
225  * @return	{Array}	the form items or <code>null</code> for none
226  */
227 ZmEditContactView.prototype.getFormItemById = function(id, formItems) {
228 	formItems = formItems || this.getFormItems() || [];
229 	for (var i = 0; i < formItems.length; i++) {
230 		var item = formItems[i];
231 		if (item.id == id) return item;
232 	}
233 	return null;
234 };
235 
236 /**
237  * Gets the email options.
238  * 
239  * @return	{Object}	returns <code>null</code>
240  */
241 ZmEditContactView.prototype.getEmailOptions = function() {
242 	return null;
243 };
244 
245 /**
246  * Gets the phone options.
247  * 
248  * @return	{Array}	an array of phone options
249  */
250 ZmEditContactView.prototype.getPhoneOptions = function() {
251 	return [
252 		{ value: ZmContact.F_mobilePhone, label: ZmMsg.phoneLabelMobile },
253 		{ value: ZmContact.F_workPhone, label: ZmMsg.phoneLabelWork },
254 		{ value: ZmContact.F_workFax, label: ZmMsg.phoneLabelWorkFax },
255 //		{ value: "office", label: ZmMsg.office },
256 		{ value: ZmContact.F_companyPhone, label: ZmMsg.phoneLabelCompany },
257 		{ value: ZmContact.F_homePhone, label: ZmMsg.phoneLabelHome },
258 		{ value: ZmContact.F_homeFax, label: ZmMsg.phoneLabelHomeFax },
259 		{ value: ZmContact.F_pager, label: ZmMsg.phoneLabelPager },
260 		{ value: ZmContact.F_callbackPhone, label: ZmMsg.phoneLabelCallback },
261 		{ value: ZmContact.F_assistantPhone, label: ZmMsg.phoneLabelAssistant },
262 		{ value: ZmContact.F_carPhone, label: ZmMsg.phoneLabelCar },
263 		{ value: ZmContact.F_otherPhone, label: ZmMsg.phoneLabelOther },
264 		{ value: ZmContact.F_otherFax, label: ZmMsg.phoneLabelOtherFax }
265 	];
266 };
267 
268 /**
269  * Gets the IM options.
270  * 
271  * @return	{Array}	an array of IM options
272  */
273 ZmEditContactView.prototype.getIMOptions = function() {
274 	return [
275 		{ value: "xmpp", label: ZmMsg.imGateway_xmpp },
276 		{ value: "yahoo", label: ZmMsg.imGateway_yahoo },
277 		{ value: "aol", label: ZmMsg.imGateway_aol },
278 		{ value: "msn", label: ZmMsg.imGateway_msn },
279 		{ value: "im", label: ZmMsg.other }
280 	];
281 };
282 
283 /**
284  * Gets the address options.
285  * 
286  * @return	{Array}	an array of address options
287  */
288 ZmEditContactView.prototype.getAddressOptions = function() {
289 	return [
290 		{ value: "home", label: ZmMsg.home },
291 		{ value: "work", label: ZmMsg.work },
292 		{ value: "other", label: ZmMsg.other }
293 	];
294 };
295 
296 /**
297  * Gets the URL options.
298  * 
299  * @return	{Array}	an array of URL options
300  */
301 ZmEditContactView.prototype.getURLOptions = function() {
302 	return [
303 		{ value: ZmContact.F_homeURL, label: ZmMsg.home },
304 		{ value: ZmContact.F_workURL, label: ZmMsg.work },
305 		{ value: ZmContact.F_otherURL, label: ZmMsg.other }
306 	];
307 };
308 
309 /**
310  * Gets the other options.
311  * 
312  * @return	{Array}	an array of other options
313  */
314 ZmEditContactView.prototype.getOtherOptions = function() {
315 	return [
316 		{ value: ZmContact.F_birthday, label: ZmMsg.AB_FIELD_birthday },
317 		{ value: ZmContact.F_anniversary, label: ZmMsg.AB_FIELD_anniversary },
318 		{ value: "custom", label: ZmMsg.AB_FIELD_custom }
319 	];
320 };
321 
322 /**
323  * Gets the "file as" options.
324  * 
325  * @return	{Array}	an array of "file as" options
326  */
327 ZmEditContactView.prototype.getFileAsOptions = function() {
328 	return [
329 		{ id: "FA_LAST_C_FIRST", value: ZmContact.FA_LAST_C_FIRST, label: ZmMsg.AB_FILE_AS_lastFirst },
330 		{ id: "FA_FIRST_LAST", value: ZmContact.FA_FIRST_LAST, label: ZmMsg.AB_FILE_AS_firstLast },
331 		{ id: "FA_COMPANY", value: ZmContact.FA_COMPANY, label: ZmMsg.AB_FILE_AS_company },
332 		{ id: "FA_LAST_C_FIRST_COMPANY", value: ZmContact.FA_LAST_C_FIRST_COMPANY, label: ZmMsg.AB_FILE_AS_lastFirstCompany },
333 		{ id: "FA_FIRST_LAST_COMPANY", value: ZmContact.FA_FIRST_LAST_COMPANY, label: ZmMsg.AB_FILE_AS_firstLastCompany },
334 		{ id: "FA_COMPANY_LAST_C_FIRST", value: ZmContact.FA_COMPANY_LAST_C_FIRST, label: ZmMsg.AB_FILE_AS_companyLastFirst },
335 		{ id: "FA_COMPANY_FIRST_LAST", value: ZmContact.FA_COMPANY_FIRST_LAST, label: ZmMsg.AB_FILE_AS_companyFirstLast }
336 		// TODO: [Q] ZmContact.FA_CUSTOM ???
337 	];
338 };
339 
340 //
341 // Constants
342 //
343 
344 // Message dialog placement
345 ZmEditContactView.DIALOG_X = 50;
346 ZmEditContactView.DIALOG_Y = 100;
347 
348 ZmEditContactView.SHOW_ID_PREFIXES = [
349 	"PREFIX","FIRST","MIDDLE","MAIDEN","LAST","SUFFIX","NICKNAME","TITLE","DEPARTMENT","COMPANY"
350 ];
351 ZmEditContactView.SHOW_ID_LABELS = [
352 	ZmMsg.AB_FIELD_prefix,
353 	ZmMsg.AB_FIELD_firstName,
354 	ZmMsg.AB_FIELD_middleName,
355 	ZmMsg.AB_FIELD_maidenName,
356 	ZmMsg.AB_FIELD_lastName,
357 	ZmMsg.AB_FIELD_suffix,
358 	ZmMsg.AB_FIELD_nickname,
359 	ZmMsg.AB_FIELD_jobTitle,
360 	ZmMsg.AB_FIELD_department,
361 	ZmMsg.AB_FIELD_company
362 ];
363 
364 ZmEditContactView.ALWAYS_SHOW = {
365 	FIRST: true, LAST: true, TITLE: true, COMPANY: true
366 };
367 
368 ZmEditContactView.ATTRS = {
369 	FILE_AS: ZmContact.F_fileAs,
370 	FOLDER: ZmContact.F_folderId,
371 	IMAGE: ZmContact.F_image,
372 	ZIMLET_IMAGE: ZmContact.F_zimletImage,
373 	PREFIX: ZmContact.F_namePrefix,
374 	SUFFIX: ZmContact.F_nameSuffix,
375 	MAIDEN: ZmContact.F_maidenName,
376 	FIRST: ZmContact.F_firstName,
377     PHONETIC_FIRST: ZmContact.F_phoneticFirstName,
378 	MIDDLE: ZmContact.F_middleName,
379 	LAST: ZmContact.F_lastName,
380     PHONETIC_LAST: ZmContact.F_phoneticLastName,
381 	NICKNAME: ZmContact.F_nickname,
382 	TITLE: ZmContact.F_jobTitle,
383 	DEPARTMENT: ZmContact.F_department,
384 	COMPANY: ZmContact.F_company,
385     PHONETIC_COMPANY: ZmContact.F_phoneticCompany,
386 	NOTES: ZmContact.F_notes
387 };
388 
389 ZmEditContactView.updateFieldLists = function() {
390 
391 ZmEditContactView.LISTS = {
392 	ADDRESS: {attrs:ZmContact.ADDRESS_FIELDS}, // NOTE: placeholder for custom handling
393 	EMAIL: {attrs:ZmContact.EMAIL_FIELDS, onlyvalue:true},
394 	PHONE: {attrs:ZmContact.PHONE_FIELDS},
395 	IM: {attrs:ZmContact.IM_FIELDS, onlyvalue:true},
396 	URL: {attrs:ZmContact.URL_FIELDS},
397 	OTHER: {attrs:ZmContact.OTHER_FIELDS}
398 };
399 
400 }; // updateFieldLists
401 ZmEditContactView.updateFieldLists();
402 
403 //
404 // Data
405 //
406 
407 ZmEditContactView.prototype.TEMPLATE = "abook.Contacts#ZmEditContactView";
408 
409 //
410 // Public methods
411 //
412 
413 /**
414  * Sets the contact.
415  * 
416  * @param	{ZmContact}	contact		the contact
417  * @param	{Boolean}	isDirty		<code>true</code> if the contact is dirty
418  */
419 ZmEditContactView.prototype.set = function(contact, isDirty) {
420 	if (typeof arguments[0] == "string") {
421 		DwtForm.prototype.set.apply(this, arguments);
422 		return;
423 	}
424 
425 	// save contact
426 	this._contact = this._item = contact;
427 
428 	// fill in base fields
429 	for (var id in ZmEditContactView.ATTRS) {
430 		var value = contact.getAttr(ZmEditContactView.ATTRS[id]);
431 		if (id === "FOLDER" || id === "IMAGE") {
432 			continue;
433 		}
434 		if (id === "FILE_AS") {
435 			value = value || ZmContact.FA_LAST_C_FIRST;
436 		}
437 		this.setValue(id, value);
438 	}
439 	this.setValue("IMAGE", (contact && contact.getImageUrl(ZmContact.NO_MAX_IMAGE_WIDTH)) || "", true);
440 
441 	// fill in folder field
442 	if (this.getControl("FOLDER")) {
443 		var folderOrId = contact && contact.getAddressBook();
444 		if (!folderOrId && (appCtxt.getCurrentViewType() == ZmId.VIEW_CONTACT_SIMPLE)) {
445 			var overview = appCtxt.getApp(ZmApp.CONTACTS).getOverview();
446 			folderOrId = overview && overview.getSelected();
447 			if (folderOrId && folderOrId.type != ZmOrganizer.ADDRBOOK) {
448 				folderOrId = null;
449 			}
450 			if (folderOrId && folderOrId.id && folderOrId.id == ZmFolder.ID_DLS) { //can't create under Distribution Lists virtual folder
451 				folderOrId = null;
452 			}
453 		}
454 
455         //check introduced to avoid choosing a readonly/shared folder as default folder location 
456 		this._setFolder((folderOrId && !folderOrId.isReadOnly()) ? folderOrId : ZmOrganizer.ID_ADDRBOOK);
457 
458 	}
459 
460 	if (this.getControl("TAG"))
461 		this._setTags(contact);
462 
463 	// check show detail items for fields with values
464 	for (var id in ZmEditContactView.ATTRS) {
465 		var showId = "SHOW_"+id;
466 		var control = this.getControl(showId);
467 		if (control == null) continue;
468 		var checked = id in ZmEditContactView.ALWAYS_SHOW || (this.getValue(id) || "") != "";
469 		this.setValue(showId, checked);
470 		control.setChecked(checked, true); // skip notify
471 	}
472 
473 	// populate lists
474 	this._listAttrs = {};
475 	var nattrs = contact.getNormalizedAttrs();
476 	for (var id in ZmEditContactView.LISTS) {
477 		switch (id) {
478 			case "ADDRESS": {
479 				this.__initRowsAddress(nattrs, id, this._listAttrs);
480 				break;
481 			}
482 			case "OTHER": {
483 				var list = ZmEditContactView.LISTS[id];
484 				this.__initRowsOther(nattrs, id, list.attrs, list.onlyvalue, this._listAttrs);
485 				break;
486 			}
487 			default: {
488 				var list = ZmEditContactView.LISTS[id];
489 				this.__initRowsControl(nattrs, id, list.attrs, list.onlyvalue, this._listAttrs);
490 			}
491 		}
492 	}
493 
494 	// mark form as clean and update display
495 	if (!isDirty) {
496 		this.reset(true);
497 	}
498 	this._handleDirty();
499 	this.update();
500 
501 	// listen to changes in the contact
502 	if (contact) {
503 		contact.removeChangeListener(this._changeListener);
504 	}
505 	contact.addChangeListener(this._changeListener);
506 
507 	// notify zimlets that a new contact is being shown.
508 	appCtxt.notifyZimlets("onContactEdit", [this, this._contact, this._htmlElId]);
509 };
510 
511 /**
512  * Gets the contact.
513  * 
514  * @return	{ZmContact}	the contact
515  */
516 ZmEditContactView.prototype.getContact = function() {
517 	return this._contact;
518 };
519 
520 /**
521  * Gets the modified attributes.
522  * 
523  * @return	{Hash}	a hash of attributes
524  */
525 ZmEditContactView.prototype.getModifiedAttrs = function() {
526 	var itemIds = this.getDirtyItems();
527 	var counts = {};
528 	var attributes = {};
529 
530 	// get list of modified attributes
531 	for (var i = 0; i < itemIds.length; i++) {
532 		var id = itemIds[i];
533 		if (id == "ACCOUNT") { continue; }
534 		var value = this.getValue(id);
535 		if (id in ZmEditContactView.LISTS) {
536 			var items = value;
537 			var addressTypeCounts = [];
538 			for (var j = 0; j < items.length; j++) {
539 				var item = items[j];
540 				if (id == "ADDRESS") {
541 					var type = item.type;
542 					addressTypeCounts[type] = addressTypeCounts[type] || 1;
543 					var itemAttributes = {};
544 					var foundNonEmptyAttr = false;
545 					for (var prop in item) {
546 						if (prop === "type") {
547 							continue;
548 						}
549 						var value = item[prop];
550 						var att = ZmContact.getAttributeName(type + prop, addressTypeCounts[type]);
551 						itemAttributes[att] = value;
552 						foundNonEmptyAttr = foundNonEmptyAttr || value;
553 					}
554 					if (foundNonEmptyAttr) {
555 						addressTypeCounts[type]++;
556 						for (var itemAtt in itemAttributes) {
557 							attributes[itemAtt] = itemAttributes[itemAtt];
558 						}
559 					}
560 				}
561 				else {
562 					var onlyvalue = ZmEditContactView.LISTS[id] && ZmEditContactView.LISTS[id].onlyvalue;
563 					var v = onlyvalue ? item : item.value;
564 					if (!v) continue;
565 					var list = ZmEditContactView.LISTS[id];
566 					var a = onlyvalue ? list.attrs[0] : item.type;
567 					if (id === "OTHER" && AjxUtil.arrayContains(AjxUtil.values(ZmEditContactView.ATTRS), a)) {
568 						//ignore attributes named the same as one of our basic attributes.
569 						continue;
570 					}
571 					if (!counts[a]) counts[a] = 0;
572 					var count = ++counts[a];
573 					a = ZmContact.getAttributeName(a, count);
574 					attributes[a] = v;
575 				}
576 			}
577 		}
578 		else {
579 			var a = ZmEditContactView.ATTRS[id];
580 			attributes[a] = value;
581 		}
582 	}
583 
584 	// compare against existing fields
585 	var anames = AjxUtil.keys(attributes);
586 	var listAttrs = this._listAttrs;
587 	for (var id in listAttrs) {
588 		if (!this.isDirty(id)) continue;
589 		var prefixes = AjxUtil.uniq(AjxUtil.map(listAttrs[id], ZmContact.getPrefix));
590 		for (var i = 0; i < prefixes.length; i++) {
591 			// clear fields from original contact from normalized attr names
592 			var attrs = AjxUtil.keys(this._contact.getAttrs(prefixes[i]));
593 			var complement = AjxUtil.complement(anames, attrs);
594 			for (var j = 0; j < complement.length; j++) {
595 				attributes[complement[j]] = "";
596 			}
597 		}
598 	}
599 
600 	// was anything modified?
601 	if (AjxUtil.keys(attributes).length == 0) {
602 		return null;
603 	}
604 
605 	// make sure we set the folder (when new)
606 	if (!attributes[ZmContact.F_folderId] && !this._contact.id) {
607 		attributes[ZmContact.F_folderId] = this.getValue("FOLDER");
608 	}
609 
610 	if (attributes[ZmContact.F_image] && attributes[ZmContact.F_image] === this._contact.getAttr(ZmContact.F_zimletImage)) {
611 		// Slightly hacky - 2 cases could lead here:
612 		// 1. user had an uploaded image, and deleted it. The field (which we use to show both cases) reverts to show the Zimlet (external) image.
613 		// 2. user didn't have an uploaded image. Field was showing the the Zimlet image.
614 		// In both cases we need to clear the Zimlet URL from F_image. For case 2 we don't really need to set the field at all since it doesn't change, but
615 		// this way it's simpler.
616 		attributes[ZmContact.F_image] = "";
617 	}
618 	// set the value for IMAGE to just the attachment id
619 	if (attributes[ZmContact.F_image]) {
620 		var value = this.getValue("IMAGE");
621 		var m = /aid=(.*)/.exec(value);
622 		if (m) {
623 			// NOTE: ZmContact.modify expects the "aid_" prefix.
624 			attributes[ZmContact.F_image] = "aid_"+m[1];
625 		}
626 	}
627 
628 	return attributes;
629 };
630 
631 /**
632  * Checks if the view is empty.
633  * 
634  * @return	{Boolean}	<code>true</code> if the view is empty
635  */
636 ZmEditContactView.prototype.isEmpty = function(items) {
637 	items = items || this._items;
638 	for (var id in items) {
639 		var item = items[id];
640 		if (this.isIgnore(id) || id == "FILE_AS") continue;
641 		var value = this.getValue(id);
642 		if (value) {
643 			if (!AjxUtil.isArray(value)) {
644 				if (id == "FOLDER") {
645 					if (value != item.ovalue) return false;
646 				} else {
647 					if (value !== "") return false;
648 				}
649 			} else {
650 				for (var i=0; i<value.length; i++) {
651 					var valueitem = value[i];
652 					if (valueitem) {
653 						if (id=="ADDRESS") {
654 							if (!ZmEditContactViewAddress.equals(valueitem, {type: valueitem.type})) return false;
655 						} else {
656 							if (!(valueitem.value==="" || valueitem==="")) return false;
657 						}
658 					}
659 				}
660 			}
661 		}
662 	}
663 	return true;
664 };
665 
666 /**
667  * @private
668  */
669 ZmEditContactView.prototype.enableInputs = function(bEnable) {
670 	// ignore
671 };
672 
673 /**
674  * Cleanup the view.
675  * 
676  */
677 ZmEditContactView.prototype.cleanup = function() {
678 	this._contact = this._item = null;
679 	this.clean = true;
680 };
681 
682 ZmEditContactView.prototype.dispose =
683 function() {
684 	ZmTagsHelper.disposeListeners(this);
685 	DwtComposite.prototype.dispose.apply(this, arguments);
686 };
687 
688 ZmEditContactView.prototype._setTags =
689 function(contact) {
690 	//use the helper to get the tags.
691 	var tagsHtml = ZmTagsHelper.getTagsHtml(contact || this._item, this);
692 
693 	var tagControl = this.getControl("TAG");
694 	if (!tagControl) {
695 		return;
696 	}
697 	tagControl.clearContent();
698 	if (tagsHtml.length > 0) {
699 		tagControl.setContent(tagsHtml);
700 		tagControl.setVisible(true);
701 	}
702 	else {
703 		tagControl.setVisible(false);
704 	}
705 };
706 
707 ZmEditContactView._onBlur =
708 function() {
709 	this._controller._updateTabTitle();
710 };
711 
712 //
713 // ZmListController methods
714 //
715 
716 /**
717  * Gets the list.
718  * 
719  * @return	{ZmContactList}	the list	
720  */
721 ZmEditContactView.prototype.getList = function() { return null; };
722 
723 /**
724  * Gets the controller.
725  * 
726  * @return	{ZmContactController}	the controller
727  */
728 ZmEditContactView.prototype.getController = function() {
729 	return this._controller;
730 };
731 
732 // Following two overrides are a hack to allow this view to pretend it's a list view
733 ZmEditContactView.prototype.getSelection = function() {
734 	return this.getContact();
735 };
736 
737 ZmEditContactView.prototype.getSelectionCount = function() {
738 	return 1;
739 };
740 
741 /**
742  * Gets the title.
743  * 
744  * @return	{String}	the title
745  */
746 ZmEditContactView.prototype.getTitle = function() {
747 	return [ZmMsg.zimbraTitle, ZmMsg.contact].join(": ");
748 };
749 
750 //
751 // ZmListView methods
752 //
753 
754 ZmEditContactView.prototype._checkItemCount = function() {};
755 ZmEditContactView.prototype._handleResponseCheckReplenish = function() {};
756 
757 //
758 // Protected methods
759 //
760 
761 /**
762  * @private
763  */
764 ZmEditContactView.prototype._getFullName = function(defaultToNull) {
765 	var contact = {
766 		fileAs: this.getValue("FILE_AS"),
767 		firstName: this.getValue("FIRST"), lastName: this.getValue("LAST"),
768 		company: this.getValue("COMPANY")
769 	};
770 	return ZmContact.computeFileAs(contact) || (defaultToNull ? null : ZmMsg.noName);
771 };
772 
773 /**
774  * @private
775  */
776 ZmEditContactView.prototype._getDefaultFocusItem = function() {
777 	return this.getControl(appCtxt.get(ZmSetting.PHONETIC_CONTACT_FIELDS) ? "LAST" : "FIRST");
778 };
779 
780 /**
781  * @private
782  */
783 ZmEditContactView.prototype._setFolder = function(organizerOrId) {
784 	var organizer = organizerOrId instanceof ZmOrganizer ? organizerOrId : appCtxt.getById(organizerOrId);
785 	this.setLabel("FOLDER", organizer.getName());
786 	this.setValue("FOLDER", organizer.id);
787 	this.setImage("FOLDER", organizer.getIconWithColor(), ZmMsg.locationLabel);
788 	if (appCtxt.multiAccounts) {
789 		this.setValue("ACCOUNT", organizer.getAccount().getDisplayName());
790 	}
791 };
792 
793 /**
794  * @private
795  */
796 ZmEditContactView.prototype._getDialogXY =
797 function() {
798 	var loc = Dwt.toWindow(this.getHtmlElement(), 0, 0, null, true);
799 	return new DwtPoint(loc.x + ZmEditContactView.DIALOG_X, loc.y + ZmEditContactView.DIALOG_Y);
800 };
801 
802 // listeners
803 
804 /**
805  * @private
806  */
807 ZmEditContactView.prototype._handleDirty = function() {
808 	var items = this.getDirtyItems();
809 	// toggle save
810 	var toolbar = this._controller && this._controller.getCurrentToolbar();
811 	if (toolbar) {
812 		var dirty = items.length > 0 ? items.length > 1 || items[0] != "IMAGE" || this._contact.id : false;
813 
814 		// Creating a new contact with only the folder set should not be saveable until at least one other field has a value
815 		var needitems = AjxUtil.hashCopy(this._items);
816 		delete needitems["FOLDER"];
817 		var empty = this.isEmpty(needitems); // false if one or more fields are set, excluding the folder field
818 
819 		toolbar.enable(ZmOperation.SAVE, dirty && !empty);
820 	}
821 	// debug information
822 	this.setValue('DEBUG', items.join(', '));
823 };
824 
825 /**
826  * @private
827  */
828 ZmEditContactView.prototype._handleDetailCheck = function(itemId, id) {
829 	this.setValue(itemId, !this.getValue(itemId));
830 	this.update();
831 	var control = this.getControl(id);
832 	if (control) {
833         control.disableFocusHdlr(); //disable focus handler so hint is displayed
834 		control.focus();
835         control.enableFocusHdlr(); //re-enable
836         //Bug fix # 80423 - attach a onKeyDown handler for Firefox.
837         if (AjxEnv.isFirefox) {
838             control.enableKeyDownHdlr();
839         }
840 	}
841 };
842 
843 ZmEditContactView.prototype._handleRemoveImage = function() {
844 	var image = this.getValue("IMAGE", ""); //could be user uploaded, or zimlet (e.g. LinkedInImage Zimlet) one since we use both in same field.
845 	var zimletImage = this.getValue("ZIMLET_IMAGE", "");
846 	if (image !== zimletImage) {
847 		//user uploaded image - this is the one we remove. Show the zimlet one instead.
848 		this.set("IMAGE", zimletImage);
849 		return;
850 	}
851 	//otherwise it's the Zimlet image we remove
852 	this.set("ZIMLET_IMAGE", "");
853 	this.set("IMAGE", "");  //show the image field empty. Again we use the same field for regular user uploaded and for zimlet provided.
854 };
855 
856 /**
857  * @private
858  */
859 ZmEditContactView.prototype._handleFileAsChange = function() {
860 	var fa = this.getValue("FILE_AS");
861 	var showCompany =
862         ZmEditContactView.ALWAYS_SHOW["COMPANY"] ||
863 		fa == ZmContact.FA_COMPANY ||
864 		fa == ZmContact.FA_LAST_C_FIRST_COMPANY ||
865 		fa == ZmContact.FA_FIRST_LAST_COMPANY ||
866 		fa == ZmContact.FA_COMPANY_LAST_C_FIRST ||
867 		fa == ZmContact.FA_COMPANY_FIRST_LAST
868 	;
869 	var company = this.getValue("COMPANY");
870 	if (showCompany) {
871 		this.setValue("SHOW_COMPANY", true);
872 		this.setVisible("COMPANY", true);
873 	}
874 	else if (!company) {
875 		this.setValue("SHOW_COMPANY", false);
876 		this.setVisible("COMPANY", false);
877 	}
878 };
879 
880 /**
881  * @private
882  */
883 ZmEditContactView.prototype._handleFolderButton = function(ev) {
884 	var dialog = appCtxt.getChooseFolderDialog();
885 	dialog.registerCallback(DwtDialog.OK_BUTTON, new AjxCallback(this, this._handleChooseFolder));
886 	var params = {
887 		overviewId:		dialog.getOverviewId(ZmApp.CONTACTS),
888 		title:			ZmMsg.chooseAddrBook,
889 		treeIds:		[ZmOrganizer.ADDRBOOK],
890 		skipReadOnly:	true,
891 		skipRemote:		false,
892 		noRootSelect:	true,
893 		appName:		ZmApp.CONTACTS
894 	};
895 	params.omit = {};
896 	params.omit[ZmFolder.ID_TRASH] = true;
897 	dialog.popup(params);
898 };
899 
900 /**
901  * @private
902  */
903 ZmEditContactView.prototype._handleChooseFolder = function(organizer) {
904 	var dialog = appCtxt.getChooseFolderDialog();
905 	dialog.popdown();
906 	this._setFolder(organizer);
907 };
908 
909 /**
910  * @private
911  */
912 ZmEditContactView.prototype._contactChangeListener = function(ev) {
913 	if (ev.type != ZmEvent.S_CONTACT) return;
914 	if (ev.event == ZmEvent.E_TAGS || ev.event == ZmEvent.E_REMOVE_ALL) {
915 		this._setTags();
916 	}
917 };
918 
919 //
920 // Private methods
921 //
922 
923 /**
924  * @private
925  */
926 ZmEditContactView.prototype.__getDetailsMenu = function() {
927 	var menu = new DwtMenu({parent: this.getControl("DETAILS"), style: DwtMenu.POPUP_STYLE, id: "ContactDetailsMenu"});
928 	var ids = ZmEditContactView.SHOW_ID_PREFIXES;
929 	var labels = ZmEditContactView.SHOW_ID_LABELS;
930 	var count = 0;
931 	for (var i = 0; i < ids.length; i++) {
932 		var id = ids[i];
933 		if (this.getControl(id)) {
934 			var menuitem = new DwtMenuItem({parent: menu, style: DwtMenuItem.CHECK_STYLE, id: "ContactDetailsMenu_" + id});
935 			menuitem.setText(labels[i]);
936 			// NOTE: Always show first and last but don't allow to change
937 			if (id in ZmEditContactView.ALWAYS_SHOW) {
938 				menuitem.setChecked(true, true);
939 				menuitem.setEnabled(false);
940 			}
941 			var itemId = "SHOW_"+id;
942 			var listener = new AjxListener(this, this._handleDetailCheck, [itemId, id]);
943 			menuitem.addSelectionListener(listener);
944 			this._registerControl({ id: itemId, control: menuitem, ignore: true });
945 			count++;
946 		}
947 	}
948 	return count > 2 ? menu : null;
949 };
950 
951 /**
952  * @private
953  */
954 ZmEditContactView.prototype.__initRowsControl =
955 function(nattrs,id,prefixes,onlyvalue,listAttrs,skipSetValue) {
956 	var array = [];
957 	for (var j = 0; j < prefixes.length; j++) {
958 		var prefix = prefixes[j];
959 		for (var i = 1; true; i++) {
960 			var a = ZmContact.getAttributeName(prefix, i);
961 			if (a != prefix && AjxUtil.indexOf(prefixes, a) != -1) break;
962 			var value = nattrs[a];
963 			if (!value) break;
964 			array.push(onlyvalue ? value : { type:prefix,value:value });
965 			if (!listAttrs[id]) listAttrs[id] = [];
966 			listAttrs[id].push(a);
967 		}
968 	}
969 	if (!skipSetValue) {
970 		this.setValue(id, array);
971 	}
972 	return array;
973 };
974 
975 /**
976  * @private
977  */
978 ZmEditContactView.prototype.__initRowsOther =
979 function(nattrs,id,prefixes,onlyvalue,listAttrs) {
980 	var array = this.__initRowsControl.call(this,nattrs,id,prefixes,onlyvalue,listAttrs,true);
981 
982 	// gather attributes we know about
983 	var attributes = {};
984 	for (var attrId in ZmEditContactView.ATTRS) {
985 		attributes[ZmEditContactView.ATTRS[attrId]] = true;
986 	}
987 	for (var listId in ZmEditContactView.LISTS) {
988 		var list = ZmEditContactView.LISTS[listId];
989 		if (!list.attrs) continue;
990 		for (var i = 0; i < list.attrs.length; i++) {
991 			attributes[list.attrs[i]] = true;
992 		}
993 	}
994 	for (var i = 0; i < ZmContact.ADDR_PREFIXES.length; i++) {
995 		var prefix = ZmContact.ADDR_PREFIXES[i];
996 		for (var j = 0; j < ZmContact.ADDR_SUFFIXES.length; j++) {
997 			var suffix = ZmContact.ADDR_SUFFIXES[j];
998 			attributes[prefix+suffix] = true;
999 		}
1000 	}
1001 
1002 	// add attributes on contact that we don't know about
1003 	for (var aname in nattrs) {
1004 		var anameNormalized = ZmContact.getPrefix(aname);
1005 		if (ZmContact.IS_IGNORE[anameNormalized]) continue;
1006 		if (!(anameNormalized in attributes)) {
1007 			array.push({type:anameNormalized,value:nattrs[aname]});
1008 			if (!listAttrs[id]) listAttrs[id] = [];
1009 			listAttrs[id].push(aname);
1010 		}
1011 	}
1012 
1013 	this.setValue(id, array);
1014 };
1015 
1016 /**
1017  * @private
1018  */
1019 ZmEditContactView.prototype.__initRowsAddress = function(nattrs,id,listAttrs) {
1020 	var array = [];
1021 	var prefixes = ZmContact.ADDR_PREFIXES;
1022 	var suffixes = ZmContact.ADDR_SUFFIXES;
1023 	for (var k = 0; k < prefixes.length; k++) {
1024 		var prefix = prefixes[k];
1025 		for (var j = 1; true; j++) {
1026 			var address = null;
1027 			for (var i = 0; i < suffixes.length; i++) {
1028 				var suffix = suffixes[i];
1029 				var a = ZmContact.getAttributeName(prefix+suffix, j);
1030 				var value = nattrs[a];
1031 				if (!value) continue;
1032 				if (!address) address = {};
1033 				address[suffix] = value;
1034 				if (!listAttrs[id]) listAttrs[id] = [];
1035 				listAttrs[id].push(a);
1036 			}
1037 			if (!address) break;
1038 			address.type = prefix;
1039 			array.push(address);
1040 		}
1041 	}
1042 	this.setValue("ADDRESS", array);
1043 };
1044 
1045 // functions
1046 
1047 
1048 //
1049 // Class: ZmEditContactViewImage
1050 //
1051 /**
1052  * Creates the contact view image.
1053  * @class
1054  * This class represents a contact view image.
1055  * 
1056  * @param	{Hash}	params		a hash of parameters
1057  * 
1058  * @extends		DwtControl
1059  * 
1060  * @private
1061  */
1062 ZmEditContactViewImage = function(params) {
1063 	if (arguments.length == 0) return;
1064 	params.className = params.className || "ZmEditContactViewImage";
1065 	params.posStyle = Dwt.RELATIVE_STYLE;
1066 	params.id = params.parent.getHTMLElId()+"_IMAGE";
1067 	DwtControl.apply(this, arguments);
1068 
1069 	var el = this.getHtmlElement();
1070 	el.innerHTML = [
1071 		"<div style='width:48;height:48'>",
1072 			"<img id='",this._htmlElId,"_img' width='48' height='48'>",
1073 		"</div>",
1074 		"<div id='",this._htmlElId,"_badge' style='position:absolute;"
1075         ,"bottom:",(AjxEnv.isMozilla ? -4 : 0), ";right:", (AjxEnv.isMozilla ? 3 : 0),"px'>"
1076 	].join("");
1077 	el.style.cursor = "pointer";
1078 
1079 	this._src = "";
1080 	this._imgEl = document.getElementById(this._htmlElId+"_img");
1081 	this._imgEl.onload = AjxCallback.simpleClosure(this._imageLoaded, this);
1082 	this._badgeEl = document.getElementById(this._htmlElId+"_badge");
1083 
1084 	this._setMouseEvents();
1085 
1086 	this.addListener(DwtEvent.ONMOUSEOVER, new AjxListener(Dwt.addClass, [el,DwtControl.HOVER]));
1087 	this.addListener(DwtEvent.ONMOUSEOUT, new AjxListener(Dwt.delClass, [el,DwtControl.HOVER]));
1088 	this.addListener(DwtEvent.ONMOUSEUP, new AjxListener(this, this._chooseImage));
1089 
1090 	this.setToolTipContent(ZmMsg.addImg);
1091 };
1092 ZmEditContactViewImage.prototype = new DwtControl;
1093 ZmEditContactViewImage.prototype.constructor = ZmEditContactViewImage;
1094 ZmEditContactViewImage.prototype.isFocusable = true;
1095 
1096 /**
1097  * Returns a string representation of the object.
1098  * 
1099  * @return		{String}		a string representation of the object
1100  * @private
1101  */
1102 ZmEditContactViewImage.prototype.toString = function() {
1103 	return "ZmEditContactViewImage";
1104 };
1105 
1106 // Constants
1107 
1108 ZmEditContactViewImage.IMAGE_URL = "/service/content/proxy?aid=@aid@";
1109 
1110 // Public methods
1111 
1112 /**
1113  * Sets the image value.
1114  * 
1115  * @param	{String}	value	the image src value
1116  * @private
1117  */
1118 ZmEditContactViewImage.prototype.setValue = function(value, promptOnError) {
1119 	// Save current image source to display in case user picks an improper file.
1120 	this._currentSrc = this._src;
1121 	// Save original image source to know if "Do you want to save..." prompt is needed
1122 	if (typeof this._originalSrc === "undefined") {
1123 		this._originalSrc = this._src;
1124 	}
1125 	this._src = value;
1126 	if (!value) {
1127 		this._imgEl.src = ZmZimbraMail.DEFAULT_CONTACT_ICON;
1128 		this._badgeEl.className = "ImgAdd";
1129 		this.setToolTipContent(ZmMsg.addImg);
1130 		this._imgEl.alt = ZmMsg.addImg;
1131 	}
1132 	else {
1133 		this._imgEl.src = value;
1134 		this._badgeEl.className = "ImgEditBadge";
1135 		this.setToolTipContent(ZmMsg.editImg);
1136 		this._imgEl.alt = ZmMsg.editImg;
1137 	}
1138 	this.parent.setDirty("IMAGE", true);
1139     this._imgEl.onerror = this._handleCorruptImageError.bind(this, promptOnError);
1140 };
1141 
1142 /**
1143  * Gets the value.
1144  * 
1145  * @return	{String}	the image src value
1146  * @private
1147  */
1148 ZmEditContactViewImage.prototype.getValue = function() {
1149 	return this._src;
1150 };
1151 
1152 // Protected methods
1153 
1154 ZmEditContactViewImage.prototype._focus = function() {
1155     Dwt.addClass(this.getHtmlElement(), DwtControl.FOCUSED);
1156 };
1157 ZmEditContactViewImage.prototype._blur = function() {
1158     Dwt.delClass(this.getHtmlElement(), DwtControl.FOCUSED);
1159 };
1160 
1161 /**
1162  * @private
1163  */
1164 ZmEditContactViewImage.prototype._imageLoaded = function() {
1165 	this._imgEl.removeAttribute("width");
1166 	this._imgEl.removeAttribute("height");
1167 	var w = this._imgEl.width;
1168 	var h = this._imgEl.height;
1169     this._imgEl.setAttribute(w>h ? 'width' : 'height', 48);
1170 };
1171 
1172 /**
1173  * @private
1174  */
1175 ZmEditContactViewImage.prototype._chooseImage = function() {
1176 	var dialog = appCtxt.getUploadDialog();
1177 	dialog.setAllowedExtensions(["png","jpg","jpeg","gif"]);
1178 	var folder = null;
1179 	var callback = new AjxCallback(this, this._handleImageSaved);
1180 	var title = ZmMsg.uploadImage;
1181 	var location = null;
1182 	var oneFileOnly = true;
1183 	var noResolveAction = true;
1184     var showNotes = false;
1185     var isImage = true;
1186 	dialog.popup(null, folder, callback, title, location, oneFileOnly, noResolveAction, showNotes ,isImage);
1187 };
1188 
1189 /**
1190  * @private
1191  */
1192 ZmEditContactViewImage.prototype._handleImageSaved = function(folder, filenames, files) {
1193 	var dialog = appCtxt.getUploadDialog();
1194 	dialog.popdown();
1195 	this.setValue(ZmEditContactViewImage.IMAGE_URL.replace(/@aid@/, files[0].guid), true);
1196 	this.parent.update();
1197 };
1198 
1199 /**
1200  * @private
1201  */
1202 ZmEditContactViewImage.prototype._createElement = function() {
1203 	return document.createElement("FIELDSET");
1204 };
1205 
1206 /**
1207  * @private
1208  */
1209 ZmEditContactViewImage.prototype._handleCorruptImageError = function(promptOnError) {
1210 	// display current image if exists, otherwise will set default contact image
1211     this.setValue(this._currentSrc);
1212 	if (this._originalSrc == this._currentSrc) {
1213 		// == not === to acount for null and "" which should satisfy the condition
1214 		this.parent.setDirty("IMAGE", false);
1215 	}
1216 	if (promptOnError) {
1217 		// Don't display this dialog in cases where image is missing not due to user input.
1218 		// E.g. LinkedIn changed their url schema which led to broken links. In such cases
1219 		// don't obtrusively prompt the user to select an image, but pretend it was never set.
1220 		this._popupCorruptImageErrorDialog();
1221 	}
1222 };
1223 
1224 /**
1225  * @private
1226  */
1227 ZmEditContactViewImage.prototype._popupCorruptImageErrorDialog = function() {
1228     var dlg = this.corruptImageErrorDlg;
1229     if(dlg){
1230        dlg.popup();
1231     }
1232     else{
1233         dlg = appCtxt.getMsgDialog();
1234         this.corruptImageErrorDlg = dlg;
1235 	    dlg.setMessage(ZmMsg.errorCorruptImageFile, DwtMessageDialog.CRITICAL_STYLE, ZmMsg.corruptFile);
1236         dlg.setButtonListener(DwtDialog.OK_BUTTON, new AjxListener(this, this._corruptImageErrorDialogOkListener));
1237         dlg.popup();
1238     }
1239 };
1240 
1241 /**
1242  * @private
1243  */
1244 ZmEditContactViewImage.prototype._corruptImageErrorDialogOkListener = function() {
1245     this.corruptImageErrorDlg.popdown();
1246     this._chooseImage();
1247 };
1248 
1249 //
1250 // Class: ZmEditContactViewRows
1251 //
1252 
1253 /**
1254  * Creates the contact view rows.
1255  * @class
1256  * This class represents the contact view rows.
1257  * 
1258  * @param	{Hash}	params		a hash of parameters
1259  * 
1260  * @extends		DwtFormRows
1261  * 
1262  * @private
1263  */
1264 ZmEditContactViewRows = function(params) {
1265 	if (arguments.length == 0) return;
1266 	if (!params.formItemDef) params.formItemDef = {};
1267 	// keep track of maximums
1268 	var rowitem = params.formItemDef.rowitem;
1269 	var rowparams = rowitem && rowitem.params;
1270 	var rowoptions = this._options = (rowparams && rowparams.options) || [];
1271 	for (var i = 0; i < rowoptions.length; i++) {
1272 		var option = rowoptions[i];
1273 		if (option.max) {
1274 			if (!this._maximums) this._maximums = {};
1275 			this._maximums[option.value] = { max: option.max, count: 0 };
1276 		}
1277 	}
1278 	// create rows control
1279 	params.formItemDef.id = params.formItemDef.id || Dwt.getNextId();
1280 	params.formItemDef.onremoverow = "this.setDirty(true)";
1281 	params.className = params.className || "ZmEditContactViewRows";
1282 	params.id = [params.parent.getHTMLElId(),params.formItemDef.id].join("_");
1283 	DwtFormRows.apply(this, arguments);
1284     this.setScrollStyle(Dwt.VISIBLE);
1285 };
1286 ZmEditContactViewRows.prototype = new DwtFormRows;
1287 ZmEditContactViewRows.prototype.constructor = ZmEditContactViewRows;
1288 
1289 /**
1290  * Returns a string representation of the object.
1291  * 
1292  * @return		{String}		a string representation of the object
1293  * @private
1294  */
1295 ZmEditContactViewRows.prototype.toString = function() {
1296 	return "ZmEditContactViewRows";
1297 };
1298 
1299 ZmEditContactViewRows.prototype.TEMPLATE = "abook.Contacts#ZmEditContactViewRows";
1300 
1301 // Public methods
1302 
1303 ZmEditContactViewRows.prototype.setDirty = function() {
1304 	DwtFormRows.prototype.setDirty.apply(this, arguments);
1305 	this.parent.setDirty(this._itemDef.id, this.isDirty());
1306 };
1307 
1308 /**
1309  * Checks if the row of the given type is at maximum.
1310  * 
1311  * @param	{constant}	type		the type
1312  * @return	{Boolean}	<code>true</code> if at maximum
1313  * @private
1314  */
1315 ZmEditContactViewRows.prototype.isMaxedOut = function(type) {
1316 	var maximums = this._maximums && this._maximums[type];
1317 	return maximums != null && maximums.count >= maximums.max;
1318 };
1319 
1320 /**
1321  * Checks if all rows are at maximum.
1322  * 
1323  * @return	{Boolean}	<code>true</code> if at maximum
1324  * @private
1325  */
1326 ZmEditContactViewRows.prototype.isAllMaxedOut = function() {
1327 	if (!this._options || this._options.length == 0) return false;
1328 	// determine which ones are maxed out
1329 	var count = 0;
1330 	for (var i = 0; i < this._options.length; i++) {
1331 		var type = this._options[i].value;
1332 		count += this.isMaxedOut(type) ? 1 : 0;
1333 	}
1334 	// are all of the options maxed out?
1335 	return count >= this._options.length;
1336 };
1337 
1338 //
1339 // Class: ZmEditContactViewInputSelectRows
1340 //
1341 
1342 /**
1343  * Creates the input select rows.
1344  * @class
1345  * This class represents the input select rows for the contact view.
1346  * 
1347  * @param	{Hash}	params		a hash of parameters
1348  * 
1349  * @extends		ZmEditContactViewRows
1350  * 
1351  * @private
1352  */
1353 ZmEditContactViewInputSelectRows = function(params) {
1354 	if (arguments.length == 0) return;
1355 	ZmEditContactViewRows.apply(this, arguments);
1356 };
1357 ZmEditContactViewInputSelectRows.prototype = new ZmEditContactViewRows;
1358 ZmEditContactViewInputSelectRows.prototype.constructor = ZmEditContactViewInputSelectRows;
1359 
1360 /**
1361  * Returns a string representation of the object.
1362  * 
1363  * @return		{String}		a string representation of the object
1364  * @private
1365  */
1366 ZmEditContactViewInputSelectRows.prototype.toString = function() {
1367 	return "ZmEditContactViewInputSelectRows";
1368 };
1369 
1370 // DwtFormRows methods
1371 
1372 /**
1373  * Gets the max rows.
1374  * 
1375  * @return	{int}	the maximum rows
1376  * @private
1377  */
1378 ZmEditContactViewInputSelectRows.prototype.getMaxRows = function() {
1379 	return this.isAllMaxedOut() ? this.getRowCount() : ZmEditContactViewRows.prototype.getMaxRows.call(this);
1380 };
1381 
1382 /**
1383  * Sets the value.
1384  * 
1385  * @param	{Array|String}		array		an array of {String} values
1386  * @private
1387  */
1388 ZmEditContactViewInputSelectRows.prototype.setValue = function(array) {
1389 	if (arguments[0] instanceof Array) {
1390 		DwtFormRows.prototype.setValue.apply(this, arguments);
1391 		this._resetMaximums();
1392 	}
1393 	else {
1394 		var id = String(arguments[0]);
1395 		var adjust1 = id && this._subtract(id);
1396 		DwtFormRows.prototype.setValue.apply(this, arguments);
1397 		var adjust2 = id && this._add(id);
1398 		if (adjust1 || adjust2) this._adjustMaximums();
1399 	}
1400 };
1401 
1402 /**
1403  * Adds a row.
1404  * 
1405  * @param		{ZmItem}	itemDef		the item definition (not used)
1406  * @param	{int}	index		the index to add the row at
1407  * @private
1408  */
1409 ZmEditContactViewInputSelectRows.prototype.addRow = function(itemDef, index) {
1410 	DwtFormRows.prototype.addRow.apply(this, arguments);
1411 	index = index != null ? index : this.getRowCount() - 1;
1412 	var adjust = this._add(index);
1413 	if (adjust) this._adjustMaximums();
1414 	var value = this.getValue(index);
1415 	// select first one that is not maxed out
1416 	if (value && this.isMaxedOut(value.type) && this._options.length > 0 && 
1417 	    this._maximums[value.type].count > this._maximums[value.type].max) {
1418 		var options = this._options;
1419 		for (var i = 0; i < options.length; i++) {
1420 			var option = options[i];
1421 			if (!this.isMaxedOut(option.value)) {
1422 				value.type = option.value;
1423 				this.setValue(index, value);
1424 				break;
1425 			}
1426 		}
1427 	}
1428 	
1429 	if (this._rowCount >= this._maxRows) {
1430 		for (var i = 0; i < this._rowCount; i++) {
1431 			this.setVisible(this._items[i]._addId, false);
1432 		}
1433 	}
1434 	if (AjxEnv.isFirefox) this._updateLayout();
1435 };
1436 
1437 /**
1438  * Removes a row.
1439  * 
1440  * @param	{String}		indexOrId	the row index or item id
1441  * @private
1442  */
1443 ZmEditContactViewInputSelectRows.prototype.removeRow = function(indexOrId) {
1444 	var adjust = this._subtract(indexOrId);
1445 	DwtFormRows.prototype.removeRow.apply(this, arguments);
1446 	if (adjust) this._adjustMaximums();
1447 	if (AjxEnv.isFirefox) this._updateLayout();
1448 };
1449 
1450 /**
1451  * @private
1452  */
1453 ZmEditContactViewInputSelectRows.prototype._setControlIds = function(rowId, index) {
1454 	DwtFormRows.prototype._setControlIds.call(this, rowId, index);
1455 	var item = this._items[rowId];
1456 	var control = item && item.control;
1457 	if (control && control._setControlIds) {
1458 		control._setControlIds(rowId, index);
1459 	}
1460 };
1461 
1462 // Protected methods
1463 
1464 /**
1465  * @private
1466  */
1467 ZmEditContactViewInputSelectRows.prototype._subtract = function(indexOrId) {
1468 	var value = this.getValue(indexOrId);
1469 	return this._subtractType(value && value.type);
1470 };
1471 ZmEditContactViewInputSelectRows.prototype._subtractType = function(type) {
1472 	if (!this._maximums || !this._maximums[type]) return false;
1473 	this._maximums[type].count--;
1474 	return true;
1475 };
1476 ZmEditContactViewInputSelectRows.prototype._add = function(indexOrId) {
1477 	var value = this.getValue(indexOrId);
1478 	return this._addType(value && value.type);
1479 };
1480 ZmEditContactViewInputSelectRows.prototype._addType = function(type) {
1481 	if (!this._maximums || !this._maximums[type]) return false;
1482 	this._maximums[type].count++;
1483 	return true;
1484 };
1485 
1486 ZmEditContactViewInputSelectRows.prototype._adjustMaximums = function() {
1487 	if (!this._maximums || !this._options) return;
1488 	// determine which ones are maxed out
1489 	var enabled = {};
1490 	var count = 0;
1491 	for (var i = 0; i < this._options.length; i++) {
1492 		var type = this._options[i].value;
1493 		var maxed = this.isMaxedOut(type);
1494 		enabled[type] = !maxed;
1495 		count += maxed ? 1 : 0;
1496 	}
1497 	// are all of the options maxed out?
1498 	var allMaxed = count == this._options.length;
1499 	// en/disable controls as needed
1500 	var rowCount = this.getRowCount();
1501 	for (var i = 0; i < rowCount; i++) {
1502 		var control = this.getControl(i);
1503 		if (control.enableOptions) {
1504 			control.enableOptions(enabled);
1505 		}
1506 		// TODO: Will this override the max rows add button visibility?
1507 		this.setVisible(this._items[i]._addId, !allMaxed);
1508 	}
1509 };
1510 
1511 // TODO: This is a hack to avoid bad counting error. Should
1512 // TODO: really find the cause of the error.
1513 ZmEditContactViewInputSelectRows.prototype._resetMaximums = function() {
1514 	if (!this._maximums) return;
1515 	for (var type in this._maximums) {
1516 		this._maximums[type].count = 0;
1517 	}
1518 	var rowCount = this.getRowCount();
1519 	for (var i = 0; i < rowCount; i++) {
1520 		var value = this.getValue(i);
1521 		var maximum = this._maximums[value && value.type];
1522 		if (maximum) {
1523 			maximum.count++;
1524 		}
1525 	}
1526 };
1527 
1528 // On FF, the selects are sometimes rendered incorrectly.
1529 ZmEditContactViewInputSelectRows.prototype._updateLayout = function() {
1530 	for (var i = 0, cnt = this.getRowCount(); i < cnt; i++) {
1531 		this.getControl(i).reRenderSelect();
1532 		this.getControl(i).reRenderInput();
1533 	}
1534 };
1535 
1536 //
1537 // Class: ZmEditContactViewInputSelect
1538 //
1539 
1540 /**
1541  * Creates the contact view input select.
1542  * @class
1543  * This class represents an input select.
1544  * 
1545  * @param	{Hash}	params		a hash of parameters
1546  * 
1547  * @extends		DwtComposite
1548  * 
1549  * @private
1550  */
1551 ZmEditContactViewInputSelect = function(params) {
1552 	if (arguments.length == 0) return;
1553 	this._formItemId = params.formItemDef.id;
1554 	this._options = params.options || [];
1555 	this._cols = params.cols;
1556 	this._rows = params.rows;
1557 	this._hint = params.hint;
1558 	DwtComposite.apply(this, arguments);
1559 	this._tabGroup = new DwtTabGroup(this._htmlElId);
1560 	this._createHtml(params.template);
1561     if (this._input && (params.inputWidth || params.inputHeight)) {
1562         Dwt.setSize(this._input.getInputElement(), params.inputWidth, params.inputHeight);
1563     }
1564 };
1565 ZmEditContactViewInputSelect.prototype = new DwtComposite;
1566 ZmEditContactViewInputSelect.prototype.constructor = ZmEditContactViewInputSelect;
1567 
1568 /**
1569  * Returns a string representation of the object.
1570  * 
1571  * @return		{String}		a string representation of the object
1572  * @private
1573  */
1574 ZmEditContactViewInputSelect.prototype.toString = function() {
1575 	return "ZmEditContactViewInputSelect";
1576 };
1577 
1578 // Data
1579 
1580 ZmEditContactViewInputSelect.prototype.TEMPLATE = "abook.Contacts#ZmEditContactViewInputSelect";
1581 
1582 // Public methods
1583 
1584 /**
1585  * Sets the value.
1586  * 
1587  * @param	{Object}	value		the value
1588  * @private
1589  */
1590 ZmEditContactViewInputSelect.prototype.setValue = function(value) {
1591 	var hasOptions = this._options.length > 0;
1592 	var inputValue = hasOptions ? value && value.value : value;
1593 	if (hasOptions && this._select) {
1594 		this._select.setSelectedValue((value && value.type) || this._options[0].value);
1595 	}
1596 	if (this._input) {
1597 		if (this._select)
1598 			this._input.setEnabled(this._select.getValue() != "_NONE");
1599 		this._input.setValue(inputValue || "");
1600 	}
1601 };
1602 
1603 /**
1604  * Gets the value.
1605  * 
1606  * @return	{Object}		the value
1607  * @private
1608  */
1609 ZmEditContactViewInputSelect.prototype.getValue = function() {
1610 	var hasOptions = this._options.length > 0;
1611 	var inputValue = this._input ? this._input.getValue() : "";
1612 	return hasOptions ? {
1613 		type:  this._select ? this._select.getValue() : "",
1614 		value: inputValue
1615 	} : inputValue;
1616 };
1617 
1618 /**
1619  * Sets the dirty flag.
1620  * 
1621  * @param	{Boolean}	dirty		(not used)
1622  * @private
1623  */
1624 ZmEditContactViewInputSelect.prototype.setDirty = function(dirty) {
1625 	if (this.parent instanceof DwtForm) {
1626 		this.parent.setDirty(true);
1627 	}
1628 };
1629 
1630 /**
1631  * Checks if the two items are equal.
1632  * 
1633  * @param	{Object}	a		item a
1634  * @param	{Object}	b		item b
1635  * 
1636  * @private
1637  */
1638 ZmEditContactViewInputSelect.equals = function(a, b) {
1639 	if (a === b) return true;
1640 	if (!a || !b) return false;
1641 	var hasOptions = this._options.length > 0;
1642 	return hasOptions ? a.type == b.type && a.value == b.value : a == b;
1643 };
1644 
1645 // Hooks
1646 
1647 ZmEditContactViewInputSelect.prototype.enableOptions = function(enabled) {
1648 	if (!this._select || !this._select.enableOption) return;
1649 	var type = this.getValue().type;
1650 	for (var id in enabled) {
1651 		this._select.enableOption(id, id == type || enabled[id]);
1652 	}
1653 };
1654 
1655 // Protected methods
1656 
1657 ZmEditContactViewInputSelect.prototype._focus =
1658 function() {
1659 	this._input.focus();
1660 };
1661 
1662 ZmEditContactViewInputSelect.prototype._setControlIds = function(rowId, index) {
1663 	var id = this.getHTMLElId();
1664 	this._setControlId(this, id+"_value");
1665 	this._setControlId(this._input, id);
1666 	this._setControlId(this._select, id+"_select");
1667 };
1668 
1669 ZmEditContactViewInputSelect.prototype._setControlId = DwtFormRows.prototype._setControlId;
1670 
1671 ZmEditContactViewInputSelect.prototype._createHtml = function(templateId) {
1672 	var tabIndexes = this._tabIndexes = [];
1673 	this._createHtmlFromTemplate(templateId || this.TEMPLATE, {id:this._htmlElId});
1674 	tabIndexes.sort(DwtForm.__byTabIndex);
1675 	for (var i = 0; i < tabIndexes.length; i++) {
1676 		var control = tabIndexes[i].control;
1677 		this._tabGroup.addMember(control.getTabGroupMember() || control);
1678 	}
1679 };
1680 
1681 ZmEditContactViewInputSelect.prototype._createHtmlFromTemplate = function(templateId, data) {
1682 	DwtComposite.prototype._createHtmlFromTemplate.apply(this, arguments);
1683 
1684 	var tabIndexes = this._tabIndexes;
1685 	var inputEl = document.getElementById(data.id+"_input");
1686 	if (inputEl) {
1687 		this._input = this._createInput();
1688 		this._input.replaceElement(inputEl);
1689 		if (inputEl.getAttribute("notab") != "true") {
1690 			tabIndexes.push({
1691 				tabindex: inputEl.getAttribute("tabindex") || Number.MAX_VALUE,
1692 				control: this._input
1693 			});
1694 		}
1695 	}
1696 
1697 	var selectEl = document.getElementById(data.id+"_select");
1698 	var hasOptions = this._options.length > 0;
1699 	if (hasOptions && selectEl) {
1700 		this._select = this._createSelect(this._options);
1701 		this._select.addChangeListener(new AjxListener(this, this._handleSelectChange));
1702 		this._select.replaceElement(selectEl);
1703 		if (selectEl.getAttribute("notab") != "true") {
1704 			tabIndexes.push({
1705 				tabindex: selectEl.getAttribute("tabindex") || Number.MAX_VALUE,
1706 				control: this._select
1707 			});
1708 		}
1709 		this._select.setVisible(this._options.length > 1);
1710 		if (this._input)
1711 			this._input.setEnabled(this._select.getValue() != "_NONE");
1712 	}
1713 };
1714 
1715 ZmEditContactViewInputSelect.prototype._createInput = function() {
1716 	var input = new DwtInputField({parent:this,size:this._cols,rows:this._rows});
1717 	input.setHint(this._hint);
1718 	input.setHandler(DwtEvent.ONKEYDOWN, AjxCallback.simpleClosure(this._handleInputKeyDown, this, input));
1719 	input.setHandler(DwtEvent.ONKEYUP, AjxCallback.simpleClosure(this._handleInputKeyUp, this, input));
1720 	input.setHandler(DwtEvent.ONMOUSEDOWN, AjxCallback.simpleClosure(this._handleMouseDown, this, input));
1721 	if (AjxEnv.isIE  && !AjxEnv.isIE9 && !AjxEnv.isIE8) {
1722 		// Add a handler to account for IE's 'clear an input field' X control. IE10+
1723 		input.setHandler(DwtEvent.ONMOUSEUP,   this._handleMouseUp.bind(this, input));
1724 	}
1725 	input.setHandler(DwtEvent.ONPASTE, AjxCallback.simpleClosure(this._onPaste, this, input));
1726 	return input;
1727 };
1728 
1729 ZmEditContactViewInputSelect.prototype._createSelect = function(options) {
1730 	var id = [this.getHTMLElId(),"select"].join("_");
1731 	var select = new DwtSelect({parent:this,id:id});
1732 	for (var i = 0; i < options.length; i++) {
1733 		var option = options[i];
1734 		var maxedOut = this.parent.isMaxedOut(option.value);
1735 		select.addOption(option.label || option.value, i == 0 && !maxedOut, option.value);
1736 		if (maxedOut) {
1737 			select.enableOption(option.value, false);
1738 		}
1739 	}
1740 	return select;
1741 };
1742 
1743 ZmEditContactViewInputSelect.prototype.reRenderSelect = function() {
1744 	if (this._select && this._select.updateRendering)
1745 		this._select.updateRendering();
1746 };
1747 
1748 ZmEditContactViewInputSelect.prototype.reRenderInput = function() {
1749 	if (this._input) {
1750 		var value = this._input.getValue();
1751 		if (value && value != "") {
1752 			this._input.setValue(value+" ");
1753 			this._input.setValue(value);
1754 		}
1755 	}
1756 };
1757 
1758 ZmEditContactViewInputSelect.prototype._handleInputKeyDown = function(input, evt) {
1759 	var value = input.getValue();
1760 	input.setData("OLD_VALUE", value);
1761 	return true;
1762 };
1763 
1764 ZmEditContactViewInputSelect.prototype._handleInputKeyUp = function(input, evt) {
1765 	var ovalue = input.getData("OLD_VALUE");
1766 	var nvalue = input.getValue();
1767 	if (ovalue != null && ovalue != nvalue) {
1768 		this.setDirty(true);
1769 	}
1770 	return true;
1771 };
1772 
1773 ZmEditContactViewInputSelect.prototype._handleMouseDown =
1774 function(input, evt) {
1775 	var value = input.getValue();
1776 	input.setData("OLD_VALUE", value);
1777 	return true;
1778 };
1779 
1780 ZmEditContactViewInputSelect.prototype._handleMouseUp = function(input, evt) {
1781 	// Handle IE's 'clear the input field' X - Delay testing until its had a chance to
1782 	// clear the field
1783 	setTimeout(this._checkCleared.bind(this, input), 0);
1784 	return true;
1785 };
1786 ZmEditContactViewInputSelect.prototype._checkCleared = function(input) {
1787 	// Check for a change in input
1788 	var ovalue = input.getData("OLD_VALUE");
1789 	var nvalue = input.getValue();
1790 	if (ovalue != null && ovalue != nvalue) {
1791 		this.setDirty(true);
1792 	}
1793 };
1794 
1795 ZmEditContactViewInputSelect.prototype._onPaste =
1796 function(input, evt) {
1797 	var ovalue = input.getData("OLD_VALUE");
1798 	if (ovalue != null) {
1799 		AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._checkInput, [input]), 100);
1800 	}
1801 };
1802 
1803 ZmEditContactViewInputSelect.prototype._checkInput =
1804 function(input) {
1805 	var ovalue = input.getData("OLD_VALUE");
1806 	var nvalue = input.getValue();
1807 	if (ovalue != null && ovalue != nvalue) {
1808 		this.setDirty(true);
1809 	}
1810 	return true;
1811 };
1812 
1813 
1814 ZmEditContactViewInputSelect.prototype._handleSelectChange = function(evt, skipFocus) {
1815 	var args = evt._args;
1816 	var adjust1 = this.parent._subtractType(args.oldValue);
1817 	var adjust2 = this.parent._addType(args.newValue);
1818 	if (adjust1 || adjust2) {
1819 		this.parent._adjustMaximums();
1820 	}
1821 	this.setDirty(true);
1822 	if (this._input && this._select) {
1823 		var enabled = this._select.getValue() != "_NONE";
1824 		this._input.setEnabled(enabled);
1825 		if (enabled && !skipFocus)
1826 			this._input.focus();
1827 	}
1828 };
1829 
1830 // DwtControl methods
1831 
1832 ZmEditContactViewInputSelect.prototype.getTabGroupMember = function() {
1833 	return this._tabGroup;
1834 };
1835 
1836 /**
1837  * Creates the input select rows.
1838  * @class
1839  * This class represents the input double select rows for the contact view.
1840  * 
1841  * @param	{Hash}	params		a hash of parameters
1842  * 
1843  * @extends		ZmEditContactViewRows
1844  * 
1845  * @private
1846  */
1847 ZmEditContactViewInputDoubleSelectRows = function(params) {
1848 	if (arguments.length == 0) return;
1849 	ZmEditContactViewInputSelectRows.apply(this, arguments);
1850 
1851 	var rowitem = params.formItemDef.rowitem;
1852 	var rowparams = rowitem && rowitem.params;
1853 	var rowoptions2 = this._options2 = (rowparams && rowparams.options2) || [];
1854 	for (var i = 0; i < rowoptions2.length; i++) {
1855 		var option = rowoptions2[i];
1856 		if (option.max) {
1857 			if (!this._maximums2) this._maximums2 = {};
1858 			this._maximums2[option.value] = { max: option.max, count: 0 };
1859 		}
1860 	}
1861 
1862 };
1863 ZmEditContactViewInputDoubleSelectRows.prototype = new ZmEditContactViewInputSelectRows;
1864 ZmEditContactViewInputDoubleSelectRows.prototype.constructor = ZmEditContactViewInputDoubleSelectRows;
1865 
1866 /**
1867  * Returns a string representation of the object.
1868  * 
1869  * @return		{String}		a string representation of the object
1870  * @private
1871  */
1872 ZmEditContactViewInputDoubleSelectRows.prototype.toString = function() {
1873 	return "ZmEditContactViewInputDoubleSelectRows";
1874 };
1875 
1876 ZmEditContactViewInputDoubleSelectRows.prototype._subtract = function(indexOrId) {
1877 	var value = this.getValue(indexOrId);
1878 	var a = this._subtractType(value && value.type);
1879 	var b = this._subtractType2(value && value.type2);
1880 	return a && b;
1881 };
1882 ZmEditContactViewInputDoubleSelectRows.prototype._subtractType2 = function(type) {
1883 	if (!this._maximums2 || !this._maximums2[type]) return false;
1884 	this._maximums2[type].count--;
1885 	return true;
1886 };
1887 ZmEditContactViewInputDoubleSelectRows.prototype._add = function(indexOrId) {
1888 	var value = this.getValue(indexOrId);
1889 	var a = this._addType(value && value.type);
1890 	var b = this._addType2(value && value.type2);
1891 	return a || b;
1892 };
1893 ZmEditContactViewInputDoubleSelectRows.prototype._addType2 = function(type) {
1894 	if (!this._maximums2 || !this._maximums2[type]) return false;
1895 	this._maximums2[type].count++;
1896 	return true;
1897 };
1898 
1899 ZmEditContactViewInputDoubleSelectRows.prototype._adjustMaximums = function() {
1900 	ZmEditContactViewInputSelectRows.prototype._adjustMaximums.call(this);
1901 	if (!this._maximums2 || !this._options2) return;
1902 	// determine which ones are maxed out
1903 	var enabled = {};
1904 	var count = 0;
1905 	for (var i = 0; i < this._options2.length; i++) {
1906 		var type = this._options2[i].value;
1907 		var maxed = this.isMaxedOut2(type);
1908 		enabled[type] = !maxed;
1909 		count += maxed ? 1 : 0;
1910 	}
1911 	// are all of the options maxed out?
1912 	var allMaxed = count == this._options2.length;
1913 	// en/disable controls as needed
1914 	var rowCount = this.getRowCount();
1915 	for (var i = 0; i < rowCount; i++) {
1916 		var control = this.getControl(i);
1917 		if (control.enableOptions) {
1918 			control.enableOptions(enabled);
1919 		}
1920 		// TODO: Will this override the max rows add button visibility?
1921 		this.setVisible(this._items[i]._addId, !allMaxed);
1922 	}
1923 };
1924 
1925 // TODO: This is a hack to avoid bad counting error. Should
1926 // TODO: really find the cause of the error.
1927 ZmEditContactViewInputDoubleSelectRows.prototype._resetMaximums = function() {
1928 	ZmEditContactViewInputSelectRows.prototype._resetMaximums.call(this);
1929 	if (!this._maximums2) return;
1930 	for (var type in this._maximums2) {
1931 		this._maximums2[type].count = 0;
1932 	}
1933 	var rowCount = this.getRowCount();
1934 	for (var i = 0; i < rowCount; i++) {
1935 		var value = this.getValue(i);
1936 		var maximum = this._maximums2[value && value.type2];
1937 		if (maximum) {
1938 			maximum.count++;
1939 		}
1940 	}
1941 };
1942 
1943 ZmEditContactViewInputDoubleSelectRows.prototype.addRow = function(itemDef, index) {
1944 	DwtFormRows.prototype.addRow.apply(this, arguments);
1945 	index = index != null ? index : this.getRowCount() - 1;
1946 	var adjust = this._add(index);
1947 	if (adjust) this._adjustMaximums();
1948 	var value = this.getValue(index);
1949 	// select first one that is not maxed out
1950 
1951 	var typeChanged = false;
1952 	if (value && this.isMaxedOut(value.type) && this._options.length > 0 && 
1953 	    this._maximums[value.type].count > this._maximums[value.type].max) {
1954 		var options = this._options;
1955 		for (var i = 0; i < options.length; i++) {
1956 			var option = options[i];
1957 			if (!this.isMaxedOut(option.value)) {
1958 				value.type = option.value;
1959 				typeChanged = true;
1960 				break;
1961 			}
1962 		}
1963 	}
1964 
1965 	if (value && this.isMaxedOut2(value.type2) && this._options2.length > 0 && 
1966 	    this._maximums2[value.type2].count > this._maximums2[value.type2].max) {
1967 		var options = this._options2;
1968 		for (var i = 0; i < options.length; i++) {
1969 			var option = options[i];
1970 			if (!this.isMaxedOut2(option.value)) {
1971 				value.type2 = option.value;
1972 				typeChanged = true;
1973 				break;
1974 			}
1975 		}
1976 	}
1977 	if (typeChanged)
1978 		this.setValue(index, value);
1979 
1980 	if (this._rowCount >= this._maxRows) {
1981 		for (var i = 0; i < this._rowCount; i++) {
1982 			this.setVisible(this._items[i]._addId, false);
1983 		}
1984 	}
1985 };
1986 
1987 ZmEditContactViewInputDoubleSelectRows.prototype.isMaxedOut2 = function(type) {
1988 	var maximums = this._maximums2 && this._maximums2[type];
1989 	return maximums != null && maximums.count >= maximums.max;
1990 };
1991 
1992 /**
1993  * Checks if all rows are at maximum.
1994  * 
1995  * @return	{Boolean}	<code>true</code> if at maximum
1996  * @private
1997  */
1998 ZmEditContactViewInputDoubleSelectRows.prototype.isAllMaxedOut = function() {
1999 	if (ZmEditContactViewInputSelectRows.prototype.isAllMaxedOut.call(this)) return true;
2000 	if (!this._options2 || this._options2.length == 0) return false;
2001 	// determine which ones are maxed out
2002 	var count = 0;
2003 	for (var i = 0; i < this._options2.length; i++) {
2004 		var type = this._options2[i].value;
2005 		count += this.isMaxedOut2(type) ? 1 : 0;
2006 	}
2007 	// are all of the options maxed out?
2008 	return count >= this._options2.length;
2009 };
2010 
2011 //
2012 // Class: ZmEditContactViewInputDoubleSelect
2013 //
2014 
2015 /**
2016  * Creates the contact view input double select.
2017  * @class
2018  * This class represents an input with two selects.
2019  * 
2020  * @param	{Hash}	params		a hash of parameters
2021  * 
2022  * @extends		ZmEditContactViewInputSelect
2023  * 
2024  * @private
2025  */
2026 
2027 ZmEditContactViewInputDoubleSelect = function(params) {
2028 	if (arguments.length == 0) return;
2029 	this._options2 = params.options2 || [];
2030 	ZmEditContactViewInputSelect.apply(this, arguments);
2031 };
2032 ZmEditContactViewInputDoubleSelect.prototype = new ZmEditContactViewInputSelect;
2033 ZmEditContactViewInputDoubleSelect.prototype.constructor = ZmEditContactViewInputDoubleSelect;
2034 
2035 /**
2036  * Returns a string representation of the object.
2037  * 
2038  * @return		{String}		a string representation of the object
2039  * @private
2040  */
2041 ZmEditContactViewInputDoubleSelect.prototype.toString = function() {
2042 	return "ZmEditContactViewInputDoubleSelect";
2043 };
2044 
2045 // Data
2046 
2047 ZmEditContactViewInputDoubleSelect.prototype.TEMPLATE = "abook.Contacts#ZmEditContactViewInputDoubleSelect";
2048 
2049 // Public methods
2050 
2051 /**
2052  * Sets the value.
2053  * 
2054  * @param	{Object}	value		the value
2055  * @private
2056  */
2057 ZmEditContactViewInputDoubleSelect.prototype.setValue = function(value) {
2058 	var hasOptions = this._options.length > 0;
2059 	var hasOptions2 = this._options2.length > 0;
2060 	var inputValue = hasOptions || hasOptions2 ? value && value.value : value;
2061 	if (hasOptions && this._select) {
2062 		this._select.setSelectedValue((value && value.type) || this._options[0].value);
2063 	}
2064 	if (hasOptions2 && this._select2) {
2065 		this._select2.setSelectedValue((value && value.type2) || this._options2[0].value);
2066 	}
2067 	if (this._input) {
2068 		if (this._select || this._select2)
2069 			this._input.setEnabled((this._select && this._select.getValue() != "_NONE") && (this._select2 && this._select2.getValue() != "_NONE"));
2070 		this._input.setValue(inputValue || "");
2071 	}
2072 };
2073 
2074 /**
2075  * Gets the value.
2076  * 
2077  * @return	{Object}		the value
2078  * @private
2079  */
2080 ZmEditContactViewInputDoubleSelect.prototype.getValue = function() {
2081 	var hasOptions = this._options.length > 0;
2082 	var hasOptions2 = this._options2.length > 0;
2083 	var inputValue = this._input ? this._input.getValue() : "";
2084 	return hasOptions || hasOptions2 ? {
2085 		type: this._select ? this._select.getValue() : "",
2086 		type2: this._select2 ? this._select2.getValue() : "",
2087 		value: inputValue
2088 	} : inputValue;
2089 };
2090 
2091 /**
2092  * Checks if the two items are equal.
2093  * 
2094  * @param	{Object}	a		item a
2095  * @param	{Object}	b		item b
2096  * 
2097  * @private
2098  */
2099 ZmEditContactViewInputDoubleSelect.equals = function(a, b) {
2100 	if (a === b) return true;
2101 	if (!a || !b) return false;
2102 	var hasOptions = this._options.length > 0;
2103 	var hasOptions2 = this._options2.length > 0;
2104 	if (hasOptions) {
2105 		if (a.type != b.type || a.value != b.value)
2106 			return false;
2107 	}
2108 	if (hasOptions2) {
2109 		if (a.type2 != b.type2 || a.value != b.value)
2110 			return false;
2111 	}
2112 	if (!hasOptions && !hasOptions2)
2113 		if (a != b)
2114 			return false;
2115 	return true;
2116 };
2117 
2118 // Hooks
2119 
2120 ZmEditContactViewInputDoubleSelect.prototype.enableOptions = function(enabled, enabled2) {
2121 	if (this._select && this._select.enableOption) {
2122 		var type = this.getValue().type;
2123 		for (var id in enabled) {
2124 			this._select.enableOption(id, id == type || enabled[id]);
2125 		}
2126 	}
2127 	if (this._select2 && this._select2.enableOption) {
2128 		var type = this.getValue().type2;
2129 		for (var id in enabled2) {
2130 			this._select2.enableOption(id, id == type || enabled2[id]);
2131 		}
2132 	}
2133 };
2134 
2135 // Protected methods
2136 
2137 ZmEditContactViewInputDoubleSelect.prototype._setControlIds = function(rowId, index) {
2138 	var id = this.getHTMLElId();
2139 	this._setControlId(this, id+"_value");
2140 	this._setControlId(this._input, id);
2141 	this._setControlId(this._select, id+"_select");
2142 	this._setControlId(this._select2, id+"_select2");
2143 };
2144 
2145 ZmEditContactViewInputDoubleSelect.prototype._setControlId = DwtFormRows.prototype._setControlId;
2146 
2147 
2148 ZmEditContactViewInputDoubleSelect.prototype._createHtmlFromTemplate = function(templateId, data) {
2149 	DwtComposite.prototype._createHtmlFromTemplate.apply(this, arguments);
2150 
2151 	var tabIndexes = this._tabIndexes;
2152 	var inputEl = document.getElementById(data.id+"_input");
2153 	if (inputEl) {
2154 		this._input = this._createInput();
2155 		this._input.replaceElement(inputEl);
2156 		if (inputEl.getAttribute("notab") != "true") {
2157 			tabIndexes.push({
2158 				tabindex: inputEl.getAttribute("tabindex") || Number.MAX_VALUE,
2159 				control: this._input
2160 			});
2161 		}
2162 	}
2163 
2164 	var selectEl = document.getElementById(data.id+"_select");
2165 	var hasOptions = this._options.length > 0;
2166 	if (hasOptions && selectEl) {
2167 		this._select = this._createSelect(this._options);
2168 		this._select.addChangeListener(new AjxListener(this, this._handleSelectChange));
2169 		this._select.replaceElement(selectEl);
2170 		if (selectEl.getAttribute("notab") != "true") {
2171 			tabIndexes.push({
2172 				tabindex: selectEl.getAttribute("tabindex") || Number.MAX_VALUE,
2173 				control: this._select
2174 			});
2175 		}
2176 		this._select.setVisible(this._options.length > 1);
2177 	}
2178 
2179 	var selectEl2 = document.getElementById(data.id+"_select2");
2180 	var hasOptions2 = this._options2.length > 0;
2181 	if (hasOptions2 && selectEl2) {
2182 		this._select2 = this._createSelect2(this._options2);
2183 		this._select2.addChangeListener(new AjxListener(this, this._handleSelectChange2));
2184 		this._select2.replaceElement(selectEl2);
2185 		if (selectEl2.getAttribute("notab") != "true") {
2186 			tabIndexes.push({
2187 				tabindex: selectEl.getAttribute("tabindex") || Number.MAX_VALUE,
2188 				control: this._select2
2189 			});
2190 		}
2191 		this._select2.setVisible(this._options2.length > 1);
2192 	}
2193 
2194 	if (this._input) {
2195 		if (this._select || this._select2)
2196 			this._input.setEnabled((this._select && this._select.getValue() != "_NONE") && (this._select2 && this._select2.getValue() != "_NONE"));
2197 	}
2198 };
2199 
2200 ZmEditContactViewInputDoubleSelect.prototype._createSelect2 = function(options) {
2201 	var id = [this.getHTMLElId(),"select2"].join("_");
2202 	var select = new DwtSelect({parent:this,id:id});
2203 	for (var i = 0; i < options.length; i++) {
2204 		var option = options[i];
2205 		select.addOption(option.label || option.value, i == 0, option.value);
2206 	}
2207 	return select;
2208 };
2209 
2210 ZmEditContactViewInputDoubleSelect.prototype.reRenderSelect = function() {
2211 	ZmEditContactViewInputSelect.prototype.reRenderSelect.call(this);
2212 	this._select2.updateRendering();
2213 };
2214 
2215 ZmEditContactViewInputDoubleSelect.prototype._handleSelectChange = function(evt, skipFocus) {
2216 	var args = evt._args;
2217 	var adjust1 = this.parent._subtractType(args.oldValue);
2218 	var adjust2 = this.parent._addType(args.newValue);
2219 	if (adjust1 || adjust2) {
2220 		this.parent._adjustMaximums();
2221 	}
2222 	this.setDirty(true);
2223 	if (this._input) {
2224 		var enabled = this._select.getValue() != "_NONE" && this._select2.getValue() != "_NONE";
2225 		this._input.setEnabled(enabled);
2226 		if (enabled && !skipFocus)
2227 			this._input.focus();
2228 	}
2229 };
2230 
2231 ZmEditContactViewInputDoubleSelect.prototype._handleSelectChange2 = function(evt, skipFocus) {
2232 	var args = evt._args;
2233 	var adjust1 = this.parent._subtractType2(args.oldValue);
2234 	var adjust2 = this.parent._addType2(args.newValue);
2235 	if (adjust1 || adjust2) {
2236 		this.parent._adjustMaximums();
2237 	}
2238 	this.setDirty(true);
2239 	if (this._input) {
2240 		var enabled = this._select.getValue() != "_NONE" && this._select2.getValue() != "_NONE";
2241 		this._input.setEnabled(enabled);
2242 		if (enabled && !skipFocus)
2243 			this._input.focus();
2244 	}
2245 };
2246 
2247 //
2248 // Class: ZmEditContactViewOther
2249 //
2250 
2251 /**
2252  * Creates the contact view other.
2253  * @class
2254  * This class represents the contact view other field.
2255  * 
2256  * @param	{Hash}	params		a hash of parameters
2257  * 
2258  * @extends		ZmEditContactViewInputSelect
2259  * 
2260  * @private
2261  */
2262 ZmEditContactViewOther = function(params) {
2263 	if (arguments.length == 0) return;
2264 	ZmEditContactViewInputSelect.apply(this, arguments);
2265 	var option = params.options && params.options[0];
2266 	this.setValue({type:option && option.value});
2267     if (this._select && (params.selectInputWidth || params.selectInputHeight)) {
2268         Dwt.setSize(this._select.input, params.selectInputWidth, params.selectInputHeight);
2269     }
2270 };
2271 ZmEditContactViewOther.prototype = new ZmEditContactViewInputSelect;
2272 ZmEditContactViewOther.prototype.constructor = ZmEditContactViewOther;
2273 
2274 /**
2275  * Returns a string representation of the object.
2276  * 
2277  * @return		{String}		a string representation of the object
2278  * @private
2279  */
2280 ZmEditContactViewOther.prototype.toString = function() {
2281 	return "ZmEditContactViewOther";
2282 };
2283 
2284 // Data
2285 
2286 ZmEditContactViewOther.prototype.TEMPLATE = "abook.Contacts#ZmEditContactViewOther";
2287 
2288 ZmEditContactViewOther.prototype.DATE_ATTRS = { "birthday": true, "anniversary": true };
2289 
2290 ZmEditContactViewOther.validator = function(item) {
2291 	if (AjxUtil.isArray(item)) {
2292 		if (!item.length) return true;
2293 		var result = [];
2294 		for (var i=0; i<item.length; i++) {
2295 			var value = ZmEditContactViewOther.validator(item[i]);
2296 			if (value || value==="")
2297 				result.push({type: item[i].type, value: value});
2298 			else
2299 				return false;
2300 		}
2301 		return result;
2302 	} else {
2303 		if (item.type in ZmEditContactViewOther.prototype.DATE_ATTRS || item.type.replace(/^other/,"").toLowerCase() in ZmEditContactViewOther.prototype.DATE_ATTRS) {
2304 			var dateStr = AjxStringUtil.trim(item.value);
2305 			if (dateStr.length) {
2306                 var aDate = ZmEditContactViewOther.parseDate(dateStr);
2307 				if (isNaN(aDate) || aDate == null) {
2308 					throw ZmMsg.errorDate;
2309 				}
2310 				return ZmEditContactViewOther.formatDate(aDate);
2311 			}
2312 			return dateStr;
2313 		}
2314 		if (/\d+$/.test(item.type)) {
2315 			throw AjxMessageFormat.format(ZmMsg.errorInvalidContactOtherFieldName, item.type);
2316 		}
2317 		return item.value;
2318 	}
2319 };
2320 
2321 // Public methods
2322 
2323 /**
2324  * Sets the value.
2325  * 
2326  * @param	{Object}	value		the value
2327  * @private
2328  */
2329 ZmEditContactViewOther.prototype.setValue = function(value) {
2330 	ZmEditContactViewInputSelect.prototype.setValue.apply(this, arguments);
2331 	this._resetPicker();
2332 };
2333 
2334 /**
2335  * Gets the value.
2336  * 
2337  * @return	{Object}	the value
2338  * @private
2339  */
2340 ZmEditContactViewOther.prototype.getValue = function() {
2341 	return {
2342 		type: this._select.getValue() || this._select.getText(),
2343 		value: this._input.getValue()
2344 	};
2345 };
2346 
2347 // Protected methods
2348 
2349 ZmEditContactViewOther.prototype._setControlIds = function(rowId, index) {
2350 	var id = this.getHTMLElId();
2351 	ZmEditContactViewInputSelect.prototype._setControlIds.apply(this, arguments);
2352 	this._setControlId(this._picker, id+"_picker");
2353 };
2354 
2355 ZmEditContactViewOther.prototype._createHtmlFromTemplate = function(templateId, data) {
2356 	ZmEditContactViewInputSelect.prototype._createHtmlFromTemplate.apply(this, arguments);
2357 
2358 	var tabIndexes = this._tabIndexes;
2359 	var pickerEl = document.getElementById(data.id+"_picker");
2360 	if (pickerEl) {
2361 		var id = [this.getHTMLElId(),"picker"].join("_");
2362 		this._picker = new DwtButton({parent:this,id:id});
2363 		this._picker.setImage("CalendarApp", null, ZmMsg.chooseDate);
2364         this._picker.popup = ZmEditContactViewOther.__DwtButton_popup; // HACK
2365 
2366         var menu = new DwtMenu({parent:this._picker,style:DwtMenu.GENERIC_WIDGET_STYLE});
2367         this._picker.getHtmlElement().className += " ZmEditContactViewOtherCalendar";
2368 		this._picker.setMenu(menu);
2369 		this._picker.replaceElement(pickerEl);
2370 
2371         var listener = new AjxListener(this, this._handleDropDown);
2372         this._picker.addSelectionListener(listener);
2373         this._picker.addDropDownSelectionListener(listener);
2374 
2375         var container = new DwtComposite({parent:menu});
2376         // TODO: use template?
2377 
2378 		var calendar = new DwtCalendar({parent:container});
2379         calendar.setSkipNotifyOnPage(true);
2380 		calendar.setDate(new Date());
2381 		calendar.setFirstDayOfWeek(appCtxt.get(ZmSetting.CAL_FIRST_DAY_OF_WEEK) || 0);
2382 		calendar.addSelectionListener(new AjxListener(this,this._handleDateSelection,[calendar]));
2383 		tabIndexes.push({
2384 			tabindex: pickerEl.getAttribute("tabindex") || Number.MAX_VALUE,
2385 			control: this._picker
2386 		});
2387         this._calendar = calendar;
2388 
2389         var checkbox = new DwtCheckbox({parent:container});
2390         checkbox.setText(ZmMsg.includeYear);
2391 		checkbox.addSelectionListener(new AjxListener(this, this._handleDateSelection,[calendar]));
2392         this._calendarIncludeYear = checkbox;
2393 	}                                                        
2394 };
2395 
2396 // HACK: This function executes in the scope of the calendar picker
2397 // HACK: button. It avoids the calendar being resized and scrolled
2398 // HACK: when there's not enough room to display the menu below the
2399 // HACK: button.
2400 ZmEditContactViewOther.__DwtButton_popup = function() {
2401     var button = this;
2402     var size = button.getSize();
2403     var location = Dwt.toWindow(button.getHtmlElement(), 0, 0);
2404     var menu = button.getMenu();
2405     var menuSize = menu.getSize();
2406     var windowSize = DwtShell.getShell(window).getSize();
2407 	if ((location.y + size.y) + menuSize.y > windowSize.y) {
2408 		button._menuPopupStyle = DwtButton.MENU_POPUP_STYLE_ABOVE;
2409 	}
2410     if (AjxEnv.isIE) {
2411         menu.getHtmlElement().style.width = "150px";
2412     }
2413     DwtButton.prototype.popup.call(button, menu);
2414 };
2415 
2416 ZmEditContactViewOther.prototype._createSelect = function() {
2417 	var id = [this.getHTMLElId(),"select"].join("_");
2418 	var select = new DwtComboBox({parent:this,inputParams:{size:14},id:id});
2419 	var options = this._options || [];
2420 	for (var i = 0; i < options.length; i++) {
2421 		var option = options[i];
2422 		select.add(option.label || option.value, option.value, i == 0);
2423 	}
2424 	select.addChangeListener(new AjxListener(this, this._resetPicker));
2425 	// HACK: Make it look like a DwtSelect.
2426 	select.setSelectedValue = select.setValue;
2427 	return select;
2428 };
2429 
2430 ZmEditContactViewOther.prototype._resetPicker = function() {
2431 	if (this._picker) {
2432 		var type = this.getValue().type;
2433 		this._picker.setVisible(type in this.DATE_ATTRS);
2434 	}
2435 };
2436 
2437 ZmEditContactViewOther.parseDate = function(dateStr) {
2438     // NOTE: Still try to parse date string in locale-specific
2439     // NOTE: format for backwards compatibility.
2440     var noYear = dateStr.match(/^--/);
2441     var pattern = noYear ? "--MM-dd" : "yyyy-MM-dd";
2442     var aDate = AjxDateFormat.parse(pattern, dateStr);
2443 
2444     if (isNaN(aDate) || aDate == null) {
2445         aDate = AjxDateUtil.simpleParseDateStr(dateStr);
2446     }
2447     else if (noYear) {
2448         aDate.setFullYear(0);
2449     }
2450     return aDate;
2451 };
2452 
2453 ZmEditContactViewOther.formatDate = function(date) {
2454     var pattern = date.getFullYear() == 0 ? "--MM-dd" : "yyyy-MM-dd";
2455     return AjxDateFormat.format(pattern, date);
2456 };
2457 
2458 ZmEditContactViewOther._getDateFormatter = function() {
2459     if (!ZmEditContactViewOther._formatter) {
2460         ZmEditContactViewOther._formatter = new AjxDateFormat("yyyy-MM-dd");
2461     }
2462     return ZmEditContactViewOther._formatter;
2463 };
2464 
2465 ZmEditContactViewOther.prototype._handleDropDown = function(evt) {
2466     var value = this.getValue().value;
2467     var date = ZmEditContactViewOther.parseDate(value) || new Date();
2468     var includeYear = date.getFullYear() !== 0;
2469     // NOTE: Temporarilly set the year to the current year in the
2470     // NOTE: case of a date without a year set (i.e. full year == 0).
2471     // NOTE: This is done so that the calendar doesn't show the
2472     // NOTE: wrong year.
2473 	if (!includeYear) {
2474 		date.setFullYear(new Date().getFullYear());
2475 	}
2476 	this._calendarIncludeYear.setSelected(includeYear); //see bug 46952 and bug 83177
2477     this._calendar.setDate(date);
2478     this._picker.popup();
2479 };
2480 
2481 ZmEditContactViewOther.prototype._handleDateSelection = function(calendar) {
2482     this._picker.getMenu().popdown();
2483 
2484 	if (!calendar) calendar = this._calendar;
2485     var date = calendar.getDate();
2486     if (!this._calendarIncludeYear.isSelected()) {
2487         date = new Date(date.getTime());
2488         date.setFullYear(0);
2489     }
2490 
2491 	var value = this.getValue();
2492 	value.value = ZmEditContactViewOther.formatDate(date);
2493 	this.setValue(value);
2494 	this.parent.setDirty(true);
2495 };
2496 
2497 ZmEditContactViewOther.prototype._handleSelectChange = function(evt) {
2498     ZmEditContactViewInputSelect.prototype._handleSelectChange.call(this, evt, true);
2499 };
2500 
2501 //
2502 // Class: ZmEditContactViewIM
2503 //
2504 /**
2505  * Creates the contact view IM field.
2506  * @class
2507  * This class represents the contact view IM field.
2508  * 
2509  * @param	{Hash}	params		a hash of parameters
2510  * 
2511  * @extends		ZmEditContactViewInputSelect
2512  * 
2513  * @private
2514  */
2515 ZmEditContactViewIM = function(params) {
2516 	if (arguments.length == 0) return;
2517 	ZmEditContactViewInputSelect.apply(this, arguments);
2518 };
2519 ZmEditContactViewIM.prototype = new ZmEditContactViewInputSelect;
2520 ZmEditContactViewIM.prototype.constructor = ZmEditContactViewIM;
2521 
2522 /**
2523  * Returns a string representation of the object.
2524  * 
2525  * @return		{String}		a string representation of the object
2526  * @private
2527  */
2528 ZmEditContactViewIM.prototype.toString = function() {
2529 	return "ZmEditContactViewIM";
2530 };
2531 
2532 // constants
2533 
2534 ZmEditContactViewIM.RE_VALUE = /^(.*?):\/\/(.*)$/;
2535 
2536 // Public methods
2537 
2538 ZmEditContactViewIM.prototype.setValue = function(value) {
2539 	var m = ZmEditContactViewIM.RE_VALUE.exec(value);
2540 	value = m ? { type:m[1],value:m[2] } : { type:"xmpp",value:value };
2541 	ZmEditContactViewInputSelect.prototype.setValue.call(this, value);
2542 };
2543 ZmEditContactViewIM.prototype.getValue = function() {
2544 	var value = ZmEditContactViewInputSelect.prototype.getValue.call(this);
2545 	return value.value ? [value.type, value.value].join("://") : "";
2546 };
2547 
2548 //
2549 // Class: ZmEditContactViewIMDouble
2550 //
2551 /**
2552  * Creates the contact view IM field.
2553  * @class
2554  * This class represents the contact view IM field.
2555  * 
2556  * @param	{Hash}	params		a hash of parameters
2557  * 
2558  * @extends		ZmEditContactViewInputSelect
2559  * 
2560  * @private
2561  */
2562 ZmEditContactViewIMDouble = function(params) {
2563 	if (arguments.length == 0) return;
2564 	ZmEditContactViewInputDoubleSelect.apply(this, arguments);
2565 };
2566 ZmEditContactViewIMDouble.prototype = new ZmEditContactViewInputDoubleSelect;
2567 ZmEditContactViewIMDouble.prototype.constructor = ZmEditContactViewIMDouble;
2568 
2569 /**
2570  * Returns a string representation of the object.
2571  * 
2572  * @return		{String}		a string representation of the object
2573  * @private
2574  */
2575 ZmEditContactViewIMDouble.prototype.toString = function() {
2576 	return "ZmEditContactViewIMDouble";
2577 };
2578 
2579 // constants
2580 
2581 ZmEditContactViewIMDouble.RE_VALUE = /^(.*?):\/\/(.*)$/;
2582 
2583 // Public methods
2584 
2585 ZmEditContactViewIMDouble.prototype.setValue = function(value) {
2586 	var obj;
2587 	if (!value || value == "") {
2588 		obj = { type2:"_NONE", value:"", type: null };
2589 	} else {
2590 		var url = value.type ? value.value : value;
2591 		var m = ZmEditContactViewIMDouble.RE_VALUE.exec(url);
2592 		obj = m ? { type2:m[1], value:m[2], type: value.type?value.type:null } : { type2: value.type2 || "other", value: url, type: value.type ? value.type : null };
2593 	}
2594 	ZmEditContactViewInputDoubleSelect.prototype.setValue.call(this, obj);
2595 };
2596 
2597 ZmEditContactViewIMDouble.prototype.getValue = function() {
2598 	var value = ZmEditContactViewInputDoubleSelect.prototype.getValue.call(this);
2599 	var url = (value.type2=="_NONE" || value.value=="") ? "" : [value.type2, value.value].join("://");
2600 	var obj = value.type2 ? {
2601 		type: value.type,
2602 		type2: value.type2,
2603 		value: url
2604 	} : url;
2605 	return obj;
2606 };
2607 
2608 ZmEditContactViewIMDouble.equals = function(a,b) {
2609 	if (a === b) return true;
2610 	if (!a || !b) return false;
2611 	return a.type == b.type &&
2612            a.type2 == b.type2 &&
2613            a.value == b.value;
2614 };
2615 
2616 //
2617 // Class: ZmEditContactViewAddress
2618 //
2619 /**
2620  * Creates the contact view address input field.
2621  * @class
2622  * This class represents the address input field.
2623  * 
2624  * @param	{Hash}	params		a hash of parameters
2625  * 
2626  * @extends		ZmEditContactViewInputSelect
2627  * 
2628  * @private
2629  */
2630 ZmEditContactViewAddress = function(params) {
2631 	if (arguments.length == 0) return;
2632 	ZmEditContactViewInputSelect.call(this, params);
2633 };
2634 ZmEditContactViewAddress.prototype = new ZmEditContactViewInputSelect;
2635 ZmEditContactViewAddress.prototype.constructor = ZmEditContactViewAddress;
2636 
2637 /**
2638  * Returns a string representation of the object.
2639  * 
2640  * @return		{String}		a string representation of the object
2641  * @private
2642  */
2643 ZmEditContactViewAddress.prototype.toString = function() {
2644 	return "ZmEditContactViewAddress";  
2645 };
2646 
2647 // Data
2648 
2649 ZmEditContactViewAddress.prototype.TEMPLATE = "abook.Contacts#ZmEditContactViewAddressSelect";
2650 
2651 // Public methods
2652 
2653 ZmEditContactViewAddress.prototype.setValue = function(value) {
2654 	ZmEditContactViewInputSelect.prototype.setValue.apply(this, arguments);
2655 	value = value || {};
2656 	this._select.setSelectedValue(value.type);
2657 	this._input.setValue("STREET", value.Street);
2658 	this._input.setValue("CITY", value.City);
2659 	this._input.setValue("STATE", value.State);
2660 	this._input.setValue("ZIP", value.PostalCode);
2661 	this._input.setValue("COUNTRY", value.Country);
2662 	this._input.setDirty(false);
2663 	this._input.update();
2664 };
2665 
2666 ZmEditContactViewAddress.prototype.getValue = function() {
2667 	return {
2668 		type: this._select.getValue(),
2669 		Street: this._input.getValue("STREET"),
2670 		City: this._input.getValue("CITY"),
2671 		State: this._input.getValue("STATE"),
2672 		PostalCode: this._input.getValue("ZIP"),
2673 		Country: this._input.getValue("COUNTRY")
2674 	};
2675 };
2676 
2677 ZmEditContactViewAddress.equals = function(a,b) {
2678 	if (a === b) return true;
2679 	if (!a || !b) return false;
2680 	return a.type == b.type &&
2681            a.Street == b.Street && a.City == b.City && a.State == b.State &&
2682            a.PostalCode == b.PostalCode && a.Country == b.Country;
2683 };
2684 
2685 // Protected methods
2686 
2687 ZmEditContactViewAddress.prototype._setControlIds = function(rowId, index) {
2688 	var id = this.getHTMLElId();
2689 	ZmEditContactViewInputSelect.prototype._setControlIds.apply(this, arguments);
2690 	var fieldIds = ["STREET", "CITY", "STATE", "ZIP", "COUNTRY"];
2691 	for (var i = 0; i < fieldIds.length; i++) {
2692 		var fieldId = fieldIds[i];
2693 		var form = this._input.getControl(fieldId);
2694 		this._setControlId.call(form, form, [id,fieldId].join("_"));
2695 	}
2696 };
2697 
2698 ZmEditContactViewAddress.prototype._createInput = function() {
2699 	var form = {
2700 		template: "abook.Contacts#ZmEditContactViewAddress",
2701 		// NOTE: The parent is a ZmEditContactViewInputSelect which knows
2702 		// NOTE: its item ID and will set the dirty state on the main
2703 		// NOTE: form appropriately.
2704 		ondirty: "this.parent._handleDirty()",
2705 		items: [
2706 			{ id: "STREET", type: "DwtInputField", width: 343, rows: 2,
2707 				hint: ZmMsg.AB_FIELD_street, params: { forceMultiRow: true }
2708 			},
2709 			{ id: "CITY", type: "DwtInputField", width: 160, hint: ZmMsg.AB_FIELD_city },
2710 			{ id: "STATE", type: "DwtInputField", width: 90, hint: ZmMsg.AB_FIELD_state },
2711 			{ id: "ZIP", type: "DwtInputField", width: 80, hint: ZmMsg.AB_FIELD_postalCode },
2712 			{ id: "COUNTRY", type: "DwtInputField", width: 343, hint: ZmMsg.AB_FIELD_country }
2713 		]
2714 	};
2715 	return new DwtForm({parent:this,form:form});
2716 };
2717 
2718 ZmEditContactViewAddress.prototype._handleDirty = function() {
2719 	if (this._input && this._input.isDirty()) {
2720 		this.parent.setDirty(true);
2721 	}
2722 };
2723