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 contacts base view classes.
 27  */
 28 
 29 /**
 30  * Creates the base view.
 31  * @class
 32  * This class represents the base view.
 33  * 
 34  * @param	{Hash}	params		a hash of parameters
 35  * 
 36  * @extends		ZmListView
 37  */
 38 ZmContactsBaseView = function(params) {
 39 
 40 	if (arguments.length == 0) { return; }
 41 
 42 	params.posStyle = params.posStyle || Dwt.ABSOLUTE_STYLE;
 43 	params.type = ZmItem.CONTACT;
 44 	params.pageless = true;
 45 	ZmListView.call(this, params);
 46 
 47 	this._handleEventType[ZmItem.GROUP] = true;
 48 };
 49 
 50 ZmContactsBaseView.prototype = new ZmListView;
 51 ZmContactsBaseView.prototype.constructor = ZmContactsBaseView;
 52 
 53 /**
 54  * Returns a string representation of the object.
 55  * 
 56  * @return		{String}		a string representation of the object
 57  */
 58 ZmContactsBaseView.prototype.toString =
 59 function() {
 60 	return "ZmContactsBaseView";
 61 };
 62 
 63 /**
 64  * Sets the list.
 65  * 
 66  * @param	{ZmContactList}		list		the list
 67  * @param	{String}	sortField		the sort field
 68  * @param	{String}	folderId		the folder id
 69  */
 70 ZmContactsBaseView.prototype.set =
 71 function(list, sortField, folderId) {
 72 
 73 	if (this._itemsToAdd) {
 74 		this.addItems(this._itemsToAdd);
 75 		this._itemsToAdd = null;
 76 	} else {
 77 		var subList;
 78 		if (list instanceof ZmContactList) {
 79 			// compute the sublist based on the folderId if applicable
 80 			list.addChangeListener(this._listChangeListener);
 81 			// for accounts where gal paging is not supported, show *all* results
 82 			subList = (list.isGal && !list.isGalPagingSupported)
 83 				? list.getVector().clone()
 84 				: list.getSubList(this.offset, this.getLimit(this.offset), folderId);
 85 		} else {
 86 			subList = list;
 87 		}
 88 		this._folderId = folderId;
 89 		DwtListView.prototype.set.call(this, subList, sortField);
 90 	}
 91 	this._setRowHeight();
 92 	this._rendered = true;
 93 };
 94 
 95 /**
 96  * @private
 97  */
 98 ZmContactsBaseView.prototype._setParticipantToolTip =
 99 function(address) {
100 	// XXX: OVERLOADED TO SUPPRESS JS ERRORS..
101 	// XXX: REMOVE WHEN IMPLEMENTED - SEE BASE CLASS ZmListView
102 };
103 
104 /**
105  * Gets the list view.
106  * 
107  * @return	{ZmContactsBaseView}	the list view
108  */
109 ZmContactsBaseView.prototype.getListView =
110 function() {
111 	return this;
112 };
113 
114 /**
115  * Gets the title.
116  * 
117  * @return	{String}	the view title
118  */
119 ZmContactsBaseView.prototype.getTitle =
120 function() {
121 	return [ZmMsg.zimbraTitle, this._controller.getApp().getDisplayName()].join(": ");
122 };
123 
124 /**
125  * @private
126  */
127 ZmContactsBaseView.prototype._changeListener =
128 function(ev) {
129 	var folderId = this._controller.getFolderId();
130 
131 	// if we dont have a folder, then assume user did a search of contacts
132 	if (folderId != null || ev.event != ZmEvent.E_MOVE) {
133 		ZmListView.prototype._changeListener.call(this, ev);
134 
135 		if (ev.event == ZmEvent.E_MODIFY) {
136 			this._modifyContact(ev);
137 			var contact = ev.item || ev._details.items[0];
138 			if (contact instanceof ZmContact) {
139 				this.setSelection(contact, false, true);
140 			}
141 		} else if (ev.event == ZmEvent.E_CREATE) {
142 			var newContact = ev._details.items[0];
143 			var newFolder = appCtxt.getById(newContact.folderId);
144 			var newFolderId = newFolder && (appCtxt.getActiveAccount().isMain ? newFolder.nId : newFolder.id);
145 			var visible = ev.getDetail("visible");
146 
147 			// only add this new contact to the listview if this is a simple
148 			// folder search and it belongs!
149 			if (folderId && newFolder && folderId == newFolderId && visible) {
150 				var index = ev.getDetail("sortIndex");
151 				var alphaBar = this.parent ? this.parent.getAlphabetBar() : null;
152 				var inAlphaBar = alphaBar ? alphaBar.isItemInAlphabetLetter(newContact) : true;
153 				if (index != null && inAlphaBar) {
154 					this.addItem(newContact, index);
155 				}
156 
157 				// always select newly added contact if its been added to the
158 				// current page of contacts
159 				if (inAlphaBar) {
160 					this.setSelection(newContact, false, true);
161 				}
162 			}
163 		} else if (ev.event == ZmEvent.E_DELETE) {
164 			// bug fix #19308 - do house-keeping on controller's list so
165 			// replenishment works as it should
166 			var list = this._controller.getList();
167 			if (list) {
168 				list.remove(ev.item);
169 			}
170 		}
171 	}
172 };
173 
174 ZmContactsBaseView.prototype.setSelection =
175 function(item, skipNotify, setPending) {
176 	if (!item) { return; }
177 
178 	var el = this._getElFromItem(item);
179 	if (el) {
180 		ZmListView.prototype.setSelection.call(this, item, skipNotify);
181 		this._pendingSelection = null;
182 	} else if (setPending) {
183 		this._pendingSelection = {item: item, skipNotify: skipNotify};
184 	}
185 };
186 
187 ZmContactsBaseView.prototype.addItems =
188 function(itemArray) {
189 	ZmListView.prototype.addItems.call(this, itemArray);
190 	if (this._pendingSelection && AjxUtil.indexOf(itemArray, this._pendingSelection.item)!=-1) {
191 		this.setSelection(this._pendingSelection.item, this._pendingSelection.skipNotify);
192 	}
193 }
194 
195 
196 /**
197  * @private
198  */
199 ZmContactsBaseView.prototype._modifyContact =
200 function(ev) {
201 	var list = this.getList();
202 	//the item was updated - the list might be "old" (not pointing to the latest items,
203 	// since we refreshed the items in the appCtxt cache by a different view. see bug 84226)
204 	//therefor let's make sure the modified contact replaces the old one in the list.
205 	var contact = ev.item;
206 	if (contact) {
207 		var arr = list.getArray();
208 		for (var i = 0; i < arr.length; i++) {
209 			if (arr[i].id === contact.id) {
210 				if (arr[i] === contact) {
211 					//nothing changed, still points to same object
212 					break;
213 				}
214 				arr[i] = contact;
215 				//update the viewed contact
216 				this.parent.setContact(contact);
217 				break;
218 			}
219 		}
220 	}
221 	// if fileAs changed, resort the internal list
222 	// XXX: this is somewhat inefficient. We should just remove this contact and reinsert
223 	if (ev.getDetail("fileAsChanged")) {
224 		if (list) {
225 			list.sort(ZmContact.compareByFileAs);
226 		}
227 	}
228 };
229 
230 /**
231  * @private
232  */
233 ZmContactsBaseView.prototype._setNextSelection =
234 function() {
235 	// set the next appropriate selected item
236 	if (this.firstSelIndex < 0) {
237 		this.firstSelIndex = 0;
238 	}
239 
240 	// get first valid item to select
241 	var item;
242 	if (this._list) {
243 		item = this._list.get(this.firstSelIndex);
244 
245 		// only get the first non-trash contact to select if we're not in Trash
246 		if (this._controller.getFolderId() == ZmFolder.ID_TRASH) {
247 			if (!item) {
248 				item = this._list.get(0);
249 			}
250 		} else if (item == null || (item && item.folderId == ZmFolder.ID_TRASH)) {
251 			item = null;
252 			var list = this._list.getArray();
253 
254 			if (this.firstSelIndex > 0 && this.firstSelIndex == list.length) {
255 				item = list[list.length-1];
256 			} else {
257 				for (var i=0; i < list.length; i++) {
258 					if (list[i].folderId != ZmFolder.ID_TRASH) {
259 						item = list[i];
260 						break;
261 					}
262 				}
263 			}
264 
265 			// reset first sel index
266 			if (item) {
267 				var div = document.getElementById(this._getItemId(item));
268 				if (div) {
269 					var data = this._data[div.id];
270 					this.firstSelIndex = this._list ? this._list.indexOf(data.item) : -1;
271 				}
272 			}
273 		}
274 	}
275 
276 	this.setSelection(item);
277 };
278 
279 /**
280  * Creates the alphabet bar.
281  * @class
282  * This class represents the contact alphabet bar.
283  * 
284  * @param {DwtComposite}	parent			the parent
285  * 
286  * @extends		DwtComposite
287  */
288 ZmContactAlphabetBar = function(parent) {
289 
290 	DwtComposite.call(this, {parent:parent});
291 
292 	this._createHtml();
293 
294 	this._all = this._current = document.getElementById(this._alphabetBarId).rows[0].cells[0];
295 	this._currentLetter = null;
296 	this.setSelected(this._all, true);
297 	this._enabled = true;
298 	this.addListener(DwtEvent.ONCLICK, this._onClick.bind(this));
299 };
300 
301 ZmContactAlphabetBar.prototype = new DwtComposite;
302 ZmContactAlphabetBar.prototype.constructor = ZmContactAlphabetBar;
303 ZmContactAlphabetBar.prototype.role = 'toolbar';
304 
305 /**
306  * Returns a string representation of the object.
307  * 
308  * @return		{String}		a string representation of the object
309  */
310 ZmContactAlphabetBar.prototype.toString =
311 function() {
312 	return "ZmContactAlphabetBar";
313 };
314 
315 /**
316  * Enables the bar.
317  * 
318  * @param	{Boolean}	enable		if <code>true</code>, enable the bar
319  */
320 ZmContactAlphabetBar.prototype.enable =
321 function(enable) {
322 	this._enabled = enable;
323 
324 	var alphabetBarEl = document.getElementById(this._alphabetBarId);
325 	if (alphabetBarEl) {
326 		alphabetBarEl.className = enable ? "AlphabetBarTable" : "AlphabetBarTable AlphabetBarDisabled";
327 	}
328 };
329 
330 /**
331  * Checks if the bar is enabled.
332  * 
333  * @return	{Boolean}	<code>true</code> if enabled
334  */
335 ZmContactAlphabetBar.prototype.enabled =
336 function() {
337 	return this._enabled;
338 };
339 
340 /**
341  * Resets the bar.
342  * 
343  * @param	{Object}	useCell		the cell or <code>null</code>
344  * @return	{Boolean}				Whether the cell was changed (false if it was already set to useCell)
345  */
346 ZmContactAlphabetBar.prototype.reset =
347 function(useCell) {
348 	var cell = useCell || this._all;
349 	if (cell != this._current) {
350 		this.setSelected(this._current, false);
351 		this._current = cell;
352 		this._currentLetter = useCell && useCell != this._all ? useCell.innerHTML : null;
353 		this.setSelected(cell, true);
354 		return true;
355 	}
356 	return false;
357 };
358 
359 /**
360  * Sets the button index.
361  * 
362  * @param	{int}	index		the index
363  */
364 ZmContactAlphabetBar.prototype.setButtonByIndex =
365 function(index) {
366 	var table = document.getElementById(this._alphabetBarId);
367 	var cell = table.rows[0].cells[index];
368 	if (cell) {
369 		this.reset(cell);
370 	}
371 };
372 
373 /**
374  * Gets the current cell.
375  * 
376  * @return	{Object}	the cell
377  */
378 ZmContactAlphabetBar.prototype.getCurrent =
379 function() {
380 	return this._current;
381 };
382 
383 /**
384  * Gets the current cell letter.
385  * 
386  * @return	{String}	the cell letter, or null for "all"
387  */
388 ZmContactAlphabetBar.prototype.getCurrentLetter =
389 function() {
390 	return this._currentLetter;
391 };
392 
393 /**
394  * Sets the cell as selected.
395  * 
396  * @param	{Object}	cell	the cell
397  * @param	{Boolean}	selected	if <code>true</code>, set as selected
398  */
399 ZmContactAlphabetBar.prototype.setSelected =
400 function(cell, selected) {
401 	cell.className = selected
402 		? "DwtButton-active AlphabetBarCell"
403 		: "DwtButton AlphabetBarCell";
404 	cell.setAttribute('aria-selected', selected);
405 	if (selected) {
406 		this.getHtmlElement().setAttribute('aria-activedescendant', cell.id);
407 		this.setFocusElement(cell);
408 	}
409 };
410 
411 /**
412  * Sets the cell as selected and performs a new search based on the selection.
413  * 
414  * @param	{Object}	cell		the cell
415  * @param	{String}	letter		the letter to begin the search with
416  * @param	{String}	endLetter	the letter to end the search with
417  */
418 ZmContactAlphabetBar.alphabetClicked =
419 function(cell, letter, endLetter) {
420 	// get reference to alphabet bar - ugh
421 	var clc = AjxDispatcher.run("GetContactListController");
422 	var alphabetBar = clc && clc.getCurrentView() && clc.getCurrentView().getAlphabetBar();
423 	if (alphabetBar && alphabetBar.enabled()) {
424 		if (alphabetBar.reset(cell)) {
425             letter = letter && String(letter).substr(0,1);
426             endLetter = endLetter && String(endLetter).substr(0,1);
427 			clc.searchAlphabet(letter, endLetter);
428         }
429 	}
430 };
431 
432 /**
433  * determine if contact belongs in the current alphabet bar.  Used when creating a new contact and not doing a reload --
434  * such as new contact group from action menu.
435  * @param item  {ZmContact}
436  * @return {boolean} true/false if item belongs in alphabet selection
437  */
438 ZmContactAlphabetBar.prototype.isItemInAlphabetLetter =
439 function(item) {
440     var inCurrentBar = false;
441 	if (item) {
442 	  if (ZmMsg.alphabet && ZmMsg.alphabet.length > 0) {
443 		  var all = ZmMsg.alphabet.split(",")[0]; //get "All" for locale
444 	  }
445 	  var fileAs = item.getFileAs();
446 	  var currentLetter = this.getCurrentLetter();
447 	  if (!currentLetter || currentLetter.toLowerCase() == all) {
448 		  inCurrentBar = true; //All is selected
449 	  }
450 	  else if (currentLetter && fileAs) {
451 		var itemLetter = String(fileAs).substr(0,1).toLowerCase();
452 		var cellLetter = currentLetter.substr(0,1).toLowerCase();
453 		if (itemLetter == cellLetter) {
454 			inCurrentBar = true;
455 		}
456 		else if(AjxStringUtil.isDigit(cellLetter) && AjxStringUtil.isDigit(itemLetter)) {
457 			//handles "123" in alphabet bar
458 			inCurrentBar = true;
459 		}
460 		else if (currentLetter.toLowerCase() == "a-z" && itemLetter.match("[a-z]")) {
461 			//handle A-Z cases for certain locales
462 			inCurrentBar = true;
463 		}
464 	  }
465   }
466   return inCurrentBar;
467 };
468 
469 /**
470  * @private
471  */
472 ZmContactAlphabetBar.prototype._createHtml =
473 function() {
474 	this._alphabetBarId = this._htmlElId + "_alphabet";
475 	var alphabet = ZmMsg.alphabet.split(",");
476 
477 	this.startSortMap =
478 		ZmContactAlphabetBar._parseSortVal(ZmMsg.alphabetSortValue);
479 
480 	this.endSortMap =
481 		ZmContactAlphabetBar._parseSortVal(ZmMsg.alphabetEndSortValue);
482 
483 	var subs = {
484 		id: 			this._htmlElId,
485 		alphabet: 		alphabet,
486 		numLetters: 	alphabet.length
487 	};
488 
489 	var element = this.getHtmlElement();
490 	element.innerHTML = AjxTemplate.expand("abook.Contacts#ZmAlphabetBar", subs);
491 	this.setAttribute('aria-label', ZmMsg.alphabetLabel);
492 
493 	AjxUtil.foreach(Dwt.byClassName('AlphabetBarCell', element), (function(cell) {
494         this._makeFocusable(cell, true);
495         this._setEventHdlrs([ DwtEvent.ONCLICK ], false, cell);
496     }).bind(this));
497 
498     // IE8 doesn't support :last-child selector
499     if (AjxEnv.isIE8) {
500         var lastCell = Dwt.byClassName('AlphabetBarCell', element).pop();
501         Dwt.addClass(lastCell, 'AlphabetBarLastCell');
502     }
503 };
504 
505 ZmContactAlphabetBar.prototype.getInputElement =
506 function() {
507 	return this._current;
508 };
509 
510 ZmContactAlphabetBar.prototype.getKeyMapName =
511 function() {
512 	return DwtKeyMap.MAP_TOOLBAR_HORIZ;
513 };
514 
515 ZmContactAlphabetBar.prototype.handleKeyAction =
516 function(actionCode, ev) {
517 	var target =
518 		Dwt.hasClass(ev.target, 'AlphabetBarCell') ? ev.target : this._current;
519 
520 	switch (actionCode) {
521 	case DwtKeyMap.PREV:
522 		var previous = Dwt.getPreviousElementSibling(target);
523 		if (previous) {
524 			this.setFocusElement(previous);
525 		}
526 		return true;
527 
528 	case DwtKeyMap.NEXT:
529 		var next = Dwt.getNextElementSibling(target);
530 		if (next) {
531 			this.setFocusElement(next);
532 		}
533 		return true;
534 
535 	case DwtKeyMap.SELECT:
536 		target.click();
537 		return true;
538 	}
539 };
540 
541 ZmContactAlphabetBar._parseSortVal =
542 function(sortVal) {
543 	if (!sortVal) {
544 		return {};
545 	}
546 	var sortMap = {};
547 	var values = sortVal.split(",");
548 	if (values && values.length) {
549 		for (var i = 0; i < values.length; i++) {
550 			var parts = values[i].split(":");
551 			sortMap[parts[0]] = parts[1];
552 		}
553 	}
554 	return sortMap;
555 };
556 
557 /**
558  * @private
559  */
560 ZmContactAlphabetBar.prototype._onClick =
561 function(ev) {
562 	var cell = DwtUiEvent.getTarget(ev);
563 
564 	if (!Dwt.hasClass(cell, 'AlphabetBarCell') ||
565 	    !this.enabled() || !this.reset(cell)) {
566 		return;
567 	}
568 
569 	var idx = AjxUtil.indexOf(cell.parentNode.children, cell);
570 	var alphabet = ZmMsg.alphabet.split(",");
571 
572 	var startLetter = null, endLetter = null;
573 
574 	if (idx > 0) {
575 		startLetter = this.startSortMap[alphabet[idx]] || alphabet[idx].substr(0, 1);
576 
577 		if (idx < alphabet.length - 1) {
578 			endLetter = this.endSortMap[alphabet[idx]] || alphabet[idx + 1].substr(0, 1);
579 		}
580 	}
581 
582 	var clc = AjxDispatcher.run("GetContactListController");
583 	clc.searchAlphabet(startLetter, endLetter);
584 };
585 
586 
587