Home | History | Annotate | Download | only in front_end
      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 }
     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 /**
    269  * @param {?Object} representedObject
    270  * @return {?TreeElement}
    271  */
    272 TreeOutline.prototype.getCachedTreeElement = function(representedObject)
    273 {
    274     if (!representedObject)
    275         return null;
    276 
    277     var elements = this._treeElementsMap.get(representedObject);
    278     if (elements && elements.length)
    279         return elements[0];
    280     return null;
    281 }
    282 
    283 /**
    284  * @param {?Object} representedObject
    285  * @return {?TreeElement}
    286  */
    287 TreeOutline.prototype.findTreeElement = function(representedObject, isAncestor, getParent)
    288 {
    289     if (!representedObject)
    290         return null;
    291 
    292     var cachedElement = this.getCachedTreeElement(representedObject);
    293     if (cachedElement)
    294         return cachedElement;
    295 
    296     // Walk up the parent pointers from the desired representedObject
    297     var ancestors = [];
    298     for (var currentObject = getParent(representedObject); currentObject;  currentObject = getParent(currentObject)) {
    299         ancestors.push(currentObject);
    300         if (this.getCachedTreeElement(currentObject))  // stop climbing as soon as we hit
    301             break;
    302     }
    303 
    304     if (!currentObject)
    305         return null;
    306 
    307     // Walk down to populate each ancestor's children, to fill in the tree and the cache.
    308     for (var i = ancestors.length - 1; i >= 0; --i) {
    309         var treeElement = this.getCachedTreeElement(ancestors[i]);
    310         if (treeElement)
    311             treeElement.onpopulate();  // fill the cache with the children of treeElement
    312     }
    313 
    314     return this.getCachedTreeElement(representedObject);
    315 }
    316 
    317 /**
    318  * @param {number} x
    319  * @param {number} y
    320  * @return {?TreeElement}
    321  */
    322 TreeOutline.prototype.treeElementFromPoint = function(x, y)
    323 {
    324     var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
    325     if (!node)
    326         return null;
    327 
    328     var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
    329     if (listNode)
    330         return listNode.parentTreeElement || listNode.treeElement;
    331     return null;
    332 }
    333 
    334 TreeOutline.prototype._treeKeyDown = function(event)
    335 {
    336     if (event.target !== this._childrenListNode)
    337         return;
    338 
    339     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
    340         return;
    341 
    342     var handled = false;
    343     var nextSelectedElement;
    344     if (event.keyIdentifier === "Up" && !event.altKey) {
    345         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
    346         while (nextSelectedElement && !nextSelectedElement.selectable)
    347             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
    348         handled = nextSelectedElement ? true : false;
    349     } else if (event.keyIdentifier === "Down" && !event.altKey) {
    350         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
    351         while (nextSelectedElement && !nextSelectedElement.selectable)
    352             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
    353         handled = nextSelectedElement ? true : false;
    354     } else if (event.keyIdentifier === "Left") {
    355         if (this.selectedTreeElement.expanded) {
    356             if (event.altKey)
    357                 this.selectedTreeElement.collapseRecursively();
    358             else
    359                 this.selectedTreeElement.collapse();
    360             handled = true;
    361         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
    362             handled = true;
    363             if (this.selectedTreeElement.parent.selectable) {
    364                 nextSelectedElement = this.selectedTreeElement.parent;
    365                 while (nextSelectedElement && !nextSelectedElement.selectable)
    366                     nextSelectedElement = nextSelectedElement.parent;
    367                 handled = nextSelectedElement ? true : false;
    368             } else if (this.selectedTreeElement.parent)
    369                 this.selectedTreeElement.parent.collapse();
    370         }
    371     } else if (event.keyIdentifier === "Right") {
    372         if (!this.selectedTreeElement.revealed()) {
    373             this.selectedTreeElement.reveal();
    374             handled = true;
    375         } else if (this.selectedTreeElement.hasChildren) {
    376             handled = true;
    377             if (this.selectedTreeElement.expanded) {
    378                 nextSelectedElement = this.selectedTreeElement.children[0];
    379                 while (nextSelectedElement && !nextSelectedElement.selectable)
    380                     nextSelectedElement = nextSelectedElement.nextSibling;
    381                 handled = nextSelectedElement ? true : false;
    382             } else {
    383                 if (event.altKey)
    384                     this.selectedTreeElement.expandRecursively();
    385                 else
    386                     this.selectedTreeElement.expand();
    387             }
    388         }
    389     } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */)
    390         handled = this.selectedTreeElement.ondelete();
    391     else if (isEnterKey(event))
    392         handled = this.selectedTreeElement.onenter();
    393     else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Space.code)
    394         handled = this.selectedTreeElement.onspace();
    395 
    396     if (nextSelectedElement) {
    397         nextSelectedElement.reveal();
    398         nextSelectedElement.select(false, true);
    399     }
    400 
    401     if (handled)
    402         event.consume(true);
    403 }
    404 
    405 TreeOutline.prototype.expand = function()
    406 {
    407     // this is the root, do nothing
    408 }
    409 
    410 TreeOutline.prototype.collapse = function()
    411 {
    412     // this is the root, do nothing
    413 }
    414 
    415 TreeOutline.prototype.revealed = function()
    416 {
    417     return true;
    418 }
    419 
    420 TreeOutline.prototype.reveal = function()
    421 {
    422     // this is the root, do nothing
    423 }
    424 
    425 TreeOutline.prototype.select = function()
    426 {
    427     // this is the root, do nothing
    428 }
    429 
    430 /**
    431  * @param {boolean=} omitFocus
    432  */
    433 TreeOutline.prototype.revealAndSelect = function(omitFocus)
    434 {
    435     // this is the root, do nothing
    436 }
    437 
    438 /**
    439  * @constructor
    440  * @param {string|!Node} title
    441  * @param {?Object=} representedObject
    442  * @param {boolean=} hasChildren
    443  */
    444 function TreeElement(title, representedObject, hasChildren)
    445 {
    446     this._title = title;
    447     this.representedObject = (representedObject || {});
    448 
    449     this.root = false;
    450     this._hidden = false;
    451     this._selectable = true;
    452     this.expanded = false;
    453     this.selected = false;
    454     this.hasChildren = hasChildren;
    455     this.children = [];
    456     this.treeOutline = null;
    457     this.parent = null;
    458     this.previousSibling = null;
    459     this.nextSibling = null;
    460     this._listItemNode = null;
    461 }
    462 
    463 TreeElement.prototype = {
    464     arrowToggleWidth: 10,
    465 
    466     get selectable() {
    467         if (this._hidden)
    468             return false;
    469         return this._selectable;
    470     },
    471 
    472     set selectable(x) {
    473         this._selectable = x;
    474     },
    475 
    476     get listItemElement() {
    477         return this._listItemNode;
    478     },
    479 
    480     get childrenListElement() {
    481         return this._childrenListNode;
    482     },
    483 
    484     get title() {
    485         return this._title;
    486     },
    487 
    488     set title(x) {
    489         this._title = x;
    490         this._setListItemNodeContent();
    491     },
    492 
    493     get tooltip() {
    494         return this._tooltip;
    495     },
    496 
    497     set tooltip(x) {
    498         this._tooltip = x;
    499         if (this._listItemNode)
    500             this._listItemNode.title = x ? x : "";
    501     },
    502 
    503     get hasChildren() {
    504         return this._hasChildren;
    505     },
    506 
    507     set hasChildren(x) {
    508         if (this._hasChildren === x)
    509             return;
    510 
    511         this._hasChildren = x;
    512 
    513         if (!this._listItemNode)
    514             return;
    515 
    516         if (x)
    517             this._listItemNode.classList.add("parent");
    518         else {
    519             this._listItemNode.classList.remove("parent");
    520             this.collapse();
    521         }
    522     },
    523 
    524     get hidden() {
    525         return this._hidden;
    526     },
    527 
    528     set hidden(x) {
    529         if (this._hidden === x)
    530             return;
    531 
    532         this._hidden = x;
    533 
    534         if (x) {
    535             if (this._listItemNode)
    536                 this._listItemNode.classList.add("hidden");
    537             if (this._childrenListNode)
    538                 this._childrenListNode.classList.add("hidden");
    539         } else {
    540             if (this._listItemNode)
    541                 this._listItemNode.classList.remove("hidden");
    542             if (this._childrenListNode)
    543                 this._childrenListNode.classList.remove("hidden");
    544         }
    545     },
    546 
    547     get shouldRefreshChildren() {
    548         return this._shouldRefreshChildren;
    549     },
    550 
    551     set shouldRefreshChildren(x) {
    552         this._shouldRefreshChildren = x;
    553         if (x && this.expanded)
    554             this.expand();
    555     },
    556 
    557     _setListItemNodeContent: function()
    558     {
    559         if (!this._listItemNode)
    560             return;
    561 
    562         if (typeof this._title === "string")
    563             this._listItemNode.textContent = this._title;
    564         else {
    565             this._listItemNode.removeChildren();
    566             if (this._title)
    567                 this._listItemNode.appendChild(this._title);
    568         }
    569     }
    570 }
    571 
    572 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild;
    573 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild;
    574 TreeElement.prototype.insertBeforeChild = TreeOutline.prototype.insertBeforeChild;
    575 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild;
    576 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex;
    577 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren;
    578 
    579 TreeElement.prototype._attach = function()
    580 {
    581     if (!this._listItemNode || this.parent._shouldRefreshChildren) {
    582         if (this._listItemNode && this._listItemNode.parentNode)
    583             this._listItemNode.parentNode.removeChild(this._listItemNode);
    584 
    585         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
    586         this._listItemNode.treeElement = this;
    587         this._setListItemNodeContent();
    588         this._listItemNode.title = this._tooltip ? this._tooltip : "";
    589 
    590         if (this.hidden)
    591             this._listItemNode.classList.add("hidden");
    592         if (this.hasChildren)
    593             this._listItemNode.classList.add("parent");
    594         if (this.expanded)
    595             this._listItemNode.classList.add("expanded");
    596         if (this.selected)
    597             this._listItemNode.classList.add("selected");
    598 
    599         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false);
    600         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
    601         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
    602 
    603         this.onattach();
    604     }
    605 
    606     var nextSibling = null;
    607     if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
    608         nextSibling = this.nextSibling._listItemNode;
    609     this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
    610     if (this._childrenListNode)
    611         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
    612     if (this.selected)
    613         this.select();
    614     if (this.expanded)
    615         this.expand();
    616 }
    617 
    618 TreeElement.prototype._detach = function()
    619 {
    620     if (this._listItemNode && this._listItemNode.parentNode)
    621         this._listItemNode.parentNode.removeChild(this._listItemNode);
    622     if (this._childrenListNode && this._childrenListNode.parentNode)
    623         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
    624 }
    625 
    626 TreeElement.treeElementMouseDown = function(event)
    627 {
    628     var element = event.currentTarget;
    629     if (!element || !element.treeElement || !element.treeElement.selectable)
    630         return;
    631 
    632     if (element.treeElement.isEventWithinDisclosureTriangle(event))
    633         return;
    634 
    635     element.treeElement.selectOnMouseDown(event);
    636 }
    637 
    638 TreeElement.treeElementToggled = function(event)
    639 {
    640     var element = event.currentTarget;
    641     if (!element || !element.treeElement)
    642         return;
    643 
    644     var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable;
    645     var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event);
    646     if (!toggleOnClick && !isInTriangle)
    647         return;
    648 
    649     if (element.treeElement.expanded) {
    650         if (event.altKey)
    651             element.treeElement.collapseRecursively();
    652         else
    653             element.treeElement.collapse();
    654     } else {
    655         if (event.altKey)
    656             element.treeElement.expandRecursively();
    657         else
    658             element.treeElement.expand();
    659     }
    660     event.consume();
    661 }
    662 
    663 TreeElement.treeElementDoubleClicked = function(event)
    664 {
    665     var element = event.currentTarget;
    666     if (!element || !element.treeElement)
    667         return;
    668 
    669     var handled = element.treeElement.ondblclick.call(element.treeElement, event);
    670     if (handled)
    671         return;
    672     if (element.treeElement.hasChildren && !element.treeElement.expanded)
    673         element.treeElement.expand();
    674 }
    675 
    676 TreeElement.prototype.collapse = function()
    677 {
    678     if (this._listItemNode)
    679         this._listItemNode.classList.remove("expanded");
    680     if (this._childrenListNode)
    681         this._childrenListNode.classList.remove("expanded");
    682 
    683     this.expanded = false;
    684 
    685     if (this.treeOutline)
    686         this.treeOutline._expandedStateMap.put(this.representedObject, false);
    687 
    688     this.oncollapse();
    689 }
    690 
    691 TreeElement.prototype.collapseRecursively = function()
    692 {
    693     var item = this;
    694     while (item) {
    695         if (item.expanded)
    696             item.collapse();
    697         item = item.traverseNextTreeElement(false, this, true);
    698     }
    699 }
    700 
    701 TreeElement.prototype.expand = function()
    702 {
    703     if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode))
    704         return;
    705 
    706     // Set this before onpopulate. Since onpopulate can add elements, this makes
    707     // sure the expanded flag is true before calling those functions. This prevents the possibility
    708     // of an infinite loop if onpopulate were to call expand.
    709 
    710     this.expanded = true;
    711     if (this.treeOutline)
    712         this.treeOutline._expandedStateMap.put(this.representedObject, true);
    713 
    714     if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
    715         if (this._childrenListNode && this._childrenListNode.parentNode)
    716             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
    717 
    718         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
    719         this._childrenListNode.parentTreeElement = this;
    720         this._childrenListNode.classList.add("children");
    721 
    722         if (this.hidden)
    723             this._childrenListNode.classList.add("hidden");
    724 
    725         this.onpopulate();
    726 
    727         for (var i = 0; i < this.children.length; ++i)
    728             this.children[i]._attach();
    729 
    730         delete this._shouldRefreshChildren;
    731     }
    732 
    733     if (this._listItemNode) {
    734         this._listItemNode.classList.add("expanded");
    735         if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode)
    736             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
    737     }
    738 
    739     if (this._childrenListNode)
    740         this._childrenListNode.classList.add("expanded");
    741 
    742     this.onexpand();
    743 }
    744 
    745 TreeElement.prototype.expandRecursively = function(maxDepth)
    746 {
    747     var item = this;
    748     var info = {};
    749     var depth = 0;
    750 
    751     // The Inspector uses TreeOutlines to represents object properties, so recursive expansion
    752     // in some case can be infinite, since JavaScript objects can hold circular references.
    753     // So default to a recursion cap of 3 levels, since that gives fairly good results.
    754     if (isNaN(maxDepth))
    755         maxDepth = 3;
    756 
    757     while (item) {
    758         if (depth < maxDepth)
    759             item.expand();
    760         item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
    761         depth += info.depthChange;
    762     }
    763 }
    764 
    765 TreeElement.prototype.hasAncestor = function(ancestor) {
    766     if (!ancestor)
    767         return false;
    768 
    769     var currentNode = this.parent;
    770     while (currentNode) {
    771         if (ancestor === currentNode)
    772             return true;
    773         currentNode = currentNode.parent;
    774     }
    775 
    776     return false;
    777 }
    778 
    779 TreeElement.prototype.reveal = function()
    780 {
    781     var currentAncestor = this.parent;
    782     while (currentAncestor && !currentAncestor.root) {
    783         if (!currentAncestor.expanded)
    784             currentAncestor.expand();
    785         currentAncestor = currentAncestor.parent;
    786     }
    787 
    788     this.onreveal();
    789 }
    790 
    791 TreeElement.prototype.revealed = function()
    792 {
    793     var currentAncestor = this.parent;
    794     while (currentAncestor && !currentAncestor.root) {
    795         if (!currentAncestor.expanded)
    796             return false;
    797         currentAncestor = currentAncestor.parent;
    798     }
    799 
    800     return true;
    801 }
    802 
    803 TreeElement.prototype.selectOnMouseDown = function(event)
    804 {
    805     if (this.select(false, true))
    806         event.consume(true);
    807 }
    808 
    809 /**
    810  * @param {boolean=} omitFocus
    811  * @param {boolean=} selectedByUser
    812  * @return {boolean}
    813  */
    814 TreeElement.prototype.select = function(omitFocus, selectedByUser)
    815 {
    816     if (!this.treeOutline || !this.selectable || this.selected)
    817         return false;
    818 
    819     if (this.treeOutline.selectedTreeElement)
    820         this.treeOutline.selectedTreeElement.deselect();
    821 
    822     this.selected = true;
    823 
    824     if (!omitFocus)
    825         this.treeOutline._childrenListNode.focus();
    826 
    827     // Focusing on another node may detach "this" from tree.
    828     if (!this.treeOutline)
    829         return false;
    830     this.treeOutline.selectedTreeElement = this;
    831     if (this._listItemNode)
    832         this._listItemNode.classList.add("selected");
    833 
    834     return this.onselect(selectedByUser);
    835 }
    836 
    837 /**
    838  * @param {boolean=} omitFocus
    839  */
    840 TreeElement.prototype.revealAndSelect = function(omitFocus)
    841 {
    842     this.reveal();
    843     this.select(omitFocus);
    844 }
    845 
    846 /**
    847  * @param {boolean=} supressOnDeselect
    848  */
    849 TreeElement.prototype.deselect = function(supressOnDeselect)
    850 {
    851     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
    852         return false;
    853 
    854     this.selected = false;
    855     this.treeOutline.selectedTreeElement = null;
    856     if (this._listItemNode)
    857         this._listItemNode.classList.remove("selected");
    858     return true;
    859 }
    860 
    861 // Overridden by subclasses.
    862 TreeElement.prototype.onpopulate = function() { }
    863 
    864 /**
    865  * @return {boolean}
    866  */
    867 TreeElement.prototype.onenter = function() { return false; }
    868 
    869 /**
    870  * @return {boolean}
    871  */
    872 TreeElement.prototype.ondelete = function() { return false; }
    873 
    874 /**
    875  * @return {boolean}
    876  */
    877 TreeElement.prototype.onspace = function() { return false; }
    878 
    879 TreeElement.prototype.onattach = function() { }
    880 
    881 TreeElement.prototype.onexpand = function() { }
    882 
    883 TreeElement.prototype.oncollapse = function() { }
    884 
    885 /**
    886  * @param {!MouseEvent} e
    887  * @return {boolean}
    888  */
    889 TreeElement.prototype.ondblclick = function(e) { return false; }
    890 
    891 TreeElement.prototype.onreveal = function() { }
    892 
    893 /**
    894  * @param {boolean=} selectedByUser
    895  * @return {boolean}
    896  */
    897 TreeElement.prototype.onselect = function(selectedByUser) { return false; }
    898 
    899 /**
    900  * @param {boolean} skipUnrevealed
    901  * @param {(!TreeOutline|!TreeElement|null)=} stayWithin
    902  * @param {boolean=} dontPopulate
    903  * @param {!Object=} info
    904  * @return {?TreeElement}
    905  */
    906 TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info)
    907 {
    908     if (!dontPopulate && this.hasChildren)
    909         this.onpopulate();
    910 
    911     if (info)
    912         info.depthChange = 0;
    913 
    914     var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0];
    915     if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) {
    916         if (info)
    917             info.depthChange = 1;
    918         return element;
    919     }
    920 
    921     if (this === stayWithin)
    922         return null;
    923 
    924     element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
    925     if (element)
    926         return element;
    927 
    928     element = this;
    929     while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) {
    930         if (info)
    931             info.depthChange -= 1;
    932         element = element.parent;
    933     }
    934 
    935     if (!element)
    936         return null;
    937 
    938     return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
    939 }
    940 
    941 /**
    942  * @param {boolean} skipUnrevealed
    943  * @param {boolean=} dontPopulate
    944  * @return {?TreeElement}
    945  */
    946 TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate)
    947 {
    948     var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
    949     if (!dontPopulate && element && element.hasChildren)
    950         element.onpopulate();
    951 
    952     while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
    953         if (!dontPopulate && element.hasChildren)
    954             element.onpopulate();
    955         element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
    956     }
    957 
    958     if (element)
    959         return element;
    960 
    961     if (!this.parent || this.parent.root)
    962         return null;
    963 
    964     return this.parent;
    965 }
    966 
    967 TreeElement.prototype.isEventWithinDisclosureTriangle = function(event)
    968 {
    969     // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446)
    970     var paddingLeftValue = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left");
    971     var computedLeftPadding = paddingLeftValue ? paddingLeftValue.getFloatValue(CSSPrimitiveValue.CSS_PX) : 0;
    972     var left = this._listItemNode.totalOffsetLeft() + computedLeftPadding;
    973     return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren;
    974 }
    975