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