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  * Creates an empty tree view.
 30  * @class
 31  * This class displays data in a tree structure.
 32  *
 33  * @author Conrad Damon
 34  * 
 35  * @param {Hash}	params				the hash of parameters
 36  * @param {DwtControl}	params.parent				the tree's parent widget
 37  * @param {constant}	params.type				the organizer type
 38  * @param {String}	params.className				the CSS class
 39  * @param {constant}	params.posStyle				the positioning style
 40  * @param {constant}	params.overviewId			theoverview ID
 41  * @param {String}	params.headerClass			the CSS class for header item
 42  * @param {DwtDragSource}	params.dragSrc				the drag source
 43  * @param {DwtDropTarget}	params.dropTgt				the drop target
 44  * @param {constant}	params.treeStyle				tree style (see {@link DwtTree})
 45  * @param {Boolean}	params.isCheckedByDefault	sets the default state of "checked" tree style
 46  * @param {Hash}	params.allowedTypes			a hash of org types this tree may display
 47  * @param {Hash}	params.allowedSubTypes		a hash of org types this tree may display below top level
 48  * @param {boolean}    params.actionSupported     (default to value from Overview if not passed)
 49  *
 50  * @extends		DwtTree
 51  */
 52 ZmTreeView = function(params) {
 53 
 54 	if (arguments.length == 0) { return; }
 55 
 56 	DwtTree.call(this, {
 57 		parent: params.parent,
 58 		parentElement: params.parentElement,
 59 		style: params.treeStyle,
 60 		isCheckedByDefault: params.isCheckedByDefault,
 61 		className: (params.className || "OverviewTree"),
 62 		posStyle: params.posStyle,
 63 		id: params.id
 64 	});
 65 
 66 	this._headerClass = params.headerClass || "overviewHeader";
 67 	this.overviewId = params.overviewId;
 68 	this.type = params.type;
 69 	this.allowedTypes = params.allowedTypes;
 70 	this.allowedSubTypes = params.allowedSubTypes;
 71 
 72 	this._overview = appCtxt.getOverviewController().getOverview(this.overviewId);
 73 	
 74 	this._dragSrc = params.dragSrc;
 75 	this._dropTgt = params.dropTgt;
 76 
 77 	this.actionSupported = params.actionSupported !== undefined
 78 							? params.actionSupported
 79 							: this._overview.actionSupported;
 80 
 81 	this.dynamicWidth = this._overview.dynamicWidth;
 82 
 83 	this._dataTree = null;
 84 	this._treeItemHash  = {};	// map organizer to its corresponding tree item by ID
 85 	this._idToOrganizer = {};	// map DwtControl htmlElId to the organizer for external Drag and Drop
 86 
 87 };
 88 
 89 ZmTreeView.KEY_TYPE	= "_type_";
 90 ZmTreeView.KEY_ID	= "_treeId_";
 91 
 92 // compare functions for each type
 93 ZmTreeView.COMPARE_FUNC = {};
 94 
 95 // add space after the following items
 96 ZmTreeView.ADD_SEP = {};
 97 ZmTreeView.ADD_SEP[ZmFolder.ID_TRASH] = true;
 98 
 99 ZmTreeView.MAX_ITEMS = 50;
100 
101 // Static methods
102 
103 /**
104  * Finds the correct position for an organizer within a node, given
105  * a sort function.
106  *
107  * @param {DwtTreeItem}	node			the node under which organizer is to be added
108  * @param {ZmOrganizer}	organizer		the organizer
109  * @param {function}	sortFunction	the function for comparing two organizers
110  * @return	{int}	the index
111  */
112 ZmTreeView.getSortIndex =
113 function(node, organizer, sortFunction) {
114 	if (!sortFunction) return null;
115 	var cnt = node.getItemCount();
116 	var children = node.getItems();
117 	for (var i = 0; i < children.length; i++) {
118 		if (children[i]._isSeparator) continue;
119 		var child = children[i].getData(Dwt.KEY_OBJECT);
120 		if (!child) continue;
121 		var test = sortFunction(organizer, child);
122 		if (test == -1) {
123 			return i;
124 		}
125 	}
126 	return i;
127 };
128 
129 ZmTreeView.prototype = new DwtTree;
130 ZmTreeView.prototype.constructor = ZmTreeView;
131 
132 // Public methods
133 
134 /**
135  * Returns a string representation of the object.
136  * 
137  * @return		{String}		a string representation of the object
138  */
139 ZmTreeView.prototype.toString = 
140 function() {
141 	return "ZmTreeView";
142 };
143 
144 
145 /**
146  * Populates the tree view with the given data and displays it.
147  *
148  * @param {Hash}	params		a hash of parameters
149  * @param   {ZmTree}	params.dataTree		data in tree form
150  * @param	{Boolean}	params.showUnread	if <code>true</code>, show unread counts
151  * @param	{Hash}	params.omit			a hash of organizer IDs to ignore
152  * @param	{Hash}	params.include		a hash of organizer IDs to include
153  * @param	{Boolean}	params.omitParents	if <code>true</code>, do NOT insert parent nodes as needed
154  * @param	{Hash}	params.searchTypes	the types of saved searches to show
155  * @param	{Boolean}	params.noTooltips	if <code>true</code>, don't show tooltips for tree items
156  * @param	{Boolean}	params.collapsed		if <code>true</code>, initially leave the root collapsed 
157  * @param 	{Hash}          params.optButton        a hash of data for showing a options button in the item: image, tooltip, callback
158  */
159 ZmTreeView.prototype.set =
160 function(params) {
161 	this._showUnread = params.showUnread;
162 	this._dataTree = params.dataTree;
163 	this._optButton = params.optButton;
164 
165 	this.clearItems();
166 
167 	// create header item
168 	var root = this._dataTree.root;
169 	var isMultiAcctSubHeader = (appCtxt.multiAccounts && (this.type == ZmOrganizer.SEARCH || this.type == ZmOrganizer.TAG));
170 	var imageInfo = this._getHeaderTreeItemImage();
171 	var ti = this._headerItem = new DwtHeaderTreeItem({
172 		parent:				this,
173 		className:			isMultiAcctSubHeader ? "DwtTreeItem" : this._headerClass,
174 		imageInfo:			imageInfo,
175 		id:					ZmId.getTreeItemId(this.overviewId, null, this.type),
176 		optButton:			params.optButton,
177 		dndScrollCallback:	this._overview && this._overview._dndScrollCallback,
178 		dndScrollId:		this._overview && this._overview._scrollableContainerId
179 	});
180 	ti._isHeader = true;
181 	var name = ZmMsg[ZmOrganizer.LABEL[this.type]];
182 	if (name) {
183 		ti.setText(name);
184 	}
185 	ti.setData(Dwt.KEY_ID, root.id);
186 	ti.setData(Dwt.KEY_OBJECT, root);
187 	ti.setData(ZmTreeView.KEY_ID, this.overviewId);
188 	ti.setData(ZmTreeView.KEY_TYPE, this.type);
189 	if (this._dropTgt) {
190 		ti.setDropTarget(this._dropTgt);
191 	}
192 	this._treeItemHash[root.id] = ti;
193 	ti.getHtmlElement().style.overflow = "hidden";
194 	// render the root item's children (ie everything else)
195 	params.treeNode = ti;
196 	params.organizer = root;
197 	this._render(params);
198 	ti.setExpanded(!params.collapsed, null, true);
199 
200 	if (!appCtxt.multiAccounts) {
201 		this.addSeparator();
202 	}
203 
204 
205 	if (appCtxt.getSkinHint("noOverviewHeaders") ||
206 		this._hideHeaderTreeItem())
207 	{
208 		ti.setVisible(false, true);
209 	}
210 };
211 
212 /**
213  * Gets the tree item that represents the organizer with the given ID.
214  *
215  * @param {int}		id		an organizer ID
216  * @return	{DwtTreeItem}		the item
217  */
218 ZmTreeView.prototype.getTreeItemById =
219 function(id) {
220 	return this._treeItemHash[id];
221 };
222 
223 /**
224  * Gets the tree view's header node.
225  * 
226  * @return	{DwtHeaderTreeItem}		the item
227  */
228 ZmTreeView.prototype.getHeaderItem =
229 function() {
230 	return this._headerItem;
231 };
232 
233 /**
234  * Gets the currently selected organizer(s). If tree view is checkbox style,
235  * return value is an {Array} otherwise, a single {DwtTreeItem} object is returned.
236  * 
237  * @return	{Array|DwtTreeItem}		the selected item(s)
238  */
239 ZmTreeView.prototype.getSelected =
240 function() {
241 	if (this.isCheckedStyle) {
242 		var selected = [];
243 		// bug #44805 - iterate thru the entire tree item hash in case there are
244 		// more than one header items in the tree view (e.g. Imap accounts)
245 		for (var i in this._treeItemHash) {
246 			var ti = this._treeItemHash[i];
247 			if (ti && ti.getChecked()) {
248 				selected.push(ti.getData(Dwt.KEY_OBJECT));
249 			}
250 		}
251 		return selected;
252 	} else {
253 		return (this.getSelectionCount() != 1)
254 			? null : this.getSelection()[0].getData(Dwt.KEY_OBJECT);
255 	}
256 };
257 
258 /**
259  * Selects the tree item for the given organizer.
260  *
261  * @param {ZmOrganizer}	organizer		the organizer to select, or its ID
262  * @param {Boolean}	skipNotify	if <code>true</code>, skip notifications
263  * @param {Boolean}	noFocus		if <code>true</code>, select item but don't set focus to it
264  */
265 ZmTreeView.prototype.setSelected =
266 function(organizer, skipNotify, noFocus) {
267 	var id = ZmOrganizer.getSystemId((organizer instanceof ZmOrganizer) ? organizer.id : organizer);
268 	if (!id || !this._treeItemHash[id]) { return; }
269 	this.setSelection(this._treeItemHash[id], skipNotify, false, noFocus);
270 };
271 
272 
273 // Private and protected methods
274 
275 /**
276  * Draws the children of the given node.
277  *
278  * @param params		[hash]			hash of params:
279  *        treeNode		[DwtTreeItem]	current node
280  *        organizer		[ZmOrganizer]	its organizer
281  *        omit			[Object]*		hash of system folder IDs to ignore	
282  *        include		[object]*		hash of system folder IDs to include
283  *        showOrphans	[boolean]*		if true, show parent chain of any
284  * 										folder of this type, as well as the folder
285  *        searchTypes	[hash]*			types of saved searches to show
286  *        noTooltips	[boolean]*		if true, don't show tooltips for tree items
287  *        startPos		[int]*			start rendering this far into list of children
288  * 
289  * TODO: Add logic to support display of folders that are not normally allowed in
290  * 		this tree, but that have children (orphans) of an allowed type
291  * TODO: Only sort folders we're showing (requires two passes).
292  * 
293  * @private
294  */
295 ZmTreeView.prototype._render =
296 function(params) {
297 
298 	params.omit = params.omit || {};
299 	this._setOmit(params.omit, params.dataTree);
300 
301 	var org = params.organizer;
302 	var children = org.children.getArray();
303 	if (org.isDataSource(ZmAccount.TYPE_IMAP)) {
304 		children.sort(ZmImapAccount.sortCompare);
305 	} else if (ZmTreeView.COMPARE_FUNC[this.type]) {
306 		if (appCtxt.isOffline && this.type == ZmOrganizer.SEARCH) {
307 			var local = [];
308 			for (var j = 0; j < children.length; j++) {
309 				var child = children[j];
310 				if (child && child.type == ZmOrganizer.SEARCH && !child.isOfflineGlobalSearch) {
311 					local.push(child);
312 				}
313 			}
314 			children = local;
315 		}
316 		// IE loses type info on the children array - the props are there and it can be iterated,
317 		// but a function call like sort() blows up. So create an array local to child win.
318 		if (appCtxt.isChildWindow && AjxEnv.isIE) {
319 			var children1 = [];
320 			for (var i = 0, len = children.length; i < len; i++) {
321 				children1.push(children[i]);
322 			}
323 			children = children1;
324 		}
325 		children.sort(eval(ZmTreeView.COMPARE_FUNC[this.type]));
326 	}
327 	DBG.println(AjxDebug.DBG3, "Render: " + org.name + ": " + children.length);
328 	var addSep = true;
329 	var numItems = 0;
330 	var len = children.length;
331     if (params.startPos === undefined && params.lastRenderedFolder ){
332         for (var i = 0, len = children.length; i < len; i++) {
333             if (params.lastRenderedFolder == children[i] ){
334                params.startPos = i + 1; // Next to lastRenderedFolder
335                break;
336             }
337         }
338         DBG.println(AjxDebug.DBG1, "load remaining folders: " + params.startPos);
339     }
340 	for (var i = params.startPos || 0; i < len; i++) {
341 		var child = children[i];
342 		if (!child || (params.omit && params.omit[child.nId])) { continue; }
343 		if (!(params.include && params.include[child.nId])) {
344 			if (!this._isAllowed(org, child)) {
345 				if (params.omitParents) continue;
346 				var proxy = AjxUtil.createProxy(params);
347 				proxy.treeNode = null;
348 				proxy.organizer = child;
349 				this._render(proxy);
350 				continue;
351 			}
352 		}
353 
354 		if (child.numTotal == 0 && (child.nId == ZmFolder.ID_SYNC_FAILURES)) {
355 			continue;
356 		}
357 
358 		var parentNode = params.treeNode;
359 		var account = appCtxt.multiAccounts && child.getAccount();
360 
361 		// bug: 43067 - reparent calendars for caldav-based accounts
362 		if (account && account.isCalDavBased() &&
363 			child.parent.nId == ZmOrganizer.ID_CALENDAR)
364 		{
365 			parentNode = parentNode.parent;
366 		}
367 
368 		// if there's a large number of folders to display, make user click on special placeholder
369 		// to display remainder; we then display them MAX_ITEMS at a time
370 		if (numItems >= ZmTreeView.MAX_ITEMS) {
371 			if (params.startPos) {
372 				// render next chunk
373 				params.startPos = i;
374 				params.len = (params.startPos + ZmTreeView.MAX_ITEMS >= len) ? len : 0;	// hint that we're done
375 				this._showRemainingFolders(params);
376 				return;
377 			} else if (numItems >= ZmTreeView.MAX_ITEMS * 2) {
378 				// add placeholder tree item "Show remaining folders"
379 				var orgs = ZmMsg[ZmOrganizer.LABEL[this.type]].toLowerCase();
380 				var name = AjxMessageFormat.format(ZmMsg.showRemainingFolders, orgs);
381 				child = new ZmFolder({id:ZmFolder.ID_LOAD_FOLDERS, name:name, parent:org});
382 				child._tooltip = AjxMessageFormat.format(ZmMsg.showRemainingFoldersTooltip, [(children.length - i), orgs]);
383 				var ti = this._addNew(parentNode, child);
384 				ti.enableSelection(true);
385 				if (this.isCheckedStyle) {
386 					ti.showCheckBox(false);
387 				}
388                 params.lastRenderedFolder  = children[i - 1];
389 				params.showRemainingFoldersNode = ti;
390 				child._showFoldersCallback = new AjxCallback(this, this._showRemainingFolders, [params]);
391 				if (this._dragSrc) {
392 					// Bug 55763 - expand placeholder on hover; replacing the _dragHover function is the easiest way, if a bit hacky
393 					ti._dragHover = this._showRemainingFolders.bind(this, params);
394 				}
395 
396 				return;
397 			}
398 		}
399 
400 		// NOTE: Separates public and shared folders
401 		if ((org.nId == ZmOrganizer.ID_ROOT) && child.link && addSep) {
402 			params.treeNode.addSeparator();
403 			addSep = false;
404 		}
405 		this._addNew(parentNode, child, null, params.noTooltips, params.omit);
406 		numItems++;
407 	}
408 };
409 
410 ZmTreeView.prototype._setOmit =
411 function(omit, dataTree) {
412 	for (var id in ZmFolder.HIDE_ID) {
413 		omit[id] = true;
414 	}
415 	//note - the dataTree thing was in the previous code so I keep it, but seems all the ZmFolder.HIDE_NAME code is commented out, so
416 	//not sure it's still needed.
417 	dataTree = this.type !== ZmOrganizer.VOICE && dataTree;
418 	if (!dataTree) {
419 		return;
420 	}
421 	for (var name in ZmFolder.HIDE_NAME) {
422 		var folder = dataTree.getByName(name);
423 		if (folder) {
424 			omit[folder.id] = true;
425 		}
426 	}
427 };
428 
429 /**
430  * a bit complicated and hard to explain - We should only allow (render on this view)
431  * a child of an "allowedSubTypes", if all its ancestors are allowed all the way to the root ("Folders"), meaning
432  * it has an ancestor that is of the allowedTypes (but is not the root)
433  * e.g.
434  * allowed:
435  * Folders-->folder1--->searchFolder1
436  * Folders--->folder1--->folder2--->folder3--->searchFolder1
437  *
438  * not allowed:
439  * Folders-->searchFolder1
440  * Folders-->searchFolder1--->searchFolder2
441  *
442  * @param org
443  * @param child
444  * @returns {*}
445  * @private
446  */
447 ZmTreeView.prototype._isAllowed =
448 function(org, child) {
449 
450 	if (!org) { //could happen, for example the Zimlets root doesn't have a parent.
451 		return true; //seems returning true in this case works... what a mess.
452 	}
453 
454 	// Within the Searches tree, only show saved searches that return a type that belongs to this app
455 	if (this.type === ZmOrganizer.SEARCH && child.type === ZmOrganizer.SEARCH && this._overview.appName) {
456 		var searchTypes = child.search.types && child.search.types.getArray();
457 		if (!searchTypes || searchTypes.length === 0) {
458 			searchTypes = [ ZmItem.MSG ];   // search with no types defaults to "message"
459 		}
460 		var common = AjxUtil.intersection(searchTypes,
461 			ZmApp.SEARCH_TYPES[this._overview.appName] ||  ZmApp.SEARCH_TYPES[appCtxt.getCurrentAppName()]);
462 		if (common.length === 0) {
463 			return false;
464 		}
465 	}
466 
467 	if (org.nId == ZmOrganizer.ID_ROOT) {
468 		return this.allowedTypes[child.type];
469 	}
470 
471 	//org is not root
472 	if (this.allowedTypes[child.type]) {
473 		return true; //optimization, end the recursion if we find a non root allowed ancestor.
474 	}
475 
476 	if (this.allowedSubTypes[child.type]) {
477 		return this._isAllowed(org.parent, org); //go up parent to see if eventually it's allowed.
478 	}
479 
480 	return false;
481 };
482 
483 /**
484  * Adds a tree item node for the given organizer to the tree, and then adds its children.
485  *
486  * @param parentNode	[DwtTreeItem]	node under which to add the new one
487  * @param organizer		[ZmOrganizer]	organizer for the new node
488  * @param index			[int]*			position at which to add the new node
489  * @param noTooltips	[boolean]*		if true, don't show tooltips for tree items
490  * @param omit			[Object]*		hash of system folder IDs to ignore
491  * 
492  * @private
493  */
494 ZmTreeView.prototype._addNew =
495 function(parentNode, organizer, index, noTooltips, omit) {
496 	var ti;
497 	var parentControlId;
498 	// check if we're adding a datasource folder
499 	var dsColl = (organizer.type == ZmOrganizer.FOLDER) && appCtxt.getDataSourceCollection();
500 	var dss = dsColl && dsColl.getByFolderId(organizer.nId);
501 	var ds = (dss && dss.length > 0) ? dss[0] : null;
502 
503 	if (ds && ds.type == ZmAccount.TYPE_IMAP) {
504 		var cname = appCtxt.isFamilyMbox ? null : this._headerClass;
505 		ti = new DwtHeaderTreeItem({
506 			parent:this,
507 			text:organizer.getName(),
508 			className:cname
509 		});
510 	} else {
511 		// create parent chain
512 		if (!parentNode) {
513 			var stack = [];
514 			var parentOrganizer = organizer.parent;
515 			if (parentOrganizer) {
516 				while ((parentNode = this.getTreeItemById(parentOrganizer.id)) == null) {
517 					stack.push(parentOrganizer);
518 					parentOrganizer = parentOrganizer.parent;
519 				}
520 			}
521 			while (parentOrganizer = stack.pop()) {
522 				parentNode = this.getTreeItemById(parentOrganizer.parent.id);
523 				parentControlId = ZmId.getTreeItemId(this.overviewId, parentOrganizer.id);
524 				parentNode = new DwtTreeItem({
525 					parent:					parentNode,
526 					text:					parentOrganizer.getName(),
527 					imageInfo:				parentOrganizer.getIconWithColor(),
528 					forceNotifySelection:	true,
529 					arrowDisabled:			!this.actionSupported,
530 					dynamicWidth:			this.dynamicWidth,
531 					dndScrollCallback:		this._overview && this._overview._dndScrollCallback,
532 					dndScrollId:			this._overview && this._overview._scrollableContainerId,
533 					id:						parentControlId
534 				});
535 				parentNode.setData(Dwt.KEY_ID, parentOrganizer.id);
536 				parentNode.setData(Dwt.KEY_OBJECT, parentOrganizer);
537 				parentNode.setData(ZmTreeView.KEY_ID, this.overviewId);
538 				parentNode.setData(ZmTreeView.KEY_TYPE, parentOrganizer.type);
539 				this._treeItemHash[parentOrganizer.id] = parentNode;
540 				this._idToOrganizer[parentControlId] = parentOrganizer.id;
541 			}
542 		}
543 		var params = {
544 			parent:				parentNode,
545 			index:				index,
546 			text:				organizer.getName(this._showUnread),
547 			arrowDisabled:		!this.actionSupported,
548 			dynamicWidth:		this.dynamicWidth,
549 			dndScrollCallback:	this._overview && this._overview._dndScrollCallback,
550 			dndScrollId:		this._overview && this._overview._scrollableContainerId,
551 			imageInfo:			organizer.getIconWithColor(),
552 			id:					ZmId.getTreeItemId(this.overviewId, organizer.id)
553 		};
554 		// now add item
555 		ti = new DwtTreeItem(params);
556 		this._idToOrganizer[params.id] = organizer.id;
557 	}
558 
559 	if (appCtxt.multiAccounts &&
560 		(organizer.type == ZmOrganizer.SEARCH ||
561 		 organizer.type == ZmOrganizer.TAG))
562 	{
563 		ti.addClassName("DwtTreeItemChildDiv");
564 	}
565 
566 	ti.setDndText(organizer.getName());
567 	ti.setData(Dwt.KEY_ID, organizer.id);
568 	ti.setData(Dwt.KEY_OBJECT, organizer);
569 	ti.setData(ZmTreeView.KEY_ID, this.overviewId);
570 	ti.setData(ZmTreeView.KEY_TYPE, organizer.type);
571 	if (!noTooltips) {
572 		var tooltip = organizer.getToolTip();
573 		if (tooltip) {
574 			ti.setToolTipContent(tooltip);
575 		}
576 	}
577 	if (this._dragSrc) {
578 		ti.setDragSource(this._dragSrc);
579 	}
580 	if (this._dropTgt) {
581 		ti.setDropTarget(this._dropTgt);
582 	}
583 	this._treeItemHash[organizer.id] = ti;
584 
585 	if (ZmTreeView.ADD_SEP[organizer.nId]) {
586 		parentNode.addSeparator();
587 	}
588 
589 	// recursively add children
590 	if (organizer.children && organizer.children.size()) {
591 		this._render({treeNode:ti, organizer:organizer, omit:omit});
592 	}
593 
594 	if (ds && ds.type == ZmAccount.TYPE_IMAP) {
595 		ti.setExpanded(!appCtxt.get(ZmSetting.COLLAPSE_IMAP_TREES));
596 	}
597 
598 	return ti;
599 };
600 
601 
602 /**
603  * Gets the data (an organizer) from the tree item nearest the one
604  * associated with the given ID.
605  *
606  * @param {int}	id	an organizer ID
607  * @return	{Object}	the data or <code>null</code> for none
608  */
609 ZmTreeView.prototype.getNextData =
610 function(id) {
611 	var treeItem = this.getTreeItemById(id);
612 	if(!treeItem || !treeItem.parent) { return null; }
613 
614 	while (treeItem && treeItem.parent) {
615 		var parentN = treeItem.parent;
616 		if (!(parentN instanceof DwtTreeItem)) {
617 			return null;
618 		}
619 		var treeItems = parentN.getItems();
620 		var result = null;
621 		if (treeItems && treeItems.length > 1) {
622 			for(var i = 0; i < treeItems.length; i++) { 
623 				var tmp = treeItems[i];
624 				if (tmp == treeItem) {
625 					var nextData = this.findNext(treeItem, treeItems, i);
626 					if (nextData) { return nextData; }
627 					var prevData = this.findPrev(treeItem, treeItems, i);
628 					if (prevData) {	return prevData; }
629 				}
630 			}
631 		}
632 		treeItem = treeItem.parent;
633 	}
634 	return null;
635 };
636 
637 ZmTreeView.prototype.findNext =
638 function(treeItem, treeItems, i) {
639 	for (var j = i + 1; j < treeItems.length; j++) {
640 		var next = treeItems[j];
641 		if (next && next.getData) {
642 			return next.getData(Dwt.KEY_OBJECT);
643 		}
644 	}
645 	return null;
646 };
647 
648 ZmTreeView.prototype.findPrev =
649 function(treeItem, treeItems, i) {
650 	for (var j = i - 1; j >= 0; j--) {
651 		var prev = treeItems[j];
652 		if (prev && prev.getData) {
653 			return prev.getData(Dwt.KEY_OBJECT);
654 		}
655 	}
656 	return null;
657 };
658 
659 /**
660  * Renders a chunk of tree items, using a timer so that the browser doesn't get overloaded.
661  * 
662  * @param params	[hash]		hash of params (see _render)
663  * 
664  * @private
665  */
666 ZmTreeView.prototype._showRemainingFolders =
667 function(params) {
668 
669 	if (params.showRemainingFoldersNode){
670 		params.showRemainingFoldersNode.dispose();
671 	}
672 
673 	AjxTimedAction.scheduleAction(new AjxTimedAction(this,
674 		function() {
675 			this._render(params);
676 			if (params.len) {
677 				var orgs = ZmMsg[ZmOrganizer.LABEL[this.type]].toLowerCase();
678 				appCtxt.setStatusMsg(AjxMessageFormat.format(ZmMsg.foldersShown, [params.len, orgs]));
679 				params.len = 0;
680 			}
681 		}), 100);
682 };
683 
684 ZmTreeView.prototype._getNextTreeItem =
685 function(next) {
686 	var nextItem = DwtTree.prototype._getNextTreeItem.apply(this, arguments);
687 	return nextItem || (this._overview && this._overview._getNextTreeItem(next, this));
688 };
689 
690 ZmTreeView.prototype._getFirstTreeItem =
691 function() {
692 	if (!this._overview) {
693 		return DwtTree.prototype._getFirstTreeItem.call(tree);
694 	}
695 
696 	var treeids = this._overview.getTreeViews();
697 	var tree = this._overview.getTreeView(treeids[0]);
698 	return tree && DwtTree.prototype._getFirstTreeItem.call(tree);
699 };
700 
701 ZmTreeView.prototype._getLastTreeItem =
702 function() {
703 	if (!this._overview) {
704 		return DwtTree.prototype._getLastTreeItem.call(tree);
705 	}
706 
707 	var treeids = this._overview.getTreeViews();
708 	var tree = this._overview.getTreeView(treeids[treeids.length - 1]);
709 	return tree && DwtTree.prototype._getLastTreeItem.call(tree);
710 };
711 
712 ZmTreeView.prototype._hideHeaderTreeItem =
713 function() {
714 	return (appCtxt.multiAccounts && appCtxt.accountList.size() > 1 &&
715 			(this.type == ZmOrganizer.FOLDER ||
716 			 this.type == ZmOrganizer.ADDRBOOK ||
717 			 this.type == ZmOrganizer.CALENDAR ||
718 			 this.type == ZmOrganizer.TASKS ||
719 			 this.type == ZmOrganizer.BRIEFCASE ||
720 			 this.type == ZmOrganizer.PREF_PAGE ||
721 			 this.type == ZmOrganizer.ZIMLET));
722 };
723 
724 ZmTreeView.prototype._getHeaderTreeItemImage =
725 function() {
726 	if (appCtxt.multiAccounts) {
727 		if (this.type == ZmOrganizer.SEARCH)	{ return "SearchFolder"; }
728 		if (this.type == ZmOrganizer.TAG)		{ return "TagStack"; }
729 	}
730 	return null;
731 };
732