Home | History | Annotate | Download | only in ui
      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