1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the contact list controller class.
 27  */
 28 
 29 /**
 30  * Creates an empty contact list controller.
 31  * @class
 32  * This class manages list views of contacts. So far there are two different list
 33  * views, one that shows the contacts in a traditional list format, and the other
 34  * which shows them as business cards. Since there are two views, we need to keep
 35  * track of which is the current view.
 36  *
 37  * @author Roland Schemers
 38  * @author Conrad Damon
 39  * 
 40  * @param {DwtControl}					container					the containing shell
 41  * @param {ZmApp}						app							the containing application
 42  * @param {constant}					type						type of controller
 43  * @param {string}						sessionId					the session id
 44  * @param {ZmSearchResultsController}	searchResultsController		containing controller
 45  * 
 46  * @extends	ZmListController
 47  */
 48 ZmContactListController = function(container, contactsApp, type, sessionId, searchResultsController) {
 49 
 50 	if (arguments.length == 0) { return; }
 51 	ZmListController.apply(this, arguments);
 52 
 53 	this._viewFactory = {};
 54 	this._viewFactory[ZmId.VIEW_CONTACT_SIMPLE] = ZmContactSplitView;
 55 
 56 	if (this.supportsDnD()) {
 57 		this._dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE);
 58 		this._dragSrc.addDragListener(this._dragListener.bind(this));
 59 	}
 60 
 61 	this._listChangeListener = this._handleListChange.bind(this);
 62 
 63 	this._listeners[ZmOperation.EDIT]			= this._editListener.bind(this);
 64 	this._listeners[ZmOperation.PRINT]			= null; // override base class to do nothing
 65 	this._listeners[ZmOperation.PRINT_CONTACT]	= this._printListener.bind(this);
 66 	this._listeners[ZmOperation.PRINT_ADDRBOOK]	= this._printAddrBookListener.bind(this);
 67 	this._listeners[ZmOperation.NEW_GROUP]		= this._groupListener.bind(this);
 68 
 69 	this._parentView = {};
 70 };
 71 
 72 ZmContactListController.prototype = new ZmListController;
 73 ZmContactListController.prototype.constructor = ZmContactListController;
 74 
 75 ZmContactListController.prototype.isZmContactListController = true;
 76 ZmContactListController.prototype.toString = function() { return "ZmContactListController"; };
 77 
 78 ZmContactListController.ICON = {};
 79 ZmContactListController.ICON[ZmId.VIEW_CONTACT_SIMPLE]		= "ListView";
 80 
 81 ZmContactListController.MSG_KEY = {};
 82 ZmContactListController.MSG_KEY[ZmId.VIEW_CONTACT_SIMPLE]	= "contactList";
 83 
 84 ZmContactListController.SEARCH_TYPE_CANONICAL	= 1 << 0;
 85 ZmContactListController.SEARCH_TYPE_GAL			= 1 << 1;
 86 ZmContactListController.SEARCH_TYPE_NEW			= 1 << 2;
 87 ZmContactListController.SEARCH_TYPE_ANYWHERE	= 1 << 3;
 88 
 89 ZmContactListController.VIEWS = [ZmId.VIEW_CONTACT_SIMPLE];
 90 
 91 // Public methods
 92 
 93 /**
 94  * Shows the search results.
 95  * 
 96  * @param	{Object}	searchResult		the search results
 97  * @param	{Boolean}	isGalSearch		<code>true</code> if results from GAL search
 98  * @param	{String}	folderId		the folder id
 99  */
100 ZmContactListController.prototype.show =
101 function(searchResult, isGalSearch, folderId) {
102 
103 	this._searchType = isGalSearch
104 		? ZmContactListController.SEARCH_TYPE_GAL
105 		: ZmContactListController.SEARCH_TYPE_CANONICAL;
106 
107 	this._folderId = folderId;
108 	var selectedContacts;
109 	
110 	if (searchResult.isZmContactList) {
111 		this.setList(searchResult);			// set as canonical list of contacts
112 		this._list._isShared = false;		// this list is not a search of shared items
113 		selectedContacts = this._listView[this._currentViewId] && this._listView[this._currentViewId].getSelection();
114 		this._contactSearchResults = false;
115     }
116 	else if (searchResult.isZmSearchResult) {
117 		this._searchType |= ZmContactListController.SEARCH_TYPE_NEW;
118 		this.setList(searchResult.getResults(ZmItem.CONTACT));
119 
120 		// HACK - find out if user did a "is:anywhere" search (for printing)
121 		if (searchResult.search && searchResult.search.isAnywhere()) {
122 			this._searchType |= ZmContactListController.SEARCH_TYPE_ANYWHERE;
123 		}
124 
125 		if (searchResult.search && searchResult.search.userText && this.getCurrentView()) {
126 			this.getCurrentView().getAlphabetBar().reset();
127 		}
128 
129 		if (isGalSearch) {
130 			this._list = this._list || new ZmContactList(searchResult.search, true);
131 			this._list._isShared = false;
132 			this._list.isGalPagingSupported = AjxUtil.isSpecified(searchResult.getAttribute("offset"));
133 		} else {
134 			// find out if we just searched for a shared address book
135 			var addrbook = folderId ? appCtxt.getById(folderId) : null;
136 			this._list._isShared = addrbook ? addrbook.link : false;
137 		}
138 
139 		this._list.setHasMore(searchResult.getAttribute("more"));
140 
141 		selectedContacts = this._listView[this._currentViewId] && this._listView[this._currentViewId].getSelection();
142 		ZmListController.prototype.show.apply(this, [searchResult, this._currentViewId]);
143 		this._contactSearchResults = true;
144 	}
145 
146 	// reset offset if list view has been created
147 	var view = this._currentViewId;
148 	if (this._listView[view]) {
149 		this._listView[view].offset = 0;
150 	}
151 	this.switchView(view, true);
152 
153 	if (selectedContacts && selectedContacts.length && this._listView[view]) {
154 		this._listView[view].setSelection(selectedContacts[0]);
155 	}
156 };
157 
158 
159 
160 /**
161  * Change how contacts are displayed. There are two views: the "simple" view
162  * shows a list of contacts on the left and the selected contact on the right;
163  * the "cards" view shows contacts as business cards.
164  * 
165  * @param {constant}	view			the view to show
166  * @param {Boolean}	force			if <code>true</code>, render view even if it's the current view
167  * @param {Boolean}	initialized		if <code>true</code>, app has been initialized
168  * @param {Boolean}	stageView		if <code>true</code>, stage the view but don't push it
169  */
170 ZmContactListController.prototype.switchView =
171 function(view, force, initialized, stageView) {
172 	if (view && ((view != this._currentViewId) || force)) {
173 		this._currentViewId = view;
174 		DBG.timePt("setting up view", true);
175 		this._setup(view);
176 		DBG.timePt("done setting up view");
177 
178 		var elements = this.getViewElements(view, this._parentView[view]);
179 
180 		// call initialize before _setView since we havent set the new view yet
181 		if (!initialized) {
182 			this._initializeAlphabetBar(view);
183 		}
184 
185 		this._setView({ view:		view,
186 						viewType:	this._currentViewType,
187 						noPush:		this.isSearchResults,
188 						elements:	elements,
189 						isAppView:	true,
190 						stageView:	stageView});
191 		if (this.isSearchResults) {
192 			// if we are switching views, make sure app view mgr is up to date on search view's components
193 			appCtxt.getAppViewMgr().setViewComponents(this.searchResultsController.getCurrentViewId(), elements, true);
194 		}
195 		this._resetNavToolBarButtons();
196 
197 		// HACK: reset search toolbar icon (its a hack we're willing to live with)
198 		if (this.isGalSearch() && !this._list.isGalPagingSupported) {
199 			appCtxt.getSearchController().setDefaultSearchType(ZmId.SEARCH_GAL);
200 			if (this._list.hasMore()) {
201 				var d = appCtxt.getMsgDialog();
202 				d.setMessage(ZmMsg.errorSearchNotExpanded);
203 				d.popup();
204 			}
205 		}
206 
207 		this._setTabGroup(this._tabGroups[view]);
208 
209 		if (!initialized) {
210 			var list = this._listView[view].getList();
211 			if (list) {
212 				this._listView[view].setSelection(list.get(0));
213 			}
214 		}
215 	}
216 };
217 
218 /**
219  * Gets the folder id.
220  * 
221  * @return	{String}	the folder id
222  */
223 ZmContactListController.prototype.getFolderId =
224 function() {
225 	return this._folderId;
226 };
227 
228 /**
229  * Checks if the search is a GAL search.
230  * 
231  * @return	{Boolean}	<code>true</code> if GAL search
232  */
233 ZmContactListController.prototype.isGalSearch =
234 function() {
235 	return ((this._searchType & ZmContactListController.SEARCH_TYPE_GAL) != 0);
236 };
237 
238 /**
239  * Returns the split view.
240  * 
241  * @return	{ZmContactSplitView}	the split view
242  */
243 ZmContactListController.prototype.getCurrentView =
244 function() {
245 	return this._parentView[this._currentViewId];
246 };
247 ZmContactListController.prototype.getParentView = ZmContactListController.prototype.getCurrentView;
248 
249 /**
250  * Search the alphabet.
251  * 
252  * @param	{String}	letter		the letter
253  * @param	{String}	endLetter	the end letter
254  */
255 ZmContactListController.prototype.searchAlphabet =
256 function(letter, endLetter) {
257 	var folderId = this._folderId || ZmFolder.ID_CONTACTS;
258 	var folder = appCtxt.getById(folderId);
259 	var query = folder ? folder.createQuery() : null;
260 
261 	if (query) {
262 		var params = {
263 			query: query,
264 			types: [ZmItem.CONTACT],
265 			offset: 0,
266 			limit: (this._listView[this._currentViewId].getLimit()),
267 			lastId: 0,
268 			lastSortVal: letter,
269 			endSortVal: endLetter
270 		};
271 		appCtxt.getSearchController().search(params);
272 	}
273 };
274 
275 /**
276  * @private
277  */
278 ZmContactListController.prototype._getMoreSearchParams =
279 function(params) {
280 	params.endSortVal = this._activeSearch && this._activeSearch.search && this._activeSearch.search.endSortVal; 
281 };
282 
283 ZmContactListController.prototype.getKeyMapName =
284 function() {
285 	return ZmKeyMap.MAP_CONTACTS;
286 };
287 
288 ZmContactListController.prototype.handleKeyAction =
289 function(actionCode) {
290 	DBG.println(AjxDebug.DBG3, "ZmContactListController.handleKeyAction");
291     var isExternalAccount = appCtxt.isExternalAccount();
292 	var isWebClientOffline = appCtxt.isWebClientOffline();
293 	switch (actionCode) {
294 
295 		case ZmKeyMap.EDIT:
296             if (isExternalAccount || isWebClientOffline) { break; }
297 			this._editListener();
298 			break;
299 
300 		case ZmKeyMap.PRINT:
301 			if (appCtxt.get(ZmSetting.PRINT_ENABLED) && !isWebClientOffline) {
302 				this._printListener();
303 			}
304 			break;
305 
306 		case ZmKeyMap.PRINT_ALL:
307 			if (appCtxt.get(ZmSetting.PRINT_ENABLED) && !isWebClientOffline) {
308 				this._printAddrBookListener();
309 			}
310 			break;
311 
312 		case ZmKeyMap.NEW_MESSAGE:
313 			if (isExternalAccount) { break; }
314 			this._composeListener();
315 			break;
316 
317 		default:
318 			return ZmListController.prototype.handleKeyAction.call(this, actionCode);
319 	}
320 	return true;
321 };
322 
323 /**
324  * @private
325  */
326 ZmContactListController.prototype.mapSupported =
327 function(map) {
328 	return (map == "list");
329 };
330 
331 
332 // Private and protected methods
333 
334 
335 /**
336  * @private
337  */
338 ZmContactListController.prototype._getToolBarOps =
339 function() {
340     var toolbarOps =  [];
341     toolbarOps.push(ZmOperation.EDIT,
342             ZmOperation.SEP,
343             ZmOperation.DELETE, ZmOperation.SEP,
344 			ZmOperation.MOVE_MENU, ZmOperation.TAG_MENU, ZmOperation.SEP,
345 			ZmOperation.PRINT);
346     return toolbarOps;
347 };
348 
349 /**
350  * @private
351  */
352 ZmContactListController.prototype._getSecondaryToolBarOps =
353 function() {
354     if (appCtxt.isExternalAccount()) { return []; }
355 	var list = [ZmOperation.SEARCH_MENU];
356 
357 	if (appCtxt.get(ZmSetting.MAIL_ENABLED)) {
358 		list.push(ZmOperation.NEW_MESSAGE);
359 	}
360 
361 	list.push(ZmOperation.SEP, ZmOperation.CONTACTGROUP_MENU);
362 //    list.push(ZmOperation.QUICK_COMMANDS);
363 
364 	return list;
365 };
366 
367 /**
368  * @private
369  */
370 ZmContactListController.prototype._getActionMenuOps =
371 function() {
372 	var list = this._participantOps();
373 	list.push(ZmOperation.SEP,
374 				ZmOperation.CONTACTGROUP_MENU,
375 				ZmOperation.TAG_MENU,
376 				ZmOperation.DELETE,
377 				ZmOperation.MOVE,
378 				ZmOperation.PRINT_CONTACT);
379 //    list.push(ZmOperation.QUICK_COMMANDS);
380 
381 	return list;
382 };
383 
384 ZmContactListController.getDefaultViewType =
385 function() {
386 	return ZmId.VIEW_CONTACT_SIMPLE;
387 };
388 ZmContactListController.prototype.getDefaultViewType = ZmContactListController.getDefaultViewType;
389 
390 /**
391  * @private
392  */
393 ZmContactListController.prototype._createNewView =
394 function(view) {
395 	var params = {parent:this._container, posStyle:Dwt.ABSOLUTE_STYLE,
396 				  controller:this, dropTgt:this._dropTgt};
397 	var viewType = this.getCurrentViewType();
398 	this._parentView[view] = new this._viewFactory[viewType](params);
399 	var listView = this._parentView[view].getListView();
400 	if (this._dragSrc) {
401 		listView.setDragSource(this._dragSrc);
402 	}
403 
404 	return listView;
405 };
406 
407 /**
408  * @private
409  */
410 ZmContactListController.prototype._getTagMenuMsg =
411 function(num) {
412 	return AjxMessageFormat.format(ZmMsg.AB_TAG_CONTACTS, num);
413 };
414 
415 /**
416  * @private
417  */
418 ZmContactListController.prototype._getMoveDialogTitle =
419 function(num) {
420 	return AjxMessageFormat.format(ZmMsg.AB_MOVE_CONTACTS, num);
421 };
422 
423 /**
424  * @private
425  */
426 ZmContactListController.prototype._getMoveParams =
427 function(dlg) {
428 	var params = ZmListController.prototype._getMoveParams.apply(this, arguments);
429     params.hideNewButton = !appCtxt.get(ZmSetting.NEW_ADDR_BOOK_ENABLED);
430     var omit = {};
431 	var folderTree = appCtxt.getFolderTree();
432 	if (!folderTree) { return params; }
433 	var folders = folderTree.getByType(ZmOrganizer.ADDRBOOK);
434 	for (var i = 0; i < folders.length; i++) {
435 		var folder = folders[i];
436 		if (folder.link && folder.isReadOnly()) {
437 			omit[folder.id] = true;
438 		}
439 	}
440 	params.omit = omit;
441 	params.description = ZmMsg.targetAddressBook;
442 
443 	return params;
444 };
445 
446 /**
447  * @private
448  */
449 ZmContactListController.prototype._getSearchFolderId = 
450 function() {
451 	return this._folderId;
452 };
453 
454 /**
455  * @private
456  */
457 ZmContactListController.prototype._initializeToolBar =
458 function(view) {
459 	if (!this._toolbar[view]) {
460 		ZmListController.prototype._initializeToolBar.call(this, view);
461 		var tb = this._toolbar[view];
462 //		this._setupViewMenu(view, true);
463 		this._setupPrintMenu(view);
464 		tb.addFiller();
465 		this._initializeNavToolBar(view);
466 		this._setupContactGroupMenu(tb);
467 		appCtxt.notifyZimlets("initializeToolbar", [this._app, this._toolbar[view], this, view], {waitUntilLoaded:true});
468 	} else {
469 //		this._setupViewMenu(view, false);
470 		this._setupDeleteButton(this._toolbar[view]);
471 	}
472 };
473 
474 ZmContactListController.prototype._initializeTabGroup =
475 function(view) {
476 	if (this._tabGroups[view]) { return; }
477 
478 	ZmListController.prototype._initializeTabGroup.call(this, view);
479 
480 	var tg = this._tabGroups[view];
481 
482 	tg.addMemberBefore(this._parentView[view].getAlphabetBar(),
483 	                   this._view[view].getTabGroupMember());
484 
485 	tg.addMember(this._parentView[view].getTabGroupMember());
486 }
487 
488 // If we're in the Trash folder, change the "Delete" button tooltip
489 ZmContactListController.prototype._setupDeleteButton = function(parent) {
490 	var folder = this._getSearchFolder();
491 	var inTrashFolder = (folder && folder.nId == ZmFolder.ID_TRASH);
492 	var tooltip = inTrashFolder ? ZmMsg.deletePermanentTooltip : ZmMsg.deleteTooltip;
493 	var deleteButton = parent.getButton(ZmOperation.DELETE);
494 	if(deleteButton){
495 		deleteButton.setToolTipContent(ZmOperation.getToolTip(ZmOperation.DELETE, this.getKeyMapName(), tooltip));
496 	}
497 };
498 
499 
500 /**
501  * @private
502  */
503 ZmContactListController.prototype._initializeNavToolBar =
504 function(view) {
505 	this._toolbar[view].addOp(ZmOperation.TEXT);
506 	var text = this._itemCountText[view] = this._toolbar[view].getButton(ZmOperation.TEXT);
507 	text.addClassName("itemCountText");
508 };
509 
510 /**
511  * @private
512  */
513 ZmContactListController.prototype._initializeActionMenu =
514 function(view) {
515 	ZmListController.prototype._initializeActionMenu.call(this);
516 
517 	var mi = this._actionMenu.getItemById(ZmOperation.KEY_ID, ZmOperation.PRINT_CONTACT);
518 	if (mi) {
519 		mi.setText(ZmMsg.print);
520 	}
521 
522 	ZmOperation.setOperation(this._actionMenu, ZmOperation.CONTACT, ZmOperation.EDIT_CONTACT);
523 	this._setupContactGroupMenu(this._actionMenu);
524 
525 };
526 
527 ZmContactListController.prototype.getSearchFromText =
528 function() {
529 	return ZmMsg.findEmailFromContact;
530 };
531 
532 ZmContactListController.prototype.getSearchToText =
533 function() {
534 	return ZmMsg.findEmailToContact;
535 };
536 
537 /**
538  * @private
539  */
540 ZmContactListController.prototype._initializeAlphabetBar =
541 function(view) {
542 	if (view == this._currentViewId) { return; }
543 
544 	var pv = this._parentView[this._currentViewId];
545 	var alphaBar = pv ? pv.getAlphabetBar() : null;
546 	var current = alphaBar ? alphaBar.getCurrent() : null;
547 	var idx = current ? current.getAttribute("_idx") : null;
548 	if (idx) {
549 		var newAlphaBar = this._parentView[view].getAlphabetBar();
550 		if (newAlphaBar)
551 			newAlphaBar.setButtonByIndex(idx);
552 	}
553 };
554 
555 /**
556  * Load contacts into the given view and perform layout.
557  * 
558  * @private
559  */
560 ZmContactListController.prototype._setViewContents =
561 function(view) {
562 	DBG.timePt("setting list");
563 	this._list.removeChangeListener(this._listChangeListener);
564 	this._list.addChangeListener(this._listChangeListener);
565 	this._listView[view].set(this._list, null, this._folderId, this.isSearchResults);
566 	DBG.timePt("done setting list");
567 };
568 
569 ZmContactListController.prototype._handleSyncAll =
570 function() {
571 	//doesn't do anything now after I removed the appCtxt.get(ZmSetting.GET_MAIL_ACTION) == ZmSetting.GETMAIL_ACTION_DEFAULT preference stuff
572 };
573 
574 ZmContactListController.prototype._syncAllListener =
575 function(view) {
576     var callback = new AjxCallback(this, this._handleSyncAll);
577     appCtxt.accountList.syncAll(callback);
578 };
579 
580 ZmContactListController.prototype.runRefresh =
581 function() {
582 	
583 	if (!appCtxt.isOffline) {
584 		return;
585 	}
586 	//should only happen in ZD
587 
588 	this._syncAllListener();
589 };
590 
591 
592 ZmContactListController.prototype._sendReceiveListener =
593 function(ev) {
594     var account = appCtxt.accountList.getAccount(ev.item.getData(ZmOperation.MENUITEM_ID));
595     if (account) {
596         account.sync();
597     }
598 };
599 
600 ZmContactListController.prototype._handleListChange =
601 function(ev) {
602 	if (ev.event == ZmEvent.E_MODIFY || ev.event == ZmEvent.E_CREATE) {
603 		if (!ev.getDetail("visible")) {
604 			return;
605 		}
606 		var items = ev.getDetail("items");
607 		var item = items && items.length && items[0];
608 		if (item instanceof ZmContact && this._currentViewType == ZmId.VIEW_CONTACT_SIMPLE && item.folderId == this._folderId) {
609 			var alphaBar = this._parentView[this._currentViewId].getAlphabetBar();
610 			//only set the view if the contact is in the list
611 			if(!alphaBar || alphaBar.isItemInAlphabetLetter(item)) {
612 				this._parentView[this._currentViewId].setContact(item, this.isGalSearch());
613 			}
614 		}
615 	}
616 };
617 
618 /**
619  * Create menu for View button and add listeners.
620  * 
621  * @private
622  */
623 ZmContactListController.prototype._setupViewMenu =
624 function(view, firstTime) {
625 	var btn;
626 
627 	if (firstTime) {
628 		btn = this._toolbar[view].getButton(ZmOperation.VIEW_MENU);
629 		var menu = btn.getMenu();
630 		if (!menu) {
631 			menu = new ZmPopupMenu(btn);
632 			btn.setMenu(menu);
633 			for (var i = 0; i < ZmContactListController.VIEWS.length; i++) {
634 				var id = ZmContactListController.VIEWS[i];
635 				var mi = menu.createMenuItem(id, {image:ZmContactListController.ICON[id],
636 													text:ZmMsg[ZmContactListController.MSG_KEY[id]],
637 													style:DwtMenuItem.RADIO_STYLE});
638 				mi.setData(ZmOperation.MENUITEM_ID, id);
639 				mi.addSelectionListener(this._listeners[ZmOperation.VIEW]);
640 				if (id == view)
641 					mi.setChecked(true, true);
642 			}
643 		}
644 	} else {
645 		// always set the switched view to be the checked menu item
646 		btn = this._toolbar[view].getButton(ZmOperation.VIEW_MENU);
647 		var menu = btn ? btn.getMenu() : null;
648 		var mi = menu ? menu.getItemById(ZmOperation.MENUITEM_ID, view) : null;
649 		if (mi) { mi.setChecked(true, true); }
650 	}
651 
652 	// always reset the view menu button icon to reflect the current view
653 	btn.setImage(ZmContactListController.ICON[view]);
654 };
655 
656 /**
657  * @private
658  */
659 ZmContactListController.prototype._setupPrintMenu =
660 function(view) {
661 	var printButton = this._toolbar[view].getButton(ZmOperation.PRINT);
662 	if (!printButton) { return; }
663 
664 	printButton.setToolTipContent(ZmMsg.printMultiTooltip);
665 	printButton.noMenuBar = true;
666 	var menu = new ZmPopupMenu(printButton);
667 	printButton.setMenu(menu);
668 
669 	var id = ZmOperation.PRINT_CONTACT;
670 	var mi = menu.createMenuItem(id, {image:ZmOperation.getProp(id, "image"), text:ZmMsg[ZmOperation.getProp(id, "textKey")]});
671 	mi.setData(ZmOperation.MENUITEM_ID, id);
672 	mi.addSelectionListener(this._listeners[ZmOperation.PRINT_CONTACT]);
673 
674 	id = ZmOperation.PRINT_ADDRBOOK;
675 	mi = menu.createMenuItem(id, {image:ZmOperation.getProp(id, "image"), text:ZmMsg[ZmOperation.getProp(id, "textKey")]});
676 	mi.setData(ZmOperation.MENUITEM_ID, id);
677 	mi.addSelectionListener(this._listeners[ZmOperation.PRINT_ADDRBOOK]);
678 };
679 
680 /**
681  * Resets the available options on a toolbar or action menu.
682  * 
683  * @private
684  */
685 ZmContactListController.prototype._resetOperations =
686 function(parent, num) {
687 
688 	ZmBaseController.prototype._resetOperations.call(this, parent, num);
689 
690 	var printMenuItem;
691 	if (parent instanceof ZmButtonToolBar) {
692 		var printButton = parent.getButton(ZmOperation.PRINT);
693 		var printMenu = printButton && printButton.getMenu();
694 		if (printMenu) {
695 			printMenuItem = printMenu.getItem(1);
696 			printMenuItem.setText(ZmMsg.printResults);
697 		}
698 	}
699 
700 	this._setContactGroupMenu(parent);
701 
702 	var printOp = (parent instanceof ZmActionMenu) ? ZmOperation.PRINT_CONTACT : ZmOperation.PRINT;
703 
704 
705 	var isDl = this._folderId  == ZmFolder.ID_DLS ||
706 			num == 1 && this._listView[this._currentViewId].getSelection()[0].isDistributionList();
707 
708 	parent.enable(printOp, !isDl);
709 
710 	parent.enable(ZmOperation.NEW_MESSAGE, num > 0 && !appCtxt.isExternalAccount());
711 
712 	var folder = this._folderId && appCtxt.getById(this._folderId);
713 
714 	parent.enable([ZmOperation.CONTACTGROUP_MENU], num > 0 && !isDl && !appCtxt.isExternalAccount());
715 	var contactGroupMenu = this._getContactGroupMenu(parent);
716 	if (contactGroupMenu) {
717 		contactGroupMenu.setNewDisabled(folder && folder.isReadOnly());
718 	}
719 	appCtxt.notifyZimlets("resetContactListToolbarOperations",[parent, num]);
720 	if (!this.isGalSearch()) {
721 		parent.enable([ZmOperation.SEARCH_MENU, ZmOperation.BROWSE, ZmOperation.NEW_MENU, ZmOperation.VIEW_MENU], true);
722 
723 		// a valid folderId means user clicked on an addrbook
724 		if (folder) {
725 			var isShare = folder.link;
726 			var isInTrash = folder.isInTrash();
727 			var canEdit = !folder.isReadOnly();
728 
729 			parent.enable([ZmOperation.TAG_MENU], canEdit && num > 0);
730 			parent.enable([ZmOperation.DELETE, ZmOperation.MOVE, ZmOperation.MOVE_MENU], canEdit && num > 0);
731 			parent.enable([ZmOperation.EDIT, ZmOperation.CONTACT], canEdit && num == 1 && !isInTrash);
732 
733 
734 			if (printMenuItem) {
735 				var text = isShare ? ZmMsg.printResults : ZmMsg.printAddrBook;
736 				printMenuItem.setText(text);
737 			}
738 		} else {
739 			// otherwise, must be a search
740 			var contact = this._listView[this._currentViewId].getSelection()[0];
741 			var canEdit = (num == 1 && !contact.isReadOnly() && !ZmContact.isInTrash(contact));
742 			parent.enable([ZmOperation.DELETE, ZmOperation.MOVE, ZmOperation.MOVE_MENU, ZmOperation.TAG_MENU], num > 0);
743 			parent.enable([ZmOperation.EDIT, ZmOperation.CONTACT], canEdit);
744 		}
745 	} else {
746 		// gal contacts cannot be tagged/moved/deleted
747 		parent.enable([ZmOperation.PRINT, ZmOperation.PRINT_CONTACT, ZmOperation.MOVE, ZmOperation.MOVE_MENU, ZmOperation.TAG_MENU], false);
748 		parent.enable([ZmOperation.SEARCH_MENU, ZmOperation.BROWSE, ZmOperation.NEW_MENU, ZmOperation.VIEW_MENU], true);
749 		parent.enable(ZmOperation.CONTACT, num == 1);
750 		var selection = this._listView[this._currentViewId].getSelection();
751 		var canEdit = false;
752 		if (num == 1) {
753 			var contact = selection[0];
754 			var isDL = contact && contact.isDistributionList();
755 			canEdit = isDL && contact.dlInfo && contact.dlInfo.isOwner;
756 		}
757 		parent.enable([ZmOperation.EDIT], canEdit);  //not sure what is this ZmOperation.CONTACT exactly, but it's used as the "edit group" below. 
758 		parent.enable([ZmOperation.CONTACT], isDL ? canEdit : num == 1);
759 		var canDelete = ZmContactList.deleteGalItemsAllowed(selection);
760 
761 		parent.enable([ZmOperation.DELETE], canDelete);
762 	}
763 
764     //this._resetQuickCommandOperations(parent);
765 
766 	var selection = this._listView[this._currentViewId].getSelection();
767 	var contact = (selection.length == 1) ? selection[0] : null;
768 
769 	var searchEnabled = num === 1 && !appCtxt.isExternalAccount() && !contact.isGroup() && contact.getEmail(); //contact.getEmail() comes from bug 72446. I had to refactor but want to keep reference to bug in this comment
770 	parent.enable([ZmOperation.SEARCH_MENU, ZmOperation.BROWSE], searchEnabled);
771 
772 	if (parent instanceof ZmPopupMenu) {
773 		this._setContactText(contact);
774 
775 		var tagMenu = parent.getMenuItem(ZmOperation.TAG_MENU);
776 		if (tagMenu) {
777 			tagMenu.setText(contact && contact.isGroup() ? ZmMsg.AB_TAG_GROUP : ZmMsg.AB_TAG_CONTACT);
778 		}
779 	}
780 
781     if (appCtxt.isExternalAccount()) {
782         parent.enable(
783                         [
784                             ZmOperation.MOVE,
785                             ZmOperation.EDIT,
786                             ZmOperation.CONTACT,
787                             ZmOperation.MOVE_MENU,
788                             ZmOperation.CONTACTGROUP_MENU,
789                             ZmOperation.DELETE,
790                             ZmOperation.SEARCH_MENU,
791                             ZmOperation.NEW_MESSAGE
792                         ],
793                         false
794                     );
795         parent.setItemVisible(ZmOperation.TAG_MENU, false);
796     }
797 
798 	if (appCtxt.isWebClientOffline()) {
799 		parent.enable(
800 			[
801 				ZmOperation.ACTIONS_MENU,
802 				ZmOperation.MOVE,
803 				ZmOperation.EDIT,
804 				ZmOperation.CONTACT,
805 				ZmOperation.MOVE_MENU,
806 				ZmOperation.CONTACTGROUP_MENU,
807 				ZmOperation.DELETE,
808 				ZmOperation.TAG_MENU,
809 				ZmOperation.PRINT,
810 				ZmOperation.PRINT_CONTACT,
811 				ZmOperation.SEND_CONTACTS_IN_EMAIL
812 			],
813 			false
814 		);
815 	}
816 };
817 
818 
819 // List listeners
820 
821 
822 /**
823  * @private
824  * return the contact for which to do the action
825  * @param {Boolean} isToolbar - true if the action is from the toolbar.  false/null if it's from right-click action
826  */
827 ZmContactListController.prototype._getActionContact = function(isToolbar) {
828 
829 	/*
830 	if you read this and don't understand why I don't do the same as in _composeListener. It's because
831 	in DwtListView.prototype.getSelection, the _rightSelItem is not set for a submenu, so the right clicked item is not the selection returned.
832 	This approach of specifically specifying if it's from the toolbar (isToolbar) is more explicit, less fragile and works.
833 	 */
834 	if (isToolbar) {
835 		var selection = this._listView[this._currentViewId].getSelection();
836 		if (selection.length != 1) {
837 			return null;
838 		}
839 		return selection[0];
840 	}
841 	if (this._actionEv) {
842 		return this._actionEv.contact;
843 	}
844 };
845 
846 
847 /**
848  * From Search based on email address
849  *
850  * @private
851  */
852 ZmContactListController.prototype._searchListener = function(addrType, isToolbar, ev) {
853 
854 	var contact = this._getActionContact(isToolbar);
855 	if (!contact) {
856 		return;
857 	}
858 
859 	var addresses = contact.getEmails(),
860 		srchCtlr = appCtxt.getSearchController();
861 
862 	if (addrType === AjxEmailAddress.FROM) {
863 		srchCtlr.fromSearch(addresses);
864 	}
865 	else if (addrType === AjxEmailAddress.TO) {
866 		srchCtlr.toSearch(addresses);
867 	}
868 };
869 
870 /**
871  * Double click displays a contact.
872  * 
873  * @private
874  */
875 ZmContactListController.prototype._listSelectionListener =
876 function(ev) {
877 	Dwt.setLoadingTime("ZmContactItem");
878 	ZmListController.prototype._listSelectionListener.call(this, ev);
879 
880 	if (ev.detail == DwtListView.ITEM_SELECTED)	{
881 //		this._resetNavToolBarButtons();
882 		if (this._currentViewType == ZmId.VIEW_CONTACT_SIMPLE) {
883 			this._parentView[this._currentViewId].setContact(ev.item, this.isGalSearch());
884 		}	
885 	} else if (ev.detail == DwtListView.ITEM_DBL_CLICKED) {
886 		var folder = appCtxt.getById(ev.item.folderId);
887 		if (ev.item.isDistributionList() && ev.item.dlInfo.isOwner) {
888 			this._editListener.call(this, ev);
889 			return;
890 		}
891 		if (!this.isGalSearch() && (!folder || (!folder.isReadOnly() && !folder.isInTrash())) && !appCtxt.isWebClientOffline()) {
892 			AjxDispatcher.run("GetContactController").show(ev.item);
893 		}
894 	}
895 };
896 
897 /**
898  * @private
899  */
900 ZmContactListController.prototype._newListener =
901 function(ev, op, params) {
902 	if (!ev && !op) { return; }
903 	op = op || ev.item.getData(ZmOperation.KEY_ID);
904 	if (op == ZmOperation.NEW_MESSAGE) {
905 		this._composeListener(ev);
906 	}else{
907         ZmListController.prototype._newListener.call(this, ev, op, params);
908     }
909 };
910 
911 /**
912  * Compose message to participant.
913  * 
914  * @private
915  */
916 ZmContactListController.prototype._composeListener =
917 function(ev) {
918 
919     var selection = this._listView[this._currentViewId].getSelection();
920     if (selection.length == 0 && this._actionEv) {
921         selection.push(this._actionEv.contact);
922     }
923     var emailStr = '', contact, email;
924     for (var i = 0; i < selection.length; i++){
925         contact = selection[i];
926 		if (contact.isGroup() && !contact.isDistributionList()) {
927 			var members = contact.getGroupMembers().good;
928 			if (members.size()) {
929 				emailStr += members.toString(AjxEmailAddress.SEPARATOR) + AjxEmailAddress.SEPARATOR;
930 			}
931 		}
932 		else {
933 			var addr = new AjxEmailAddress(contact.getEmail(), AjxEmailAddress.TO, contact.getFullName());
934 			emailStr += addr.toString() + AjxEmailAddress.SEPARATOR;
935 		}
936     }
937 
938 	AjxDispatcher.run("Compose", {action: ZmOperation.NEW_MESSAGE, inNewWindow: this._app._inNewWindow(ev),
939 								  toOverride: emailStr});
940 };
941 
942 /**
943  * Get info on selected contact to provide context for action menu.
944  * 
945  * @private
946  */
947 ZmContactListController.prototype._listActionListener =
948 function(ev) {
949 	ZmListController.prototype._listActionListener.call(this, ev);
950 	this._actionEv.contact = ev.item;
951 	var actionMenu = this.getActionMenu();
952 	if (!this._actionEv.contact.getEmail()  && actionMenu) {
953 		var menuItem = actionMenu.getMenuItem(ZmOperation.SEARCH_MENU);
954 		if (menuItem) {
955 			menuItem.setEnabled(false);
956 		}
957 	}
958 	actionMenu.popup(0, ev.docX, ev.docY);
959 	if (ev.ersatz) {
960 		// menu popped up via keyboard nav
961 		actionMenu.setSelectedItem(0);
962 	}
963 };
964 
965 
966 /**
967  * @private
968  */
969 ZmContactListController.prototype._dropListener =
970 function(ev) {
971 	var view = this._listView[this._currentViewId];
972 	//var item = view.getTargetItem(ev); - this didn't seem to return any item in my tests, while the below (copied from ZmListController.prototype._dropListener) does, and thus solves the gal issue for DLs as well.
973 	var div = view.getTargetItemDiv(ev.uiEvent);
974 	var item = view.getItemFromElement(div);
975 
976 	// only tags can be dropped on us
977 	if (ev.action == DwtDropEvent.DRAG_ENTER) {
978 		if (item && (item.type == ZmItem.CONTACT) && (item.isGal || item.isShared())) {
979 			ev.doIt = false; // can't tag a GAL or shared contact
980 			view.dragSelect(div);
981 			return;
982 		}
983 	}
984 	ZmListController.prototype._dropListener.call(this, ev);
985 };
986 
987 /**
988  * @private
989  */
990 ZmContactListController.prototype._editListener =
991 function(ev, contact) {
992 	contact = contact || this._listView[this._currentViewId].getSelection()[0];
993 	AjxDispatcher.run("GetContactController").show(contact, false);
994 };
995 
996 /**
997  * @private
998  */
999 ZmContactListController.prototype._printListener =
1000 function(ev) {
1001 
1002 	var contacts = this._listView[this._currentViewId].getSelection();
1003 	var ids = [];
1004 	for (var i = 0; i < contacts.length; i++) {
1005 		if (contacts[i].isDistributionList()) {
1006 			continue; //don't print DLs
1007 		}
1008 		ids.push(contacts[i].id);
1009 	}
1010 	if (ids.length == 0) {
1011 		return;
1012 	}
1013 
1014 	var url = "/h/printcontacts?id=" + ids.join(",");
1015 	if (this.isGalSearch()) {
1016 		url = "/h/printcontacts?id=" + ids.join("&id=");
1017 		url = url + "&st=gal";
1018 		var query = this._currentSearch && this._currentSearch.query;
1019 		if (query && contacts.length > 1)
1020 			url += "&sq="+query;
1021         else if(contacts.length==1)
1022             url += "&sq=" + contacts[0].getFileAs();
1023 	}
1024 	if (appCtxt.isOffline) {
1025 		var folderId = this._folderId || ZmFolder.ID_CONTACTS;
1026 		var acctName = appCtxt.getById(folderId).getAccount().name;
1027 		url += "&acct=" + acctName ;
1028 	}
1029 	window.open(appContextPath+url, "_blank");
1030 };
1031 
1032 /**
1033  * @private
1034  */
1035 ZmContactListController.prototype._printAddrBookListener =
1036 function(ev) {
1037 	var url;
1038 	if (this._folderId && !this._list._isShared) {
1039 		url = "/h/printcontacts?sfi=" + this._folderId;
1040 	} else {
1041 		var contacts = ((this._searchType & ZmContactListController.SEARCH_TYPE_ANYWHERE) != 0)
1042 			? AjxDispatcher.run("GetContacts")
1043 			: this._list;
1044 
1045 		var ids = [];
1046 		var list = contacts.getArray();
1047 		for (var i = 0; i < list.length; i++) {
1048 			ids.push(list[i].id);
1049 		}
1050 		// XXX: won't this run into GET limits for large addrbooks? would be better to have
1051 		// URL that prints all contacts (maybe "id=all")
1052 		url = "/h/printcontacts";
1053 		if (this.isGalSearch()) {
1054 			url += "?id=" + ids.join("&id=");
1055 		} else {
1056 			url += "?id=" + ids.join(",");
1057 		}
1058 	}
1059 	if (this.isGalSearch()) {
1060 		url = url + "&st=gal";
1061 		var query = this._currentSearch && this._currentSearch.query;
1062 		if (query && list && list.length > 1)
1063 			url += "&sq="+query;
1064         else if (list && list.length == 1)
1065             url += "&sq="+list[0].getFileAs();
1066 	}
1067 	if (appCtxt.isOffline) {
1068 		var folderId = this._folderId || ZmFolder.ID_CONTACTS;
1069 		var acctName = appCtxt.getById(folderId).getAccount().name;
1070 		url += "&acct=" + acctName ;
1071 	}
1072 	window.open(appContextPath+url, "_blank");
1073 };
1074 
1075 
1076 // Callbacks
1077 
1078 /**
1079  * @private
1080  */
1081 ZmContactListController.prototype._preShowCallback =
1082 function(view) {
1083 	if ((this._searchType & ZmContactListController.SEARCH_TYPE_NEW) != 0) {
1084 		this._searchType &= ~ZmContactListController.SEARCH_TYPE_NEW;
1085 	} else {
1086 		this._resetNavToolBarButtons(view);
1087 	}
1088 
1089 	return true;
1090 };
1091 
1092 /**
1093  * @private
1094  */
1095 ZmContactListController.prototype._doMove =
1096 function(items, folder, attrs, isShiftKey) {
1097 
1098 	items = AjxUtil.toArray(items);
1099 
1100 	var move = [];
1101 	var copy = [];
1102 	var moveFromGal = [];
1103 	for (var i = 0; i < items.length; i++) {
1104 		var item = items[i];
1105 		if (item.isGal) {
1106 			moveFromGal.push(item);
1107 		} else if (!item.folderId || item.folderId != folder.id) {
1108 			if (!this._isItemMovable(item, isShiftKey, folder)) {
1109 				copy.push(item);
1110 			} else {
1111 				move.push(item);
1112 			}
1113 		}
1114 	}
1115 
1116 	var moveOutFolder = appCtxt.getById(this.getFolderId());
1117 	var outOfTrash = (moveOutFolder && moveOutFolder.isInTrash() && !folder.isInTrash());
1118 
1119     var allDoneCallback = this._getAllDoneCallback();
1120 	if (move.length) {
1121         var params = {items:move, folder:folder, attrs:attrs, outOfTrash:outOfTrash};
1122 		var list = params.list = this._getList(params.items);
1123         this._setupContinuation(this._doMove, [folder, attrs, isShiftKey], params, allDoneCallback);
1124         list = outOfTrash ? this._list : list;
1125 		list.moveItems(params);
1126 	}
1127 
1128 	if (copy.length) {
1129         var params = {items:copy, folder:folder, attrs:attrs};
1130 		var list = params.list = this._getList(params.items);
1131         this._setupContinuation(this._doMove, [folder, attrs, isShiftKey], params, allDoneCallback);
1132         list = outOfTrash ? this._list : list;
1133 		list.copyItems(params);
1134 	}
1135 
1136 	if (moveFromGal.length) {
1137 		var batchCmd = new ZmBatchCommand(true, null, true);
1138 		for (var j = 0; j < moveFromGal.length; j++) {
1139 			var contact = moveFromGal[j];
1140 			contact.attr[ZmContact.F_folderId] = folder.id;
1141 			batchCmd.add(new AjxCallback(contact, contact.create, [contact.attr]));
1142 		}
1143 		batchCmd.run(new AjxCallback(this, this._handleMoveFromGal));
1144 	}
1145 };
1146 
1147 /**
1148  * @private
1149  */
1150 ZmContactListController.prototype._handleMoveFromGal =
1151 function(result) {
1152 	var resp = result.getResponse().BatchResponse.CreateContactResponse;
1153 	if (resp != null && resp.length > 0) {
1154 		var msg = AjxMessageFormat.format(ZmMsg.itemCopied, resp.length);
1155 		appCtxt.getAppController().setStatusMsg(msg);
1156 	}
1157 };
1158 
1159 /**
1160  * @private
1161  */
1162 ZmContactListController.prototype._doDelete =
1163 function(items, hardDelete, attrs) {
1164 	ZmListController.prototype._doDelete.call(this, items, hardDelete, attrs);
1165 	for (var i=0; i<items.length; i++) {
1166 		appCtxt.getApp(ZmApp.CONTACTS).updateIdHash(items[i], true);
1167 	}
1168 	// if more contacts to show,
1169 	var size = this._listView[this._currentViewId].getSelectedItems().size();
1170 	if (size == 0) {
1171 		// and if in split view allow split view to clear
1172 		if (this._currentViewType == ZmId.VIEW_CONTACT_SIMPLE)
1173 			this._listView[this._currentViewId].parent.clear();
1174 
1175 		this._resetOperations(this._toolbar[this._currentViewId], 0);
1176 	}
1177 };
1178 
1179 /**
1180  * @private
1181  */
1182 ZmContactListController.prototype._moveListener =
1183 function(ev) {
1184 	ZmListController.prototype._moveListener.call(this, ev);
1185 };
1186 
1187 /**
1188  * @private
1189  */
1190 ZmContactListController.prototype._checkReplenish =
1191 function() {
1192 	// reset the listview
1193 	var lv = this._listView[this._currentViewId];
1194 	lv.set(this._list);
1195 	lv._setNextSelection();
1196 };
1197 
1198 
1199 ZmContactListController.prototype._getContactGroupMenu =
1200 function(parent) {
1201 	var menu = parent instanceof ZmButtonToolBar ? parent.getActionsMenu() : parent;
1202 	return menu ? menu.getContactGroupMenu() : null;
1203 };
1204 
1205 
1206 ZmContactListController.prototype._setContactGroupMenu =
1207 function(parent) {
1208 	if (!parent || appCtxt.isExternalAccount()) { return; }
1209 
1210 	var groupMenu = this._getContactGroupMenu(parent);
1211 	if (!groupMenu) {
1212 		return;
1213 	}
1214 	var items = this.getItems();
1215 	items = AjxUtil.toArray(items);
1216 	var contacts = this._getContactsFromCache();
1217 	var contactGroups = this._filterGroups(contacts);
1218 	var sortedGroups = this._sortContactGroups(contactGroups);
1219 	groupMenu.set(items, sortedGroups, this._folderId == ZmFolder.ID_DLS); //disabled "new" from this for DLs folder.
1220 };
1221 
1222 ZmContactListController.prototype._setupContactGroupMenu =
1223 function(parent) {
1224 	if (!parent) return;
1225 	var groupMenu = this._getContactGroupMenu(parent);
1226 	if (groupMenu) {
1227 		groupMenu.addSelectionListener(this._listeners[ZmOperation.NEW_GROUP]);
1228 	}
1229 };
1230 
1231 /**
1232  * handles updating the group item data
1233  * @param ev
1234  */
1235 ZmContactListController.prototype._contactListChange =
1236 function(ev) {
1237 	if (ev && ev.source && ev.type == ZmId.ITEM_CONTACT) {
1238 			var item = ev.source;
1239 			var id = DwtId.WIDGET_ITEM + "__" + this._currentViewId + "__" + ev.source.id;
1240 			var view = this._listView[this._currentViewId];
1241 			view._setItemData(null, "item", item, id);
1242 	}
1243 
1244 
1245 };
1246 
1247 ZmContactListController.prototype._groupListener =
1248 function(ev, items) {
1249 
1250 	if (this.isCurrent()) {
1251 		var groupEvent = ev.getData(ZmContactGroupMenu.KEY_GROUP_EVENT);
1252 		var groupAdded = ev.getData(ZmContactGroupMenu.KEY_GROUP_ADDED);
1253 		items = items || this.getItems();
1254 		if (groupEvent == ZmEvent.E_MODIFY) {
1255 			var mods = {};
1256 			var groupId = ev.getData(Dwt.KEY_OBJECT).id;
1257 			var group = appCtxt.getApp(ZmApp.CONTACTS).getContactList().getById(groupId);
1258 			if (group) {
1259 				group.addChangeListener(this._contactListChange.bind(this), 0);//update the group data
1260 				var modifiedGroups = this._getGroupMembers(items, group);
1261 				if (modifiedGroups) {
1262 					mods[ZmContact.F_groups] = modifiedGroups;
1263 				}
1264 				this._doModify(group, mods);
1265 				this._menuPopdownActionListener();
1266 				var idx = this._list.getIndexById(group.id);
1267 				if (idx != null) {
1268 					this._resetSelection(idx);
1269 				}
1270 			}
1271 		}
1272 		else if (groupEvent == ZmEvent.E_CREATE) {
1273 			this._pendingActionData = items;
1274 			var newContactGroupDialog = appCtxt.getNewContactGroupDialog();
1275 			if (!this._newContactGroupCb) {
1276 				this._newContactGroupCb = new AjxCallback(this, this._newContactGroupCallback);
1277 			}
1278 			ZmController.showDialog(newContactGroupDialog, this._newContactGroupCb);
1279 			newContactGroupDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, newContactGroupDialog);
1280 		}
1281 	}
1282 };
1283 
1284 ZmContactListController.prototype._newContactGroupCallback =
1285 function(params) {
1286 	var groupName = params.name;
1287 	appCtxt.getNewContactGroupDialog().popdown();
1288 	var items = this.getItems();
1289 	var mods = {};
1290 	mods[ZmContact.F_groups] = this._getGroupMembers(items);
1291 	mods[ZmContact.F_folderId] = this._folderId;
1292 	mods[ZmContact.F_fileAs] = ZmContact.computeCustomFileAs(groupName);
1293 	mods[ZmContact.F_nickname] = groupName;
1294 	mods[ZmContact.F_type] = "group";
1295 	this._doCreate(this._list, mods);
1296 	this._pendingActionData = null;
1297 	this._menuPopdownActionListener();
1298 };
1299 
1300 //methods for dealing with contact groups
1301 ZmContactListController.prototype._getGroupMembers =
1302 function(items, group) {
1303 	var mods = {};
1304 	var newMembers = {};
1305 	var groupId = [];
1306 	var memberType;
1307 	var obj = {};
1308 	var id, contact;
1309 	
1310 	for (var i=0; i<items.length; i++) {
1311 		if (items[i].isDistributionList() || !items[i].isGroup()) {
1312 			obj = this._createContactRefObj(items[i], group);
1313 			if (obj.value) {
1314 				newMembers[obj.value] = obj;
1315 			}		
1316 		}
1317 		else {
1318 			var groups = items[i].attr[ZmContact.F_groups];  //getAttr only returns first value in array
1319 			if (!groups) {
1320 				obj = this._createContactRefObj(items[i], group);
1321 				if (obj.value) {
1322 					newMembers[obj.value] = obj;
1323 				}
1324 			}
1325 			else {
1326 				for (var j=0; j <groups.length; j++) {
1327 					id = groups[j].value;
1328 					contact = ZmContact.getContactFromCache(id);
1329 					if (contact) {
1330 						memberType = contact.isGal ? ZmContact.GROUP_GAL_REF : ZmContact.GROUP_CONTACT_REF;
1331 						obj = {value : contact.isGal ? contact.ref : id, type : memberType};
1332 						if (group) {
1333 							obj.op = "+";
1334 						} 
1335 						newMembers[id] = obj;
1336 					}
1337 					else if (groups[j].type == ZmContact.GROUP_INLINE_REF) {
1338 						obj = {value: groups[j].value, type : ZmContact.GROUP_INLINE_REF};
1339 						if (group) {
1340 							obj.op = "+";
1341 						}
1342 						newMembers[id] = obj;				
1343 					}
1344 				}
1345 			}
1346 		}
1347 	}
1348 	var newMembersArr = [];
1349 	for (var id in newMembers) {
1350 		newMembersArr.push(newMembers[id]);
1351 	}
1352 	if (group) {
1353 		//handle potential duplicates
1354 		var groupArr = group.attr[ZmContact.F_groups];
1355 		var noDups = [];
1356 		var found = false;
1357 		for (var i=0; i<newMembersArr.length; i++) {
1358 			found = false;
1359 			for (var j=0; j<groupArr.length && !found; j++) {				
1360 				if (newMembersArr[i].value == groupArr[j].value) {
1361 					found = true;	
1362 				}
1363 			}
1364 			if (!found) {
1365 				noDups.push(newMembersArr[i]);
1366 			}
1367 		}
1368 		return noDups;
1369 	}
1370 	else {
1371 		return newMembersArr;
1372 	}
1373 };
1374 
1375 ZmContactListController.prototype._createContactRefObj = 
1376 function(contactToAdd, group) {
1377 	var obj = {};
1378 	var memberType = contactToAdd.isGal ? ZmContact.GROUP_GAL_REF : ZmContact.GROUP_CONTACT_REF;
1379 	var id = memberType == ZmContact.GROUP_CONTACT_REF ? contactToAdd.getId(true) : (contactToAdd.ref || contactToAdd.id);
1380 	if (id) {
1381 		var obj = {value: id, type: memberType};
1382 		if (group) {
1383 			obj.op = "+"; //modifying group with new member	
1384 		}
1385 	}
1386 	return obj;
1387 	
1388 };
1389 
1390 ZmContactListController.prototype._getContactsFromCache =
1391 function() {
1392 	var contactList = appCtxt.getApp(ZmApp.CONTACTS).getContactList();
1393 	if (contactList){
1394 		return contactList.getIdHash();
1395 	}
1396 	return {};
1397 };
1398 
1399 ZmContactListController.prototype._sortContactGroups =
1400 function(contactGroups) {
1401 	var sortByNickname = function(a, b) {
1402 		var aNickname = ZmContact.getAttr(a, "nickname");
1403 		var bNickname = ZmContact.getAttr(b, "nickname");
1404 
1405 		if (!aNickname || !bNickname) {
1406 			return 0;
1407 		}
1408 
1409 		if (aNickname.toLowerCase() > bNickname.toLowerCase())
1410 			return 1;
1411 		if (aNickname.toLowerCase() < bNickname.toLowerCase())
1412 			return -1;
1413 
1414 		return 0;
1415 	};
1416 
1417 	return contactGroups.sort(sortByNickname);
1418 };
1419 
1420 ZmContactListController.prototype._filterGroups =
1421 function(contacts) {
1422 	var groups = [];
1423 	for (var id in contacts) {
1424 		var typeAttr = ZmContact.getAttr(contacts[id], "type");
1425 		if (typeAttr && typeAttr.toUpperCase() == ZmItem.GROUP.toUpperCase()) {
1426 			groups.push(contacts[id]);
1427 		}
1428 	}
1429 	return groups;
1430 };
1431 
1432 /**
1433  * @private
1434  */
1435 ZmContactListController.prototype._paginate =
1436 function(view, forward, loadIndex, limit) {
1437 	if (this._list.isGal && !this._list.isGalPAgingSupported) {
1438 		return;
1439 	}
1440 	ZmListController.prototype._paginate.call(this, view, forward, loadIndex, limit);
1441 };
1442 
1443 /**
1444  * @private
1445  */
1446 ZmContactListController.prototype._getDefaultFocusItem =
1447 function() {
1448 	return this.getCurrentView().getListView();
1449 };
1450