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