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