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