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 */ 34 function 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 61 TreeOutline.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 */ 72 TreeOutline.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 */ 86 TreeOutline.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 */ 105 TreeOutline.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 */ 156 TreeOutline.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 */ 194 TreeOutline.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 206 TreeOutline.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 */ 230 TreeOutline.prototype._rememberTreeElement = function(element) 231 { 232 if (!this._treeElementsMap.get(element.representedObject)) 233 this._treeElementsMap.set(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 */ 247 TreeOutline.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 */ 260 TreeOutline.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 */ 273 TreeOutline.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 */ 289 TreeOutline.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 */ 324 TreeOutline.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 336 TreeOutline.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 407 TreeOutline.prototype.expand = function() 408 { 409 // this is the root, do nothing 410 } 411 412 TreeOutline.prototype.collapse = function() 413 { 414 // this is the root, do nothing 415 } 416 417 /** 418 * @return {boolean} 419 */ 420 TreeOutline.prototype.revealed = function() 421 { 422 return true; 423 } 424 425 TreeOutline.prototype.reveal = function() 426 { 427 // this is the root, do nothing 428 } 429 430 TreeOutline.prototype.select = function() 431 { 432 // this is the root, do nothing 433 } 434 435 /** 436 * @param {boolean=} omitFocus 437 */ 438 TreeOutline.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 */ 449 function 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 468 TreeElement.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 577 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild; 578 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild; 579 TreeElement.prototype.insertBeforeChild = TreeOutline.prototype.insertBeforeChild; 580 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild; 581 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex; 582 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren; 583 584 TreeElement.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("selectstart", TreeElement.treeElementSelectStart, false); 606 this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false); 607 this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false); 608 609 this.onattach(); 610 } 611 612 var nextSibling = null; 613 if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode) 614 nextSibling = this.nextSibling._listItemNode; 615 this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling); 616 if (this._childrenListNode) 617 this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling); 618 if (this.selected) 619 this.select(); 620 if (this.expanded) 621 this.expand(); 622 } 623 624 TreeElement.prototype._detach = function() 625 { 626 if (this._listItemNode && this._listItemNode.parentNode) 627 this._listItemNode.parentNode.removeChild(this._listItemNode); 628 if (this._childrenListNode && this._childrenListNode.parentNode) 629 this._childrenListNode.parentNode.removeChild(this._childrenListNode); 630 } 631 632 TreeElement.treeElementMouseDown = function(event) 633 { 634 var element = event.currentTarget; 635 if (!element) 636 return; 637 delete element._selectionStarted; 638 639 if (!element.treeElement || !element.treeElement.selectable) 640 return; 641 642 if (element.treeElement.isEventWithinDisclosureTriangle(event)) 643 return; 644 645 element.treeElement.selectOnMouseDown(event); 646 } 647 648 TreeElement.treeElementSelectStart = function(event) 649 { 650 var element = event.currentTarget; 651 if (!element) 652 return; 653 element._selectionStarted = true; 654 } 655 656 TreeElement.treeElementToggled = function(event) 657 { 658 var element = event.currentTarget; 659 if (!element) 660 return; 661 if (element._selectionStarted) { 662 delete element._selectionStarted 663 var selection = window.getSelection(); 664 if (selection && !selection.isCollapsed && element.isSelfOrAncestor(selection.anchorNode) && element.isSelfOrAncestor(selection.focusNode)) 665 return; 666 } 667 668 if (!element.treeElement) 669 return; 670 671 var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable; 672 var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event); 673 if (!toggleOnClick && !isInTriangle) 674 return; 675 676 if (event.target && event.target.enclosingNodeOrSelfWithNodeName("a")) 677 return; 678 679 if (element.treeElement.expanded) { 680 if (event.altKey) 681 element.treeElement.collapseRecursively(); 682 else 683 element.treeElement.collapse(); 684 } else { 685 if (event.altKey) 686 element.treeElement.expandRecursively(); 687 else 688 element.treeElement.expand(); 689 } 690 event.consume(); 691 } 692 693 TreeElement.treeElementDoubleClicked = function(event) 694 { 695 var element = event.currentTarget; 696 if (!element || !element.treeElement) 697 return; 698 699 var handled = element.treeElement.ondblclick.call(element.treeElement, event); 700 if (handled) 701 return; 702 if (element.treeElement.hasChildren && !element.treeElement.expanded) 703 element.treeElement.expand(); 704 } 705 706 TreeElement.prototype.collapse = function() 707 { 708 if (this._listItemNode) 709 this._listItemNode.classList.remove("expanded"); 710 if (this._childrenListNode) 711 this._childrenListNode.classList.remove("expanded"); 712 713 this.expanded = false; 714 715 if (this.treeOutline) 716 this.treeOutline._expandedStateMap.set(this.representedObject, false); 717 718 this.oncollapse(); 719 } 720 721 TreeElement.prototype.collapseRecursively = function() 722 { 723 var item = this; 724 while (item) { 725 if (item.expanded) 726 item.collapse(); 727 item = item.traverseNextTreeElement(false, this, true); 728 } 729 } 730 731 TreeElement.prototype.expand = function() 732 { 733 if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode)) 734 return; 735 736 // Set this before onpopulate. Since onpopulate can add elements, this makes 737 // sure the expanded flag is true before calling those functions. This prevents the possibility 738 // of an infinite loop if onpopulate were to call expand. 739 740 this.expanded = true; 741 if (this.treeOutline) 742 this.treeOutline._expandedStateMap.set(this.representedObject, true); 743 744 if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) { 745 if (this._childrenListNode && this._childrenListNode.parentNode) 746 this._childrenListNode.parentNode.removeChild(this._childrenListNode); 747 748 this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol"); 749 this._childrenListNode.parentTreeElement = this; 750 this._childrenListNode.classList.add("children"); 751 752 if (this.hidden) 753 this._childrenListNode.classList.add("hidden"); 754 755 this.onpopulate(); 756 757 for (var i = 0; i < this.children.length; ++i) 758 this.children[i]._attach(); 759 760 delete this._shouldRefreshChildren; 761 } 762 763 if (this._listItemNode) { 764 this._listItemNode.classList.add("expanded"); 765 if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode) 766 this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling); 767 } 768 769 if (this._childrenListNode) 770 this._childrenListNode.classList.add("expanded"); 771 772 this.onexpand(); 773 } 774 775 TreeElement.prototype.expandRecursively = function(maxDepth) 776 { 777 var item = this; 778 var info = {}; 779 var depth = 0; 780 781 // The Inspector uses TreeOutlines to represents object properties, so recursive expansion 782 // in some case can be infinite, since JavaScript objects can hold circular references. 783 // So default to a recursion cap of 3 levels, since that gives fairly good results. 784 if (isNaN(maxDepth)) 785 maxDepth = 3; 786 787 while (item) { 788 if (depth < maxDepth) 789 item.expand(); 790 item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info); 791 depth += info.depthChange; 792 } 793 } 794 795 /** 796 * @param {?TreeElement} ancestor 797 * @return {boolean} 798 */ 799 TreeElement.prototype.hasAncestor = function(ancestor) { 800 if (!ancestor) 801 return false; 802 803 var currentNode = this.parent; 804 while (currentNode) { 805 if (ancestor === currentNode) 806 return true; 807 currentNode = currentNode.parent; 808 } 809 810 return false; 811 } 812 813 TreeElement.prototype.reveal = function() 814 { 815 var currentAncestor = this.parent; 816 while (currentAncestor && !currentAncestor.root) { 817 if (!currentAncestor.expanded) 818 currentAncestor.expand(); 819 currentAncestor = currentAncestor.parent; 820 } 821 822 this.onreveal(); 823 } 824 825 /** 826 * @return {boolean} 827 */ 828 TreeElement.prototype.revealed = function() 829 { 830 var currentAncestor = this.parent; 831 while (currentAncestor && !currentAncestor.root) { 832 if (!currentAncestor.expanded) 833 return false; 834 currentAncestor = currentAncestor.parent; 835 } 836 837 return true; 838 } 839 840 TreeElement.prototype.selectOnMouseDown = function(event) 841 { 842 if (this.select(false, true)) 843 event.consume(true); 844 } 845 846 /** 847 * @param {boolean=} omitFocus 848 * @param {boolean=} selectedByUser 849 * @return {boolean} 850 */ 851 TreeElement.prototype.select = function(omitFocus, selectedByUser) 852 { 853 if (!this.treeOutline || !this.selectable || this.selected) 854 return false; 855 856 if (this.treeOutline.selectedTreeElement) 857 this.treeOutline.selectedTreeElement.deselect(); 858 859 this.selected = true; 860 861 if (!omitFocus) 862 this.treeOutline._childrenListNode.focus(); 863 864 // Focusing on another node may detach "this" from tree. 865 if (!this.treeOutline) 866 return false; 867 this.treeOutline.selectedTreeElement = this; 868 if (this._listItemNode) 869 this._listItemNode.classList.add("selected"); 870 871 return this.onselect(selectedByUser); 872 } 873 874 /** 875 * @param {boolean=} omitFocus 876 */ 877 TreeElement.prototype.revealAndSelect = function(omitFocus) 878 { 879 this.reveal(); 880 this.select(omitFocus); 881 } 882 883 /** 884 * @param {boolean=} supressOnDeselect 885 * @return {boolean} 886 */ 887 TreeElement.prototype.deselect = function(supressOnDeselect) 888 { 889 if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected) 890 return false; 891 892 this.selected = false; 893 this.treeOutline.selectedTreeElement = null; 894 if (this._listItemNode) 895 this._listItemNode.classList.remove("selected"); 896 return true; 897 } 898 899 // Overridden by subclasses. 900 TreeElement.prototype.onpopulate = function() { } 901 902 /** 903 * @return {boolean} 904 */ 905 TreeElement.prototype.onenter = function() { return false; } 906 907 /** 908 * @return {boolean} 909 */ 910 TreeElement.prototype.ondelete = function() { return false; } 911 912 /** 913 * @return {boolean} 914 */ 915 TreeElement.prototype.onspace = function() { return false; } 916 917 TreeElement.prototype.onattach = function() { } 918 919 TreeElement.prototype.onexpand = function() { } 920 921 TreeElement.prototype.oncollapse = function() { } 922 923 /** 924 * @param {!MouseEvent} e 925 * @return {boolean} 926 */ 927 TreeElement.prototype.ondblclick = function(e) { return false; } 928 929 TreeElement.prototype.onreveal = function() { } 930 931 /** 932 * @param {boolean=} selectedByUser 933 * @return {boolean} 934 */ 935 TreeElement.prototype.onselect = function(selectedByUser) { return false; } 936 937 /** 938 * @param {boolean} skipUnrevealed 939 * @param {(!TreeOutline|!TreeElement|null)=} stayWithin 940 * @param {boolean=} dontPopulate 941 * @param {!Object=} info 942 * @return {?TreeElement} 943 */ 944 TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info) 945 { 946 if (!dontPopulate && this.hasChildren) 947 this.onpopulate(); 948 949 if (info) 950 info.depthChange = 0; 951 952 var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0]; 953 if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) { 954 if (info) 955 info.depthChange = 1; 956 return element; 957 } 958 959 if (this === stayWithin) 960 return null; 961 962 element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling; 963 if (element) 964 return element; 965 966 element = this; 967 while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) { 968 if (info) 969 info.depthChange -= 1; 970 element = element.parent; 971 } 972 973 if (!element) 974 return null; 975 976 return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling); 977 } 978 979 /** 980 * @param {boolean} skipUnrevealed 981 * @param {boolean=} dontPopulate 982 * @return {?TreeElement} 983 */ 984 TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate) 985 { 986 var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling; 987 if (!dontPopulate && element && element.hasChildren) 988 element.onpopulate(); 989 990 while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) { 991 if (!dontPopulate && element.hasChildren) 992 element.onpopulate(); 993 element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]); 994 } 995 996 if (element) 997 return element; 998 999 if (!this.parent || this.parent.root) 1000 return null; 1001 1002 return this.parent; 1003 } 1004 1005 /** 1006 * @return {boolean} 1007 */ 1008 TreeElement.prototype.isEventWithinDisclosureTriangle = function(event) 1009 { 1010 // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446) 1011 var paddingLeftValue = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left"); 1012 var computedLeftPadding = paddingLeftValue ? paddingLeftValue.getFloatValue(CSSPrimitiveValue.CSS_PX) : 0; 1013 var left = this._listItemNode.totalOffsetLeft() + computedLeftPadding; 1014 return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren; 1015 } 1016