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
29function 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
48TreeOutline._knownTreeElementNextIdentifier = 1;
49
50TreeOutline._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
91TreeOutline._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
139TreeOutline._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
174TreeOutline._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
186TreeOutline._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
207TreeOutline._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
234TreeOutline.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
248TreeOutline.prototype._forgetTreeElement = function(element)
249{
250    if (this._knownTreeElements[element.identifier])
251        this._knownTreeElements[element.identifier].remove(element, true);
252}
253
254TreeOutline.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
263TreeOutline.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
281TreeOutline.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
332TreeOutline.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
341TreeOutline.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
411TreeOutline.prototype.expand = function()
412{
413    // this is the root, do nothing
414}
415
416TreeOutline.prototype.collapse = function()
417{
418    // this is the root, do nothing
419}
420
421TreeOutline.prototype.revealed = function()
422{
423    return true;
424}
425
426TreeOutline.prototype.reveal = function()
427{
428    // this is the root, do nothing
429}
430
431TreeOutline.prototype.select = function()
432{
433    // this is the root, do nothing
434}
435
436TreeOutline.prototype.appendChild = TreeOutline._appendChild;
437TreeOutline.prototype.insertChild = TreeOutline._insertChild;
438TreeOutline.prototype.removeChild = TreeOutline._removeChild;
439TreeOutline.prototype.removeChildAtIndex = TreeOutline._removeChildAtIndex;
440TreeOutline.prototype.removeChildren = TreeOutline._removeChildren;
441TreeOutline.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
442
443function 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
467TreeElement.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
581TreeElement.prototype.appendChild = TreeOutline._appendChild;
582TreeElement.prototype.insertChild = TreeOutline._insertChild;
583TreeElement.prototype.removeChild = TreeOutline._removeChild;
584TreeElement.prototype.removeChildAtIndex = TreeOutline._removeChildAtIndex;
585TreeElement.prototype.removeChildren = TreeOutline._removeChildren;
586TreeElement.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
587
588TreeElement.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
628TreeElement.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
636TreeElement.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
648TreeElement.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
671TreeElement.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
683TreeElement.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
698TreeElement.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
708TreeElement.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
750TreeElement.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
770TreeElement.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
784TreeElement.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
797TreeElement.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
809TreeElement.prototype.selectOnMouseDown = function(event)
810{
811    this.select(false, true);
812}
813
814TreeElement.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
836TreeElement.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
851TreeElement.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
886TreeElement.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
907TreeElement.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