1/* 2 * Copyright (C) 2007 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 14 * its contributors may be used to endorse or promote products derived 15 * from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29/** 30 * @constructor 31 * @param {!Element} listNode 32 * @param {boolean=} nonFocusable 33 */ 34function TreeOutline(listNode, nonFocusable) 35{ 36 /** @type {!Array.<!TreeElement>} */ 37 this.children = []; 38 this.selectedTreeElement = null; 39 this._childrenListNode = listNode; 40 this.childrenListElement = this._childrenListNode; 41 this._childrenListNode.removeChildren(); 42 this.expandTreeElementsWhenArrowing = false; 43 this.root = true; 44 this.hasChildren = false; 45 this.expanded = true; 46 this.selected = false; 47 this.treeOutline = this; 48 /** @type {?function(!TreeElement, !TreeElement):number} */ 49 this.comparator = null; 50 51 this.setFocusable(!nonFocusable); 52 this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true); 53 54 /** @type {!Map.<!Object, !Array.<!TreeElement>>} */ 55 this._treeElementsMap = new Map(); 56 /** @type {!Map.<!Object, boolean>} */ 57 this._expandedStateMap = new Map(); 58 this.element = listNode; 59} 60 61TreeOutline.prototype.setFocusable = function(focusable) 62{ 63 if (focusable) 64 this._childrenListNode.setAttribute("tabIndex", 0); 65 else 66 this._childrenListNode.removeAttribute("tabIndex"); 67} 68 69/** 70 * @param {!TreeElement} child 71 */ 72TreeOutline.prototype.appendChild = function(child) 73{ 74 var insertionIndex; 75 if (this.treeOutline.comparator) 76 insertionIndex = insertionIndexForObjectInListSortedByFunction(child, this.children, this.treeOutline.comparator); 77 else 78 insertionIndex = this.children.length; 79 this.insertChild(child, insertionIndex); 80} 81 82/** 83 * @param {!TreeElement} child 84 * @param {!TreeElement} beforeChild 85 */ 86TreeOutline.prototype.insertBeforeChild = function(child, beforeChild) 87{ 88 if (!child) 89 throw("child can't be undefined or null"); 90 91 if (!beforeChild) 92 throw("beforeChild can't be undefined or null"); 93 94 var childIndex = this.children.indexOf(beforeChild); 95 if (childIndex === -1) 96 throw("beforeChild not found in this node's children"); 97 98 this.insertChild(child, childIndex); 99} 100 101/** 102 * @param {!TreeElement} child 103 * @param {number} index 104 */ 105TreeOutline.prototype.insertChild = function(child, index) 106{ 107 if (!child) 108 throw("child can't be undefined or null"); 109 110 var previousChild = (index > 0 ? this.children[index - 1] : null); 111 if (previousChild) { 112 previousChild.nextSibling = child; 113 child.previousSibling = previousChild; 114 } else { 115 child.previousSibling = null; 116 } 117 118 var nextChild = this.children[index]; 119 if (nextChild) { 120 nextChild.previousSibling = child; 121 child.nextSibling = nextChild; 122 } else { 123 child.nextSibling = null; 124 } 125 126 this.children.splice(index, 0, child); 127 this.hasChildren = true; 128 child.parent = this; 129 child.treeOutline = this.treeOutline; 130 child.treeOutline._rememberTreeElement(child); 131 132 var current = child.children[0]; 133 while (current) { 134 current.treeOutline = this.treeOutline; 135 current.treeOutline._rememberTreeElement(current); 136 current = current.traverseNextTreeElement(false, child, true); 137 } 138 139 if (child.hasChildren && typeof(child.treeOutline._expandedStateMap.get(child.representedObject)) !== "undefined") 140 child.expanded = child.treeOutline._expandedStateMap.get(child.representedObject); 141 142 if (!this._childrenListNode) { 143 this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol"); 144 this._childrenListNode.parentTreeElement = this; 145 this._childrenListNode.classList.add("children"); 146 if (this.hidden) 147 this._childrenListNode.classList.add("hidden"); 148 } 149 150 child._attach(); 151} 152 153/** 154 * @param {number} childIndex 155 */ 156TreeOutline.prototype.removeChildAtIndex = function(childIndex) 157{ 158 if (childIndex < 0 || childIndex >= this.children.length) 159 throw("childIndex out of range"); 160 161 var child = this.children[childIndex]; 162 this.children.splice(childIndex, 1); 163 164 var parent = child.parent; 165 if (child.deselect()) { 166 if (child.previousSibling) 167 child.previousSibling.select(); 168 else if (child.nextSibling) 169 child.nextSibling.select(); 170 else 171 parent.select(); 172 } 173 174 if (child.previousSibling) 175 child.previousSibling.nextSibling = child.nextSibling; 176 if (child.nextSibling) 177 child.nextSibling.previousSibling = child.previousSibling; 178 179 if (child.treeOutline) { 180 child.treeOutline._forgetTreeElement(child); 181 child.treeOutline._forgetChildrenRecursive(child); 182 } 183 184 child._detach(); 185 child.treeOutline = null; 186 child.parent = null; 187 child.nextSibling = null; 188 child.previousSibling = null; 189} 190 191/** 192 * @param {!TreeElement} child 193 */ 194TreeOutline.prototype.removeChild = function(child) 195{ 196 if (!child) 197 throw("child can't be undefined or null"); 198 199 var childIndex = this.children.indexOf(child); 200 if (childIndex === -1) 201 throw("child not found in this node's children"); 202 203 this.removeChildAtIndex.call(this, childIndex); 204} 205 206TreeOutline.prototype.removeChildren = function() 207{ 208 for (var i = 0; i < this.children.length; ++i) { 209 var child = this.children[i]; 210 child.deselect(); 211 212 if (child.treeOutline) { 213 child.treeOutline._forgetTreeElement(child); 214 child.treeOutline._forgetChildrenRecursive(child); 215 } 216 217 child._detach(); 218 child.treeOutline = null; 219 child.parent = null; 220 child.nextSibling = null; 221 child.previousSibling = null; 222 } 223 224 this.children = []; 225} 226 227/** 228 * @param {!TreeElement} element 229 */ 230TreeOutline.prototype._rememberTreeElement = function(element) 231{ 232 if (!this._treeElementsMap.get(element.representedObject)) 233 this._treeElementsMap.put(element.representedObject, []); 234 235 // check if the element is already known 236 var elements = this._treeElementsMap.get(element.representedObject); 237 if (elements.indexOf(element) !== -1) 238 return; 239 240 // add the element 241 elements.push(element); 242} 243 244/** 245 * @param {!TreeElement} element 246 */ 247TreeOutline.prototype._forgetTreeElement = function(element) 248{ 249 if (this._treeElementsMap.get(element.representedObject)) { 250 var elements = this._treeElementsMap.get(element.representedObject); 251 elements.remove(element, true); 252 if (!elements.length) 253 this._treeElementsMap.remove(element.representedObject); 254 } 255} 256 257/** 258 * @param {!TreeElement} parentElement 259 */ 260TreeOutline.prototype._forgetChildrenRecursive = function(parentElement) 261{ 262 var child = parentElement.children[0]; 263 while (child) { 264 this._forgetTreeElement(child); 265 child = child.traverseNextTreeElement(false, parentElement, true); 266 } 267} 268 269/** 270 * @param {?Object} representedObject 271 * @return {?TreeElement} 272 */ 273TreeOutline.prototype.getCachedTreeElement = function(representedObject) 274{ 275 if (!representedObject) 276 return null; 277 278 var elements = this._treeElementsMap.get(representedObject); 279 if (elements && elements.length) 280 return elements[0]; 281 return null; 282} 283 284/** 285 * @param {?Object} representedObject 286 * @param {function(!Object):?Object} getParent 287 * @return {?TreeElement} 288 */ 289TreeOutline.prototype.findTreeElement = function(representedObject, getParent) 290{ 291 if (!representedObject) 292 return null; 293 294 var cachedElement = this.getCachedTreeElement(representedObject); 295 if (cachedElement) 296 return cachedElement; 297 298 // Walk up the parent pointers from the desired representedObject 299 var ancestors = []; 300 for (var currentObject = getParent(representedObject); currentObject; currentObject = getParent(currentObject)) { 301 ancestors.push(currentObject); 302 if (this.getCachedTreeElement(currentObject)) // stop climbing as soon as we hit 303 break; 304 } 305 306 if (!currentObject) 307 return null; 308 309 // Walk down to populate each ancestor's children, to fill in the tree and the cache. 310 for (var i = ancestors.length - 1; i >= 0; --i) { 311 var treeElement = this.getCachedTreeElement(ancestors[i]); 312 if (treeElement) 313 treeElement.onpopulate(); // fill the cache with the children of treeElement 314 } 315 316 return this.getCachedTreeElement(representedObject); 317} 318 319/** 320 * @param {number} x 321 * @param {number} y 322 * @return {?TreeElement} 323 */ 324TreeOutline.prototype.treeElementFromPoint = function(x, y) 325{ 326 var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y); 327 if (!node) 328 return null; 329 330 var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]); 331 if (listNode) 332 return listNode.parentTreeElement || listNode.treeElement; 333 return null; 334} 335 336TreeOutline.prototype._treeKeyDown = function(event) 337{ 338 if (event.target !== this._childrenListNode) 339 return; 340 341 if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey) 342 return; 343 344 var handled = false; 345 var nextSelectedElement; 346 if (event.keyIdentifier === "Up" && !event.altKey) { 347 nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true); 348 while (nextSelectedElement && !nextSelectedElement.selectable) 349 nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing); 350 handled = nextSelectedElement ? true : false; 351 } else if (event.keyIdentifier === "Down" && !event.altKey) { 352 nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true); 353 while (nextSelectedElement && !nextSelectedElement.selectable) 354 nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing); 355 handled = nextSelectedElement ? true : false; 356 } else if (event.keyIdentifier === "Left") { 357 if (this.selectedTreeElement.expanded) { 358 if (event.altKey) 359 this.selectedTreeElement.collapseRecursively(); 360 else 361 this.selectedTreeElement.collapse(); 362 handled = true; 363 } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) { 364 handled = true; 365 if (this.selectedTreeElement.parent.selectable) { 366 nextSelectedElement = this.selectedTreeElement.parent; 367 while (nextSelectedElement && !nextSelectedElement.selectable) 368 nextSelectedElement = nextSelectedElement.parent; 369 handled = nextSelectedElement ? true : false; 370 } else if (this.selectedTreeElement.parent) 371 this.selectedTreeElement.parent.collapse(); 372 } 373 } else if (event.keyIdentifier === "Right") { 374 if (!this.selectedTreeElement.revealed()) { 375 this.selectedTreeElement.reveal(); 376 handled = true; 377 } else if (this.selectedTreeElement.hasChildren) { 378 handled = true; 379 if (this.selectedTreeElement.expanded) { 380 nextSelectedElement = this.selectedTreeElement.children[0]; 381 while (nextSelectedElement && !nextSelectedElement.selectable) 382 nextSelectedElement = nextSelectedElement.nextSibling; 383 handled = nextSelectedElement ? true : false; 384 } else { 385 if (event.altKey) 386 this.selectedTreeElement.expandRecursively(); 387 else 388 this.selectedTreeElement.expand(); 389 } 390 } 391 } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) 392 handled = this.selectedTreeElement.ondelete(); 393 else if (isEnterKey(event)) 394 handled = this.selectedTreeElement.onenter(); 395 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Space.code) 396 handled = this.selectedTreeElement.onspace(); 397 398 if (nextSelectedElement) { 399 nextSelectedElement.reveal(); 400 nextSelectedElement.select(false, true); 401 } 402 403 if (handled) 404 event.consume(true); 405} 406 407TreeOutline.prototype.expand = function() 408{ 409 // this is the root, do nothing 410} 411 412TreeOutline.prototype.collapse = function() 413{ 414 // this is the root, do nothing 415} 416 417/** 418 * @return {boolean} 419 */ 420TreeOutline.prototype.revealed = function() 421{ 422 return true; 423} 424 425TreeOutline.prototype.reveal = function() 426{ 427 // this is the root, do nothing 428} 429 430TreeOutline.prototype.select = function() 431{ 432 // this is the root, do nothing 433} 434 435/** 436 * @param {boolean=} omitFocus 437 */ 438TreeOutline.prototype.revealAndSelect = function(omitFocus) 439{ 440 // this is the root, do nothing 441} 442 443/** 444 * @constructor 445 * @param {string|!Node} title 446 * @param {?Object=} representedObject 447 * @param {boolean=} hasChildren 448 */ 449function TreeElement(title, representedObject, hasChildren) 450{ 451 this._title = title; 452 this.representedObject = (representedObject || {}); 453 454 this.root = false; 455 this._hidden = false; 456 this._selectable = true; 457 this.expanded = false; 458 this.selected = false; 459 this.hasChildren = hasChildren; 460 this.children = []; 461 this.treeOutline = null; 462 this.parent = null; 463 this.previousSibling = null; 464 this.nextSibling = null; 465 this._listItemNode = null; 466} 467 468TreeElement.prototype = { 469 arrowToggleWidth: 10, 470 471 get selectable() { 472 if (this._hidden) 473 return false; 474 return this._selectable; 475 }, 476 477 set selectable(x) { 478 this._selectable = x; 479 }, 480 481 get listItemElement() { 482 return this._listItemNode; 483 }, 484 485 get childrenListElement() { 486 return this._childrenListNode; 487 }, 488 489 get title() { 490 return this._title; 491 }, 492 493 set title(x) { 494 this._title = x; 495 this._setListItemNodeContent(); 496 }, 497 498 get tooltip() { 499 return this._tooltip; 500 }, 501 502 set tooltip(x) { 503 this._tooltip = x; 504 if (this._listItemNode) 505 this._listItemNode.title = x ? x : ""; 506 }, 507 508 get hasChildren() { 509 return this._hasChildren; 510 }, 511 512 set hasChildren(x) { 513 if (this._hasChildren === x) 514 return; 515 516 this._hasChildren = x; 517 518 if (!this._listItemNode) 519 return; 520 521 if (x) 522 this._listItemNode.classList.add("parent"); 523 else { 524 this._listItemNode.classList.remove("parent"); 525 this.collapse(); 526 } 527 }, 528 529 get hidden() { 530 return this._hidden; 531 }, 532 533 set hidden(x) { 534 if (this._hidden === x) 535 return; 536 537 this._hidden = x; 538 539 if (x) { 540 if (this._listItemNode) 541 this._listItemNode.classList.add("hidden"); 542 if (this._childrenListNode) 543 this._childrenListNode.classList.add("hidden"); 544 } else { 545 if (this._listItemNode) 546 this._listItemNode.classList.remove("hidden"); 547 if (this._childrenListNode) 548 this._childrenListNode.classList.remove("hidden"); 549 } 550 }, 551 552 get shouldRefreshChildren() { 553 return this._shouldRefreshChildren; 554 }, 555 556 set shouldRefreshChildren(x) { 557 this._shouldRefreshChildren = x; 558 if (x && this.expanded) 559 this.expand(); 560 }, 561 562 _setListItemNodeContent: function() 563 { 564 if (!this._listItemNode) 565 return; 566 567 if (typeof this._title === "string") 568 this._listItemNode.textContent = this._title; 569 else { 570 this._listItemNode.removeChildren(); 571 if (this._title) 572 this._listItemNode.appendChild(this._title); 573 } 574 } 575} 576 577TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild; 578TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild; 579TreeElement.prototype.insertBeforeChild = TreeOutline.prototype.insertBeforeChild; 580TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild; 581TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex; 582TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren; 583 584TreeElement.prototype._attach = function() 585{ 586 if (!this._listItemNode || this.parent._shouldRefreshChildren) { 587 if (this._listItemNode && this._listItemNode.parentNode) 588 this._listItemNode.parentNode.removeChild(this._listItemNode); 589 590 this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li"); 591 this._listItemNode.treeElement = this; 592 this._setListItemNodeContent(); 593 this._listItemNode.title = this._tooltip ? this._tooltip : ""; 594 595 if (this.hidden) 596 this._listItemNode.classList.add("hidden"); 597 if (this.hasChildren) 598 this._listItemNode.classList.add("parent"); 599 if (this.expanded) 600 this._listItemNode.classList.add("expanded"); 601 if (this.selected) 602 this._listItemNode.classList.add("selected"); 603 604 this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false); 605 this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false); 606 this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false); 607 608 this.onattach(); 609 } 610 611 var nextSibling = null; 612 if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode) 613 nextSibling = this.nextSibling._listItemNode; 614 this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling); 615 if (this._childrenListNode) 616 this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling); 617 if (this.selected) 618 this.select(); 619 if (this.expanded) 620 this.expand(); 621} 622 623TreeElement.prototype._detach = function() 624{ 625 if (this._listItemNode && this._listItemNode.parentNode) 626 this._listItemNode.parentNode.removeChild(this._listItemNode); 627 if (this._childrenListNode && this._childrenListNode.parentNode) 628 this._childrenListNode.parentNode.removeChild(this._childrenListNode); 629} 630 631TreeElement.treeElementMouseDown = function(event) 632{ 633 var element = event.currentTarget; 634 if (!element || !element.treeElement || !element.treeElement.selectable) 635 return; 636 637 if (element.treeElement.isEventWithinDisclosureTriangle(event)) 638 return; 639 640 element.treeElement.selectOnMouseDown(event); 641} 642 643TreeElement.treeElementToggled = function(event) 644{ 645 var element = event.currentTarget; 646 if (!element || !element.treeElement) 647 return; 648 649 var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable; 650 var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event); 651 if (!toggleOnClick && !isInTriangle) 652 return; 653 654 if (element.treeElement.expanded) { 655 if (event.altKey) 656 element.treeElement.collapseRecursively(); 657 else 658 element.treeElement.collapse(); 659 } else { 660 if (event.altKey) 661 element.treeElement.expandRecursively(); 662 else 663 element.treeElement.expand(); 664 } 665 event.consume(); 666} 667 668TreeElement.treeElementDoubleClicked = function(event) 669{ 670 var element = event.currentTarget; 671 if (!element || !element.treeElement) 672 return; 673 674 var handled = element.treeElement.ondblclick.call(element.treeElement, event); 675 if (handled) 676 return; 677 if (element.treeElement.hasChildren && !element.treeElement.expanded) 678 element.treeElement.expand(); 679} 680 681TreeElement.prototype.collapse = function() 682{ 683 if (this._listItemNode) 684 this._listItemNode.classList.remove("expanded"); 685 if (this._childrenListNode) 686 this._childrenListNode.classList.remove("expanded"); 687 688 this.expanded = false; 689 690 if (this.treeOutline) 691 this.treeOutline._expandedStateMap.put(this.representedObject, false); 692 693 this.oncollapse(); 694} 695 696TreeElement.prototype.collapseRecursively = function() 697{ 698 var item = this; 699 while (item) { 700 if (item.expanded) 701 item.collapse(); 702 item = item.traverseNextTreeElement(false, this, true); 703 } 704} 705 706TreeElement.prototype.expand = function() 707{ 708 if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode)) 709 return; 710 711 // Set this before onpopulate. Since onpopulate can add elements, this makes 712 // sure the expanded flag is true before calling those functions. This prevents the possibility 713 // of an infinite loop if onpopulate were to call expand. 714 715 this.expanded = true; 716 if (this.treeOutline) 717 this.treeOutline._expandedStateMap.put(this.representedObject, true); 718 719 if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) { 720 if (this._childrenListNode && this._childrenListNode.parentNode) 721 this._childrenListNode.parentNode.removeChild(this._childrenListNode); 722 723 this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol"); 724 this._childrenListNode.parentTreeElement = this; 725 this._childrenListNode.classList.add("children"); 726 727 if (this.hidden) 728 this._childrenListNode.classList.add("hidden"); 729 730 this.onpopulate(); 731 732 for (var i = 0; i < this.children.length; ++i) 733 this.children[i]._attach(); 734 735 delete this._shouldRefreshChildren; 736 } 737 738 if (this._listItemNode) { 739 this._listItemNode.classList.add("expanded"); 740 if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode) 741 this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling); 742 } 743 744 if (this._childrenListNode) 745 this._childrenListNode.classList.add("expanded"); 746 747 this.onexpand(); 748} 749 750TreeElement.prototype.expandRecursively = function(maxDepth) 751{ 752 var item = this; 753 var info = {}; 754 var depth = 0; 755 756 // The Inspector uses TreeOutlines to represents object properties, so recursive expansion 757 // in some case can be infinite, since JavaScript objects can hold circular references. 758 // So default to a recursion cap of 3 levels, since that gives fairly good results. 759 if (isNaN(maxDepth)) 760 maxDepth = 3; 761 762 while (item) { 763 if (depth < maxDepth) 764 item.expand(); 765 item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info); 766 depth += info.depthChange; 767 } 768} 769 770/** 771 * @param {?TreeElement} ancestor 772 * @return {boolean} 773 */ 774TreeElement.prototype.hasAncestor = function(ancestor) { 775 if (!ancestor) 776 return false; 777 778 var currentNode = this.parent; 779 while (currentNode) { 780 if (ancestor === currentNode) 781 return true; 782 currentNode = currentNode.parent; 783 } 784 785 return false; 786} 787 788TreeElement.prototype.reveal = function() 789{ 790 var currentAncestor = this.parent; 791 while (currentAncestor && !currentAncestor.root) { 792 if (!currentAncestor.expanded) 793 currentAncestor.expand(); 794 currentAncestor = currentAncestor.parent; 795 } 796 797 this.onreveal(); 798} 799 800/** 801 * @return {boolean} 802 */ 803TreeElement.prototype.revealed = function() 804{ 805 var currentAncestor = this.parent; 806 while (currentAncestor && !currentAncestor.root) { 807 if (!currentAncestor.expanded) 808 return false; 809 currentAncestor = currentAncestor.parent; 810 } 811 812 return true; 813} 814 815TreeElement.prototype.selectOnMouseDown = function(event) 816{ 817 if (this.select(false, true)) 818 event.consume(true); 819} 820 821/** 822 * @param {boolean=} omitFocus 823 * @param {boolean=} selectedByUser 824 * @return {boolean} 825 */ 826TreeElement.prototype.select = function(omitFocus, selectedByUser) 827{ 828 if (!this.treeOutline || !this.selectable || this.selected) 829 return false; 830 831 if (this.treeOutline.selectedTreeElement) 832 this.treeOutline.selectedTreeElement.deselect(); 833 834 this.selected = true; 835 836 if (!omitFocus) 837 this.treeOutline._childrenListNode.focus(); 838 839 // Focusing on another node may detach "this" from tree. 840 if (!this.treeOutline) 841 return false; 842 this.treeOutline.selectedTreeElement = this; 843 if (this._listItemNode) 844 this._listItemNode.classList.add("selected"); 845 846 return this.onselect(selectedByUser); 847} 848 849/** 850 * @param {boolean=} omitFocus 851 */ 852TreeElement.prototype.revealAndSelect = function(omitFocus) 853{ 854 this.reveal(); 855 this.select(omitFocus); 856} 857 858/** 859 * @param {boolean=} supressOnDeselect 860 * @return {boolean} 861 */ 862TreeElement.prototype.deselect = function(supressOnDeselect) 863{ 864 if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected) 865 return false; 866 867 this.selected = false; 868 this.treeOutline.selectedTreeElement = null; 869 if (this._listItemNode) 870 this._listItemNode.classList.remove("selected"); 871 return true; 872} 873 874// Overridden by subclasses. 875TreeElement.prototype.onpopulate = function() { } 876 877/** 878 * @return {boolean} 879 */ 880TreeElement.prototype.onenter = function() { return false; } 881 882/** 883 * @return {boolean} 884 */ 885TreeElement.prototype.ondelete = function() { return false; } 886 887/** 888 * @return {boolean} 889 */ 890TreeElement.prototype.onspace = function() { return false; } 891 892TreeElement.prototype.onattach = function() { } 893 894TreeElement.prototype.onexpand = function() { } 895 896TreeElement.prototype.oncollapse = function() { } 897 898/** 899 * @param {!MouseEvent} e 900 * @return {boolean} 901 */ 902TreeElement.prototype.ondblclick = function(e) { return false; } 903 904TreeElement.prototype.onreveal = function() { } 905 906/** 907 * @param {boolean=} selectedByUser 908 * @return {boolean} 909 */ 910TreeElement.prototype.onselect = function(selectedByUser) { return false; } 911 912/** 913 * @param {boolean} skipUnrevealed 914 * @param {(!TreeOutline|!TreeElement|null)=} stayWithin 915 * @param {boolean=} dontPopulate 916 * @param {!Object=} info 917 * @return {?TreeElement} 918 */ 919TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info) 920{ 921 if (!dontPopulate && this.hasChildren) 922 this.onpopulate(); 923 924 if (info) 925 info.depthChange = 0; 926 927 var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0]; 928 if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) { 929 if (info) 930 info.depthChange = 1; 931 return element; 932 } 933 934 if (this === stayWithin) 935 return null; 936 937 element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling; 938 if (element) 939 return element; 940 941 element = this; 942 while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) { 943 if (info) 944 info.depthChange -= 1; 945 element = element.parent; 946 } 947 948 if (!element) 949 return null; 950 951 return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling); 952} 953 954/** 955 * @param {boolean} skipUnrevealed 956 * @param {boolean=} dontPopulate 957 * @return {?TreeElement} 958 */ 959TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate) 960{ 961 var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling; 962 if (!dontPopulate && element && element.hasChildren) 963 element.onpopulate(); 964 965 while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) { 966 if (!dontPopulate && element.hasChildren) 967 element.onpopulate(); 968 element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]); 969 } 970 971 if (element) 972 return element; 973 974 if (!this.parent || this.parent.root) 975 return null; 976 977 return this.parent; 978} 979 980/** 981 * @return {boolean} 982 */ 983TreeElement.prototype.isEventWithinDisclosureTriangle = function(event) 984{ 985 // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446) 986 var paddingLeftValue = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left"); 987 var computedLeftPadding = paddingLeftValue ? paddingLeftValue.getFloatValue(CSSPrimitiveValue.CSS_PX) : 0; 988 var left = this._listItemNode.totalOffsetLeft() + computedLeftPadding; 989 return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren; 990} 991