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