1     /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * Creates a Tree widget.
 27  * @constructor
 28  * @class
 29  * This class implements a tree widget. Tree widgets may contain one or more DwtTreeItems.
 30  *
 31  * @author Ross Dargahi
 32  * 
 33  * @param {hash}	params				a hash of parameters
 34  * @param  {DwtComposite}     params.parent			the parent widget
 35  * @param  {DwtTree.SINGLE_STYLE|DwtTree.MULTI_STYLE|DwtTree.CHECKEDITEM_STYLE}     params.style 	the tree style
 36  * @param  {string}     params.className				the CSS class
 37  * @param  {constant}     params.posStyle				the positioning style (see {@link DwtControl})
 38  * @param  {boolean}     params.isCheckedByDefault	default checked state if tree styles is "checked"
 39  * 
 40  * @extends		DwtComposite
 41  */
 42 DwtTree = function(params) {
 43 	if (arguments.length == 0) { return; }
 44 	params = Dwt.getParams(arguments, DwtTree.PARAMS);
 45 	params.className = params.className || "DwtTree";
 46 	DwtComposite.call(this, params);
 47 
 48 	var events = [DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEUP, DwtEvent.ONDBLCLICK];
 49 	if (!AjxEnv.isIE) {
 50 		events = events.concat([DwtEvent.ONMOUSEOVER, DwtEvent.ONMOUSEOUT]);
 51 	}
 52 	this._setEventHdlrs(events);
 53 
 54 	var style = params.style;
 55 	if (!style) {
 56 		this._style = DwtTree.SINGLE_STYLE;
 57 	} else {
 58 		if (style == DwtTree.CHECKEDITEM_STYLE) {
 59 			style |= DwtTree.SINGLE_STYLE;
 60 		}
 61 		this._style = style;
 62 	}
 63 	this.isCheckedStyle = ((this._style & DwtTree.CHECKEDITEM_STYLE) != 0);
 64 	this.isCheckedByDefault = params.isCheckedByDefault;
 65 
 66 	this._selectedItems = new AjxVector();
 67 	this._selEv = new DwtSelectionEvent(true);
 68 	this._selByClickEv = new DwtSelectionEvent(true);
 69 	this._selByClickEv.clicked = true;
 70 	this._selByEnterEv = new DwtSelectionEvent(true);
 71 	this._selByEnterEv.enter = true;
 72 
 73     // Let tree be a single tab stop, then manage focus among items using arrow keys
 74     this.tabGroupMember = this;
 75 };
 76 
 77 DwtTree.PARAMS = ["parent", "style", "className", "posStyle"];
 78 
 79 DwtTree.prototype = new DwtComposite;
 80 DwtTree.prototype.constructor = DwtTree;
 81 DwtTree.prototype.role = "tree";
 82 
 83 DwtTree.prototype.toString = 
 84 function() {
 85 	return "DwtTree";
 86 };
 87 
 88 /**
 89  * Defines the "single" style.
 90  */
 91 DwtTree.SINGLE_STYLE = 1;
 92 /**
 93  * Defines the "multi" style.
 94  */
 95 DwtTree.MULTI_STYLE = 2;
 96 /**
 97  * Defines the "checked-item" style.
 98  */
 99 DwtTree.CHECKEDITEM_STYLE = 4;
100 
101 DwtTree.ITEM_SELECTED = 0;
102 DwtTree.ITEM_DESELECTED = 1;
103 DwtTree.ITEM_CHECKED = 2;
104 DwtTree.ITEM_ACTIONED = 3;
105 DwtTree.ITEM_DBL_CLICKED = 4;
106 
107 DwtTree.ITEM_EXPANDED = 1;
108 DwtTree.ITEM_COLLAPSED = 2;
109 
110 /**
111  * Gets the style.
112  * 
113  * @return	{constant}	the style
114  */
115 DwtTree.prototype.getStyle =
116 function() {
117 	return this._style;
118 };
119 
120 /**
121  * Get the nesting level; this is zero for trees.
122  *
123  * @return	{number}	the child item count
124  */
125 DwtTree.prototype.getNestingLevel =
126 function() {
127 	return 0;
128 };
129 
130 /**
131  * Adds a selection listener.
132  * 
133  * @param	{AjxListener}	listener	the listener
134  */
135 DwtTree.prototype.addSelectionListener = 
136 function(listener) {
137 	this.addListener(DwtEvent.SELECTION, listener);
138 };
139 
140 /**
141  * Removes a selection listener.
142  * 
143  * @param	{AjxListener}	listener	the listener
144  */
145 DwtTree.prototype.removeSelectionListener = 
146 function(listener) {
147 	this.removeListener(DwtEvent.SELECTION, listener);    	
148 };
149 
150 /**
151  * Adds a tree listener.
152  * 
153  * @param	{AjxListener}	listener	the listener
154  */
155 DwtTree.prototype.addTreeListener = 
156 function(listener) {
157 	this.addListener(DwtEvent.TREE, listener);
158 };
159 
160 /**
161  * Removes a selection listener.
162  * 
163  * @param	{AjxListener}	listener	the listener
164  */
165 DwtTree.prototype.removeTreeListener = 
166 function(listener) {
167 	this.removeListener(DwtEvent.TREE, listener);
168 };
169 
170 /**
171  * Gets the tree item count.
172  * 
173  * @return	{number}	the item count
174  */
175 DwtTree.prototype.getItemCount =
176 function() {
177 	return this.getItems().length;
178 };
179 
180 /**
181  * Gets the items.
182  * 
183  * @return	{array}	an array of {@link DwtTreeItem} objects
184  */
185 DwtTree.prototype.getItems =
186 function() {
187 	return this._children.getArray();
188 };
189 
190 /** Clears the tree items. */
191 DwtTree.prototype.clearItems = function() {
192     var items = this.getItems();
193     for (var i = 0; i < items.length; i++) {
194         this.removeChild(items[i]);
195     }
196     this._getContainerElement().innerHTML = "";
197 };
198 
199 
200 /**
201  * De-selects all items.
202  * 
203  */
204 DwtTree.prototype.deselectAll =
205 function() {
206 	var a = this._selectedItems.getArray();
207 	var sz = this._selectedItems.size();
208 	for (var i = 0; i < sz; i++) {
209 		if (a[i]) {
210 			a[i]._setSelected(false);
211 		}
212 	}
213 	if (sz > 0) {
214 		this._notifyListeners(DwtEvent.SELECTION, this._selectedItems.getArray(), DwtTree.ITEM_DESELECTED, null, this._selEv);
215 	}
216 	this._selectedItems.removeAll();
217 };
218 
219 /**
220  * Gets an array of selection items.
221  * 
222  * @return	{array}	an array of {@link DwtTreeItem} objects
223  */
224 DwtTree.prototype.getSelection =
225 function() {
226 	return this._selectedItems.getArray();
227 };
228 
229 DwtTree.prototype.setEnterSelection =
230 function(treeItem, kbNavEvent) {
231 	if (!treeItem) {
232 		return;
233 	}
234 	this._notifyListeners(DwtEvent.SELECTION, [treeItem], DwtTree.ITEM_SELECTED, null, this._selByEnterEv, kbNavEvent);
235 };
236 
237 
238 DwtTree.prototype.setSelection =
239 function(treeItem, skipNotify, kbNavEvent, noFocus) {
240 	if (!treeItem || !treeItem.isSelectionEnabled()) {
241 		return;
242 	}
243 
244 	// Remove currently selected items from the selection list. if <treeItem> is in that list, then note it and return
245 	// after we are done processing the selected list
246 	var a = this._selectedItems.getArray();
247 	var sz = this._selectedItems.size();
248 	var da;
249 	var j = 0;
250 	var alreadySelected = false;
251 	for (var i = 0; i < sz; i++) {
252 		if (a[i] == treeItem) {
253 			alreadySelected = true;
254 		} else {
255 			a[i]._setSelected(false);
256 			this._selectedItems.remove(a[i]);
257 			if (da == null) {
258 				da = new Array();
259 			}
260 			da[j++] = a[i];
261 		}
262 	}
263 
264 	if (da && !skipNotify) {
265 		this._notifyListeners(DwtEvent.SELECTION, da, DwtTree.ITEM_DESELECTED, null, this._selEv, kbNavEvent);
266 	}
267 
268 	if (alreadySelected) { return; }
269 	this._selectedItems.add(treeItem);
270 
271 	// Expand all parent nodes, and then set item selected
272 	this._expandUp(treeItem);
273 	if (treeItem._setSelected(true, noFocus) && !skipNotify) {
274 		this._notifyListeners(DwtEvent.SELECTION, [treeItem], DwtTree.ITEM_SELECTED, null, this._selEv, kbNavEvent);
275 	}
276 };
277 
278 DwtTree.prototype.getSelectionCount =
279 function() {
280 	return this._selectedItems.size();
281 };
282 
283 DwtTree.prototype.addChild = function(child) {
284 
285     // HACK: Tree items are added via _addItem. But we need to keep
286     // HACK: the original addChild behavior for other controls that
287     // HACK: may be added to the tree view.
288     if (child.isDwtTreeItem) {
289         return;
290     }
291 
292     DwtComposite.prototype.addChild.apply(this, arguments);
293 };
294 
295 /**
296  * Adds a separator.
297  * 
298  */
299 DwtTree.prototype.addSeparator =
300 function() {
301 	var sep = document.createElement("div");
302 	sep.className = "vSpace";
303 	this._getContainerElement().appendChild(sep);
304 };
305 
306 // Expand parent chain from given item up to root
307 DwtTree.prototype._expandUp =
308 function(item) {
309 	var parent = item.parent;
310 	while (parent instanceof DwtTreeItem) {
311 		parent.setExpanded(true);
312 		parent.setVisible(true);
313 		parent = parent.parent;
314 	}
315 };
316 
317 DwtTree.prototype._addItem = function(item, index) {
318 
319 	this._children.add(item, index);
320 	var thisHtmlElement = this._getContainerElement();
321 	var numChildren = thisHtmlElement.childNodes.length;
322 	if (index == null || index > numChildren) {
323 		thisHtmlElement.appendChild(item.getHtmlElement());
324 	} else {
325 		//IE Considers undefined as an illegal value for second argument in the insertBefore method
326 		thisHtmlElement.insertBefore(item.getHtmlElement(), thisHtmlElement.childNodes[index] || null);
327 	}
328 };
329 
330 DwtTree.prototype._getContainerElement = DwtTree.prototype.getHtmlElement;
331 
332 DwtTree.prototype.sort =
333 function(cmp) {
334     var children = this.getItems();
335     children.sort(cmp);
336     var fragment = document.createDocumentFragment();
337     AjxUtil.foreach(children, function(item, i){
338         fragment.appendChild(item.getHtmlElement());
339         item._index = i;
340     });
341     this._getContainerElement().appendChild(fragment);
342 };
343 
344 DwtTree.prototype.removeChild =
345 function(child) {
346 	this._children.remove(child);
347 	this._selectedItems.remove(child);
348     var childEl = child.getHtmlElement();
349     if (childEl.parentNode) {
350         childEl.parentNode.removeChild(childEl);
351     }
352 };
353 
354 /**
355  * Returns the next (or previous) tree item relative to the currently selected
356  * item, in top-to-bottom order as the tree appears visually. Items such as
357  * separators that cannot be selected are skipped.
358  * </p><p>
359  * If there is no currently selected item, return the first or last item. If we go past
360  * the beginning or end of the tree, return null.
361  * </p><p>
362  * For efficiency, a flattened list of the visible and selectable tree items is maintained.
363  * It will be cleared on any change to the tree's display, then regenerated when it is
364  * needed.
365  *
366  * @param {boolean}	next		if <code>true</code>, return next tree item; otherwise, return previous tree item
367  * 
368  * @private
369  */
370 DwtTree.prototype._getNextTreeItem =
371 function(next) {
372 
373 	var sel = this.getSelection();
374 	var curItem = (sel && sel.length) ? sel[0] : null;
375 
376 	var nextItem = null, idx = -1;
377 	var list = this.getTreeItemList(true);
378 	if (curItem) {
379 		for (var i = 0, len = list.length; i < len; i++) {
380 			var ti = list[i];
381 			if (ti == curItem) {
382 				idx = next ? i + 1 : i - 1;
383 				break;
384 			}
385 		}
386 		nextItem = list[idx]; // if array index out of bounds, nextItem is undefined
387 	} else {
388 		// if nothing is selected yet, return the first or last item
389 		if (list && list.length) {
390 			nextItem = next ? list[0] : list[list.length - 1];
391 		}
392 	}
393 	return nextItem;
394 };
395 
396 DwtTree.prototype._getFirstTreeItem =
397 function() {
398 	var a = this.getTreeItemList(true);
399 	if (a && a.length > 0) {
400 		return a[0];
401 	}
402 	return null;
403 };
404 
405 DwtTree.prototype._getLastTreeItem =
406 function() {
407 	var a = this.getTreeItemList(true);
408 	if (a && a.length > 0) {
409 		return a[a.length - 1];
410 	}
411 	return null;
412 };
413 
414 /**
415  * Creates a flat list of this tree's items, going depth-first.
416  *
417  * @param {boolean}	visible		if <code>true</code>, only include visible/selectable items
418  * @return	{array}	an array of {@link DwtTreeItem} objects
419  */
420 DwtTree.prototype.getTreeItemList =
421 function(visible) {
422 	return this._addToList([], visible);
423 };
424 
425 DwtTree.prototype._addToList =
426 function(list, visible, treeItem) {
427 	if (treeItem && !treeItem._isSeparator &&
428 		(!visible || (treeItem.getVisible() && treeItem._selectionEnabled))) {
429 
430 		list.push(treeItem);
431 	}
432 	if (!treeItem || !visible || treeItem._expanded) {
433 		var parent = treeItem || this;
434 		var children = parent.getChildren ? parent.getChildren() : [];
435 		for (var i = 0; i < children.length; i++) {
436 			this._addToList(list, visible, children[i]);
437 		}
438 	}
439 	return list;
440 };
441 
442 DwtTree.prototype._deselect =
443 function(item) {
444 	if (this._selectedItems.contains(item)) {
445 		this._selectedItems.remove(item);
446 		item._setSelected(false);
447 		this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_DESELECTED, null, this._selEv);
448 	}
449 };
450 
451 DwtTree.prototype._itemActioned =
452 function(item, ev) {
453 	if (this._actionedItem && !this._actionedItem.isDisposed()) {
454 		this._actionedItem._setActioned(false);
455 		this._notifyListeners(DwtEvent.SELECTION, [this._actionedItem], DwtTree.ITEM_DESELECTED, ev, this._selEv);
456 	}
457 	this._actionedItem = item;
458 	item._setActioned(true);
459 	this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_ACTIONED, ev, this._selEv);
460 };
461 
462 DwtTree.prototype._itemChecked =
463 function(item, ev) {
464 	this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_CHECKED, ev, this._selEv);
465 };
466 
467 DwtTree.prototype._itemClicked =
468 function(item, ev) {
469 	var i;
470 	var a = this._selectedItems.getArray();
471 	var numSelectedItems = this._selectedItems.size();
472 	if (this._style & DwtTree.SINGLE_STYLE || (!ev.shiftKey && !ev.ctrlKey)) {
473 		if (numSelectedItems > 0) {
474 			for (i = 0; i < numSelectedItems; i++) {
475 				a[i]._setSelected(false);
476 			}
477 			// Notify listeners of deselection
478 			this._notifyListeners(DwtEvent.SELECTION, this._selectedItems.getArray(), DwtTree.ITEM_DESELECTED, ev, this._selByClickEv);
479 			this._selectedItems.removeAll();
480 		}
481 		this._selectedItems.add(item);
482 		if (item._setSelected(true)) {
483 			this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_SELECTED, ev, this._selByClickEv);
484 		}
485 	} else {
486 		if (ev.ctrlKey) {
487 			if (this._selectedItems.contains(item)) {
488 				this._selectedItems.remove(item);
489 				item._setSelected(false);
490 				this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_DESELECTED, ev, this._selByClickEv);
491 			} else {
492 				this._selectedItems.add(item);
493 				if (item._setSelected(true)) {
494 					this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_SELECTED, ev, this._selByClickEv);
495 				}
496 			}
497 		} else {
498 			// SHIFT KEY
499 		}
500 	}
501 };
502 
503 DwtTree.prototype._itemDblClicked = 
504 function(item, ev) {
505 	this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_DBL_CLICKED, ev, this._selEv);
506 };
507 
508 DwtTree.prototype._itemExpanded =
509 function(item, ev, skipNotify) {
510 	if (!skipNotify) {
511 		this._notifyListeners(DwtEvent.TREE, [item], DwtTree.ITEM_EXPANDED, ev, DwtShell.treeEvent);
512 	}
513 };
514 
515 DwtTree.prototype._itemCollapsed =
516 function(item, ev, skipNotify) {
517 	var i;
518 	if (!skipNotify) {
519 		this._notifyListeners(DwtEvent.TREE, [item], DwtTree.ITEM_COLLAPSED, ev, DwtShell.treeEvent);
520 	}
521 	var setSelection = false;
522 	var a = this._selectedItems.getArray();
523 	var numSelectedItems = this._selectedItems.size();
524 	var da;
525 	var j = 0;
526 	for (i = 0; i < numSelectedItems; i++) {
527 		if (a[i]._isChildOf(item)) {
528 			setSelection = true;
529 			if (da == null) {
530 				da = new Array();
531 			}
532 			da[j++] = a[i];
533 			a[i]._setSelected(false);
534 			this._selectedItems.remove(a[i]);
535 		}		
536 	}
537 
538 	if (da) {
539 		this._notifyListeners(DwtEvent.SELECTION, da, DwtTree.ITEM_DESELECTED, ev, this._selEv);
540 	}
541 
542 	if (setSelection && !this._selectedItems.contains(item)) {
543 		if (item._setSelected(true)) {
544 			this._selectedItems.add(item);
545 			this._notifyListeners(DwtEvent.SELECTION, [item], DwtTree.ITEM_SELECTED, ev, this._selEv);
546 		}
547 	}
548 };
549 
550 DwtTree.prototype._notifyListeners =
551 function(listener, items, detail, srcEv, destEv, kbNavEvent) {
552 	if (this.isListenerRegistered(listener)) {
553 		if (srcEv) {
554 			DwtUiEvent.copy(destEv, srcEv);
555 		}
556 		destEv.items = items;
557 		if (items.length == 1) {
558 			destEv.item = items[0];
559 		}
560 		destEv.detail = detail;
561 		destEv.kbNavEvent = kbNavEvent;
562 		this.notifyListeners(listener, destEv);
563 		if (listener == DwtEvent.SELECTION) {
564 			this.shell.notifyGlobalSelection(destEv);
565 		}
566 	}
567 };
568