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