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  * 
 27  */
 28 
 29 /**
 30  * Creates a list view.
 31  * @class
 32  * A list view presents a list of items as rows with fields (columns).
 33  *
 34  * @author Parag Shah
 35  * @author Conrad Damon
 36  *
 37  * @param {Hash}	params		a hash of parameters
 38  * @param {DwtComposite}	params.parent		the parent widget
 39  * @param {String}	params.className		the CSS class
 40  * @param {constant}	params.posStyle		the positioning style
 41  * @param {String}	params.id			the HTML ID for element
 42  * @param {Array}	params.headerList	the list of IDs for columns
 43  * @param {Boolean}	params.noMaximize	if <code>true</code>, all columns are fixed-width (otherwise, one will expand to fill available space)
 44  * @param {constant}	params.view			the ID of view
 45  * @param {constant}	params.type			the type of item displayed
 46  * @param {ZmListController}	params.controller	the owning controller
 47  * @param {DwtDropTarget}	params.dropTgt		the drop target
 48  * @param {Boolean}	params.pageless		if <code>true</code>, enlarge page via scroll rather than pagination
 49  *        
 50  * @extends		DwtListView
 51  */
 52 ZmListView = function(params) {
 53 
 54 	if (arguments.length == 0) { return; }
 55 	
 56 	params.id = params.id || ZmId.getViewId(params.view);
 57 	DwtListView.call(this, params);
 58 
 59 	this.view = params.view;
 60 	this.type = params.type;
 61 	this._controller = params.controller;
 62 	this.setDropTarget(params.dropTgt);
 63 
 64 	// create listeners for changes to the list model, folder tree, and tag list
 65 	this._listChangeListener = new AjxListener(this, this._changeListener);
 66 	this._tagListChangeListener = new AjxListener(this, this._tagChangeListener);
 67 	var tagList = appCtxt.getTagTree();
 68 	if (tagList) {
 69 		tagList.addChangeListener(this._tagListChangeListener);
 70 	}
 71 	var folderTree = appCtxt.getFolderTree();
 72 	if (folderTree) {
 73 		this._boundFolderChangeListener =  this._folderChangeListener.bind(this);
 74 		folderTree.addChangeListener(this._boundFolderChangeListener);
 75 	}
 76 
 77 	this._handleEventType = {};
 78 	this._handleEventType[this.type] = true;
 79 	this._disallowSelection = {};
 80 	this._disallowSelection[ZmItem.F_FLAG] = true;
 81 	this._disallowSelection[ZmItem.F_MSG_PRIORITY] = true;
 82 	this._selectAllEnabled = false;
 83 
 84 	if (params.dropTgt) {
 85 		var args = {container:this._parentEl, threshold:15, amount:5, interval:10, id:params.id};
 86 		this._dndScrollCallback = new AjxCallback(null, DwtControl._dndScrollCallback, [args]);
 87 		this._dndScrollId = params.id;
 88 	}
 89 
 90 	this._isPageless = params.pageless;
 91 	if (this._isPageless) {
 92 		Dwt.setHandler(this._getScrollDiv(), DwtEvent.ONSCROLL, ZmListView.handleScroll);
 93 	}
 94 	this._state = {};
 95 };
 96 
 97 ZmListView.prototype = new DwtListView;
 98 ZmListView.prototype.constructor = ZmListView;
 99 ZmListView.prototype.isZmListView = true;
100 
101 ZmListView.prototype.toString =
102 function() {
103 	return "ZmListView";
104 };
105 
106 
107 // Consts
108 
109 ZmListView.KEY_ID							= "_keyId";
110 
111 // column widths
112 ZmListView.COL_WIDTH_ICON 					= 19;
113 ZmListView.COL_WIDTH_NARROW_ICON			= 11;
114 
115 // TD class for fields
116 ZmListView.FIELD_CLASS = {};
117 ZmListView.FIELD_CLASS[ZmItem.F_TYPE]		= "ListViewIcon";
118 ZmListView.FIELD_CLASS[ZmItem.F_FLAG]		= "Flag";
119 ZmListView.FIELD_CLASS[ZmItem.F_TAG]		= "Tag";
120 ZmListView.FIELD_CLASS[ZmItem.F_ATTACHMENT]	= "Attach";
121 
122 ZmListView.ITEM_FLAG_CLICKED 				= DwtListView._LAST_REASON + 1;
123 ZmListView.DEFAULT_REPLENISH_THRESHOLD		= 0;
124 
125 ZmListView.COL_JOIN = "|";
126 
127 ZmListView.CHECKED_IMAGE = "CheckboxChecked";
128 ZmListView.UNCHECKED_IMAGE = "CheckboxUnchecked";
129 ZmListView.CHECKED_CLASS = "ImgCheckboxChecked";
130 ZmListView.UNCHECKED_CLASS = "ImgCheckboxUnchecked";
131 ZmListView.ITEM_CHECKED_ATT_NAME = "itemChecked";
132 
133 
134 ZmListView.prototype._getHeaderList = function() {};
135 
136 /**
137  * Gets the controller.
138  * 
139  * @return	{ZmListController}		the list controller
140  */
141 ZmListView.prototype.getController =
142 function() {
143 	return this._controller;
144 };
145 
146 ZmListView.prototype.set =
147 function(list, sortField) {
148 
149 	this._sortByString = this._controller._currentSearch && this._controller._currentSearch.sortBy;
150     //TODO: We need a longer term fix but this is to prevent a sort by that doesn't match our ZmSearch
151 	//constants and lead to notification issues.
152 	if (this._sortByString) {
153     	this._sortByString = this._sortByString.replace("asc", "Asc").replace("desc", "Desc");// bug 75687
154 	}
155 
156 	var settings = appCtxt.getSettings();
157 	if (!appCtxt.isExternalAccount() && this.view) {
158 		appCtxt.set(ZmSetting.SORTING_PREF,
159 					this._sortByString,
160 					this.view,
161 					false, //setDefault
162 					false, //skipNotify
163 					null, //account
164 					settings && !settings.persistImplicitSortPrefs(this.view)); //skipImplicit - do not persist
165 	}
166 
167 	this.setSelectionHdrCbox(false);
168 
169 	// bug fix #28595 - in multi-account, reset tag list change listeners
170 	if (appCtxt.multiAccounts) {
171 		var tagList = appCtxt.getTagTree();
172 		if (tagList) {
173 			tagList.addChangeListener(this._tagListChangeListener);
174 		}
175 	}
176 
177 	if (this._isPageless) {
178 		if (this._itemsToAdd) {
179 			if (this._itemsToAdd.length) {
180 				this.addItems(this._itemsToAdd);
181 				this._itemsToAdd = null;
182 			}
183 		} else {
184 			var lvList = list;
185 			if (list && list.isZmList) {
186 				list.addChangeListener(this._listChangeListener);
187 				lvList = list.getSubList(0, list.size());
188 			}
189 			DwtListView.prototype.set.call(this, lvList, sortField);
190 		}
191 		this._setRowHeight();
192 	} else {
193 		var subList;
194 		if (list && list.isZmList) {
195 			list.addChangeListener(this._listChangeListener);
196 			subList = list.getSubList(this.offset, this.getLimit());
197 		} else {
198 			subList = list;
199 		}
200 		DwtListView.prototype.set.call(this, subList, sortField);
201 	}
202 	this._rendered = true;
203 
204 	// check in case there are more items but no scrollbar
205 	if (this._isPageless) {
206 		AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._checkItemCount), 1000);
207 	}
208 };
209 
210 ZmListView.prototype.reset =
211 function() {
212 	this._rendered = false;
213 };
214 
215 ZmListView.prototype.setUI =
216 function(defaultColumnSort) {
217 	DwtListView.prototype.setUI.call(this, defaultColumnSort);
218 	this._resetColWidth();	// reset column width in case scrollbar is set
219 };
220 
221 /**
222  * Gets the limit value.
223  * 
224  * @param	{Boolean}	offset		if <code>true</code>, offset
225  * @return	{int}	the limit page size
226  */
227 ZmListView.prototype.getLimit =
228 function(offset) {
229 	if (this._isPageless) {
230 		var limit = appCtxt.get(ZmSetting.PAGE_SIZE);
231 		return offset ? limit : 2 * limit;
232 	} else {
233 		return appCtxt.get(ZmSetting.PAGE_SIZE);
234 	}
235 };
236 
237 /**
238  * Gets the pageless threshold.
239  * 
240  * @return	{int}		the pageless threshold
241  */
242 ZmListView.prototype.getPagelessThreshold =
243 function() {
244 	return Math.ceil(this.getLimit() / 5);
245 };
246 
247 /**
248  * Gets the replenish threshold.
249  * 
250  * @return	{int}	the replenish threshold
251  */
252 ZmListView.prototype.getReplenishThreshold =
253 function() {
254 	return ZmListView.DEFAULT_REPLENISH_THRESHOLD;
255 };
256 
257 /**
258  * Returns the underlying ZmList.
259  */
260 ZmListView.prototype.getItemList =
261 function() {
262 	return this._controller && this._controller._list;
263 };
264 
265 ZmListView.prototype._changeListener =
266 function(ev) {
267 
268 	var item = this._getItemFromEvent(ev);
269 	if (!item || ev.handled || !this._handleEventType[item.type]) {
270 		return;
271 	}
272 
273 	if (ev.event === ZmEvent.E_TAGS || ev.event === ZmEvent.E_REMOVE_ALL) {
274 		this._replaceTagImage(item, ZmItem.F_TAG, this._getClasses(ZmItem.F_TAG));
275 	}
276 
277 	if (ev.event === ZmEvent.E_FLAGS) {
278 		var flags = ev.getDetail("flags");
279 		for (var j = 0; j < flags.length; j++) {
280 			var flag = flags[j];
281 			var on = item[ZmItem.FLAG_PROP[flag]];
282 			if (flag === ZmItem.FLAG_FLAGGED) {
283 				this._setImage(item, ZmItem.F_FLAG, on ? "FlagRed" : "FlagDis", this._getClasses(ZmItem.F_FLAG));
284 			}
285 			else if (flag === ZmItem.FLAG_ATTACH) {
286 				this._setImage(item, ZmItem.F_ATTACHMENT, on ? "Attachment" : null, this._getClasses(ZmItem.F_ATTACHMENT));
287 			}
288 			else if (flag === ZmItem.FLAG_PRIORITY) {
289 				this._setImage(item, ZmItem.F_MSG_PRIORITY, on ? "Priority" : "PriorityDis", this._getClasses(ZmItem.F_MSG_PRIORITY));
290 			}
291 		}
292 	}
293 
294 	// Note: move and delete support batch notification mode
295 	if (ev.event === ZmEvent.E_DELETE || ev.event === ZmEvent.E_MOVE) {
296 		var items = ev.batchMode ? this._getItemsFromBatchEvent(ev) : [item];
297 		var needsSort = false;
298 		for (var i = 0, len = items.length; i < len; i++) {
299 			var item = items[i];
300             var movedHere = (item.type === ZmId.ITEM_CONV) ? item.folders[this._folderId] : item.folderId === this._folderId;
301 			if (movedHere && ev.event === ZmEvent.E_MOVE) {
302 				// We've moved the item into this folder
303 				if (this._getRowIndex(item) === null) { // Not already here
304 					this.addItem(item);
305 					// TODO: couldn't we just find the sort index and insert it?
306 					needsSort = true;
307 				}
308 			}
309 			else {
310 				// remove the item if the user is working in this view,
311 				// if we know the item no longer matches the search, or if the item was hard-deleted
312 				if (ev.event === ZmEvent.E_DELETE || this.view == appCtxt.getCurrentViewId() || this._controller._currentSearch.matches(item) === false) {
313 					this.removeItem(item, true, ev.batchMode);
314 					// if we've removed it from the view, we should remove it from the reference
315 					// list as well so it doesn't get resurrected via replenishment *unless*
316 					// we're dealing with a canonical list (i.e. contacts)
317 					var itemList = this.getItemList();
318 					if (ev.event !== ZmEvent.E_MOVE || !itemList.isCanonical) {
319 						itemList.remove(item);
320 					}
321 				}
322 			}
323 		}
324 		if (needsSort) {
325 			this._saveState({scroll: true, selection:true, focus: true});
326 			this._redoSearch(this._restoreState.bind(this, this._state));
327 		}
328 		if (ev.batchMode) {
329 			this._fixAlternation(0);
330 		}
331 		this._checkReplenishOnTimer();
332 		this._controller._resetToolbarOperations();
333 	}
334 
335 	this._updateLabelForItem(item);
336 };
337 
338 ZmListView.prototype._getItemFromEvent =
339 function(ev) {
340 	var item = ev.item;
341 	if (!item) {
342 		var items = ev.getDetail("items");
343 		item = (items && items.length) ? items[0] : null;
344 	}
345 	return item;
346 };
347 
348 ZmListView.prototype._getItemsFromBatchEvent =
349 function(ev) {
350 
351 	if (!ev.batchMode) { return []; }
352 
353 	var items = ev.items;
354 	if (!items) {
355 		items = [];
356 		var notifs = ev.getDetail("notifs");
357 		if (notifs && notifs.length) {
358 			for (var i = 0, len = notifs.length; i < len; i++) {
359 				var mod = notifs[i];
360 				items.push(mod.item || appCtxt.cacheGet(mod.id));
361 			}
362 		}
363 	}
364 
365 	return items;
366 };
367 
368 // refreshes the content of the given field for the given item
369 ZmListView.prototype._updateField =
370 function(item, field) {
371 	var fieldId = this._getFieldId(item, field);
372 	var el = document.getElementById(fieldId);
373 	if (el) {
374 		var html = [];
375 		var colIdx = this._headerHash[field] && this._headerHash[field]._index;
376 		this._getCellContents(html, 0, item, field, colIdx, new Date());
377 		//replace the old inner html with the new updated data
378 		el.innerHTML = $(html.join("")).html();
379 	}
380 
381 	this._updateLabelForItem(item);
382 };
383 
384 ZmListView.prototype._checkReplenishOnTimer =
385 function(ev) {
386 	if (!this.allSelected) {
387 		if (!this._isPageless) {
388 			this._controller._app._checkReplenishListView = this;
389 		} else {
390 			// Many rows may be removed quickly, so skip unnecessary replenishes
391 			if (!this._replenishTimedAction) {
392 				this._replenishTimedAction = new AjxTimedAction(this, this._handleResponseCheckReplenish);
393 			}
394 			AjxTimedAction.scheduleAction(this._replenishTimedAction, 10);
395 		}
396 	}
397 };
398 
399 ZmListView.prototype._checkReplenish =
400 function(item, forceSelection) {
401 	var respCallback = new AjxCallback(this, this._handleResponseCheckReplenish, [false, item, forceSelection]);
402 	this._controller._checkReplenish(respCallback);
403 };
404 
405 ZmListView.prototype._handleResponseCheckReplenish =
406 function(skipSelection, item, forceSelection) {
407 	if (this.size() == 0) {
408 		this._controller._handleEmptyList(this);
409 	} else {
410 		this._controller._resetNavToolBarButtons();
411 	}
412 	if (!skipSelection) {
413 		this._setNextSelection(item, forceSelection);
414 	}
415 };
416 
417 ZmListView.prototype._folderChangeListener =
418 function(ev) {
419 	// make sure this is current list view
420 	if (appCtxt.getCurrentController() != this._controller) { return; }
421 	// see if it will be handled by app's postNotify()
422 	if (this._controller._app._checkReplenishListView == this) { return; }
423 
424 	var organizers = ev.getDetail("organizers");
425 	var organizer = (organizers && organizers.length) ? organizers[0] : ev.source;
426 
427 	var id = organizer.id;
428 	var fields = ev.getDetail("fields");
429 	if (ev.event == ZmEvent.E_MODIFY) {
430 		if (!fields) { return; }
431 		if (fields[ZmOrganizer.F_TOTAL]) {
432 			this._controller._resetNavToolBarButtons();
433 		}
434 	}
435 };
436 
437 ZmListView.prototype._tagChangeListener =
438 function(ev) {
439 	if (ev.type != ZmEvent.S_TAG) return;
440 
441 	var fields = ev.getDetail("fields");
442 
443 	var divs = this._getChildren();
444 	var tag = ev.getDetail("organizers")[0];
445 	for (var i = 0; i < divs.length; i++) {
446 		var item = this.getItemFromElement(divs[i]);
447 		if (!item || !item.tags || !item.hasTag(tag.name)) {
448 			continue;
449 		}
450 		var updateRequired = false;
451 		if (ev.event == ZmEvent.E_MODIFY && (fields && (fields[ZmOrganizer.F_COLOR] || fields[ZmOrganizer.F_NAME]))) {
452 			//rename could change the color (for remote shared items, from the remote gray icon to local color and vice versa)
453 			updateRequired = item.tags.length == 1;
454 		}
455 		else if (ev.event == ZmEvent.E_DELETE) {
456 			updateRequired = true;
457 		}
458 		else if (ev.event == ZmEvent.E_CREATE) {
459 			//this could affect item if it had a tag not on tag list (remotely created on shared item, either shared by this user or shared to this user)
460 			updateRequired = true;
461 		}
462 		if (updateRequired) {
463 			this._replaceTagImage(item, ZmItem.F_TAG, this._getClasses(ZmItem.F_TAG));
464 		}
465 	}
466 };
467 
468 // returns all child divs for this list view
469 ZmListView.prototype._getChildren =
470 function() {
471 	return this._parentEl.childNodes;
472 };
473 
474 // Common routines for createItemHtml()
475 
476 ZmListView.prototype._getRowId =
477 function(item) {
478 	return DwtId.getListViewItemId(DwtId.WIDGET_ITEM_FIELD, this._view, item ? item.id : Dwt.getNextId(), ZmItem.F_ITEM_ROW);
479 };
480 
481 // Note that images typically get IDs in _getCellContents().
482 ZmListView.prototype._getCellId =
483 function(item, field) {
484 	if (field == ZmItem.F_DATE) {
485 		return this._getFieldId(item, field);
486 	} else if (field == ZmItem.F_SELECTION) {
487 		return this._getFieldId(item, ZmItem.F_SELECTION_CELL);
488 
489 	} else {
490 		return DwtListView.prototype._getCellId.apply(this, arguments);
491 	}
492 };
493 
494 ZmListView.prototype._getCellClass =
495 function(item, field, params) {
496 	return ZmListView.FIELD_CLASS[field];
497 };
498 
499 ZmListView.prototype._getCellContents =
500 function(htmlArr, idx, item, field, colIdx, params, classes) {
501 	if (field == ZmItem.F_SELECTION) {
502 		idx = this._getImageHtml(htmlArr, idx, "CheckboxUnchecked", this._getFieldId(item, field), classes);
503 	} else if (field == ZmItem.F_TYPE) {
504 		idx = this._getImageHtml(htmlArr, idx, ZmItem.ICON[item.type], this._getFieldId(item, field), classes);
505 	} else if (field == ZmItem.F_FLAG) {
506 		idx = this._getImageHtml(htmlArr, idx, this._getFlagIcon(item.isFlagged), this._getFieldId(item, field), classes);
507 	} else if (field == ZmItem.F_TAG) {
508 		idx = this._getImageHtml(htmlArr, idx, item.getTagImageInfo(), this._getFieldId(item, field), classes);
509 	} else if (field == ZmItem.F_ATTACHMENT) {
510 		idx = this._getImageHtml(htmlArr, idx, item.hasAttach ? "Attachment" : null, this._getFieldId(item, field), classes);
511 	} else if (field == ZmItem.F_DATE) {
512 		htmlArr[idx++] = AjxDateUtil.computeDateStr(params.now || new Date(), item.date);
513 	} else if (field == ZmItem.F_PRIORITY) {
514         var priorityImage = null;
515         if (item.isHighPriority) {
516             priorityImage = "PriorityHigh_list";
517         } else if (item.isLowPriority) {
518 			priorityImage = "PriorityLow_list";
519 		}
520 		if (priorityImage) {
521         	idx = this._getImageHtml(htmlArr, idx, priorityImage, this._getFieldId(item, field), classes);
522 		} else {
523 			htmlArr[idx++] = "<div id='" + this._getFieldId(item, field) + "' " + AjxUtil.getClassAttr(classes) + "></div>";
524 		}
525 	} else {
526 		idx = DwtListView.prototype._getCellContents.apply(this, arguments);
527 	}
528 	return idx;
529 };
530 
531 ZmListView.prototype._getImageHtml =
532 function(htmlArr, idx, imageInfo, id, classes) {
533 	htmlArr[idx++] = "<div";
534 	if (id) {
535 		htmlArr[idx++] = [" id='", id, "' "].join("");
536 	}
537 	htmlArr[idx++] = AjxUtil.getClassAttr(classes);
538 	htmlArr[idx++] = ">";
539 	htmlArr[idx++] = AjxImg.getImageHtml(imageInfo || "Blank_16");
540 	htmlArr[idx++] = "</div>";
541 	return idx;
542 };
543 
544 ZmListView.prototype._getClasses =
545 function(field, classes) {
546 	if (this.isMultiColumn && this.isMultiColumn() && this._headerHash[field]) {
547 		classes = classes || [];
548 		classes = [this._headerHash[field]._cssClass];
549 	}
550 	return classes;
551 };
552 
553 ZmListView.prototype._setImage =
554 function(item, field, imageInfo, classes) {
555 	var cell = this._getElement(item, field);
556 	if (cell) {
557 		if (classes) {
558 			cell.className = AjxUtil.uniq(classes).join(" ");
559 		}
560 		cell.innerHTML = AjxImg.getImageHtml(imageInfo || "Blank_16");
561 	}
562 };
563 
564 ZmListView.prototype._replaceTagImage =
565 function(item, field, classes) {
566 	this._setImage(item, field, item.getTagImageInfo(), classes);
567 };
568 
569 ZmListView.prototype._getFragmentSpan =
570 function(item) {
571 	return ["<span class='ZmConvListFragment' aria-hidden='true' id='",
572 			this._getFieldId(item, ZmItem.F_FRAGMENT),
573 			"'>", this._getFragmentHtml(item), "</span>"].join("");
574 };
575 
576 ZmListView.prototype._getFragmentHtml =
577 function(item) {
578 	return [" - ", AjxStringUtil.htmlEncode(item.fragment, true)].join("");
579 };
580 
581 ZmListView.prototype._getFlagIcon =
582 function(isFlagged, isMouseover, disabled) {
583 	if (!isFlagged && !isMouseover) {
584 		return "Blank_16";
585 	} else if (disabled) {
586 		return "FlagDis";
587 	} else {
588 		return "FlagRed";
589 	}
590 };
591 
592 /**
593  * Parse the DOM ID to figure out what got clicked. IDs consist of three to five parts
594  * joined by the "|" character.
595  *
596  *		type		type of ID (zli, zlir, zlic, zlif) - see DwtId.WIDGET_ITEM*)
597  * 		view		view identifier (eg "TV")
598  * 		item ID		usually numeric
599  * 		field		field identifier (eg "fg") - see ZmId.FLG_*
600  * 		participant	index of participant
601  */
602 ZmListView.prototype._parseId =
603 function(id) {
604 	var parts = id.split(DwtId.SEP);
605 	if (parts && parts.length) {
606 		return {view:parts[1], item:parts[2], field:parts[3], participant:parts[4]};
607 	} else {
608 		return null;
609 	}
610 };
611 
612 ZmListView.prototype._mouseDownAction =
613 function(ev, div) {
614 	return !Dwt.ffScrollbarCheck(ev);
615 };
616 
617 ZmListView.prototype._mouseUpAction =
618 function(ev, div) {
619 	return !Dwt.ffScrollbarCheck(ev);
620 };
621 
622 ZmListView.prototype._getField =
623 function(ev, div) {
624 
625 	var target = this._getEventTarget(ev);
626 
627 	var id = target && target.id || div.id;
628 	if (!id) {
629 		return null;
630 	}
631 
632 	var data = this._data[div.id];
633 	var type = data.type;
634 	if (!type || type != DwtListView.TYPE_LIST_ITEM) {
635 		return null;
636 	}
637 
638 	var m = this._parseId(id);
639 	if (!m || !m.field) {
640 		return null;
641 	}
642 	return m.field;
643 
644 };
645 
646 
647 ZmListView.prototype._mouseOutAction =
648 function(ev, div) {
649 	DwtListView.prototype._mouseOutAction.call(this, ev, div);
650 
651 	var field = this._getField(ev, div);
652 	if (!field) {
653 		return true;
654 	}
655 
656 	if (field == ZmItem.F_FLAG) {
657 		var item = this.getItemFromElement(div);
658 		if (!item.isFlagged) {
659 			var target = this._getEventTarget(ev);
660 			AjxImg.setImage(target, this._getFlagIcon(item.isFlagged, false), false, false);
661 			target.className = this._getClasses(field);
662 		}
663 	}
664 	return true;
665 };
666 
667 
668 ZmListView.prototype._mouseOverAction =
669 function(ev, div) {
670 	DwtListView.prototype._mouseOverAction.call(this, ev, div);
671 
672 	var field = this._getField(ev, div);
673 	if (!field) {
674 		return true;
675 	}
676 
677 	if (field === ZmItem.F_FLAG) {
678 		var item = this.getItemFromElement(div);
679 		if (!item.isReadOnly() && !item.isFlagged) {
680 			var target = this._getEventTarget(ev);
681 			AjxImg.setDisabledImage(target, this._getFlagIcon(item.isFlagged, true), false);
682 			target.className = this._getClasses(field);
683 		}
684 	}
685 	return true;
686 };
687 
688 
689 
690 ZmListView.prototype._doubleClickAction =
691 function(ev, div) {
692 	var target = this._getEventTarget(ev);
693 	var id = target && target.id || div.id;
694 	if (!id) { return true; }
695 
696 	var m = this._parseId(id);
697 	return (!(m && (m.field == ZmItem.F_FLAG)));
698 };
699 
700 ZmListView.prototype._itemClicked =
701 function(clickedEl, ev) {
702 	if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX) && ev.button == DwtMouseEvent.LEFT) {
703 		if (!ev.shiftKey && !ev.ctrlKey) {
704 			// get the field being clicked
705 			var target = this._getEventTarget(ev);
706 			var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id;
707 			var m = id ? this._parseId(id) : null;
708 			if (m && (m.field == ZmItem.F_SELECTION || m.field == ZmItem.F_SELECTION_CELL)) {
709 				//user clicked on a checkbox
710 				if (this._selectedItems.size() == 1) {
711 					var sel = this._selectedItems.get(0);
712 					var item = this.getItemFromElement(sel);
713 					var selFieldId = item ? this._getFieldId(item, ZmItem.F_SELECTION) : null;
714 					var selField = selFieldId ? document.getElementById(selFieldId) : null;
715 					if (selField && sel == clickedEl) {
716 						var isChecked = this._getItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME);
717 						this._setImage(item, ZmItem.F_SELECTION, isChecked ? ZmListView.UNCHECKED_IMAGE : ZmListView.CHECKED_IMAGE);
718 						this._setItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME, !isChecked);
719 						if (!isChecked) {
720 							return; //nothing else to do. It's already selected, and was the only selected one. Nothing to remove
721 						}
722 					} else {
723 						if (selField && !this._getItemData(sel, ZmListView.ITEM_CHECKED_ATT_NAME)) {
724 							this.deselectAll();
725 							this._markUnselectedViewedItem(true);
726 						}
727 					}
728 				}
729 				var bContained = this._selectedItems.contains(clickedEl);
730 				this.setMultiSelection(clickedEl, bContained);
731 				this._controller._setItemSelectionCountText();
732 				return;	// do not call base class if "selection" field was clicked
733 			}
734 		} else if (ev.shiftKey) {
735 			// uncheck all selected items first
736 			this._checkSelectedItems(false);
737 
738 			// run base class first so we get the finalized list of selected items
739 			DwtListView.prototype._itemClicked.call(this, clickedEl, ev);
740 
741 			// recheck new list of selected items
742 			this._checkSelectedItems(true);
743 
744 			return;
745 		}
746 	}
747 
748 	DwtListView.prototype._itemClicked.call(this, clickedEl, ev);
749 };
750 
751 ZmListView.prototype._columnClicked =
752 function(clickedCol, ev) {
753 	DwtListView.prototype._columnClicked.call(this, clickedCol, ev);
754 	this._checkSelectionColumnClicked(clickedCol, ev);
755 };
756 
757 ZmListView.prototype._checkSelectionColumnClicked =
758 function(clickedCol, ev) {
759 
760 	if (!appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) { return; }
761 
762 	var list = this.getList();
763 	var size = list ? list.size() : null;
764 	if (size > 0) {
765 		var idx = this._data[clickedCol.id].index;
766 		var item = this._headerList[idx];
767 		if (item && (item._field == ZmItem.F_SELECTION)) {
768 			var hdrId = DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, item._field);
769 			var hdrDiv = document.getElementById(hdrId);
770 			if (hdrDiv) {
771 				if (hdrDiv.className == ZmListView.CHECKED_CLASS) {
772 					if (ev.shiftKey && !this.allSelected) {
773 						this.selectAll(ev.shiftKey);
774 					} else {
775 						this.deselectAll();
776 						hdrDiv.className = ZmListView.UNCHECKED_CLASS;
777 					}
778 				} else {
779 					this.allSelected = false;
780 					hdrDiv.className = ZmListView.CHECKED_CLASS;
781 					this.selectAll(ev.shiftKey);
782 				}
783 			}
784 		}
785 		this._controller._resetToolbarOperations();
786 	}
787 };
788 
789 ZmListView.prototype.handleKeyAction =
790 function(actionCode, ev) {
791 	var rv = DwtListView.prototype.handleKeyAction.call(this, actionCode, ev);
792 
793 	if (actionCode == DwtKeyMap.SELECT_ALL) {
794 		this._controller._resetToolbarOperations();
795 	}
796 
797 	return rv;
798 };
799 
800 ZmListView.prototype.setMultiSelection =
801 function(clickedEl, bContained, ev) {
802 	if (ev && ev.ctrlKey && this._selectedItems.size() == 1) {
803 		this._checkSelectedItems(true);
804 	}
805 
806 	// call base class
807 	DwtListView.prototype.setMultiSelection.call(this, clickedEl, bContained);
808 
809 	this.setSelectionCbox(clickedEl, bContained);
810 	this.setSelectionHdrCbox(this._isAllChecked());
811 
812 	// reset toolbar operations LAST
813 	this._controller._resetToolbarOperations();
814 };
815 
816 /**
817  * check whether all items in the list are checked
818  * @return {Boolean} true if all items are checked
819  */
820 ZmListView.prototype._isAllChecked = 
821 function() {
822 	var list = this.getList();
823 	return (list && (this.getSelection().length == list.size()));
824 };
825 
826 
827 /**
828  * Sets the selection checkbox.
829  * 
830  * @param	{Element}	obj		the item element object
831  * @param	{Boolean}	bContained		(not used)
832  * 
833  */
834 ZmListView.prototype.setSelectionCbox =
835 function(obj, bContained) {
836 	if (!obj) { return; }
837 
838 	var item = obj.tagName ? this.getItemFromElement(obj) : obj;
839 	var selFieldId = item ? this._getFieldId(item, ZmItem.F_SELECTION) : null;
840 	var selField = selFieldId ? document.getElementById(selFieldId) : null;
841 	if (selField) {
842 		this._setImage(item, ZmItem.F_SELECTION, bContained ? ZmListView.UNCHECKED_IMAGE : ZmListView.CHECKED_IMAGE);
843 		this._setItemData(this._getElFromItem(item), ZmListView.ITEM_CHECKED_ATT_NAME, !bContained);
844 	}
845 };
846 
847 /**
848  * Sets the selection header checkbox.
849  * 
850  * @param	{Boolean}	check		if <code>true</code>, check the header checkbox
851  */
852 ZmListView.prototype.setSelectionHdrCbox =
853 function(check) {
854 	var col = this._headerHash ? this._headerHash[ZmItem.F_SELECTION] : null;
855 	var hdrId = col ? DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, col._field) : null;
856 	var hdrDiv = hdrId ? document.getElementById(hdrId) : null;
857 	if (hdrDiv) {
858 		hdrDiv.className = check
859 			? ZmListView.CHECKED_CLASS
860 			: ZmListView.UNCHECKED_CLASS;
861 	}
862 };
863 
864 /**
865  * Sets the selected items.
866  * 
867  * @param	{Array}	selectedArray		an array of {Element} objects to select
868  * @param	{boolean}	dontCheck		do not check the selected item. (special case. see ZmListView.prototype._restoreState)
869  */
870 ZmListView.prototype.setSelectedItems =
871 function(selectedArray, dontCheck) {
872 	DwtListView.prototype.setSelectedItems.call(this, selectedArray);
873 
874 	if (!dontCheck && appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) {
875 		this._checkSelectedItems(true, true);
876 	}
877 };
878 
879 /**
880  * Selects all items.
881  * 
882  * @param	{Boolean}	allResults		if <code>true</code>, set all search selected
883  */
884 ZmListView.prototype.selectAll =
885 function(allResults) {
886 
887 	DwtListView.prototype.selectAll.apply(this, arguments);
888 
889 	if (this._selectAllEnabled) {
890 		var curResult = this._controller._activeSearch;
891 		if (curResult && curResult.getAttribute("more")) {
892 
893 			var list = this.getList(),
894 				type = this.type,
895 				countKey = 'type' + AjxStringUtil.capitalize(ZmItem.MSG_KEY[type]),
896 				typeText = AjxMessageFormat.format(ZmMsg[countKey], list ? list.size() : 2),
897 				shortcut = appCtxt.getShortcutHint(null, ZmKeyMap.SELECT_ALL),
898 				args = [list ? list.size() : ZmMsg.all, typeText, shortcut, "ZmListView.selectAllResults()"],
899 				toastMsg = AjxMessageFormat.format(ZmMsg.allPageSelected, args);
900 
901 			if (allResults) {
902 				this.allSelected = true;
903 				toastMsg = ZmMsg.allSearchSelected;
904 			}
905 			appCtxt.setStatusMsg(toastMsg);
906 		}
907 
908 		var sel = this._selectedItems.getArray();
909 		for (var i = 0; i < sel.length; i++) {
910 			this.setSelectionCbox(sel[i], false);
911 		}
912 	}
913 };
914 
915 // Handle click of link in toast
916 ZmListView.selectAllResults =
917 function() {
918 	var ctlr = appCtxt.getCurrentController();
919 	var view = ctlr && ctlr.getListView();
920 	if (view && view.selectAll) {
921 		view.selectAll(true);
922 	}
923 };
924 
925 /**
926  * Deselects all items.
927  * 
928  */
929 ZmListView.prototype.deselectAll =
930 function() {
931 
932 	this.allSelected = false;
933 	if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) {
934 		this._checkSelectedItems(false);
935 		var hdrId = DwtId.getListViewHdrId(DwtId.WIDGET_HDR_ICON, this._view, ZmItem.F_SELECTION);
936 		var hdrDiv = document.getElementById(hdrId);
937 		if (hdrDiv) {
938 			hdrDiv.className = ZmListView.UNCHECKED_CLASS;
939 		}
940 		var sel = this._selectedItems.getArray();
941 		for (var i=0; i<sel.length; i++) {
942 			this.setSelectionCbox(sel[i], true);
943 		}
944 	}
945 
946 	DwtListView.prototype.deselectAll.call(this);
947 };
948 
949 ZmListView.prototype._checkSelectedItems =
950 function(check) {
951 	var sel = this.getSelection();
952 	for (var i = 0; i < sel.length; i++) {
953 		this.setSelectionCbox(sel[i], !check);
954 	}
955 
956 	var list = this.getList();
957 	var size = list && list.size();
958 	this.setSelectionHdrCbox(size && sel.length == size);
959 };
960 
961 ZmListView.prototype._setNoResultsHtml =
962 function() {
963 	DwtListView.prototype._setNoResultsHtml.call(this);
964 	this.setSelectionHdrCbox(false);
965 	this._rendered = true;
966 };
967 
968 /**
969  * override to call _resetToolbarOperations since we change the selection.
970  * @private
971  */
972 ZmListView.prototype._clearRightSel =
973 function() {
974 	DwtListView.prototype._clearRightSel.call(this);
975 	this._controller._resetToolbarOperations();
976 };
977 
978 
979 /*
980  get sort menu for views that provide a right-click sort by menu in single-column view (currently mail and briefcase)
981  */
982 ZmListView.prototype._getSortMenu = function (sortFields, defaultSortField, parent) {
983 
984 	// create an action menu for the header list
985 	var menu = new ZmPopupMenu(parent || this, null, Dwt.getNextId("SORT_MENU_"));
986 	var actionListener = this._sortMenuListener.bind(this);
987 
988 	for (var i = 0; i < sortFields.length; i++) {
989 		var column = sortFields[i];
990 		var fieldName = ZmMsg[column.msg];
991 		var mi = menu.createMenuItem(column.field, {
992 			text:   parent && parent.isDwtMenuItem ? fieldName : AjxMessageFormat.format(ZmMsg.arrangeBy, fieldName),
993 			style:  DwtMenuItem.RADIO_STYLE
994 		});
995 		if (column.field == defaultSortField) {
996 			mi.setChecked(true, true);
997 		}
998 		mi.setData(ZmListView.KEY_ID, column.field);
999 		menu.addSelectionListener(column.field, actionListener);
1000 	}
1001 
1002 	return menu;
1003 };
1004 
1005 /*
1006 listener used by views that provide a right-click sort by menu in single-column view (currently mail and briefcase)
1007  */
1008 ZmListView.prototype._sortMenuListener =
1009 function(ev) {
1010 	var column;
1011 	if (this.isMultiColumn()) { //this can happen when called from the view menu, that now, for accessibility reasons, includes the sort, for both reading pane on right and at the bottom.
1012 		var sortField = ev && ev.item && ev.item.getData(ZmOperation.MENUITEM_ID);
1013 		column = this._headerHash[sortField];
1014 	}
1015 	else {
1016 		column = this._headerHash[ZmItem.F_SORTED_BY];
1017 		var cell = document.getElementById(DwtId.getListViewHdrId(DwtId.WIDGET_HDR_LABEL, this._view, column._field));
1018 		if (cell) {
1019 	        var text = ev.item.getText();
1020 	        cell.innerHTML = text && text.replace(ZmMsg.sortBy, ZmMsg.sortedBy);
1021 		}
1022 		column._sortable = ev.item.getData(ZmListView.KEY_ID);
1023 	}
1024 	this._bSortAsc = (column._sortable === this._currentSortColId) ? !this._bSortAsc : this._isDefaultSortAscending(column);
1025 	this._sortColumn(column, this._bSortAsc);
1026 };
1027 
1028 ZmListView.prototype._getActionMenuForColHeader = function(force, parent, context) {
1029 
1030 	var menu;
1031 	if (!this._colHeaderActionMenu || force) {
1032 		// create an action menu for the header list
1033 		menu = new ZmPopupMenu(parent || this);
1034 		var actionListener = this._colHeaderActionListener.bind(this);
1035 		for (var i = 0; i < this._headerList.length; i++) {
1036 			var hCol = this._headerList[i];
1037 			// lets not allow columns w/ relative width to be removed (for now) - it messes stuff up
1038 			if (hCol._width) {
1039 				var id = ZmId.getMenuItemId([ this._view, context ].join("_"), hCol._field);
1040 				var mi = menu.createMenuItem(id, {text:hCol._name, style:DwtMenuItem.CHECK_STYLE});
1041 				mi.setData(ZmListView.KEY_ID, hCol._id);
1042 				mi.setChecked(hCol._visible, true);
1043                 if (hCol._noRemove) {
1044 					mi.setEnabled(false);
1045 				}
1046 				menu.addSelectionListener(id, actionListener);
1047 			}
1048 		}
1049 	}
1050 
1051 	return menu;
1052 };
1053 
1054 ZmListView.prototype._colHeaderActionListener =
1055 function(ev) {
1056 
1057 	var menuItemId = ev.item.getData(ZmListView.KEY_ID);
1058 
1059 	for (var i = 0; i < this._headerList.length; i++) {
1060 		var col = this._headerList[i];
1061 		if (col._id == menuItemId) {
1062 			col._visible = !col._visible;
1063 			break;
1064 		}
1065 	}
1066 
1067 	this._relayout();
1068 };
1069 
1070 /**
1071  * Gets the tool tip content.
1072  * 
1073  * @param	{Object}	ev		the hover event
1074  * @return	{String}	the tool tip content
1075  */
1076 ZmListView.prototype.getToolTipContent = function(ev) {
1077 
1078 	var div = this.getTargetItemDiv(ev);
1079 	if (!div) {
1080         return "";
1081     }
1082 	var target = Dwt.findAncestor(this._getEventTarget(ev), "id"),
1083 	    id = (target && target.id) || div.id;
1084 
1085 	if (!id) {
1086         return "";
1087     }
1088 
1089 	// check if we're hovering over a column header
1090 	var data = this._data[div.id];
1091 	var type = data.type;
1092 	var tooltip;
1093 	if (type && type == DwtListView.TYPE_HEADER_ITEM) {
1094 		var itemIdx = data.index;
1095 		var field = this._headerList[itemIdx]._field;
1096 		tooltip = this._getHeaderToolTip(field, itemIdx);
1097 	}
1098     else {
1099 		var match = this._parseId(id);
1100 		if (match && match.field) {
1101 			var item = this.getItemFromElement(div);
1102 			var params = {field:match.field, item:item, ev:ev, div:div, match:match};
1103 			tooltip = this._getToolTip(params);
1104 		}
1105 	}
1106 
1107 	return tooltip;
1108 };
1109 
1110 ZmListView.prototype.getTooltipBase =
1111 function(hoverEv) {
1112 	return hoverEv ? DwtUiEvent.getTargetWithProp(hoverEv.object, "id") : DwtListView.prototype.getTooltipBase.apply(this, arguments);
1113 };
1114 
1115 ZmListView.prototype._getHeaderToolTip =
1116 function(field, itemIdx, isOutboundFolder) {
1117 
1118 	var tooltip = null;
1119 	var sortable = this._headerList[itemIdx]._sortable;
1120 	if (field == ZmItem.F_SELECTION) {
1121 		tooltip = ZmMsg.selectionColumn;
1122 	} else if (field == ZmItem.F_FLAG) {
1123         tooltip = ZmMsg.flagHeaderToolTip;
1124     } else if (field == ZmItem.F_PRIORITY){
1125         tooltip = ZmMsg.priorityHeaderTooltip;
1126     } else if (field == ZmItem.F_TAG) {
1127         tooltip = ZmMsg.tag;
1128     } else if (field == ZmItem.F_ATTACHMENT) {
1129         tooltip = ZmMsg.attachmentHeaderToolTip;
1130     } else if (field == ZmItem.F_SUBJECT) {
1131         tooltip = sortable ? ZmMsg.sortBySubject : ZmMsg.subject;
1132     } else if (field == ZmItem.F_DATE) {
1133 		if (sortable) {
1134 			if (isOutboundFolder) {
1135 				tooltip = (this._folderId == ZmFolder.ID_DRAFTS) ? ZmMsg.sortByLastSaved : ZmMsg.sortBySent;
1136 			} else {
1137 				tooltip = ZmMsg.sortByReceived;
1138 			}
1139 		} else {
1140 			tooltip = ZmMsg.date;
1141 		}
1142     } else if (field == ZmItem.F_FROM) {
1143         tooltip = sortable ? isOutboundFolder ? ZmMsg.sortByTo : ZmMsg.sortByFrom : isOutboundFolder ? ZmMsg.to : ZmMsg.from ;
1144     } else if (field == ZmItem.F_SIZE){
1145         tooltip = sortable ? ZmMsg.sortBySize : ZmMsg.sizeToolTip;
1146 	} else if (field == ZmItem.F_ACCOUNT) {
1147 		tooltip = ZmMsg.account;
1148     } else if (field == ZmItem.F_FOLDER) {
1149         tooltip = ZmMsg.folder;
1150     } else if (field == ZmItem.F_MSG_PRIORITY) {
1151 		tooltip = ZmMsg.messagePriority
1152 	} 
1153     
1154     return tooltip;
1155 };
1156 
1157 /**
1158  * @param params		[hash]			hash of params:
1159  *        field			[constant]		column ID
1160  *        item			[ZmItem]*		underlying item
1161  *        ev			[DwtEvent]*		mouseover event
1162  *        div			[Element]*		row div
1163  *        match			[hash]*			fields from div ID
1164  *        callback		[AjxCallback]*	callback (in case tooltip content retrieval is async)
1165  *        
1166  * @private
1167  */
1168 ZmListView.prototype._getToolTip =
1169 function(params) {
1170     var tooltip, field = params.field, item = params.item, div = params.div;
1171 	if (field == ZmItem.F_FLAG) {
1172 		return null; //no tooltip for the flag
1173     } else if (field == ZmItem.F_PRIORITY) {
1174         if (item.isHighPriority) {
1175             tooltip = ZmMsg.highPriorityTooltip;
1176         } else if (item.isLowPriority) {
1177             tooltip = ZmMsg.lowPriorityTooltip;
1178         }
1179     } else if (field == ZmItem.F_TAG) {
1180         tooltip = this._getTagToolTip(item);
1181     } else if (field == ZmItem.F_ATTACHMENT) {
1182         // disable att tooltip for now, we only get att info once msg is loaded
1183         // tooltip = this._getAttachmentToolTip(item);
1184     } else if (div && (field == ZmItem.F_DATE)) {
1185         tooltip = this._getDateToolTip(item, div);
1186     }
1187     return tooltip;
1188 };
1189 
1190 /*
1191  * Get the list of fields for the accessibility label. Normally, this
1192  * corresponds to the header columns.
1193  *
1194  * @protected
1195  */
1196 ZmListView.prototype._getLabelFieldList =
1197 function() {
1198 	var headers = this._getHeaderList();
1199 
1200 	if (headers) {
1201 		return AjxUtil.map(headers, function(header) {
1202 			return header._field;
1203 		});
1204 	}
1205 };
1206 
1207 /*
1208  * Get the accessibility label corresponding to the given field.
1209  *
1210  * @protected
1211  */
1212 ZmListView.prototype._getLabelForField =
1213 function(item, field) {
1214 	var tooltip = this._getToolTip({ item: item, field: field });
1215 	// TODO: fix for tooltips that are callbacks (such as for appts)
1216 	return AjxStringUtil.stripTags(tooltip);
1217 };
1218 
1219 ZmListView.prototype._updateLabelForItem =
1220 function(item) {
1221 	var fields = this._getLabelFieldList();
1222 	var itemel = this._getElFromItem(item);
1223 
1224 	if (!item || !fields || !itemel) {
1225 		return;
1226 	}
1227 
1228 	var buf = [];
1229 
1230 	for (var i = 0; i < fields.length; i++) {
1231 		var label = this._getLabelForField(item, fields[i]);
1232 
1233 		if (label) {
1234 			buf.push(label);
1235 		}
1236 	}
1237 
1238 	if (buf.length > 0) {
1239 		itemel.setAttribute('aria-label', buf.join(', '));
1240 	} else {
1241 		itemel.removeAttribute('aria-label');
1242 	}
1243 };
1244 
1245 ZmListView.prototype._getTagToolTip =
1246 function(item) {
1247 	if (!item) { return; }
1248 	var numTags = item.tags && item.tags.length;
1249 	if (!numTags) { return; }
1250 	var tagList = appCtxt.getAccountTagList(item);
1251 	var tags = item.tags;
1252 	var html = [];
1253 	var idx = 0;
1254     for (var i = 0; i < numTags; i++) {
1255 		var tag = tagList.getByNameOrRemote(tags[i]);
1256         if (!tag) { continue; }        
1257 		var nameText = tag.notLocal ? AjxMessageFormat.format(ZmMsg.tagNotLocal, tag.name) : tag.name;
1258         html[idx++] = "<table><tr><td>";
1259 		html[idx++] = AjxImg.getImageHtml(tag.getIconWithColor());
1260 		html[idx++] = "</td><td valign='middle'>";
1261 		html[idx++] = AjxStringUtil.htmlEncode(nameText);
1262 		html[idx++] = "</td></tr></table>";
1263 	}
1264 	return html.join("");
1265 };
1266 
1267 ZmListView.prototype._getAttachmentToolTip =
1268 function(item) {
1269 	var tooltip = null;
1270 	var atts = item && item.attachments ? item.attachments : [];
1271 	if (atts.length == 1) {
1272 		var info = ZmMimeTable.getInfo(atts[0].ct);
1273 		tooltip = info ? info.desc : null;
1274 	} else if (atts.length > 1) {
1275 		tooltip = AjxMessageFormat.format(ZmMsg.multipleAttachmentsTooltip, [atts.length]);
1276 	}
1277 	return tooltip;
1278 };
1279 
1280 ZmListView.prototype._getDateToolTip =
1281 function(item, div) {
1282 	div._dateStr = div._dateStr || this._getDateToolTipText(item.date);
1283 	return div._dateStr;
1284 };
1285 
1286 ZmListView.prototype._getDateToolTipText =
1287 function(date, prefix) {
1288 	if (!date) { return ""; }
1289 	var dateStr = [];
1290 	var i = 0;
1291 	dateStr[i++] = prefix;
1292 	var dateFormatter = AjxDateFormat.getDateTimeInstance(AjxDateFormat.FULL, AjxDateFormat.MEDIUM);
1293 	dateStr[i++] = dateFormatter.format(new Date(date));
1294 	var delta = AjxDateUtil.computeDateDelta(date);
1295 	if (delta) {
1296 		dateStr[i++] = "<br><center><span style='white-space:nowrap'>(";
1297 		dateStr[i++] = delta;
1298 		dateStr[i++] = ")</span></center>";
1299 	}
1300 	return dateStr.join("");
1301 };
1302 
1303 /*
1304 * Add a few properties to the list event for the listener to pick up.
1305 */
1306 ZmListView.prototype._setListEvent =
1307 function (ev, listEv, clickedEl) {
1308 	DwtListView.prototype._setListEvent.call(this, ev, listEv, clickedEl);
1309 	var target = this._getEventTarget(ev);
1310 	var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id;
1311 	if (!id) return false; // don't notify listeners
1312 
1313 	var m = this._parseId(id);
1314 	if (ev.button == DwtMouseEvent.LEFT) {
1315 		this._selEv.field = m ? m.field : null;
1316 	} else if (ev.button == DwtMouseEvent.RIGHT) {
1317 		this._actionEv.field = m ? m.field : null;
1318 		if (m && m.field) {
1319 			if (m.field == ZmItem.F_PARTICIPANT) {
1320 				var item = this.getItemFromElement(clickedEl);
1321 				this._actionEv.detail = item.participants ? item.participants.get(m.participant) : null;
1322 			}
1323 		}
1324 	}
1325 	return true;
1326 };
1327 
1328 ZmListView.prototype._allowLeftSelection =
1329 function(clickedEl, ev, button) {
1330 	// We only care about mouse events
1331 	if (!(ev instanceof DwtMouseEvent)) { return true; }
1332 	var target = this._getEventTarget(ev);
1333 	var id = (target && target.id && target.id.indexOf("AjxImg") == -1) ? target.id : clickedEl.id;
1334 	var data = this._data[clickedEl.id];
1335 	var type = data.type;
1336 	if (id && type && type == DwtListView.TYPE_LIST_ITEM) {
1337 		var m = this._parseId(id);
1338 		if (m && m.field) {
1339 			return this._allowFieldSelection(m.item, m.field);
1340 		}
1341 	}
1342 	return true;
1343 };
1344 
1345 ZmListView.prototype._allowFieldSelection =
1346 function(id, field) {
1347 	return (!this._disallowSelection[field]);
1348 };
1349 
1350 ZmListView.prototype._redoSearch =
1351 function(callback) {
1352 	var search = this._controller._currentSearch;
1353 	if (!search) {
1354 		return;
1355 	}
1356 	var sel = this.getSelection();
1357 	var selItem = sel && sel[0];
1358 	var changes = {
1359 		isRedo: true,
1360 		selectedItem: selItem
1361 	};
1362 	appCtxt.getSearchController().redoSearch(search, false, changes, callback);
1363 };
1364 
1365 ZmListView.prototype._sortColumn = function(columnItem, bSortAsc, callback) {
1366 
1367 	// change the sort preference for this view in the settings
1368 	var sortBy;
1369 	switch (columnItem._sortable) {
1370 		case ZmItem.F_FROM:		    sortBy = bSortAsc ? ZmSearch.NAME_ASC : ZmSearch.NAME_DESC; break;
1371         case ZmItem.F_TO:           sortBy = bSortAsc ? ZmSearch.RCPT_ASC : ZmSearch.RCPT_DESC; break;
1372 		case ZmItem.F_NAME:		    sortBy = bSortAsc ? ZmSearch.SUBJ_ASC : ZmSearch.SUBJ_DESC; break; //used for Briefcase only now. SUBJ is mappaed to the filename of the document on the server side
1373 		case ZmItem.F_SUBJECT:	    sortBy = bSortAsc ? ZmSearch.SUBJ_ASC : ZmSearch.SUBJ_DESC;	break;
1374 		case ZmItem.F_DATE:		    sortBy = bSortAsc ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;	break;
1375 		case ZmItem.F_SIZE:		    sortBy = bSortAsc ? ZmSearch.SIZE_ASC : ZmSearch.SIZE_DESC;	break;
1376         case ZmItem.F_FLAG:		    sortBy = bSortAsc ? ZmSearch.FLAG_ASC : ZmSearch.FLAG_DESC;	break;
1377         case ZmItem.F_ATTACHMENT:   sortBy = bSortAsc ? ZmSearch.ATTACH_ASC : ZmSearch.ATTACH_DESC; break;
1378 		case ZmItem.F_READ:		    sortBy = bSortAsc ? ZmSearch.READ_ASC : ZmSearch.READ_DESC;	break;
1379         case ZmItem.F_PRIORITY:     sortBy = bSortAsc ? ZmSearch.PRIORITY_ASC : ZmSearch.PRIORITY_DESC; break;
1380 		case ZmItem.F_SORTED_BY:    sortBy = bSortAsc ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;	break;
1381 	}
1382 
1383 	if (sortBy) {
1384 		this._currentSortColId = columnItem._sortable;
1385 		//special case - switching from read/unread to another sort column - remove it from the query, so users are not confused that they still see only unread messages after clicking on another sort column.
1386 		if (columnItem._sortable != ZmItem.F_READ && (this._sortByString == ZmSearch.READ_ASC || this._sortByString == ZmSearch.READ_DESC)) {
1387 			var controller = this._controller;
1388 			var query = controller.getSearchString();
1389 			if (query) {
1390 				 controller.setSearchString(AjxStringUtil.trim(query.replace("is:unread", "")));
1391 			}
1392 		}
1393 		this._sortByString = sortBy;
1394 		var skipFirstNotify = this._folderId ? true : false; //just making it explicit boolean
1395         if (!appCtxt.isExternalAccount()) {
1396 			var settings = appCtxt.getSettings();
1397            	appCtxt.set(ZmSetting.SORTING_PREF,
1398 					   sortBy,
1399 					   this.view,
1400 					   false, //setDefault
1401 					   skipFirstNotify, //skipNotify
1402 					   null, //account
1403 					   settings && !settings.persistImplicitSortPrefs(this.view)); //skipImplicit
1404             if (this._folderId) {
1405                 appCtxt.set(ZmSetting.SORTING_PREF, sortBy, this._folderId);
1406             }
1407         }
1408 		if (!this._isMultiColumn) {
1409 			this._setSortedColStyle(columnItem._id);
1410 		}
1411 	}
1412 	if (callback) {
1413 		callback.run();
1414 	}
1415 };
1416 
1417 ZmListView.prototype._setNextSelection =
1418 function(item, forceSelection) {
1419 	// set the next appropriate selected item
1420 	if (this.firstSelIndex < 0) {
1421 		this.firstSelIndex = 0;
1422 	}
1423 	if (this._list && !item) {
1424 		item = this._list.get(this.firstSelIndex) || this._list.getLast();
1425     }
1426 	if (item) {
1427 		this.setSelection(item, false, forceSelection);
1428 	}
1429 };
1430 
1431 ZmListView.prototype._relayout =
1432 function() {
1433 	DwtListView.prototype._relayout.call(this);
1434 	this._checkColumns();
1435 };
1436 
1437 ZmListView.prototype._checkColumns =
1438 function() {
1439 	var numCols = this._headerList.length;
1440 	var fields = [];
1441 	for (var i = 0; i < numCols; i++) {
1442 		var headerCol = this._headerList[i];
1443 		// bug 43540: always skip account header since its a multi-account only
1444 		// column and we don't want it to sync
1445 		if (headerCol && headerCol._field != ZmItem.F_ACCOUNT) {
1446 			fields.push(headerCol._field + (headerCol._visible ? "" : "*"));
1447 		}
1448 	}
1449 	var value = fields.join(ZmListView.COL_JOIN);
1450 	value = (value == this._defaultCols) ? "" : value;
1451     if (!appCtxt.isExternalAccount() && !this._controller.isSearchResults) {
1452 	    appCtxt.set(ZmSetting.LIST_VIEW_COLUMNS, value, appCtxt.getViewTypeFromId(this.view));
1453     }
1454 
1455 	this._colHeaderActionMenu = this._getActionMenuForColHeader(true); // re-create action menu so order is correct
1456 };
1457 
1458 /**
1459  * Scroll-based paging. Make sure we have at least one page of items below the visible list.
1460  * 
1461  * @param ev
1462  */
1463 ZmListView.handleScroll =
1464 function(ev) {
1465 	var target = DwtUiEvent.getTarget(ev);
1466 	var lv = DwtControl.findControl(target);
1467 	if (lv) {
1468 		lv._checkItemCount();
1469 	}
1470 };
1471 
1472 /**
1473  * Figure out if we should fetch some more items, based on where the scroll is. Our goal is to have
1474  * a certain number available below the bottom of the visible view.
1475  */
1476 ZmListView.prototype._checkItemCount =
1477 function() {
1478 	var itemsNeeded = this._getItemsNeeded();
1479 	if (itemsNeeded) {
1480 		this._controller._paginate(this._view, true, null, itemsNeeded);
1481 	}
1482 };
1483 
1484 /**
1485  * Figure out how many items we need to fetch to maintain a decent number
1486  * below the fold. Nonstandard list views may override.
1487  */
1488 ZmListView.prototype._getItemsNeeded =
1489 function(skipMoreCheck) {
1490 
1491 	if (!skipMoreCheck) {
1492 		var itemList = this.getItemList();
1493 		if (!(itemList && itemList.hasMore()) || !this._list) { return 0; }
1494 	}
1495 	if (!this._rendered || !this._rowHeight) { return 0; }
1496 
1497 	DBG.println(AjxDebug.DBG2, "List view: checking item count");
1498 
1499 	var sbCallback = new AjxCallback(null, AjxTimedAction.scheduleAction, [new AjxTimedAction(this, this._resetColWidth), 100]);
1500 	var params = {scrollDiv:	this._getScrollDiv(),
1501 				  rowHeight:	this._rowHeight,
1502 				  threshold:	this.getPagelessThreshold(),
1503 				  limit:		this.getLimit(1),
1504 				  listSize:		this._list.size(),
1505 				  sbCallback:	sbCallback};
1506 	return ZmListView.getRowsNeeded(params);
1507 };
1508 
1509 ZmListView.prototype._getScrollDiv =
1510 function() {
1511 	return this._parentEl;
1512 };
1513 
1514 ZmListView.getRowsNeeded =
1515 function(params) {
1516 
1517 	var div = params.scrollDiv;
1518 	var sh = div.scrollHeight, st = div.scrollTop, rh = params.rowHeight;
1519 
1520 	// view (porthole) height - everything measured relative to its top
1521 	// prefer clientHeight since (like scrollHeight) it doesn't include borders
1522 	var h = div.clientHeight || Dwt.getSize(div).y;
1523 
1524 	// where we'd like bottom of list view to be (with extra hidden items at bottom)
1525 	var target = h + (params.threshold * rh);
1526 
1527 	// where bottom of list view is (including hidden items)
1528 	var bottom = sh - st;
1529 
1530 	if (bottom == h) {
1531 		// handle cases where there's no scrollbar, but we have more items (eg tall browser, or replenishment)
1532 		bottom = (params.listSize * rh) - st;
1533 		if (st == 0 && params.sbCallback) {
1534 			// give list view a chance to fix width since it may be getting a scrollbar
1535 			params.sbCallback.run();
1536 		}
1537 	}
1538 
1539 	var rowsNeeded = 0;
1540 	if (bottom < target) {
1541 		// buffer below visible bottom of list view is not full
1542 		rowsNeeded = Math.max(Math.floor((target - bottom) / rh), params.limit);
1543 	}
1544 	return rowsNeeded;
1545 };
1546 
1547 ZmListView.prototype._sizeChildren =
1548 function(height) {
1549 	if (DwtListView.prototype._sizeChildren.apply(this, arguments)) {
1550 		this._checkItemCount();
1551 	}
1552 };
1553 
1554 // Allow list view classes to override type used in nav text. Return null to say "items".
1555 ZmListView.prototype._getItemCountType =
1556 function() {
1557 	return this.type;
1558 };
1559 
1560 /**
1561  * Checks if the given item is in this view's list. Note that the view's list may
1562  * be only part of the controller's list (the currently visible page).
1563  *
1564  * @param {String|ZmItem}	item		the item ID, or item to check for
1565  * @return	{Boolean}	<code>true</code> if the item is in the list
1566  */
1567 ZmListView.prototype.hasItem =
1568 function(item) {
1569 
1570 	var id = (typeof item == "string") ? item : item && item.id;
1571 	if (id && this._list) {
1572 		var a = this._list.getArray();
1573 		for (var i = 0, len = a.length; i < len; i++) {
1574 			var item = a[i];
1575 			if (item && item.id == id) {
1576 				return true;
1577 			}
1578 		}
1579 	}
1580 	return false;
1581 };
1582 
1583 /**
1584  * The following methods allow a list view to maintain state after it has
1585  * been rerendered. State may include such elements as: which items are selected,
1586  * focus, scroll position, etc.
1587  *
1588  * @private
1589  * @param {hash}		params		hash of parameters:
1590  * @param {boolean}		selection	if true, preserve selection
1591  * @param {boolean}		focus		if true, preserve focus
1592  * @param {boolean}		scroll		if true, preserve scroll position
1593  */
1594 ZmListView.prototype._saveState =
1595 function(params) {
1596 
1597 	var s = this._state = {};
1598 	params = params || {};
1599 	if (params.selection) {
1600 		s.selected = this.getSelection();
1601 		if (s.selected.length == 1) {
1602 			//still a special case for now till we rewrite this thing.
1603 			var el = this._getElFromItem(s.selected[0]); //terribly ugly, get back to the html element so i can have access to the item data
1604 			s.singleItemChecked = this._getItemData(el, ZmListView.ITEM_CHECKED_ATT_NAME);
1605 		}
1606 	}
1607 	if (params.focus) {
1608 		s.focused = this.hasFocus();
1609 		s.anchorItem = this._kbAnchor && this.getItemFromElement(this._kbAnchor);
1610 	}
1611 	if (params.scroll) {
1612 		s.rowHeight = this._rowHeight;
1613 		s.scrollTop = this._listDiv.scrollTop;
1614 	}
1615 };
1616 
1617 ZmListView.prototype._restoreState =
1618 function(state) {
1619 
1620 	var s = state || this._state;
1621 	if (s.selected && s.selected.length) {
1622 		var dontCheck = s.selected.length == 1 && !s.singleItemChecked;
1623 		this.setSelectedItems(s.selected, dontCheck);
1624 	}
1625 	if (s.anchorItem) {
1626 		var el = this._getElFromItem(s.anchorItem);
1627 		if (el) {
1628 			this._setKbFocusElement(el);
1629 		}
1630 	}
1631 	if (s.focused) {
1632 		this.focus();
1633 	}
1634 	// restore scroll position based on row height ratio
1635 	if (s.rowHeight) {
1636 		this._listDiv.scrollTop = s.scrollTop * (this._rowHeight / s.rowHeight);
1637 	}
1638 	this._state = {};
1639 };
1640 
1641 ZmListView.prototype._renderList =
1642 function(list, noResultsOk, doAdd) {
1643     var group = this._group;
1644     if (!group) {
1645         return DwtListView.prototype._renderList.call(this, list, noResultsOk, doAdd);
1646     }
1647 	if (list instanceof AjxVector && list.size()) {
1648 		var now = new Date();
1649 		var size = list.size();
1650 		var htmlArr = [];
1651         var section;
1652         var headerDiv;
1653 		for (var i = 0; i < size; i++) {
1654 			var item = list.get(i);
1655 			var div = this._createItemHtml(item, {now:now}, !doAdd, i);
1656 			if (div) {
1657 				if (div instanceof Array) {
1658 					for (var j = 0; j < div.length; j++){
1659                         section = group.addMsgToSection(item, div[j]);
1660                         if (group.getSectionSize(section) == 1){
1661                             headerDiv = this._getSectionHeaderDiv(group, section);
1662                             this._addRow(headerDiv);
1663                         }
1664 						this._addRow(div[j]);
1665 					}
1666 				} else if (div.tagName || doAdd) {
1667                     section = group.addMsgToSection(item, div);
1668                     if (group.getSectionSize(section) == 1){
1669                         headerDiv = this._getSectionHeaderDiv(group, section);
1670                         this._addRow(headerDiv);
1671                     }
1672                     this._addRow(div);
1673 				} else {
1674                     group.addMsgToSection(item, div);
1675 				}
1676 			}
1677 		}
1678 		if (group && !doAdd) {
1679 			group.resetSectionHeaders();
1680 			htmlArr.push(group.getAllSections(this._bSortAsc));
1681 		}
1682 
1683 		if (htmlArr.length && !doAdd) {
1684 			this._parentEl.innerHTML = htmlArr.join("");
1685 		}
1686 	} else if (!noResultsOk) {
1687 		this._setNoResultsHtml();
1688 	}
1689 
1690 };
1691 
1692 ZmListView.prototype._addRow =
1693 function(row, index) {
1694 	DwtListView.prototype._addRow.apply(this, arguments);
1695 
1696 	this._updateLabelForItem(this.getItemFromElement(row));
1697 };
1698 
1699 ZmListView.prototype._itemAdded = function(item) {
1700     item.refCount++;
1701 };
1702 
1703 ZmListView.prototype._getSectionHeaderDiv =
1704 function(group, section) {
1705     if (group && section) {
1706         var headerDiv = document.createElement("div");
1707         var sectionTitle = group.getSectionTitle(section);
1708         var html = group.getSectionHeader(sectionTitle);
1709         headerDiv.innerHTML = html;
1710         return headerDiv.firstChild;
1711     }
1712 };
1713 
1714 ZmListView.prototype.deactivate =
1715 function() {
1716 	this._controller.inactive = true;
1717 };
1718 
1719 ZmListView.prototype._getEventTarget =
1720 function(ev) {
1721 	var target = ev && ev.target;
1722 	if (target && (target.nodeName === "IMG" || (target.className && target.className.match(/\bImg/)))) {
1723 		return target.parentNode;
1724 	}
1725 	return target;
1726 };
1727